Skip to content

Instantly share code, notes, and snippets.

@nphmuller
Last active October 9, 2018 15:38
Show Gist options
  • Save nphmuller/8891c315d79aaaf720f9164cd0f10400 to your computer and use it in GitHub Desktop.
Save nphmuller/8891c315d79aaaf720f9164cd0f10400 to your computer and use it in GitHub Desktop.
Sample showing an EF Core QueryFilter use case of soft delete, tenant id and toggling one or both on/off.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using Remotion.Linq.Parsing.ExpressionVisitors;
namespace MyApp
{
// Dynamic query filters are currently pretty limited in EF Core 2.0.
// They require the filter value to be a field of the DbContext class.
// Hence the partial class, to split up the filter logic in at least a
// seperate file.
// See: https://github.com/aspnet/EntityFrameworkCore/issues/10274
public abstract partial class MyContext
{
private bool filtersDisabled = false;
private int tenantId;
private bool tenantIdEnabled = true;
private bool softDeleteEnabled = true;
// Called by OnModelCreating()
private void ApplyQueryFilters(ModelBuilder modelBuilder)
{
var clrTypes = modelBuilder.Model.GetEntityTypes().Select(et => et.ClrType).ToList();
var baseFilter = (Expression<Func<IEntity, bool>>) (e => filtersDisabled);
var tenantFilter = (Expression<Func<ITenantEntity, bool>>) (e => !tenantIdEnabled || e.tenantId == tenantId);
var softDeleteFilter = (Expression<Func<ISoftDeletableEntity, bool>>) (e => !softDeleteEnabled || e.IsDeleted == false);
// Apply base + tenant + softdelete
var bothTypes = clrTypes
.Where(t => typeof(ITenantEntity).IsAssignableFrom(t) &&
typeof(ISoftDeletableEntity).IsAssignableFrom(t))
.ToList();
for (var i = bothTypes.Count - 1; i >= 0; i--)
{
var type = bothTypes[i];
var filter = CombineQueryFilters(type, baseFilter, new LambdaExpression[] { tenantFilter, softDeleteFilter });
modelBuilder.Entity(type).HasQueryFilter(filter);
clrTypes.Remove(type); // Remove, or query filter will be overridden by code below.
}
// Apply base + tenant
var administrationTypes = clrTypes.Where(t => typeof(ITenantEntity).IsAssignableFrom(t));
foreach (var type in administrationTypes)
{
var filter = CombineQueryFilters(type, baseFilter, new LambdaExpression[] { tenantFilter });
modelBuilder.Entity(type).HasQueryFilter(filter);
}
// Apply base + softdelete
var softDeleteTypes = clrTypes.Where(t => typeof(ISoftDeletableEntity).IsAssignableFrom(t));
foreach (var type in softDeleteTypes)
{
var filter = CombineQueryFilters(type, baseFilter, new LambdaExpression[] { softDeleteFilter });
modelBuilder.Entity(type).HasQueryFilter(filter);
}
}
// Called by SaveChanges(Async)()
private void ApplySaveFilters()
{
// TODO
}
// TODO:
// - Method to toggle filter bool fields.
// - Method to set tenantId field.
// EFCore currently has 2 limitations:
//
// - In Expression<Func<TEntity, bool>>, TEntity has to be to 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. 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>>) (e => 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);
}
}
}
@akinix
Copy link

akinix commented Nov 15, 2017

var administrationFilter = (Expression<Func<ITenantEntity, bool>>) (e => tenantIdEnabled && e.TenantId == tenantId);
var softDeleteFilter = (Expression<Func<ISoftDeletableEntity, bool>>) (e => softDeleteEnabled && e.IsDeleted == false);

this code is correct :

var administrationFilter = (Expression<Func<ITenantEntity, bool>>) (e => !tenantIdEnabled || (tenantIdEnabled && e.TenantId == tenantId));
var softDeleteFilter = (Expression<Func<ISoftDeletableEntity, bool>>) (e => !softDeleteEnabled || (softDeleteEnabled && e.IsDeleted == false));

@nphmuller
Copy link
Author

@akinix Thanks! I didn't see your message (got no notification), but I added the fixes. There were some other bugs, like the global filter toggle, that didn't work correctly either, which I also fixed.

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