How can I have multiple Identity user types when using ASP.NET Core Identity and EntityFrameworkCore?

Arthur D. Andrade 0 Reputation points
2023-04-29T02:14:45.36+00:00

I have two types of users within my application, Customers and Professionals. I'm using ASP.NET Identity to manage my users. Currently I have only a single user of typle ApplicationUser and use flags to differentiate between Customers and Professionals.

However this approach seems rather messy to me. So I'm trying to to store identity related information on the Identity Framework default table, Customer related information on a Customers table and Professional related information on a Professionals table.

These are my new user entities on my domain:

public class Account : IdentityUser<Guid>
{
    public string FullName { get; set; } = string.Empty;
    public string ImageUrl { get; set; } = string.Empty;
    public bool HasAcceptedTermsOfUse { get; set; } = default;
}

public class Customer : Account
{
    // Customer specific properties
}

public class Professional : Account
{
    // Professional specific properties
}

This is my modified DbContext to support both Customers and Professionals:

public class XenDbContext: IdentityDbContext<Account, IdentityRole<Guid>, Guid>
{
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Professional> Professionals { get; set; }
    // Other DbSets

    public XenDbContext(DbContextOptions<XenDbContext> options) 
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
        builder.Entity<Account>(entity => { entity.ToTable("Accounts"); });
        builder.Entity<Customer>(entity => { entity.ToTable("Customers"); });
        builder.Entity<Professional>(entity => { entity.ToTable("Professionals"); });
        builder.Entity<IdentityRole<Guid>>(entity => { entity.ToTable("Roles"); });
        builder.Entity<IdentityUserRole<Guid>>(entity => { entity.ToTable("AccountRoles"); });
        builder.Entity<IdentityUserClaim<Guid>>(entity => { entity.ToTable("AccountClaims"); });
        builder.Entity<IdentityUserLogin<Guid>>(entity => { entity.ToTable("AccountLogins"); });
        builder.Entity<IdentityUserToken<Guid>>(entity => { entity.ToTable("AccountTokens"); });
        builder.Entity<IdentityRoleClaim<Guid>>(entity => { entity.ToTable("RoleClaims"); });
    }
}

I'm able to run Add-Migration and Update-Database with this structure. However when I run my API I get this error:

