Accessing user claims via a navigation property in EF Core

Accessing user claims via a navigation property in EF Core

I am working on an app that uses ASP.NET Core Identity, and relies on claims for various things. I always find working with claims a little frustrating, as you seem to need to do such a lot of work to manage them.

I noticed that the DbContext class has a DbSet named UserClaims . Hey, you mean I can access these claims in my context? Yup.

The blunt, and quite frankly inefficient way to do this is something like the following. This code gets all users, and sets a non-mapped bool property on the Userto say if they are an admin or a parent...

  public async Task<List<UserDto>> GetUsers() {
    foreach (var item in await _context.Users
               .Select(u => new {
                 User = u,
                 Claims = _context.UserClaims
                   .Where(c => c.UserId == u.Id)
                   .Select(c => c.ClaimType)
                   .ToList()
               })
               .ToListAsync()) {
      item.User.IsAdmin = item.Claims.Contains("IsAdmin");
      item.User.IsParent = item.Claims.Contains("IsParent");
    }
    return (await _context.Users
      .Select(u => new {
        User = u,
        Claims = _context.UserClaims
          .Where(c => c.UserId == u.Id)
          .Select(c => c.ClaimType)
          .ToList()
      })
      .ToListAsync());
  }

Now that's pretty ugly!

Turns out that you can simply this with just a few lines of code.

The User class needs a navigation property...

public virtual ICollection<IdentityUserClaim<string>> Claims { get; set; } = [];

In your DbContext, you need to add a DbSet for the claims...

public virtual DbSet<IdentityUserClaim<string>> Claims { get; set; } = null!;

I named both of these Claims, rather than UserClaims, as the "User" part is irrelevant when mapping this as a navigation property on a User class. I don't intend to access the DbSet independently, so don't mind about changing the name there. If you really want to name it UserClaims, then you'll need to use the new on the DbSet line to supress a compiler warning.

You'll then need to add some code to OnModelCreating to do the mapping...

  protected override void OnModelCreating(ModelBuilder builder) {
    base.OnModelCreating(builder);
    builder.Entity<User>()
      .HasMany(u => u.Claims)
      .WithOne()
      .HasForeignKey(uc => uc.UserId)
      .IsRequired();
  }

That's all you need. In my case, I added some computed properties to the User class, so I could keep the queries a bit cleaner, but this is optional...

  [NotMapped]
  public bool IsAdmin => Claims.Any(c => c.ClaimType == ClaimsHelper.IsAdmin) == true;

  [NotMapped]
  public bool IsParent => Claims.Any(c => c.ClaimType == ClaimsHelper.IsParent) == true;

With that in place, the horrible query I showed at the top can now be replaced with just this...

  public async Task<List<User>> GetUsers() =>
    await context.Users
      .Include(u => u.Claims)
      .ToListAsync();

Now in truth it's a wee bit more than that, as in the production code I'm mapping to a DTO, and am using AutoMapper to sort out the claims for me. The profile includes the following...

  CreateMap<User, UserDto>()
    .ForMember(dto => dto.IsAdmin, opt => opt.MapFrom(u => u.IsAdmin))
    .ForMember(dto => dto.IsParent, opt => opt.MapFrom(u => u.IsParent));

However, even with those extra lines, it's way cleaner and more efficient than the monstrosity I started with.

The annoying thing is that I did actually discover this a few years ago, but forgot about it! Hopefully next time I want to do it, I will read my own blog and remember 😎.

What I don't understand is why Microsoft didn't include the claims as a navigation property on the IdentityUser class in the first place. It seems such an obvious thing to do, and would make life a whole lot easier.

Comments

No approved comments yet.

Leave a comment