Node.JS
Node.JS

Reputation: 1592

.NET Core identity multiple User types

I have multiple classes (A, B and C) each extends IdentityUser<Guid>. I also have a class called UserRole which extends IdentityRole<Guid>.

The following is my DbContext:

public sealed class EntityDbContext: DbContext
{
    public DbSet<A> As { get; set; }

    public DbSet<B> Bs { get; set; }

    public DbSet<C> Cs { get; set; }

}

I added identities to IServiceCollection:

        services
            .AddIdentityCore<A>()
            .AddEntityFrameworkStores<EntityDbContext>()
            .AddRoles<UserRole>()
            .AddUserStore<AUserStore>()
            // .AddRoleStore<TRoleStore>()
            .AddDefaultTokenProviders();

        // Same for B, C

I also have the following stores:

public class AUserStore : UserStore<A, UserRole, EntityDbContext, Guid> { } 
public class BUserStore : UserStore<B, UserRole, EntityDbContext, Guid> { } 
public class CUserStore : UserStore<C, UserRole, EntityDbContext, Guid> { } 

The following is the error I'm getting:

Specified argument was out of the range of valid values. (Parameter 'instance 'AUserStore' with ReturnType AUserStore cannot be cast to IUserStore')

I don't know if what I'm doing is possible or not. Thanks for any help or hint.


Update

I think I got it working:

class GenericUserRoleStore : RoleStore<UserRole, EntityDbContext, Guid> { }

        services.AddIdentity<A, UserRole>()
            .AddDefaultTokenProviders()
            .AddUserStore<AUserStore>()
            .AddRoleStore<GenericUserRoleStore>();

        services.AddIdentityCore<B>()
            .AddRoles<UserRole>()
            .AddDefaultTokenProviders()
            .AddUserStore<BUserStore>()
            .AddRoleStore<GenericUserRoleStore>();

        services.AddIdentityCore<C>()
            .AddRoles<UserRole>()
            .AddDefaultTokenProviders()
            .AddUserStore<CUserStore>()
            .AddRoleStore<GenericUserRoleStore>();

Upvotes: 4

Views: 5594

Answers (1)

Dongdong
Dongdong

Reputation: 2508

Both comments on AddIdentity and AddIdentityCore have this:

Adds and configures the identity system for the specified User and Role types.

and,

  1. Compare source code for AddIdentity<> and AddIdentityCore<>,
  2. Review the default code from project template:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("DefaultConnection")));
        services.AddIdentity<ApplicationUser, ApplicationRole>(options => options.SignIn.RequireConfirmedAccount = true)
            .AddEntityFrameworkStores<ApplicationDbContext>();
        ....
    }
    

I would say: IdentityFramework got confused when you register multiple identity types to it, but we do need it.

I believe what you are looking for are these posts:

  1. Inheritance with EF Code First: Part 1 – Table per Hierarchy (TPH)
  2. Inheritance with EF Code First: Part 2 – Table per Type (TPT)
  3. Inheritance with EF Code First: Part 3 – Table per Concrete Type (TPC)

you have 3 above normal options to map your any UserType data to database. and the 1st options give you best performance, but give you very messive datatable when your usertypes are pretty complex. you would choose either of them for your real project as a balance.

Here is sample code with 1st approach:

public class ApplicationUser : IdentityUser<int>
{
    public ApplicationUser() : base()
    {
        UserRoles = new HashSet<ApplicationUserRole>();
    }

    public int YearsOfExperience { get; set; }

    [InverseProperty("User")]
    public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
}


public class ProjectManager : ApplicationUser
{
    public bool Talktive { get; set; }
}

public class Developer : ApplicationUser
{
    public bool IsCSharper { get; set; }
}

public class Tester : Developer
{
    public bool WhiteBox { get; set; }
}

public class Documenter : Tester
{
    public List<string> Languages { get; set; } = new List<string>();
}


public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, int>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    //get following users directly by following properties
    public DbSet<ProjectManager>  ProjectManagers { get; set; }
    public DbSet<Developer>  Developers { get; set; }
    public DbSet<Tester>  Testers { get; set; }
    public DbSet<Documenter>  Documenters { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        //prevent creating tables for following usertypes
        builder.Ignore<ProjectManager>();
        builder.Ignore<Developer>();
        builder.Ignore<Tester>();
        builder.Ignore<Documenter>();


        base.OnModelCreating(builder);

        builder.Entity<ApplicationUser>(entity =>
        {
            entity.HasMany(u => u.UserRoles).WithOne(x => x.User).HasForeignKey(c => c.UserId).IsRequired().OnDelete(DeleteBehavior.Cascade);

            //tell database to use this column as Discriminator
            entity.HasDiscriminator<string>("UserType");
        });

        builder.Entity<ApplicationRole>(entity =>
        {
            entity.HasKey(x => x.Id);
        });

        builder.Entity<ApplicationUserRole>(entity =>
        {
            entity.HasKey(c => new { c.UserId, c.RoleId });
            entity.HasOne(x => x.Role).WithMany(x => x.UserRoles).HasForeignKey(x => x.RoleId).IsRequired().OnDelete(DeleteBehavior.Cascade);
            entity.HasOne(x => x.User).WithMany(x => x.UserRoles).HasForeignKey(x => x.UserId).IsRequired().OnDelete(DeleteBehavior.Cascade);
        });
    }
}

when you need your users:

var allUsers = await _dbContext.Users.ToListAsync();
var allProjectManagers = await _dbContext.ProjectManagers.ToListAsync();
var allDevelopers = await _dbContext.Developers.ToListAsync();
var allTesters = await _dbContext.Testers.ToListAsync();

The next thing you want to configure is UserManager, instead of IUserStore.

public class ApplicationUserManager<TUser, TRole>
    where TUser : ApplicationUser
    where TRole : ApplicationRole

{
    private readonly ApplicationDbContext _context;
    private readonly UserManager<TUser> _userManager;
    private readonly RoleManager<TRole> _roleManager;

    public ApplicationUserManager(ApplicationDbContext context,
        UserManager<TUser> userManager,
        RoleManager<TRole> roleManager)
    {
        _context = context;
        _userManager = userManager;
        _roleManager = roleManager;
    }
    //customize your own base logics here.

}

public class DeveloperUserManager : ApplicationUserManager<Developer, ApplicationRole>
{

}

public class DocumenterUserManager : ApplicationUserManager<Documenter, ApplicationRole>
{

}

Enjoy it.

Upvotes: 6

Related Questions