System.InvalidOperationException HResult=0x80131509 Message=Scheme already exists: Identity.Application Source=Microsoft.AspNetCore.Authentication.Abstractions StackTrace: at Microsoft.AspNetCore.Authentication.AuthenticationOptions.AddScheme(String name, Action1 configureBuilder) at Microsoft.AspNetCore.Authentication.AuthenticationBuilder.<>c__DisplayClass4_02.b__0(AuthenticationOptions o) at Microsoft.Extensions.Options.ConfigureNamedOptions1.Configure(String name, TOptions options) at Microsoft.Extensions.Options.OptionsFactory1.Create(String name) at Microsoft.Extensions.Options.UnnamedOptionsManager1.get_Value() at Microsoft.AspNetCore.Authentication.AuthenticationSchemeProvider..ctor(IOptions1 options, IDictionary2 schemes) at Microsoft.AspNetCore.Authentication.AuthenticationSchemeProvider..ctor(IOptions1 options) at System.RuntimeMethodHandle.InvokeMethod(Object target, Span1& arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) at System.Reflection.RuntimeConstructorInfo.Invoke(BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(ServiceCallSite callSite, TArgument argument) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope) at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(Type serviceType) at System.Collections.Concurrent.ConcurrentDictionary2.GetOrAdd(TKey key, Func`2 valueFactory) at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope) at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType) at Microsoft.Extensions.Internal.ActivatorUtilities.ConstructorMatcher.CreateInstance(IServiceProvider provider) at Microsoft.Extensions.Internal.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters) at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.<>c__DisplayClass5_0.b__0(RequestDelegate next) at Microsoft.AspNetCore.Builder.ApplicationBuilder.Build() at Microsoft.AspNetCore.Builder.WebApplicationBuilder.b__27_0(RequestDelegate next) at Microsoft.AspNetCore.Builder.ApplicationBuilder.Build() at Microsoft.AspNetCore.Hosting.GenericWebHostService.d__37.MoveNext() at Microsoft.Extensions.Hosting.Internal.Host.d__12.MoveNext() at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.d__4.MoveNext() at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.d__4.MoveNext() at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host) at Microsoft.AspNetCore.Builder.WebApplication.Run(String url) at Xen.Presentation.API.Program.Main(String[] args) in C:\Users\Arthur\Development\Xen\src\backend\Presentation\Xen.Presentation.API\Program.cs:line 53

I don't understand a lot about Identity Framework but it seems like the problem arises when I have multiple AddIdentity calls.

This is my PersistenceServiceRegistration on my persistence layer:

public static class PersistenceServiceRegistration
{
    public static void AddPersistenceServices(this IServiceCollection services, IConfiguration configuration)
    {
        var connectionString = configuration.GetConnectionString("XenDbLocalConnectionString");

        
        services.AddDbContext<XenDbContext>(options => options.UseSqlServer(connectionString));

        services.AddIdentity<Account, IdentityRole<Guid>>(options =>
        {
            options.Password.RequireDigit = false;
        }).AddEntityFrameworkStores<XenDbContext>().AddDefaultTokenProviders();

        services.AddIdentity<Customer, IdentityRole<Guid>>(options =>
        {
            options.Password.RequireDigit = false;
        }).AddEntityFrameworkStores<XenDbContext>().AddDefaultTokenProviders();

        services.AddIdentity<Professional, IdentityRole<Guid>>(options =>
        {
            options.Password.RequireDigit = false;
        }).AddEntityFrameworkStores<XenDbContext>().AddDefaultTokenProviders();


        // Register repositories
    }

}

In addition to Professionals and Customers I have AddIdentity for Account because I want to be able to generate tokens for both Customers and Professionals via Account. This is my full PersistenceServiceRegistration.cs with the code I removed for brevity.

This is the Program.cs on my API:

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        builder.Services.AddApplicationServices();

        builder.Services.AddPersistenceServices(builder.Configuration);

        builder.Services.AddInfrastructureServices(builder.Configuration);

        builder.Services.AddHttpContextAccessor();

        builder.Services.AddControllers().AddJsonOptions(options =>
        {
            options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
        });

        builder.Services.AddCors(options =>
        {
            options.AddPolicy("Open", builder => builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
        });

        builder.Services.AddEndpointsApiExplorer();

        builder.Services.AddSwaggerGen();

        var app = builder.Build();

     
        app.UseSwagger();

        app.UseSwaggerUI();

        app.UseHttpsRedirection();

        app.UseAuthentication();

        app.UseAuthorization();

        app.UseMiddleware<ExceptionHandlerMiddleware>();

        app.MapControllers();

        app.Run();
    }
}

One of the references I've used to implement this was this post. I have also tried this approach to work around the issue. I also gave this a shot, but I'm not sure I understand it.

Is it possible to do what I'm trying? And if so, how can I achieve it?

Entity Framework Core
Entity Framework Core
A lightweight, extensible, open-source, and cross-platform version of the Entity Framework data access technology.
732 questions
ASP.NET Core
ASP.NET Core
A set of technologies in the .NET Framework for building web applications and XML web services.
4,517 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
10,844 questions
ASP.NET API
ASP.NET API
ASP.NET: A set of technologies in the .NET Framework for building web applications and XML web services.API: A software intermediary that allows two applications to interact with each other.
331 questions
{count} votes

1 answer

Sort by: Most helpful
  1. Zhi Lv - MSFT 32,146 Reputation points Microsoft Vendor
    2023-05-01T06:56:29.19+00:00

    Hi @Arthur D. Andrade

    I don't understand a lot about Identity Framework but it seems like the problem arises when I have multiple AddIdentity calls.

    Yes, you are right, the issue relates the multiple AddIdentity(). You cannot repeatedly use AddIdentity to add an identity. ASP.NET Core provides a built-in method: AddIdentityCore<TUser>. Code like this:

    builder.Services.AddDefaultIdentity<Account>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    
    builder.Services.AddIdentityCore<Customer>().AddEntityFrameworkStores<ApplicationDbContext>();
    builder.Services.AddIdentityCore<Professional>().AddEntityFrameworkStores<ApplicationDbContext>();
    

    The detail sample code as below:

    Model:

        public class Account : IdentityUser
        {
            public string FullName { get; set; } = string.Empty;
            public string ImageUrl { get; set; } = string.Empty;
            public bool HasAcceptedTermsOfUse { get; set; } = default;
        }
    
        public class Customer : Account
        {
            // Customer specific properties
    
            public string CustomInfo { get; set; }
        }
    
        public class Professional : Account
        {
            // Professional specific properties
            public string PerfessionalInfo { get; set; }
        }
    

    DbContext:

        public class ApplicationDbContext : IdentityDbContext
        {
            public DbSet<Customer> Customers { get; set; }
            public DbSet<Professional> Professionals { get; set; }
            public DbSet<Account> Accounts { get; set; }
    
            protected override void OnModelCreating(ModelBuilder builder)
            {
                base.OnModelCreating(builder); 
                builder.Entity<Account>(entity => { entity.ToTable("Accounts"); });
                builder.Entity<Customer>(entity => { entity.ToTable("Customers"); });
                builder.Entity<Professional>(entity => { entity.ToTable("Professionals"); });
            }
            public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
                : base(options)
            { 
            }
        }
    

    Program.cs:

    // Add services to the container.
    var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
    builder.Services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(connectionString));
    builder.Services.AddDatabaseDeveloperPageExceptionFilter();
    
    builder.Services.AddDefaultIdentity<Account>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    
    builder.Services.AddIdentityCore<Customer>().AddEntityFrameworkStores<ApplicationDbContext>();
    builder.Services.AddIdentityCore<Professional>().AddEntityFrameworkStores<ApplicationDbContext>();
    
    

    Then in the controller, use the following code to use the Identity:

        public class HomeController : Controller
        {
            private readonly ILogger<HomeController> _logger;
    
            private readonly UserManager<Customer> _customerManaer;
            private readonly UserManager<Professional> _promanager;
    
            public HomeController(ILogger<HomeController> logger, UserManager<Customer> customerusermanager, UserManager<Professional> promanager)
            {
                _logger = logger;
                _customerManaer = customerusermanager;
                _promanager=promanager;
            }
    
            public async Task<IActionResult> Index()
            {
    
                var cusomer = new Customer() { Email="cutomer1@hotmail.com", UserName="Tom", FullName="Ton L", CustomInfo="customer information"};
    
                var result = await _customerManaer.CreateAsync(cusomer, "Password01!");
    
                var professional = new Professional() { Email="pro1@hotmail.com", UserName="Jack", FullName="Jack.D", PerfessionalInfo="hello" };
    
                var result2 = await _promanager.CreateAsync(professional, "Password01!");
                return View();
            }
    

    The result as below:

    User's image


    If the answer is the right solution, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    Best regards,

    Dillion

    4 people found this answer helpful.
    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.