0

I am developing a mini social media web app and I use ASP.NET Identity to create and manage user accounts. I want to add another user account as a friend to my account. I could succesfully do that but the problem is when I checked my added friend's account, there is no update in his friends list. It's empty.

Here is my User class inherited from IdentityUser,

public class AppUser : IdentityUser
{
    public AppUser()
    {
        this.Friends = new HashSet<AppUser>();
    }
    public int Age { get; set; }
    public string Sex { get; set; }
    public string City { get; set; }
    public string Education { get; set; }
    public string Description { get; set; }
    public string? FriendOfUserId { get; set; }
    public virtual AppUser FriendOf { get; set; }

    public ICollection<AppUser> Friends { get; set; }
}

My DbContext class,

public class ApplicationDbContext : IdentityDbContext<AppUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
builder.Entity<AppUser>(x =>
        {
            x
                .HasMany(x => x.Friends)
                .WithOne(x => x.FriendOf)
                .HasForeignKey(x => x.FriendOfUserId);
        });
    }
    public DbSet<AppUser> Users { get; set; }
}

My Controller Class method to add friend,

public async Task<IActionResult> AddFriend(string id)
    {
        var addFriend = await context.Users.Include(u => u.Friends).FirstOrDefaultAsync(u => u.Id == id);
        var user = await userManager.GetUserAsync(this.User);
        var u = await context.Users.Include(u => u.Friends).FirstOrDefaultAsync(u => u.Id == user.Id);
        user.FriendOf = addFriend;
        user.Friends.Add(addFriend);
        await context.SaveChangesAsync();
        return Redirect("/");
    }
ithuRik
  • 5
  • 5
  • Based on David Ling's answer and your update to the original post, I have updated my answer. – Dan Barrett Aug 05 '20 at 12:13
  • @ithuRik: sorry for taking that long. I've been extremely busy at work until now. I've updated my answer to reflect this self-referencing many-to-many relationship rather than just many-to-one I previously was thinking about. Please check it out. – David Liang Aug 07 '20 at 23:12

2 Answers2

0

You will need to use the Include function.

// What you have is fine.
var friend = context.Users.Select ( u => u == id );

// this is what needs to occur in await userManager.GetUserAsync(this.User);
// Without the include the Navigation Property will not be tracked.
var user = context.Users
    .Select ( u => u == id )
    .Include ( u => u.Friends );

user.Friends.Add ( friend );
context.SaveChanges ();

Check out loading related data. https://learn.microsoft.com/en-us/ef/core/querying/related-data

EDIT:

Take a look at this post it is a duplicate of this one.

Many-to-many self referencing relationship

EDIT 2: So the issue you are having is that you are still trying to model this as one table. Think about how this would be structured in a SQL database. How could a single table contain a collection (Friend) in a single column? To accomplish this we will create a new table to model the relationship between AppUsers.

public class Friendship 
{
    // Foreign Key
    Int32 MeId { get; set; }

    // Foreign Key
    Int32 FriendId { get; set; }

    // Navigation Property
    AppUser Me { get; set; }

    // Navigation Property
    AppUser Friend { get; set; }
}
public class AppUser : IdentityUser
{
    public AppUser()
    {
        this.Friends = new HashSet<AppUser>();
    }
    
    // Primary Key. Is this defined in IdentityUser?
    public int Id { get; }
    public int Age { get; set; }
    public string Sex { get; set; }
    public string City { get; set; }
    public string Education { get; set; }
    public string Description { get; set; }

    // This is considered a Navigation Property 
    public ICollection<Friendship> Friends { get; set; }
}
protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);
    builder.Entity<AppUser>(x =>
    {
        x.HasPrimaryKey ( x => x.Id );
    });

    builder.Entity<Friendship>( f =>
    {
        f.HasKey ( f => new { f.MeId, f.FriendId } );
        f
            .HasOne( f => f.Me )
            .WithMany ( u => u.Friends )
            .HasForeignKey( f => f.MeId );

        f
            .HasOne( f => f.Friend )
            .WithMany ( u => u.Friends )
            .HasForeignKey( f => f.FriendId );
    });        
}

At this point you should be able to query the join table for friendships.

public void AddFriend ( AppUser user, AppUser friend ) {
    var trackedUser = context.AppUsers
         .Select ( u => u.Id == user.Id )
         .Include ( u => u.Friends );          
         .FirstOrDefault ();

   trackedUser.Friends.Add ( new Friendship () { 
       MeId = user.Id, 
       FriendId = friend.Id 
   });
   context.SaveChanges ();
}
Dan Barrett
  • 225
  • 1
  • 6
  • thanks for helping me. That post really helped me to understand what the problem is. But when I try that in my code, it shows the error **"The entity type 'Friends' requires a primary key to be defined. If you intended to use a keyless entity type call 'HasNoKey()'."** – ithuRik Aug 05 '20 at 14:06
  • Now I corrected the "primary key" error by adding a HasKey property to modelbuilder but I still couldn't the get my name updated on my friends list. I think there is something wrong with my controller methods. I have been pulling my hair for this problem for a week now and I still haven't found the solution. – ithuRik Aug 05 '20 at 17:26
0

I think you're not modeling your entity correctly. Since an user can have a list of friends, and also be a friend of other users, I guess you need to capture the latter part in the model.

Since this is a many-to-many relationship, and EF Core still hasn't supported it without declaring an entity to represent the join table, you need to defind that entity as well:

public class AppUser : IdentityUser
{
    public int Age { get; set; }
    public string Sex { get; set; }
    public string City { get; set; }
    public string Education { get; set; }
    public string Description { get; set; }

    public ICollection<AppUserFriendship> FriendsOf { get; set; }
    public ICollection<AppUserFriendship> Friends { get; set; }
}

public class AppUserFriendship
{
    public string UserId { get; set; }
    public AppUser User { get; set; }

    public string UserFriendId { get; set; }
    public AppUser UserFriend { get; set; }
}

And then you need to configure their relationships:

public class ApplicationDbContext : IdentityDbContext<AppUser>
{
    ...

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        builder.Entity<AppUserFriendship>(b =>
        {
            b.HasKey(x => new { x.UserId, x.UserFriendId };

            b.HasOne(x => x.User)
                .WithMany(x => x.Friends)
                .HasForeignKey(x => x.UserId)
                .OnDelete(DeleteBehavior.Restrict);

            b.HasOne(x => x.UserFriend)
                .WithMany(x => x.FriendsOf)
                .HasForeignKey(x => x.UserFriendId)
                .OnDelete(DeleteBehavior.Restrict);
        });
    }

    public DbSet<AppUser> Users { get; set; }
}

Note OnDelete(DeleteBehavior.Restrict). You have to set it to something other than DeleteBehavior.Cascade which is the default to prevent the cascade deletion.

Disclaimer: I wrote all by hand. Never test it.

David Liang
  • 20,385
  • 6
  • 44
  • 70
  • I edited the my code as you suggested now I can see my name in my friend account too but now I got a new problem. When I add two friends, my name is only is in the last added friend's account. it is not a problem when I only added one friend. But when i try to add two or more friends, this problem occurs. I have edited my controller method now above. You can see it. – ithuRik Aug 05 '20 at 09:19
  • @ithuRik: Opps, I assumed you could have a list of friends, but could only be one person's friend. That was why I only modeled `FriendOf` as one to many. If it's indeed many-to-many, we might have to create an intermediate join table. I am not sure if you support a scenario where you're my friend but I'm not your friend? – David Liang Aug 06 '20 at 00:22