Skip to content

Instantly share code, notes, and snippets.

@nphmuller
Last active July 29, 2024 13:03
Show Gist options
  • Save nphmuller/05ff66dfa67e1d02cdefcd785661a34d to your computer and use it in GitHub Desktop.
Save nphmuller/05ff66dfa67e1d02cdefcd785661a34d to your computer and use it in GitHub Desktop.
CombineQueryFilers
public class MyDbContext : DbContext
{
private ITenantIdLocator tenantIdLocator;
public MyDbContext(ITenantIdLocator tenantIdLocator)
{
if (tenantIdLocator == null) throw new ArgumentNullException(nameof(tenantIdLocator));
this.tenantId = tenantIdLocator.GetTenantId();
}
// Leave these as fields, because else the filters won't work correctly.
// If you want to change them, make a method which changes the value.
private bool filtersDisabled = false;
private bool tenantFilterEnabled = true;
private bool softDeleteFilterEnabled = true;
private int tenantId = 0;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
ApplyQueryFilters(modelBuilder);
}
private void ApplyQueryFilters(ModelBuilder modelBuilder)
{
var clrTypes = modelBuilder.Model.GetEntityTypes().Select(et => et.ClrType).ToList();
var baseFilter = (Expression<Func<IEntity, bool>>) (_ => filtersDisabled);
var tenantFilter = (Expression<Func<IMultiTenantEntity, bool>>) (e => !tenantFilterEnabled || e.TenantId == tenantId);
var softDeleteFilter = (Expression<Func<ISoftDeletableEntity, bool>>) (e => !softDeleteFilterEnabled || e.IsDeleted == false);
foreach (var type in clrTypes)
{
var filters = new List<LambdaExpression>();
if (typeof(IMultiTenantEntity).IsAssignableFrom(type))
filters.Add(tenantFilter);
if (typeof(ISoftDeletableEntity).IsAssignableFrom(type))
filters.Add(softDeleteFilter);
var queryFilter = CombineQueryFilters(type, baseFilter, filters);
modelBuilder.Entity(type).HasQueryFilter(queryFilter);
}
}
// EFCore currently has 2 limitations:
//
// - In Expression<Func<TEntity, bool>>, TEntity has to be the final entity type and cannot
// be, for example, the interface type. To work around it, we change the type in the expression
// with ReplacingExpressionVisitor. See: https://github.com/aspnet/EntityFrameworkCore/issues/10257
//
// - Only 1 HasQueryFilter() call is supported per entity type. The last one will overwrite each call that
// came before it. To work around this, we combine the multiple query filters in a single expression.
// See: https://github.com/aspnet/EntityFrameworkCore/issues/10275
private LambdaExpression CombineQueryFilters(Type entityType, LambdaExpression baseFilter, IEnumerable<LambdaExpression> andAlsoExpressions)
{
var newParam = Expression.Parameter(entityType);
var andAlsoExprBase = (Expression<Func<IEntity, bool>>) (_ => true);
var andAlsoExpr = ReplacingExpressionVisitor.Replace(andAlsoExprBase.Parameters.Single(), newParam, andAlsoExprBase.Body);
foreach (var expressionBase in andAlsoExpressions)
{
var expression = ReplacingExpressionVisitor.Replace(expressionBase.Parameters.Single(), newParam, expressionBase.Body);
andAlsoExpr = Expression.AndAlso(andAlsoExpr, expression);
}
var baseExp = ReplacingExpressionVisitor.Replace(baseFilter.Parameters.Single(), newParam, baseFilter.Body);
var exp = Expression.OrElse(baseExp, andAlsoExpr);
return Expression.Lambda(exp, newParam);
}
}
@phamvietdung
Copy link

@nphmuller oh well yes. :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment