Register AspNetCore 2.1 Identity system with DbContext interface

Viewed 2864

I am working on an AspNetCore 2.1 WebApi. I am using Microsoft.Extensions.DependencyInjection, AspNetCore.Identity, and EntityFrameworkCore.

In my DI registrations, I am calling:

services.AddDbContext<IMyDbContext, MyDbContext>();

Good, right? Pass around the contract. But then I hit an exception that the Identity Manager classes (UserManager, RoleManager, etc.) cannot be respolved in the Dependency Injection container because the Identity Stores they use (UserStore, RoleStore, etc.) cannot resolve their DI parameter (MyDbContext).

It all points to this line when setting up Identity:

builder.AddEntityFrameworkStores<MyDbContext>();

... which is puking since the Identity Stores are looking for the concrete context and it is not registered with DI. The extension is expecting a class that it can resolve down to DbContext - and I cannot add an implicit operator to the interface IMyDbContext to give it an implicit cast the DI extension method could use.

All this has me performing a rather ugly DI registration:

services
        .AddDbContext<IMyDbContext, MyDbContext>()
        .AddDbContext<MyDbContext>();
  • a DI registration that may cause other issues (thread safety, transactional) downstream that I haven't encountered yet.

My guess is that I could clean up the DI registration by rolling my own IServiceCollection extension and/or custom Identity Stores - which really seems like overkill here as I otherwise have no need to go beyond the default built-in Identity Stores.

I also don't really want to remove the <interface, concrete> context registration as that will trickle down to all my constructor injections, and just seems wrong.

Has anybody tackled this already and found a work-around? Or can anybody at least confirm/deny that the double (interface && concrete) context registration in the DI will not cause other issues?

Thanks in advance!

2 Answers

By default, when you call AddDbContext, you register a scoped DbContext instance. This means that anywhere within the handling of a single request, requesting said DbContext via DI will give you the same instance. With the double registration you have, the DI system will give you a different instance depending upon whether you ask for an IMyDbContext or a MyDbContext.

To instruct the DI system to give you the same instance for both types, use the following approach:

services.AddDbContext<MyDbContext>();
services.AddScoped<IMyDbContext>(sp => sp.GetRequiredService<MyDbContext>());

The first call registers MyDbContext and the second simply forwards requests for IMyDbContext to that same scoped MyDbContext instance.

