EF Core 2 How To Include Roles Navigation Property On IdentityUser?
Asked Answered
G

4

15

I am setting up a new project using EF Core 2, and I need to have a navigation property on the IdentityUser so when I query for a user I can include(x => x.Roles) and get the Roles the user is in.

This post on Github has some ideas, but I have tried each one and all cause issues, by creating new/duplicate fields on the Identity tables or cause issues with migrations. And no official comment from anyone in the EF team.

https://github.com/aspnet/Identity/issues/1361

I was wondering if anyone has this working correctly? And could share their EF DB mappings and models.

Gunas answered 12/12, 2017 at 7:26 Comment(1)
The latest fix for the .Net Core 2.1 fixed my problem #51005016Juxtapose
L
24

See the documentation for 'Migrating Authentication and Identity to ASP.NET Core 2.0', specifically the section 'Add IdentityUser POCO Navigation Properties':

The Entity Framework (EF) Core navigation properties of the base IdentityUser POCO (Plain Old CLR Object) have been removed. If your 1.x project used these properties, manually add them back to the 2.0 project:

/// <summary>
/// Navigation property for the roles this user belongs to.
/// </summary>
public virtual ICollection<IdentityUserRole<int>> Roles { get; } = new List<IdentityUserRole<int>>();

To prevent duplicate foreign keys when running EF Core Migrations, add the following to your IdentityDbContext class' OnModelCreating method (after the base.OnModelCreating(); call):

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);
    // Customize the ASP.NET Identity model and override the defaults if needed.
    // For example, you can rename the ASP.NET Identity table names and more.
    // Add your customizations after calling base.OnModelCreating(builder);

    builder.Entity<ApplicationUser>()
        .HasMany(e => e.Roles)
        .WithOne()
        .HasForeignKey(e => e.UserId)
        .IsRequired()
        .OnDelete(DeleteBehavior.Cascade);
}

Edit

The above will only satisfy the task of accessing the role Ids held against a user via the IdentityUserRole link table. To access the role entity itself via a navigation property, you would need to add another navigation property (this time against an entity inheriting from IdentityUserRole). See the steps below:

  1. Modify the Roles navigation property on your IdentityUser entity as follows:
public virtual ICollection<UserRole> Roles { get; set; } = new List<UserRole>();
  1. Create the UserRole entity referenced above:
public class UserRole : IdentityUserRole<int>
{
    public virtual IdentityRole<int> Role { get; set; }
}
  1. Construct the mapping for UserRole as follows:
builder.Entity<UserRole>()
    .HasOne(e => e.Role)
    .WithMany()
    .HasForeignKey(e => e.RoleId)
    .IsRequired()
    .OnDelete(DeleteBehavior.Cascade);
  1. You can then retrieve entities (with the navigation property populated) as follows:
User user = context.Set<User>()
    .Include(u => u.Roles)
    .ThenInclude(r => r.Role)
    .FirstOrDefault();

Note:

  • As this is loading another side of a many-to-many relatiohship, this may result in more than one call to the database (see N+1 problem).
  • As you are creating a new entity that inherits from IdentityUserRole you will need to migrate or re-create the database.
  • If you want to use this navigation property with UserManager or RoleManager you will need to use the long-form overload of AddUserStore() and AddRoleStore in your startup class, e.g.
services.AddIdentity<User, IdentityRole<int>>()
    .AddUserStore<UserStore<User, IdentityRole<int>, SqlContext, int, IdentityUserClaim<int>, UserRole, IdentityUserLogin<int>, IdentityUserToken<int>, IdentityRoleClaim<int>>>()
    .AddRoleStore<RoleStore<IdentityRole<int>, SqlContext, int, UserRole, IdentityRoleClaim<int>>>()
    .AddDefaultTokenProviders();
