组合多个表达式以在 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;
我正在努力尝试将许多表达式组合成一个表达式以将其传递给我的服务,后者将使用它来请求数据库。
这是我的方法以及我现在的实施情况:
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;