Your Identity based DB context should look like this:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace MyNamespaseForIdentityDatabase
{
    public class MyDbContext : IdentityDbContext<User, Role, int, UserClaim, UserRole, UserLogin, RoleClaim, UserToken>
    {
        public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
        {
        }

        // DB sets for Identity entities like Users, Roles, etc. are defined in base class so you can add here your custom sets like: 
        // public virtual DbSet<Company> Companies { get; set; }

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

            ConfigureUserTable(builder);
            ConfigureRoleClaimTable(builder);
            ConfigureUserRoleTable(builder);
            ConfigureUserLoginTable(builder);
            ConfigureUserClaimTable(builder);
            ConfigureUserTokenTable(builder);

            // ConfigureCompaniesTable(builder);
        }

        private static void ConfigureUserTable(ModelBuilder builder)
        {
            builder.Entity<User>(
                entity =>
                {
                    entity.ToTable("Users");
                    entity.HasMany(user => user.UserRoles)
                        .WithOne(userRole => userRole.User)
                        .HasForeignKey(userRole => userRole.UserId)
                        .IsRequired()
                        .OnDelete(DeleteBehavior.Cascade);

                    entity.HasMany(user => user.Claims)
                        .WithOne()
                        .HasForeignKey(userClaim => userClaim.UserId)
                        .IsRequired()
                        .OnDelete(DeleteBehavior.Cascade);

                    entity.Property(e => e.FirstName)
                        .IsRequired();

                    entity.Property(e => e.LastName)
                        .IsRequired();
                });
        }

        private static void ConfigureRoleClaimTable(ModelBuilder builder)
        {
            builder.Entity<RoleClaim>(
                entity =>
                {
                    entity.HasKey(roleClaim => roleClaim.Id);
                    entity.ToTable("RoleClaims");
                });
        }

        private static void ConfigureUserRoleTable(ModelBuilder builder)
        {
            builder.Entity<UserRole>(
                userRole =>
                {
                    userRole.ToTable("UserRoles");
                    userRole.HasKey(
                        r => new
                        {
                            r.UserId,
                            r.RoleId
                        });
                });
        }

        private static void ConfigureUserLoginTable(ModelBuilder builder)
        {
            builder.Entity<UserLogin>().ToTable("UserLogins");
        }

        private static void ConfigureUserClaimTable(ModelBuilder builder)
        {
            builder.Entity<UserClaim>().ToTable("UserClaims");
        }

        private static void ConfigureUserTokenTable(ModelBuilder builder)
        {
            builder.Entity<UserToken>().ToTable("UserTokens");
        }
    }

    public class User : IdentityUser<int>
    {
        // Some additional custom properties for th euser
        [NotMapped] public string FullName => $"{FirstName} {LastName}";
        public string FirstName { get; set; }
        public string LastName { get; set; }
        // Some additional collections for related stuff to include by queries like dbContext.Users.Include(user=>user.UserRoles).ToList()
        public virtual ICollection<UserRole> UserRoles { get; set; }
        public virtual ICollection<UserClaim> Claims { get; } = new List<UserClaim>();
    }

    public class Role : IdentityRole<int>
    {
        public Role()
        {
            // Default constructor is used by the framework
        }

        public Role(string roleName)
            : base(roleName)
        {
        }

        // Custom property in addition to Identity base ones for the role
        public string Description { get; set; }

        public virtual ICollection<RoleClaim> Claims { get; } = new List<RoleClaim>();
    }

    public class RoleClaim : IdentityRoleClaim<int>
    {
    }

    public class UserClaim : IdentityUserClaim<int>
    {
    }

    public class UserLogin : IdentityUserLogin<int>
    {
    }

    public class UserRole : IdentityUserRole<int>
    {
        public virtual User User { get; set; }
        public virtual Role Role { get; set; }
    }

    public class UserToken : IdentityUserToken<int>
    {
    }

    public class UserClaimsFactory : UserClaimsPrincipalFactory<User, Role>
    {
        public UserClaimsFactory(
            UserManager<User> userManager,
            RoleManager<Role> roleManager,
            IOptions<IdentityOptions> optionsAccessor)
            : base(userManager, roleManager, optionsAccessor)
        {
        }

        protected override async Task<ClaimsIdentity> GenerateClaimsAsync(User user)
        {
            var userId = user.Id;
            user = await UserManager.Users.SingleAsync(u => u.Id == userId);

            // Add role claims
            var identity = await base.GenerateClaimsAsync(user);

            // Add custom claims for application user properties we want to store in claims (in cookies) which allows to get common values on UI without DB hit)
            identity.AddClaim(new Claim(ClaimTypes.GivenName, user.FirstName ?? ""));
            identity.AddClaim(new Claim(ClaimTypes.Surname, user.LastName ?? ""));
            identity.AddClaim(new Claim(ClaimTypes.Email, user.Email ?? ""));

            return identity;
        }
    }
}

In your Startup class use something like that:

public void ConfigureServices(IServiceCollection services)
{
    // ... 
    services.AddDbContext<MyDbContext>(options =>
    {
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
    });
    services.AddIdentity<User, Role>()
        .AddEntityFrameworkStores<MyDbContext>()
        .AddDefaultTokenProviders();
    // ...
    services.Configure<IdentityOptions>(options =>
    {
        // Configure your password, lockout, user settings here via `options`
    });
    // ...
    services.AddScoped<IUserClaimsPrincipalFactory<User>, UserClaimsFactory>();
    // ...
    services.AddMvc(options =>{ /* ... */ });
}
Related