组合多个表达式以在 linq where 子句中使用它们

Combining multiple Expression to use them in linq where clause

我正在努力尝试将许多表达式组合成一个表达式以将其传递给我的服务,后者将使用它来请求数据库。

这是我的方法以及我现在的实施情况:

private Expression<Func<EntityObject, bool>>? BuildSearchExpression()
{
    List<Expression<Func<EntityObject, bool>>> exprBuilderList = new List<Expression<Func<EntityObject, bool>>>();

    if (!string.IsNullOrWhiteSpace(filter.City))
    {
        if (filter.StringSearchChoice == StringSearchType.BeginBy)
            exprBuilderList.Add(x => x.Ville != null && x.Ville.ToLower().StartsWith(filter.City.ToLower()));
        if (filter.StringSearchChoice == StringSearchType.Contain)
            exprBuilderList.Add(x => x.Ville != null && x.Ville.ToLower().Contains(filter.City.ToLower()));
    }

    if (!string.IsNullOrWhiteSpace(filter.CompanyName))
    {
        if (filter.StringSearchChoice == StringSearchType.BeginBy)
            exprBuilderList.Add(x => x.NomEntreprise != null && x.NomEntreprise.ToLower().StartsWith(filter.CompanyName.ToLower()));
        if (filter.StringSearchChoice == StringSearchType.Contain)
            exprBuilderList.Add(x => x.NomEntreprise != null && x.NomEntreprise.ToLower().Contains(filter.CompanyName.ToLower()));
    }

    if (!string.IsNullOrWhiteSpace(filter.ContactName))
    {
        if (filter.StringSearchChoice == StringSearchType.BeginBy)
            exprBuilderList.Add(x => x.Contacts.Count > 0 && x.Contacts.Exists(con => (con.Prenom + " " + con.Nom).ToLower().StartsWith(filter.ContactName.ToLower())));
        if (filter.StringSearchChoice == StringSearchType.Contain)
            exprBuilderList.Add(x => x.Contacts.Count > 0 && x.Contacts.Exists(con => (con.Prenom + " " + con.Nom).ToLower().Contains(filter.ContactName.ToLower())));
    }

    if (!string.IsNullOrWhiteSpace(filter.Operator))
    {
        if (filter.StringSearchChoice == StringSearchType.BeginBy)
            exprBuilderList.Add(x => x.Operateur != null && x.Operateur.ToLower().StartsWith(filter.Operator.ToLower()));
        if (filter.StringSearchChoice == StringSearchType.Contain)
            exprBuilderList.Add(x => x.Operateur != null && x.Operateur.ToLower().Contains(filter.Operator.ToLower()));
    }

    if (!string.IsNullOrWhiteSpace(filter.PostalCode))
    {
        if (filter.StringSearchChoice == StringSearchType.BeginBy)
            exprBuilderList.Add(x => x.Cp != null && x.Cp.ToLower().StartsWith(filter.PostalCode.ToLower()));
        if (filter.StringSearchChoice == StringSearchType.Contain)
            exprBuilderList.Add(x => x.Cp != null && x.Cp.ToLower().Contains(filter.PostalCode.ToLower()));
    }

    if (!string.IsNullOrWhiteSpace(filter.PhoneNumber))
    {
        if (filter.StringSearchChoice == StringSearchType.BeginBy)
            exprBuilderList.Add(x => x.Tel != null && x.Tel.ToLower().StartsWith(filter.PhoneNumber.ToLower()));
        if (filter.StringSearchChoice == StringSearchType.Contain)
            exprBuilderList.Add(x => x.Tel != null && x.Tel.ToLower().Contains(filter.PhoneNumber.ToLower()));
    }

    if (filter.SheetNumber is not null)
        exprBuilderList.Add(x => x.NumFiche == filter.SheetNumber);
    if (filter.PartnerChoice != Partner.All)
        exprBuilderList.Add(x => x.Partenaire == Convert.ToBoolean((int)filter.PartnerChoice));
    if (filter.SchoolGrantOrCesu)
        exprBuilderList.Add(x => x.SubventionScolaireOuCesu == true);
 
// My first try working of course when i have a single filter available not 
// when combining multiple expression raising an InvalidOperationException
// The binary operator AndAlso is not defined for the types 
// 'System.Func`2[Domain.Entities.EntityObject,System.Boolean]' and 
// 'System.Func`2[Domain.Entities.EntityObject,System.Boolean]'
// at System.Linq.Expressions.Expression.AndAlso(Expression left, Expression right, 
// MethodInfo method)
Expression < Func<EntityObject, bool>>? resultExpr = null;
    foreach (var expr in exprBuilderList)
    {
        if (resultExpr == null)
            resultExpr = expr;
        else
            resultExpr = Expression.Lambda<Func<EntityObject, bool>>(Expression.AndAlso(resultExpr, expr), expr.Parameters);
    }
    return resultExpr;

    // My current try raising an exception of type System.ArgumentException 
    // saying my number of parameters supplied for lambda is incorrect i'm not 
    // sure what kind of parameter i have to pass?
    Type delegateType = typeof(Func<,>).GetGenericTypeDefinition().MakeGenericType(typeof(EntityObject), typeof(bool));

    var combined = exprBuilderList.Count > 0 ? 
        exprBuilderList.Cast<Expression>().Aggregate((x, y) => Expression.AndAlso(x, y)) : 
        null;

    return combined != null ? (Expression<Func<EntityObject, bool>>)Expression.Lambda(delegateType, combined) : null;
}

