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 type 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 in 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.AbstractionsStackTrace:
at Microsoft.AspNetCore.Authentication.AuthenticationOptions.AddScheme(String name, Action1 configureBuilder) at Microsoft.AspNetCore.Authentication.AuthenticationBuilder.<>c__DisplayClass4_0
2.b__0(AuthenticationOptions o)
at Microsoft.Extensions.Options.ConfigureNamedOptions1.Configure(String name, TOptions options) at Microsoft.Extensions.Options.OptionsFactory
1.Create(String name) at Microsoft.Extensions.Options.UnnamedOptionsManager1.get_Value() at Microsoft.AspNetCore.Authentication.AuthenticationSchemeProvider..ctor(IOptions
1 options, IDictionary2 schemes) at Microsoft.AspNetCore.Authentication.AuthenticationSchemeProvider..ctor(IOptions
1 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.CallSiteVisitor
2.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.ConcurrentDictionary
2.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
in 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?
Edit:
I managed to make it work by commenting out the app.UseAuthorization()
method in Program.cs
. However that's not a solution, since I need authentication and authorization.
Here is a sample with the sample program on GitHub, if you run it you will run into the same problem.
OK. After a lot failure I finally managed to get it working. If you're having the same issue, this is how I managed to do it:
Simply replace AddIdentity
with AddIdentityCore
. From what I was able to gather reading the source code they are quite similar, but AddIdentity
by default register some authentication schemes, while AddIdentityCore
doesn't. Mainly, AddIdentity
already calls AddAuthentication()
for you. So when you have three calls to AddIdentity
you are calling AddAuthentication
three times. So while AddIdentity
can be useful most of the times, in scenarios like this you need to use AddIdentityCore
to avoid creating multiple auth schemes with the same name.
services.AddDbContext<XenDbContext>(options => options.UseSqlServer(connectionString));
services.AddIdentityCore<Account>(options =>
{
options.Password.RequireDigit = false;
})
.AddRoles<IdentityRole<Guid>>()
.AddEntityFrameworkStores<XenDbContext>()
.AddDefaultTokenProviders();
services.AddIdentityCore<Customer>(options =>
{
options.Password.RequireDigit = false;
})
.AddRoles<IdentityRole<Guid>>()
.AddEntityFrameworkStores<XenDbContext>()
.AddDefaultTokenProviders();
services.AddIdentityCore<Professional>(options =>
{
options.Password.RequireDigit = false;
})
.AddRoles<IdentityRole<Guid>>()
.AddEntityFrameworkStores<XenDbContext>()
.AddDefaultTokenProviders();