Combine BinaryExpression and Expression<Func<dynamic, bool>> in C#

前提是你 提交于 2021-02-16 13:49:08

问题


How can I combine BinaryExpression and Expression<Func<dynamic / T, bool>>?

For example:

void AddGlobalFilter<T>(Expression<Func<T, bool>> expr)
{
    var parameter = Expression.Parameter(type, "t");
    var member = Expression.Property(filter.Parameter, field);
    var constant = Expression.Constant(null);
    var body = Expression.Equal(member, constant);

    var combine = Expression.AndAlso(body, expr);
}

I am trying to define global filter for Entity Framework (EF) Core. The problem is I must manually combine multiple filters.

One filter may be added in ModelBuilder if model implements IDbDeleted interface.
Another could be added manually for specific model. Basic idea is I have a list of all Expressions, and then combine them:

var expression = listExpressions.First();
foreach (var second in listExpressions.Skip(1))
{
    expression = Expression.AndAlso(expression, second);
}
var lambdaExpression = Expression.Lambda(expression, parameter);
modelBuilder.Entity(item.Key).HasQueryFilter(lambdaExpression);

Of course I get error (first is from Expression.Equal and second from t => t...):

The filter expression 't => t => (Not(t. ...

Edited: code looks something like that:

[Table("MyEntities")]
public class DbMyEntity : IDeleted
{
    public string Name { get; set; }
    public DateTime? DateTimeDeleted { get; set; }
}

public interface IDeleted
{
    DateTime? DateTimeDeleted { get; set; }
}

public class MyContext : IdentityDbContext
{
    private Dictionary<Type, List<Expression>> dict = new Dictionary<Type, List<Expression>>();
    private Dictionary<Type, ParameterExpression> dictParameter = new Dictionary<Type, ParameterExpression>();

    private ParameterExpression GetParameter(Type type)
    {
        if (!this.dictParameter.ContainsKey(type))
        {
            this.dictParameter.Add(type, Expression.Parameter(type, "t"));
        }
        return this.dictParameter[type];
    }

    private void AddToDict(Type type, Expression expr)
    {
        if (!this.dict.ContainsKey(type))
        {
            this.dict.Add(type, new List<Expression>());
            this.GetParameter(type);  //Just to create ParameterExpression if not exists.
        }

        this.dict[type].Add(expr);
    }

    private void AddToDict<T>(Expression<Func<T, bool>> expr)
    {
        this.AddToDict(typeof(T), expr);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {

        base.OnModelCreating(modelBuilder);

        foreach (var entity in modelBuilder.Model.GetEntityTypes())
        {
            if (typeof(IDeleted).IsAssignableFrom(entity.ClrType))
            {
                var member = Expression.Property(this.GetParameter(entity.ClrType), "DateTimeDeleted");
                var constant = Expression.Constant(null);
                var body = Expression.Equal(member, constant);
                this.AddToDict(entity.ClrType, body);
            }
        }

        //This is done in another project in same solution. See comment bellow.
        this.AddToDict<DbMyEntity>(t => t.Name == null || t.Name == "Something");
        //foreach (var builderType in allDllModules)
        //{
        //    if (builderType != null && builderType != typeof(ICustomModelBuilder))
        //    {
        //        var builder = (ICustomModelBuilder)Activator.CreateInstance(builderType);
        //        builder.Build(modelBuilder);
        //    }
        //}

        foreach (var item in this.dict)
        {
            var expression = item.Value.First();
            foreach (var second in item.Value.Skip(1))
            {
                expression = Expression.AndAlso(expression, second);
            }
            var lambdaExpression = Expression.Lambda(expression, this.dictParameter[item.Key]);
            modelBuilder.Entity(item.Key).HasQueryFilter(lambdaExpression);
        }
    }
}

回答1:


You are mixing expressions with lambda expressions. There are many posts showing how you can combine lambda expressions, but the essential part is to compose expressions from lambda expression bodies and rebind the parameters.

The later is usually achieved by a custom ExpressionVisitor like this:

using System.Linq.Expressions;

public static class ExpressionExtensions
{
    public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
    {
        return new ParameterReplacer { Source = source, Target = target }.Visit(expression);
    }

    class ParameterReplacer : ExpressionVisitor
    {
        public ParameterExpression Source;
        public Expression Target;
        protected override Expression VisitParameter(ParameterExpression node)
        {
            return node == Source ? Target : base.VisitParameter(node);
        }
    }
}

Now regarding the EF Core combined query filters.

Using dictionaries and expression lists seems overcomplicated for what are you doing. Since IMutableEntityType provides read/write access to the QueryFilter, the same can be achieved with a small set of a custom extension methods.

All they go inside a class like this:

using System;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

public static class QueryFilterExtensions
{
}

First method:

public static void AddQueryFilter(this IMutableEntityType target, LambdaExpression filter)
{
    if (target.QueryFilter == null)
        target.QueryFilter = filter;
    else
    {
        var parameter = target.QueryFilter.Parameters[0];
        var left = target.QueryFilter.Body;
        var right = filter.Body.ReplaceParameter(filter.Parameters[0], parameter);
        var body = Expression.AndAlso(left, right);
        target.QueryFilter = Expression.Lambda(body, parameter);
    }
}

This is a non generic method which combines the exiting filter with passed filter using AndAlso (C# &&) operator and shows the aforementioned lambda expression combining principles.

However it's not so useful directly, like inside your entity type configuration loop (it can, but requires you to build manually the lambda expression instead of letting C# compiler do that). So here comes the second method:

public static void AddQueryFilter<T>(this IMutableEntityType target, Expression<Func<T, bool>> filter)
{
    LambdaExpression targetFilter = filter;
    if (target.ClrType != typeof(T))
    {
        var parameter = Expression.Parameter(target.ClrType, "e");
        var body = filter.Body.ReplaceParameter(filter.Parameters[0], parameter);
        targetFilter = Expression.Lambda(body, parameter);
    }
    target.AddQueryFilter(targetFilter);
}

It's a generic method - not quite type-safe, but allows you to use a compile time lambda expression and bind it to the actual entity type as follows:

foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
    if (typeof(IDeleted).IsAssignableFrom(entityType.ClrType))
        entityType.AddQueryFilter<IDeleted>(e => e.DateTimeDeleted == null);
}

Looks better, isn't it :)

The last custom extension method is complement to (replacement of) the standard EF Core generic HasQueryFilter method:

public static EntityTypeBuilder<TEntity> AddQueryFilter<TEntity>(this EntityTypeBuilder<TEntity> target, Expression<Func<TEntity, bool>> filter)
    where TEntity : class
{
    target.Metadata.AddQueryFilter(filter);
    return target;
}

and allows you to replace

this.AddToDict<DbMyEntity>(t => t.Name == null || t.Name == "Something");

with the more convenient

modelBuilder.Entity<DbMyEntity>()
    .AddQueryFilter(t => t.Name == null || t.Name == "Something");

Update (EF Core 3.0): QueryFilter property has been replaced with GetQueryFilter and SetQueryFilter extension methods.



来源:https://stackoverflow.com/questions/51497089/combine-binaryexpression-and-expressionfuncdynamic-bool-in-c-sharp

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!