Node.JS
Node.JS

Reputation: 1592

Multiple user type Identity - DbContext design

I'm trying to use the Identity package of .NET Core with multiple classes that extend IdentityUser<Guid> but with a single UserRole class.

I have multiple classes that extend UserStore<T> for each user type and a single class that extends RoleStore<UserRole>.

The following is my startup.cs:

services.AddIdentity<InternalUser, UserRole>(IdentityOptions)
    .AddDefaultTokenProviders()
    .AddUserStore<InternalUserStore>()
    .AddRoleStore<GenericUserRoleStore>();

services.AddIdentityCore<Contractor>(IdentityOptions)
    .AddRoles<UserRole>()
    .AddDefaultTokenProviders()
    .AddUserStore<ContractorUserStore>()
    .AddRoleStore<GenericUserRoleStore>();

services.AddIdentityCore<Homeowner>(IdentityOptions)
    .AddRoles<UserRole>()
    .AddDefaultTokenProviders()
    .AddUserStore<HomeownerUserStore>()
    .AddRoleStore<GenericUserRoleStore>();

My DbContext is not extending IdentityDbContext:

public sealed class EntityDbContext: DbContext { }

I was getting multiple errors so I added the following to DbContext but I commented it out:

public DbSet<IdentityUserClaim<Guid>> UserClaims { get; set; }

public DbSet<IdentityUserRole<Guid>> UserRoles { get; set; }

I'm getting many different errors:

build Error on Instance 'Dal.IdentityStores.InternalUserStore' for PluginType IUserStore - and Instance 'RoleManager' for PluginType Microsoft.AspNetCore.Identity.RoleManager1[Models.Entities.Users.UserRole] - and Instance 'Dal.IdentityStores.GenericUserRoleStore' for PluginType Microsoft.AspNetCore.Identity.IRoleStore1[Models.Entities.Users.UserRole] - and Instance 'Dal.IdentityStores.GenericUserRoleStore' for PluginType Microsoft.AspNetCore.Identity.IRoleStore1[Models.Entities.Users.UserRole] - and Instance 'Dal.IdentityStores.ContractorUserStore' for PluginType Microsoft.AspNetCore.Identity.IUserStore1[Models.Entities.Contractors.Contractor] - and Instance 'UserClaimsPrincipalFactory' for PluginType Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory1[Models.Entities.Contractors.Contractor] - and Instance 'UserClaimsPrincipalFactory<Contractor, UserRole>' for PluginType Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory1[Models.Entities.Contractors.Contractor] - and Instance 'UserManager' for PluginType Microsoft.AspNetCore.Identity.UserManager1[Models.Entities.Homeowners.Homeowner] - and Instance 'UserClaimsPrincipalFactory<Homeowner>' for PluginType Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory1[Models.Entities.Homeowners.Homeowner]

This is the link to my repo

Upvotes: 8

Views: 2201

Answers (2)

Brian Ogden
Brian Ogden

Reputation: 19232

Based on the requirements the OP explained in the comments and that the OP said he/she does not have database design experience, I will attempt to provide an answer that hopefully helps make life easier for the OP's project and software development career:

First off, you want a single "Users" table:

|---------------------|------------------|------------------|
|      FirstName      |     LastName     |       Role       |
|---------------------|------------------|------------------|
|          Joe        |       Black      |     Contractor   |
|---------------------|------------------|------------------|

You have no requirements for multiple User tables for the project, nor would any project or software product require such a thing generally speaking. When/IF a User table scales to N number billions of records, than it would be sharded across partitions for performance, and/or denormalized for reads versus writes, but it would still be logically one table in many scaled complex software system designs.

You state:

How can I have different user metadata for different types of a user? A contractor has it's own properties, a homeowner has its own set of properties and etc. I don't want to have a giant table with all possible properties

A concern of yours is that you don't end up with a giant table. In short, this is not a concern for you. You don't have a giant table yet and if you added 30 columns ("properties" as you said) that would NOT be a giant table.

Create a single User table. Add nullable columns for the specific contractor properties and nullable columns for the specific homeowner properties. Make the columns Not Null when the property applies to all users.

A column that will apply to all users is the Role column for your Users, so you have a Not Nullable column called "Role"

Note, if you are using a relational database, you do not need to create a Role table that contains the Role row records. You can do this and your course instructor may expect it but you can also create a const strings class or enum in your backend that represents the possible Roles and that is a form of persistence that provides integrity for those roles. I mention this because this is an area where developers quickly go into referential integrity overkill with relational database design.

Upvotes: 1

Vano Maisuradze
Vano Maisuradze

Reputation: 5909

I reproduced your problem and below is a solution to it, but I would think again about creating multiple tables for different user roles.

Here are two main reasons against multiple user tables:

  1. When you want to find the user by id (Assuming you don't know the role), you will need to run multiple queries against different tables, which decreases performance and increases complexity in code.
  2. It may also increase database complexity, because you will need to set multiple foreign keys to other tables.

In case you still want to have multiple tables for different user roles, here is a little "hack". You just need to override OnModelCreating method and configure entities:

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

        builder.Entity<Contractor>(b =>
        {
            b.HasMany<IdentityUserRole<Guid>>().WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
        });

        builder.Entity<UserRole>(b =>
        {
            b.HasKey(r => r.Id);
            b.HasIndex(r => r.NormalizedName).HasName("RoleNameIndex").IsUnique();
            b.ToTable("AspNetRoles");
            b.Property(r => r.ConcurrencyStamp).IsConcurrencyToken();

            b.Property(u => u.Name).HasMaxLength(256);
            b.Property(u => u.NormalizedName).HasMaxLength(256);

            b.HasMany<IdentityUserRole<Guid>>().WithOne().HasForeignKey(ur => ur.RoleId).IsRequired();
            b.HasMany<IdentityRoleClaim<Guid>>().WithOne().HasForeignKey(rc => rc.RoleId).IsRequired();
        });

        builder.Entity<IdentityRoleClaim<Guid>>(b =>
        {
            b.HasKey(rc => rc.Id);
            b.ToTable("AspNetRoleClaims");
        });

        builder.Entity<IdentityUserRole<Guid>>(b =>
        {
            b.HasKey(r => new { r.UserId, r.RoleId });
            b.ToTable("AspNetUserRoles");
        });

        builder.Entity<UserRole>().ToTable("Roles");
        builder.Entity<IdentityUserRole<Guid>>().ToTable("UserRoles");
        builder.Entity<IdentityRoleClaim<Guid>>().ToTable("RoleClaims");
        builder.Entity<IdentityUserClaim<Guid>>().ToTable("UserClaims");
    }

After that, you should be able to login.

Upvotes: 5

Related Questions