Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Example of applying an EF Core global query filter on all entity types that implement an interface
/*
Copyright Phil Haack
Licensed under the MIT license - https://github.com/haacked/CodeHaacks/blob/main/LICENSE.
*/
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Query;
public static class ModelBuilderExtensions
{
static readonly MethodInfo SetQueryFilterMethod = typeof(ModelBuilderExtensions)
.GetMethods(BindingFlags.NonPublic | BindingFlags.Static)
.Single(t => t.IsGenericMethod && t.Name == nameof(SetQueryFilter));
public static void SetQueryFilterOnAllEntities<TEntityInterface>(
this ModelBuilder builder,
Expression<Func<TEntityInterface, bool>> filterExpression)
{
foreach (var type in builder.Model.GetEntityTypes()
.Where(t => t.BaseType == null)
.Select(t => t.ClrType)
.Where(t => typeof(TEntityInterface).IsAssignableFrom(t)))
{
builder.SetEntityQueryFilter(
type,
filterExpression);
}
}
static void SetEntityQueryFilter<TEntityInterface>(
this ModelBuilder builder,
Type entityType,
Expression<Func<TEntityInterface, bool>> filterExpression)
{
SetQueryFilterMethod
.MakeGenericMethod(entityType, typeof(TEntityInterface))
.Invoke(null, new object[] { builder, filterExpression });
}
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
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);
if (entityTypeBuilder.Metadata.GetQueryFilter() != null)
{
var currentQueryFilter = entityTypeBuilder.Metadata.GetQueryFilter();
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);
}
class ParameterTypeVisitor<TSource, TTarget> : ExpressionVisitor
{
private ReadOnlyCollection<ParameterExpression> _parameters;
protected override Expression VisitParameter(ParameterExpression node)
{
return _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);
}
}
}
@fiseni

This comment has been minimized.

Copy link

@fiseni fiseni commented Apr 25, 2020

This is great, but it would work up to ef core 2.2
Have you tried on ef core 3.1?

@haacked

This comment has been minimized.

Copy link
Owner Author

@haacked haacked commented Apr 26, 2020

Yes, it is currently working for me on 3.1

@fiseni

This comment has been minimized.

Copy link

@fiseni fiseni commented Apr 26, 2020

You mean entity core 3.1, not just runtime 3.1, right? Actually I just checked now, it worked for me too, with few minor changes.

The namespace is not available. Instead of using Remotion.Linq.Parsing.ExpressionVisitors;, we should use using Microsoft.EntityFrameworkCore.Query;

Also, internalEntityTypeBuilder.Metadata.QueryFilter should be replaced with internalEntityTypeBuilder.Metadata.GetQueryFilter()

@haacked

This comment has been minimized.

Copy link
Owner Author

@haacked haacked commented Apr 30, 2020

@fiseni Ah yeah, I hadn't updated the gist with my latest changes. Done!

@Grauenwolf

This comment has been minimized.

Copy link

@Grauenwolf Grauenwolf commented Jul 30, 2020

May we have permission to include this in the .NET ORM Cookbook?

https://grauenwolf.github.io/DotNet-ORM-Cookbook/index.htm

@haacked

This comment has been minimized.

Copy link
Owner Author

@haacked haacked commented Jul 30, 2020

@Grauenwolf be my guest!

@Grauenwolf

This comment has been minimized.

Copy link

@Grauenwolf Grauenwolf commented Jul 30, 2020

Thank you

@stzoran1

This comment has been minimized.

Copy link

@stzoran1 stzoran1 commented Aug 7, 2020

Thank you for great class. May we include it and use it inside next project?
https://github.com/AshkanAbd/efCoreSoftDeletes/tree/master/SoftDeletes

@haacked

This comment has been minimized.

Copy link
Owner Author

@haacked haacked commented Aug 7, 2020

@stzoran1 yes, I updated the code sample to include the license terms. This code sample is licensed under the MIT license. Anyone can use it with proper attribution.

@stzoran1

This comment has been minimized.

Copy link

@stzoran1 stzoran1 commented Aug 8, 2020

Thank you very much. I updated license header ;)

@peteralbanese

This comment has been minimized.

Copy link

@peteralbanese peteralbanese commented Oct 29, 2020

superhuman skills...thanks this saved my behind.

@haacked

This comment has been minimized.

Copy link
Owner Author

@haacked haacked commented Mar 23, 2021

I've updated the code for EF Core 5.0.2 and above. It no longer relies on internal interfaces. If you want the version for EF Core 3, you'll have to look at the previous revision for this gist.

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