Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clean Architecture and Asp.Net Core Identity

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

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;
                }
            }
        }
    }

EDIT #01

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;
        }
    }

like image 986
hlapointe Avatar asked Jan 24 '23 22:01

hlapointe


2 Answers

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"

Where to go from here

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.

Update: Issue and solution

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()));
}
like image 129
lauxjpn Avatar answered Jan 27 '23 10:01

lauxjpn


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);
        }
    }
}
like image 21
Francesc Castells Avatar answered Jan 27 '23 12:01

Francesc Castells