Search code examples
.netasp.net-coreasp.net-identityentity-framework-coreasp.net-core-2.1

Link ASP.Net Identity table to a user detail table


I am trying to link my Identity user table to a user detail table I have created to track other user information. That user detail table is called UserProfile.

I came across this link but it is not working in .NET Core 2.1: Link ASP.NET Identity users to user detail table

Here is what I have currently:

public class ApplicationUser : IdentityUser
{

    [Key]
    public override string Id { get; set; }

    [ForeignKey("Id")]
    public virtual UserProfile UserProfile { get; set; }

}

[Table("UserProfile")]
public class UserProfile
{
    [Key, ForeignKey("User")]
    public string UserId { get; set; } // UserId (Primary key) 
    public string UserName { get; set; } // UserName 
    public string FirstName { get; set; } // FirstName
    public string LastName { get; set; } // LastName

    //[ForeignKey("Id")]
    public virtual ApplicationUser User { get; set; }
}

However, elsewhere in the code I call:

var user = await _userMgr.FindByNameAsync(model.UserName);

And user.UserProfile is null.

I have tried many combinations of data annotations and even fluent api to no avail.

   modelBuilder.Entity<ApplicationUser>()
       .HasOne(c => c.UserProfile)
       .WithOne(t => t.User)
       .HasForeignKey<UserProfile>(b => b.UserId);

I also tried turning on lazy loading but that couldn't even load the property.

Does anyone know how to do this in .Net Core 2.1?

Thanks


Solution

  • Your main issue is simply that UserManager has no functionality to include related entities, such as your UserProfile. As a result, you have two options:

    1. Use your context directly instead. Then you can eagerly load your UserProfile along with the ApplicationUser instance, in just one query to the database:

      var user = await _context.Users.Include(x => x.UserProfile).SingleOrDefaultAsync(x => x.UserName ==  model.UserName);
      
    2. You can explicitly load the related UserProfile instead. This will result in an extra query, though, for a total of two: one to get the user and one to get the related profile:

      await _context.Entry(user).Reference(x => x.UserProfile).LoadAsync();
      

    However, frankly, you should not have UserProfile at all. ASP.NET Identity is not like ASP.NET Membership. With the latter, you had to have a separate UserProfile because the "user" in Membership was not extensible. In Identity, the user is extensible, so if you want additional profile information on it, just add it to the class:

    public class ApplicationUser : IdentityUser
    {
        public string FirstName { get; set; } // FirstName
        public string LastName { get; set; } // LastName
    }
    

    Note that I trimmed a lot of cruft here as well. There's no point in overriding the Id and then leaving it auto-implemented. Additionally, you obviously don't need the UserName property from UserProfile because IdentityUser already has that, which then of course means your ApplicationUser has it as well.

    UPDATE

    How the user data is persisted doesn't have to affect whether it can be a claim or not. In other words, you don't have to literally save data as claim in order to access it as claim. Just derive from UserClaimsPrincipalFactory<TUser>, override CreateAsync, and then register it with service collection as scoped.

    public class MyClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser>
    {
        public MyClaimsPrincipalFactory(UserManager<TUser> userManager, IOptions<IdentityOptions> optionsAccessor)
            : base(userManager, optionsAccessor)
        {
        }
    
        public async override Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
        {
            var principal = await base.CreateAsync(user);
            ((ClaimsIdentity)principal.Identity).AddClaims(new[]
            {
                new Claim(ClaimTypes.GivenName, user.FirstName),
                new Claim(ClaimTypes.Surname, user.LastName),
                // etc.
            });
    
            return principal;
        }
    }
    

    Then in ConfigureServices:

    services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, MyClaimsPrincipalFactory>();