方法 return 正在发送到我的服务:

var (count, result) = await service.GetObjectsAsync(state.Page, state.PageSize, BuildSearchExpression());

然后我在 where 子句中使用表达式:

public async Task<(int, IEnumerable<myModel>)> GetObjectsAsync(int page, int pageSize, Expression<Func<EntityObject, bool>>? predicate = null)
{
        var query = predicate != null ? _context.EntityObjects.Where(predicate).Include(x => x.Child) : _context.EntityObjects.Include(x => x.Child);

所以我有我的数据库实体 EntityObject 和过滤器 class 我用来动态构建表达式树。当我尝试聚合表达式时出现问题。我阅读了一些关于使用参数和泛型构建具有非常复杂逻辑的表达式树的文章,对于我的情况来说,我不确定是否需要那么复杂。

在此先感谢您的建议和帮助。

最简单的选择是更改您的方法 - 您的 BuildSearchExpression 方法将 return List<Expression<Func<EntityObject, bool>>>,而 GetObjectsAsync 方法将简单地遍历列表并将每个过滤器传递给 Where 方法。 (将多个调用链接到 Where 相当于将筛选器与 AndAlso 组合在一起。)

var query = _context.EntityObjects.Include(x => x.Child).AsQueryable();
foreach (Expression<Func<EntityObject, bool>> filter in exprBuilderList)
{
    query = query.Where(filter);
}

如果您真的想将所有过滤器组合成一个谓词,您会发现每个过滤器的参数是不同的对象。您需要一个 ExpressionVisitor 来用单个 ParameterExpression 实例替换这些参数:

private sealed class ReplacementVisitor : ExpressionVisitor
{
    private ReadOnlyCollection<ParameterExpression> SourceParameters { get; set; }
    private Expression ToFind { get; set; }
    private Expression ReplaceWith { get; set; }

    private Expression ReplaceNode(Expression node) 
        => node == ToFind ? ReplaceWith : node;

    protected override Expression VisitConstant(ConstantExpression node) 
        => ReplaceNode(node);

    protected override Expression VisitBinary(BinaryExpression node)
    {
        var result = ReplaceNode(node);
        if (result == node) result = base.VisitBinary(node);
        return result;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        if (SourceParameters.Contains(node)) return ReplaceNode(node);
        return SourceParameters.FirstOrDefault(p => p.Name == node.Name) ?? node;
    }

    private static Expression Transform(
        LambdaExpression source, 
        Expression find, 
        Expression replace)
    {
        var visitor = new ReplacementVisitor
        {
            SourceParameters = source.Parameters,
            ToFind = find,
            ReplaceWith = replace,
        };

        return visitor.Visit(source.Body);
    }

    public static Expression ReplaceParameter<TSource, TResult>(
        this Expression<Func<TSource, TResult>> expression, 
        ParameterExpression newParameter)
        => Transform(expression, expression.Parameters.Single(), newParameter)
        ?? expression.Body;
}

设置好之后,您可以组合过滤器:

if (exprBuilderList.Count == 0) return null;

var x = Expression.Parameter(typeof(EntityObject), "x");
var filters = exprBuilderList.Select(fn => ReplacementVisitor.ReplaceParameter(fn, x));
var body = filters.Aggregate(Expression.AndAlso);
var result = Expression.Lambda<Func<EntityObject, bool>>(body, x);
return result;