Last active
October 9, 2018 15:38
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@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.