I'm trying to abstract Asp.Net Core Identity from my Application in order to respect a Clean Architecture.
Currently, my project is divided into 4 projects : WebApi, Infrastructure, Application and Core. I want all configuration of Asp.Net EF Core and Asp.Net Core Identity to be encapsulate into the Infrastructure project. Both services will be exposed to the WebApi project by some interfaces defined into the Application Project (e.g. IApplicationDbcontext
, IUserService
, ICurrentUserService
).
Unfortunetely, I'm unable to create a migration with the package manager command : Add-Migration -Project src\Infrastructure -StartupProject src\WebApi -OutputDir Persistence\Migrations "SmartCollaborationDb_V1"
.
Error : Unable to create an object of type 'ApplicationDbContext'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728
.
Can you help me?
Solution Structure
src\WebApi\Startup.cs
public class Startup {
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration) {
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services) {
services.AddApplication(Configuration);
services.AddInfrastructure(Configuration);
services.AddHttpContextAccessor();
...
services.AddScoped<ICurrentUserService, CurrentUserService>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
...
}
}
src\Infrastructure\DependencyInjection.cs
public static class DependencyInjection {
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration config) {
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
config.GetConnectionString("DefaultConnection"),
context => context.MigrationsAssembly(Assembly.GetExecutingAssembly().FullName)));
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddScoped<IApplicationDbContext>(provider => provider.GetService<ApplicationDbContext>());
services.AddTransient<IDateTimeService, DateTimeService>();
services.AddTransient<IUserService, UserService>();
return services;
}
}
src\Infrastructure\Persistence\ApplicationDbContext.cs
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, Guid>, IApplicationDbContext {
private readonly ICurrentUserService currentUserService;
private readonly IDateTimeService dateTimeService;
public DbSet<Student> Students { get; set; }
public DbSet<Group> Groups { get; set; }
public DbSet<Course> Courses { get; set; }
public ApplicationDbContext(
DbContextOptions options,
ICurrentUserService currentUserService,
IDateTimeService dateTimeService) :
base(options) {
this.currentUserService = currentUserService;
this.dateTimeService = dateTimeService;
}
protected override void OnModelCreating(ModelBuilder builder) {
builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
base.OnModelCreating(builder);
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) {
UpdateAuditableEntities();
return base.SaveChangesAsync(cancellationToken);
}
private void UpdateAuditableEntities() {
foreach (var entry in ChangeTracker.Entries<AuditableEntity>()) {
switch (entry.State) {
case EntityState.Added:
entry.Entity.CreatedBy = currentUserService.UserId.ToString();
entry.Entity.Created = dateTimeService.Now;
break;
case EntityState.Modified:
entry.Entity.LastModifiedBy = currentUserService.UserId.ToString();
entry.Entity.LastModified = dateTimeService.Now;
break;
}
}
}
}
src\WebApi\Services\CurrentUserService.cs
public class CurrentUserService : ICurrentUserService {
public Guid UserId { get; }
public bool IsAuthenticated { get; }
public CurrentUserService(IHttpContextAccessor httpContextAccessor) {
var claim = httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
IsAuthenticated = claim != null;
UserId = IsAuthenticated ? Guid.Parse(claim) : Guid.Empty;
}
}
Your code should (and does) generally work without issue and without the need for an IDesignTimeDbContextFactory<DbContext>
derived class.
I uploaded a minimal project to GitHub, that mimics your design, and works without issue with the following package manager console command, for creating a migration:
Add-Migration -Project "Infrastructure" -StartupProject "WebApi" -OutputDir Persistence\Migrations "Initial"
First, take a look at Design-time DbContext Creation, to understand how EF Core is looking for your DbContext
derived class.
Then put Debugger.Launch()
(and Debugger.Break()
) instructions in your code, to trigger the JIT debugger when executing the Add-Migration
command.
Finally, step through your code. Ensure, that your DependencyInjection.AddInfrastructure()
, ApplicationDbContext.ApplicationDbContext()
, ApplicationDbContext.OnModelCreating()
etc. methods are getting called as expected.
You might also want to let your IDE break on any raised exception while debugging.
Your issue is likely related to something entirely unrelated to EF Core, that goes wrong before the context can be instantiated. It does not seem to be the CurrentUserService
constructor, but it could be the the constructor of the IDateTimeService
implementing class or something else that runs during the initialization process.
You should be able to find out when stepping throw the code.
As expected, the issue is unrelated to EF Core. The AddFluentValidation()
method throws the following exception:
System.NotSupportedException: The invoked member is not supported in a dynamic assembly.
at at System.Reflection.Emit.InternalAssemblyBuilder.GetExportedTypes()
at FluentValidation.AssemblyScanner.FindValidatorsInAssembly(Assembly assembly) in /home/jskinner/code/FluentValidation/src/FluentValidation/AssemblyScanner.cs:49
at FluentValidation.ServiceCollectionExtensions.AddValidatorsFromAssembly(IServiceCollection services, Assembly assembly, ServiceLifetime lifetime) in /home/jskinner/code/FluentValidation/src/FluentValidation.DependencyInjectionExtensions/ServiceCollectionExtensions.cs:48
at FluentValidation.ServiceCollectionExtensions.AddValidatorsFromAssemblies(IServiceCollection services, IEnumerable`1 assemblies, ServiceLifetime lifetime) in /home/jskinner/code/FluentValidation/src/FluentValidation.DependencyInjectionExtensions/ServiceCollectionExtensions.cs:35
at FluentValidation.AspNetCore.FluentValidationMvcExtensions.AddFluentValidation(IMvcBuilder mvcBuilder, Action`1 configurationExpression) in /home/jskinner/code/FluentValidation/src/FluentValidation.AspNetCore/FluentValidationMvcExtensions.cs:72
at WebApi.Startup.ConfigureServices(IServiceCollection services) in E:\Sources\SmartCollaboration\WebApi\Startup.cs:52
at at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
at at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at at Microsoft.AspNetCore.Hosting.ConfigureServicesBuilder.InvokeCore(Object instance, IServiceCollection services)
at at Microsoft.AspNetCore.Hosting.ConfigureServicesBuilder.<>c__DisplayClass9_0.<Invoke>g__Startup|0(IServiceCollection serviceCollection)
at at Microsoft.AspNetCore.Hosting.ConfigureServicesBuilder.Invoke(Object instance, IServiceCollection services)
at at Microsoft.AspNetCore.Hosting.ConfigureServicesBuilder.<>c__DisplayClass8_0.<Build>b__0(IServiceCollection services)
at at Microsoft.AspNetCore.Hosting.GenericWebHostBuilder.UseStartup(Type startupType, HostBuilderContext context, IServiceCollection services)
at at Microsoft.AspNetCore.Hosting.GenericWebHostBuilder.<>c__DisplayClass12_0.<UseStartup>b__0(HostBuilderContext context, IServiceCollection services)
at at Microsoft.Extensions.Hosting.HostBuilder.CreateServiceProvider()
at at Microsoft.Extensions.Hosting.HostBuilder.Build()
One way of dealing with this is to just detect, whether the code is being called from the EF Core Tools or not and to setup only the necessary services, if that is the case:
public void ConfigureServices(IServiceCollection services)
{
Debugger.Launch(); // <-- Remove this after debugging!
services.AddApplication(Configuration);
services.AddInfrastructure(Configuration);
services.AddScoped<ICurrentUserService, CurrentUserService>();
if (new StackTrace()
.GetFrames()
.Any(f => f?.GetMethod()?.DeclaringType?.Namespace == "Microsoft.EntityFrameworkCore.Tools"))
{
// Called by EF Core design-time tools.
// No need to initialize further.
return;
}
services.AddSwaggerGen(options => {
options.SwaggerDoc("v1", new OpenApiInfo {
Version = "v1",
Title = "SmartCollaboration API"
});
options.AddFluentValidationRules();
});
services.AddHttpContextAccessor();
services.AddControllers().AddFluentValidation(options =>
options.RegisterValidatorsFromAssemblies(AppDomain.CurrentDomain.GetAssemblies()));
}
From the information provided, I would say that the problem is that some of the dependencies required in the DbContext's constructor cannot be created by the service provider (eg ICurrentUserService
is not registered in the code you provided or maybe IDateTimeService
has a dependency that is not registered).
Either make sure that all dependencies of the DbContext can be created or create a design-time factory. Sample from the linked Microsoft's documentation:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace MyProject
{
public class BloggingContextFactory : IDesignTimeDbContextFactory<BloggingContext>
{
public BloggingContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<BloggingContext>();
optionsBuilder.UseSqlite("Data Source=blog.db");
return new BloggingContext(optionsBuilder.Options);
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With