Lynellelynett answered 12/12, 2017 at 12:15 Comment(6)
Thanks for taking the time to reply and appreciate the info. This is only the UserRoles though and not the Roles? Unfortunately I still have no way without another database lookup to get the Role name?Gunas
@leen3o: Ahh, I understand the question now. See my edit. However, note that it probably won't be achieved via a single database call.Lynellelynett
outstanding. I wish I could give this more upvotes. Thanks.Gunas
You are welcome :). The fact it results in multiple database calls may make this a bit useless, but thought it was worth adding anyway.Lynellelynett
@leen3o One more note: As this creates a new entity that inherits from IdentityUserRole you will need to migrate or re-create the database.Lynellelynett
FYI for others... I had to use a different RoleStore signature to get this working. I was able to use RoleStore<TRole, TContext, TKey, TUserRole, TClaim>. I'm in asp.net core 2Deltoid
D
6

I fetch roles by custom query and might be helpful.

var roles = (from role in _dbContext.Roles
    let userRoles = _dbContext.UserRoles.Where(ur => ur.UserId == user.Id).Select(ur => ur.RoleId)
    where userRoles.Contains(role.Id)
    select role
).ToList();
Danelledanete answered 12/12, 2017 at 9:28 Comment(0)
I
2

Adding my 10 cents to the already brilliant accepted answer. It is important to remember to adapt your DbContext to the changes you will be making.

In the case of the accepted answer, you DbContext should look similar to this:

public class DbContext: IdentityDbContext<User, IdentityRole<int>, string,
        IdentityUserClaim<string>, UserRole, IdentityUserLogin<string>, 
        IdentityRoleClaim<string>, IdentityUserToken<string>>, 
        IDesignTimeDbContextFactory<ApplicationContext>
{
    // ...Rest of the code goes here
}

Notice that UserRole replaces the default TUserRole parameter. Took me a couple of hours to figure this out, I hope it saves some else's time.

Intangible answered 7/6, 2020 at 12:41 Comment(3)
With this implementation while adding a migration, have you got an error like this? "The entity type 'IdentityUserLogin<string>' requires a primary key to be defined. If you intended to use a keyless entity type call 'HasNoKey()'." It's very annoying, I have tried almost all solutions on the web but no luck.Petrography
Not that I can remember. Have you tried something like builder.Entity<IdentityUserLogin<string>>().HasKey(x => x.UserId); in an override of the OnModelCreating method. Something similar was done in this answer (https://mcmap.net/q/179870/-the-entity-type-39-identityuserlogin-lt-string-gt-39-requires-a-primary-key-to-be-defined-duplicate)Intangible
Thanks for the suggestion, I had managed to fix it in some other way. I figured out the reason that in my case there were some mappings in onModelCreating() making use of both IdentityUserLogin and ApplicationUserLogin at the same time. Appeared like EF was confused about choosing one of the models. I fixed it by replacing all the default(IdentityUserLogin) models with my custom one(ApplicationUserLogin). That actually worked for me.Petrography
L
0

Adding some more comments to accepted answer and to comment Correct me if I wrong but in above case we already have all tables in DB and all relations are there, so we just need to reflect that in our DbContext and we DON'T need migrations for that.

Here what I did:

  1. My TKey is Guid, so I changed my DbContext to look like this:

    public class ApplicationDbContext : IdentityDbContext<User, IdentityRole<Guid>, Guid, IdentityUserClaim<Guid>, UserRole, IdentityUserLogin<Guid>, IdentityRoleClaim<Guid>, IdentityUserToken<Guid>>
    

    as it was suggested by Prince Owen

  2. Changed mapping:

        modelBuilder.Entity<UserRole>()
            .HasOne<User>()
            .WithMany(u => u.UserRoles)
            .HasForeignKey(x => x.UserId).IsRequired()
            .OnDelete(DeleteBehavior.Cascade);
    
        modelBuilder.Entity<UserRole>()
            .HasOne(e => e.Role)
            .WithMany()
            .HasForeignKey(e => e.RoleId)
            .IsRequired()
            .OnDelete(DeleteBehavior.Cascade);
    
  3. Then you can access user roles easily by:

    user.Roles.Select(r => r.Name).ToList()
    

    to get all roles for user.

No migration is needed.

Lyte answered 5/4, 2022 at 9:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.