I'm implementing multi-tenancy in my ASP.NET Core application with Entity Framework Core. I already have a solution that applies global query filters to entities with a single TenantId
, as shown below:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Company>(builder =>
{
builder.HasIndex(c => c.TenantId);
builder.HasQueryFilter(c => c.TenantId == _tenantId);
});
modelBuilder.Entity<Sale>(builder =>
{
builder.HasIndex(s => s.TenantId);
builder.HasQueryFilter(s => s.TenantId == _tenantId);
});
}
This can then be simplified using the extension methods below:
modelBuilder.SetQueryFilterOnAllEntities<ITenantEntity>(x => x.TenantId == _tenantId);
However, I have some entities (like Deal
and FinanceAgreement
) that belong to multiple tenants, represented as a collection of Tenants
. Here's how I handle them manually:
modelBuilder.Entity<Deal>(builder =>
{
builder.HasQueryFilter(x => x.Tenants.Any(le => le.Id == _tenantId));
});
modelBuilder.Entity<FinanceAgreement>(builder =>
{
builder.HasQueryFilter(x => x.Tenants.Any(le => le.Id == _tenantId));
});
Is there a way for me to adapt the extension methods below to support this scenario above where we don't have a single TenantId
but multiple ones?
public static class ModelBuilderExtensions
{
private static readonly MethodInfo SetQueryFilterMethod = typeof(ModelBuilderExtensions)
.GetMethods(BindingFlags.NonPublic | BindingFlags.Static)
.Single(t => t is { IsGenericMethod: true, Name: nameof(SetQueryFilter) });
public static void SetQueryFilterOnAllEntities<TEntityInterface>(
this ModelBuilder builder,
Expression<Func<TEntityInterface, bool>> filterExpression)
{
var entityTypes = builder.Model.GetEntityTypes()
.Where(t => t.BaseType == null)
.Select(t => t.ClrType)
.Where(t => typeof(TEntityInterface).IsAssignableFrom(t));
foreach (var type in entityTypes)
{
builder.SetEntityQueryFilter(type, filterExpression);
}
}
private static void SetEntityQueryFilter<TEntityInterface>(
this ModelBuilder builder,
Type entityType,
Expression<Func<TEntityInterface, bool>> filterExpression) =>
SetQueryFilterMethod
.MakeGenericMethod(entityType, typeof(TEntityInterface))
.Invoke(null, [builder, filterExpression]);
private static void SetQueryFilter<TEntity, TEntityInterface>(
this ModelBuilder builder,
Expression<Func<TEntityInterface, bool>> filterExpression)
where TEntityInterface : class
where TEntity : class, TEntityInterface
{
var concreteExpression = filterExpression
.Convert<TEntityInterface, TEntity>();
builder.Entity<TEntity>()
.AppendQueryFilter(concreteExpression);
}
// CREDIT: This comment by magiak on GitHub https://github.com/dotnet/efcore/issues/10275#issuecomment-785916356
private static void AppendQueryFilter<T>(this EntityTypeBuilder entityTypeBuilder, Expression<Func<T, bool>> expression)
where T : class
{
var parameterType = Expression.Parameter(entityTypeBuilder.Metadata.ClrType);
var expressionFilter = ReplacingExpressionVisitor.Replace(expression.Parameters.Single(), parameterType, expression.Body);
var currentQueryFilter = entityTypeBuilder.Metadata.GetQueryFilter();
if (currentQueryFilter is not null)
{
var currentExpressionFilter = ReplacingExpressionVisitor.Replace(currentQueryFilter.Parameters.Single(), parameterType, currentQueryFilter.Body);
expressionFilter = Expression.AndAlso(currentExpressionFilter, expressionFilter);
}
var lambdaExpression = Expression.Lambda(expressionFilter, parameterType);
entityTypeBuilder.HasQueryFilter(lambdaExpression);
}
}
public static class ExpressionExtensions
{
// This magic is courtesy of this StackOverflow post.
// https://stackoverflow.com/questions/38316519/replace-parameter-type-in-lambda-expression
// I made some tweaks to adapt it to our needs - @haacked
public static Expression<Func<TTarget, bool>> Convert<TSource, TTarget>(this Expression<Func<TSource, bool>> root)
{
var visitor = new ParameterTypeVisitor<TSource, TTarget>();
return (Expression<Func<TTarget, bool>>)visitor.Visit(root);
}
private class ParameterTypeVisitor<TSource, TTarget> : ExpressionVisitor
{
private ReadOnlyCollection<ParameterExpression>? _parameters;
protected override Expression VisitParameter(ParameterExpression node) =>
_parameters?.FirstOrDefault(p => p.Name == node.Name) ?? (node.Type == typeof(TSource) ? Expression.Parameter(typeof(TTarget), node.Name) : node);
protected override Expression VisitLambda<T>(Expression<T> node)
{
_parameters = VisitAndConvert(node.Parameters, "VisitLambda");
return Expression.Lambda(Visit(node.Body), _parameters);
}
}
}
You could add another interface IMultiTenantEntity
for all such entities
public interface IMultiTenantEntity
{
ICollection<Tenant> Tenants { get; set; }
}
Then
modelBuilder.SetQueryFilterOnAllEntities<ITenantEntity>(x =>
x.Tenants.Any(t =>
t.Id == _tenantId
)
);