Search code examples
c#entity-frameworkasp.net-identity

ASP.NET Core - custom AspNetCore.Identity implementation not working


I'm building a completely custom AspNetCore.Identity Implementation because I want TKey to be System.Guid across the board. With respect, I have derived types for...

  • Role : IdentityRole<Guid, UserRole, RoleClaim>
  • RoleClaim : IdentityRoleClaim<Guid>
  • User : IdentityUser<Guid, UserClaim, UserRole, UserLogin>
  • UserClaim : IdentityUserClaim<Guid>
  • UserLogin : IdentityUserLogin<Guid>
  • UserRole : IdentityUserRole<Guid>
  • UserToken : IdentityUserToken<Guid>

  • ApplicationDbContext : IdentityDbContext<User, Role, Guid, UserClaim, UserRole, UserLogin, RoleClaim, UserToken>

  • ApplicationRoleManager : RoleManager<Role>
  • ApplicationRoleStore : RoleStore<Role, ApplicationDbContext, Guid, UserRole, RoleClaim>
  • ApplicationSignInManager : SignInManager<User>
  • ApplicationUserManager : UserManager<User>
  • **ApplicationUserStore** : UserStore<User, Role, ApplicationDbContext, Guid, UserClaim, UserRole, UserLogin, UserToken>

ApplicationUserStore is the problem child!

Implementation

namespace NewCo.Identity
{
    using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
    using System;

    public sealed class Role : IdentityRole<Guid, UserRole, RoleClaim>
    {
    }
}

namespace NewCo.Identity
{
    using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
    using System;

    public sealed class UserRole : IdentityUserRole<Guid>
    {
    }
}

namespace NewCo.Identity
{
    using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
    using System;

    public sealed class RoleClaim : IdentityRoleClaim<Guid>
    {
    }
}

// The problem is here...

namespace NewCo.Identity
{
    using Microsoft.AspNetCore.Identity;
    using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
    using System;
    using System.Security.Claims;

    public sealed class ApplicationUserStore : UserStore<User, Role, ApplicationDbContext, Guid, UserClaim, UserRole, UserLogin, UserToken>
    {
    }
}

Error

The type 'NewCo.Identity.Role' cannot be used as type parameter 'TRole' in the generic type or method 'UserStore'. There is no implicit reference conversion from 'NewCo.Identity.Role' to 'Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole>'.

As far as I can see, unless this is some (co/contra/in)variance issue, all the code checks out...what did I get wrong?


Solution

  • Your ApplicationUserStore needs the RoleClaim at the end too (don't forget to update the related NuGet packages, otherwise you can't use these new additions):

        ApplicationUserStore : UserStore<
                User, Role, ApplicationDbContext, 
                Guid, UserClaim, UserRole, 
                UserLogin, UserToken, RoleClaim>
    

    Plus your ApplicationRoleStore should provide how to create the RoleClaim,

    protected override RoleClaim CreateRoleClaim(Role role, Claim claim)
    {
        return new RoleClaim
        {
            RoleId = role.Id,
            ClaimType = claim.Type,
            ClaimValue = claim.Value
        };
    }
    

    And also the ApplicationUserStore should provide these mappings too:

    protected override UserClaim CreateUserClaim(User user, Claim claim)
    {
        var userClaim = new UserClaim { UserId = user.Id };
        userClaim.InitializeFromClaim(claim);
        return userClaim;
    }
    
    protected override UserLogin CreateUserLogin(User user, UserLoginInfo login)
    {
        return new UserLogin
        {
            UserId = user.Id,
            ProviderKey = login.ProviderKey,
            LoginProvider = login.LoginProvider,
            ProviderDisplayName = login.ProviderDisplayName
        };
    }
    
    protected override UserRole CreateUserRole(User user, Role role)
    {
        return new UserRole
        {
            UserId = user.Id,
            RoleId = role.Id
        };
    }
    
    protected override UserToken CreateUserToken(User user, string loginProvider, string name, string value)
    {
        return new UserToken
        {
            UserId = user.Id,
            LoginProvider = loginProvider,
            Name = name,
            Value = value
        };
    }
    

    Then redirect built-in services to your custom services:

    services.AddScoped<UserStore<User, Role, ApplicationDbContext, int, UserClaim, UserRole, UserLogin, UserToken, RoleClaim>, ApplicationUserStore>();
    services.AddScoped<UserManager<User>, ApplicationUserManager>();
    services.AddScoped<RoleManager<Role>, ApplicationRoleManager>();
    services.AddScoped<SignInManager<User>, ApplicationSignInManager>();
    services.AddScoped<RoleStore<Role, ApplicationDbContext, int, UserRole, RoleClaim>, ApplicationRoleStore>();
    services.AddScoped<IEmailSender, AuthMessageSender>();
    services.AddScoped<ISmsSender, AuthMessageSender>();
    

    now introduce your custom services:

    services.AddIdentity<User, Role>(identityOptions =>
                {
                 // ...
                }).AddUserStore<ApplicationUserStore>()
                  .AddUserManager<ApplicationUserManager>()
                  .AddRoleStore<ApplicationRoleStore>()
                  .AddRoleManager<ApplicationRoleManager>()
                  .AddSignInManager<ApplicationSignInManager>()
                  // You **cannot** use .AddEntityFrameworkStores() when you customize everything
                  //.AddEntityFrameworkStores<ApplicationDbContext, int>()
                  .AddDefaultTokenProviders();