dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.81k stars 3.2k forks source link

Add dependency injection support to `IEntityTypeConfiguration` when using `ApplyConfigurationsFromAssembly` #35233

Open julealgon opened 1 day ago

julealgon commented 1 day ago

Problem

We have a multi-tenant solution and have decided to model it in EF by passing the TenantId to the DbContext and using that value for both grabbing the tenant-specific connection string (inside of the OnConfiguring method), as well as for declaring a global filter on each entity.

At the same time, we are using dedicated IEntityTypeConfiguration implementations for each of our entities, to keep code more manageable (vs putting everything in the DbContext's OnModelCreating method), and leveraging the ApplyConfigurationsFromAssembly method to automatically find and apply all such instances.

In this scenario, we are unable to specify the global filter inside of each IEntityTypeConfiguration implementation, because there is no way to transfer the tenantId value from the DbContext instance to the configuration classes when using that scanning/auto-construction approach.

We currently have a dependency injection-based mechanism to provide tenant information to classes in our codebase, by injecting a ITenantContext abstraction in the constructor.

We can provide the tenantId to the DbContext this way, since it supports DI, but the same cannot be done for the underlying IEntityTypeConfiguration classes since they are not resolved by DI. Instead, IEntityTypeConfiguration instances are created using the raw Activator.CreateInstance method, which in turn require the classes to have parameterless constructors.

https://github.com/dotnet/efcore/blob/d1e1dfa531dbf2d90df4a42ba71890ed5025dffd/src/EFCore/ModelBuilder.cs#L538

This feels like an arbitrary and confusing limitation: if DI is used when resolving dependencies in the context, it should be used by the configuration class instances as well.

In essence, we'd like for this change to work:

public sealed class SomeDbContext(ITenantContext tenantContext) : DbContext
{
    public DbSet<Customer> Customers { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(tenantContext.TenantConnectionString);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
-
-       modelBuilder.Entity<Customer>().HasQueryFilter(d => d.TenantId == tenantContext.TenantId);
    }
}

public sealed partial class Customer
{
-   private sealed class EntityConfiguration : IEntityTypeConfiguration<Customer>
+   private sealed class EntityConfiguration(ITenantContext tenantContext) : IEntityTypeConfiguration<Customer>
    {
        public void Configure(EntityTypeBuilder<Customer> builder)
        {
            builder
                .ToTable("Customers", "someSchema")
                .HasKey(c => c.RandomCustomerId);
+
+           builder.HasQueryFilter(d => d.TenantId == tenantContext.TenantId);
        }
    }
}

Proposal

The proposal is that ApplyConfigurationsFromAssembly is changed to rely on ActivatorUtilities.CreateInstance (or equivalent) instead of on the non-DI Activator.CreateInstance method for constructing the configuration classes, so that we can inject dependencies into the IEntityTypeConfiguration implementations.

In our particular use case, we want to be able to inject tenant-specific abstractions for declaring a tenant-limiting global filter on the entity as per above.

Workaround

We have 2 less-than-ideal ways to work around this limitation today.

  1. Manually provide each implementation of IEntityTypeConfiguration and propagate the ITenantContext via constructor argument this way

    modelBuilder.ApplyConfiguration(new Customer.EntityConfiguration(tenantContext));

    This of course breaks down today, since the implementation is an inner private class on the entity class. This not only would force us to add each class manually but also change our organization and start exposing the configuration implementation to the DbContext.

  2. Create our own scanning extension that uses ActivatorUtilities in conjunction with IServiceProvider to construct the configuration instances This is probably what we'll eventually end up doing but we'd rather the built-in method just did it for us so we didn't have to maintain such custom extension. And since we don't control the DbContext base dependencies, we are forced to manually pass IServiceProvider this way:

    modelBuilder.ApplyConfigurationsFromAssemblyCustom(this.GetType().Assembly, serviceProvider);
ajcvickers commented 6 hours ago

Note for team: we have always recommended people do their own implementations for these types of thing.