filterentity-framework-coremulti-tenant

EF Core - Multitenancy using a dynamic global filter


I'm trying to implement multitenancy in an app with a single database. I introduced a middleware that fetches the TenantId per request and a global filter to modelbuilder that filters all relevant entities by TenantId.

My problem is that the value of TenantId = tenantAccessor.TenantId is not dynamically set per http request but instead the filter with the default TenantID value of 0 is applied once during model creation.

    public static ModelBuilder AddTenantQueryFilter(this ModelBuilder modelBuilder, ITenantAccessor tenantAccessor)
    {
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            if (typeof(TenantBaseEntity).IsAssignableFrom(entityType.ClrType))
            {
                var method = typeof(TenantsExtension)
                    .GetMethod(nameof(GetTenantFilter), System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)
                    .MakeGenericMethod(entityType.ClrType);

                var filter = method.Invoke(null, new object[] { tenantAccessor });
                entityType.SetQueryFilter((LambdaExpression)filter);
            }
        }
        return modelBuilder;
    }

    private static LambdaExpression GetTenantFilter<TEntity>(ITenantAccessor tenantAccessor) where TEntity : TenantBaseEntity
    {
        Expression<Func<TEntity, bool>> filter = e => e.TenantId == tenantAccessor.TenantId;
        return filter;
    }

This is my current filter. What do i need to change that my filter applies/uses the requests current TenantId value?
The rest of the implemantation works. The middleware gets called and sets the TenantId correctly for each request and the filter also works if i set it manually to an existing TenantId when the model gets created.

That is another filter implementation i tried but same problem:

    public static ModelBuilder AddTenantQueryFilter(this ModelBuilder modelBuilder, ITenantAccessor tenantAccessor)
    {
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            if (typeof(TenantBaseEntity).IsAssignableFrom(entityType.ClrType))
            {
                var parameter = Expression.Parameter(entityType.ClrType, "e");
                var property = Expression.Property(parameter, nameof(TenantBaseEntity.TenantId));
                var constant = Expression.Constant(tenantAccessor.TenantId);
                var body = Expression.Equal(property, constant);
                var lambda = Expression.Lambda(body, parameter);

                modelBuilder.Entity(entityType.ClrType).HasQueryFilter(lambda);
            }
        }
        return modelBuilder;
    }

Solution

  • Your solution has two issues:

    1. Query filters can only utilize properties directly accessible from the DbContext.
    2. The OnModelCreating method is invoked only once per DbContext class, making your tenantAccessor parameter ineffective.

    Let’s consider the following implementation of a DbContext descendant, which addresses these concerns:

    public class MyDbContext : DbContext
    {
        public ITenantAccessor? TenantAccessor { get; set; }
    
        public int? TenantId => TenantAccessor?.TenantId;
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Entity configuration
    
            modelBuilder.ApplyQueryFilter<TenantBaseEntity>(e => e.TenantId == TenantId);
        }    
    }
    

    In this example, I've utilized my extension method ApplyQueryFilter, which simplifies the application of such filters.

    Here, TenantId is a property of the DbContext, satisfying the first requirement. The TenantAccessor property can be set via Dependency Injection or middleware, so the fact that OnModelCreating is called only once is not a concern.