Skip to content

Instantly share code, notes, and snippets.

@nphmuller

nphmuller/MyDbContext.cs

Last active Jun 26, 2020
Embed
What would you like to do?
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);
}
}
@malballah

This comment has been minimized.

Copy link

@malballah malballah commented Nov 7, 2019

Thanks for your awesome workaround to solve the issue of multiple filters with interfaces
I am wondering if there is nay way I could disable one of the filters for one read operation only and it get back to work again?

@nphmuller

This comment has been minimized.

Copy link
Owner Author

@nphmuller nphmuller commented Nov 7, 2019

I usually did something like this:

public void MyMethod()
{
    myContext.DisableFilterOne();
    try
    {
        myContext.MyEntities.ToList(); // Replace by business logic that accesses db context.
    }
    finally
    {
        myContext.EnableFilterOne();
    }
}

This ensures that the filter can not accidentally be left off if an exception occurs. And since DbContext is not thread safe it should work in most scenarios just fine.

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