0

I'm trying to make a simple Social Network (MicroBlog) with ASP.NET Core 5+ with Entity Framework Core. I ran into a problem when modeling the friendships between the users and fetching friends of a user.

While there are a lot of guides to modeling the entity as a Many-to-Many relation, for example, Self Referencing Many-to-Many relations gives a guide, link the Relation entity to the Ticket entity with two Relation's collection ICollection<Relation> RelatedTo and ICollection<Relation> RelatedFrom in Ticket, I'm wondering that if the relationship can be expressed more directly.

More specifically, My BlogUser and BlogUserFriendship is defined as follow:

public class BlogUser : IdentityUser
{
    public virtual ICollection<Blog> Blogs { get; set; }

    public virtual ICollection<BlogUser> Friends { get; set; }

    public virtual ICollection<BlogUserFriendship> Friendships { get; set; }
}

public class BlogUserFriendship
{
    public string User1Id { get; set; }
    public virtual BlogUser User1 { get; set; }

    public string User2Id { get; set; }
    public virtual BlogUser User2 { get; set; }
}

I follow the instructions in Many-to-many self referencing relationship to model the relations in OnModelCreating:

protected override void OnModelCreating(ModelBuilder builder)
{
    builder.Entity<BlogUser>()
        .HasMany(u => u.Friends)
        .WithMany(u => u.Friends)
        .UsingEntity<BlogUserFriendship>(
            j => j
                .HasOne(fs => fs.User2)
                .WithMany()
                .HasForeignKey(fs => fs.User2Id)
                .OnDelete(DeleteBehavior.Restrict),
            j => j
                .HasOne(fs => fs.User1)
                .WithMany(u => u.Friendships)
                .HasForeignKey(fs => fs.User1Id),
            j =>
            {
                j.HasKey(fs => new { fs.User1Id, fs.User2Id });
            }
        );
}

I'm wondering if I can access one or more friends of a BlogUser by just accessing its Friends property. Or I can add another user to a user's Friends as simple as follow:

// Inside an ASP.NET Core Controller.
// The BlogUser is a subclass of IdentityUser
var user = userManager.GetUserAsync(this.User);
var friendToAdd = userManager.FindByIdAsync(model.UserId);
user.Friends.Add(friendToAdd);
await _userManager.UpdateAsync(user);

I tried the codes above, but it ran into a problem as follow:

System.InvalidOperationException: Unable to track an entity of type 'BlogUserFriendship' because its primary key property 'User1Id' is null.
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NullableKeyIdentityMap`1.Add(InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges, Boolean modifyProperties)
......

What is the problems in the codes above?


What's more, I finally make it works with following codes, where User1 is the current user (for example, let's say UserName is Alice), and User2 is another user (let's say UserName is Bob):

_context.BlogUserFriendship.Add(new BlogUserFriendship
{
    User1 = user,
    User1Id = user.Id,
    User2 = friendToAdd,
    User2Id = friendToAdd.Id
});
await _context.SaveChangesAsync();

And the table in database succeed in showing up a friendship record, Column User1Id is Alice's UserId, and Column User2Id is Bob's UserId, correctly.

But when I try to fetch usernames of all friends of Alice as follow:

// Inside a Controller
[Route("Friends")]
[HttpGet]
public async Task<IActionResult> GetFriends()
{
    var user = await _userManager.GetUserAsync(this.User);
    return Ok(user.Friends.Select(f => new {f.UserName}).ToList());
}

The result is an empty JSON array: []. And when I put a breakpoint to debug, I saw the Alice user's Friend property is empty (Count = 0, not a null object).

I also tried to swap the User1 and User2 in OnModelCreating:

builder.Entity<BlogUser>()
    .HasMany(u => u.Friends)
    .WithMany(u => u.Friends)
    .UsingEntity<BlogUserFriendship>(
        j => j
            .HasOne(fs => fs.User1)
            .WithMany()
            .HasForeignKey(fs => fs.User1Id)
            .OnDelete(DeleteBehavior.Restrict),
        j => j
            .HasOne(fs => fs.User2)
            .WithMany(u => u.Friendships)
            .HasForeignKey(fs => fs.User2Id),
        j =>
        {
            j.HasKey(fs => new { fs.User1Id, fs.User2Id });
        }
    );

Then I receive a non empty JSON array, but contains only one user, Alice, who is the one I set to User1 when add the relationship record. Alice's friend, Bob, was not in the array:

[{"userName":"Alice"}]

When debugging, Alice became herself's friend, shows up in her Friends Collection.

I think there must be some error in my relationship model, but I can't find out what's wrong with it, or how to make it correctly.

Do you have any solutions? Or any better ideas? Thank you for your anwsering.

DHao2001
  • 1
  • 2
  • You can't use one and the same collection for two relationships (because many-to-many is no more or less than two one-to-many relationships). The model **must** have 2 collections. Then you can `Concat` them *in code* where needed. – Ivan Stoev Nov 11 '21 at 11:12
  • 1
    @IvanStoev Thank you for your comment. I'm new to EF so there might be some... silly mistakes in my codes. `Concat` will work well if there are 2 collections in model, and I think it can be queried easily. I just want to find out that if there is a way to make the process of creating a relationship more simple, like adding a user to friends list by `user.Friends.Add(anotherUser)`. Is there a solution in EF? Again, really appreciate your help. :-) – DHao2001 Nov 11 '21 at 16:24
  • No, there isn't. I guess that's because of the relational db model. Or even just logically, if you have just one collection, when you add object to it, how it will know who is `User1` and who - `User2` in the linking table? And how to perform the navigation fixup, i.e. imagine `User u1, u2;`, and then `u1.Friends.Add(u2)` should automatically do `u2.Friends.Add(u1)`, right? But that's still 1 link entry, and again it's unclear who is who there. – Ivan Stoev Nov 11 '21 at 17:49
  • @IvanStoev I got that. Thank you very much! – DHao2001 Nov 12 '21 at 02:21

0 Answers0