JonPSmith / EfCore.GenericServices

A library to help you quickly code CRUD accesses for a web/mobile/desktop application using EF Core.
https://www.thereformedprogrammer.net/genericservices-a-library-to-provide-crud-front-end-services-from-a-ef-core-database/
MIT License
601 stars 94 forks source link

Question: Why is the DbContext initialized during registration #50

Closed chrisbbe closed 4 years ago

chrisbbe commented 4 years ago

Hi,

Why is the DbContext initialized during configuration of the service? This makes it hard to use GenericServices for DbContexts which overrides the void OnConfigure(DbContextBuilder optionsBuilder) method in situations where the DbContext is configured dynamically, ex. where the connection string is determined based on the UserClaims during an request.

Thanks

JonPSmith commented 4 years ago

Hi @chrisbbe,

EfCore.GenericServices is designed to work with Dependency Injection (DI). Therefore the DbContext provided by the DI. That's why I have extension methods such as ConfigureGenericServicesEntities to register your DTOs and the CrudServices and CrudServicesAsync services. ASP.NET Core is built around DI.

You have two options.

  1. Pass the claims to the DbContext via DI. This is the recommended way of doing things in ASP.NET Core or when using DI. I have an example of passing in claims to the DbContext via its constructor, see this article. That sets up some query filters, but the idea is the same (note: EF Core 5 has a new feature to help you change connection strings - see this link).
  2. Don't use DI. You can register your DTOs and manually create an instance of the CrudServices and CrudServicesAsync classes. That's what I do for unit testing - see this link on how I do that with unit tests. The same approach would work in a non-DI system (I know one person who did that).
chrisbbe commented 4 years ago

Thanks for the feedback.

All of this is clear to me. The reason I ask is that registering my DbContext with the GenericServices causes the DbContext constructor and OnConfigure(...) method to be executed during startup (before any user requests are hitting ASP.Net Core). While removing the GenericServices DI registration does not cause the DbContext to be executed. The issue here is that the ITenantProvider injected into the DbContext is used to determine the connection string based on the authenticated user, leading to an empty connection string, which makes EF Core throw an exception.

Is the DbContext expected to be executed during initialization?

Code example:

public class TenantDbContext : DbContext
{
    private readonly Tenant _tenant;

    public TenantDbContext(DbContextOptions<TenantDbContext> options, ITenantProvider tenantProvider) : base(options)
    {
        _tenant = tenantProvider.GetTenant();
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
        optionsBuilder.UseNpgsql(_tenant.DatabaseConnectionString);
            base.OnConfiguring(optionsBuilder);
    }
}

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<TenantDbContext>();
        services.GenericServicesSimpleSetup<TenantDbContext>(Assembly.GetAssembly(typeof(TenantDto)));
    }
}
JonPSmith commented 4 years ago

hi @chrisbbe,

Yes, GenericServices has to scan the entities in the DbContext to link the DTOs to the entities. There is no way around that. But GenericServices doesn't query the database.

Not sure there is a solution - maybe a default, empty database?? Or maybe a valid database connection but no database on the other end? As I said, there is a the new EF Core 5 feature about changing the connection that might help.

JonPSmith commented 4 years ago

PS. I needed to research the new EF Core 5 feature about changing the connection string for chapter 11 in my book Entity Framework Core in Action, second edition. There is no documentation but a looking at the commit related to this feaure it does look useful.

The comments in the code state that the connection string has to be non-null before you access the database, and I see other changes which allows connection string to be null before hand (I think!). It also offers a SetConnectionString method to set/change the connection string any time (a connection mustn't be open).

I won't be playing with this new feature for a week or so, but it does sound like the right way to handle the issue you have irrespective of using GenericServices.