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;
}
Your solution has two issues:
DbContext
.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.