I'm struggling trying to combine many expressions into one to pass it to my service who whill use it to request the database.
Here is my method and where i'm now with the implementation:
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;
}
The method return is sending to my Service:
var (count, result) = await service.GetObjectsAsync(state.Page, state.PageSize, BuildSearchExpression());
Then i use the expression in a where clause:
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);
So I Have my db entity EntityObject and a filter class i use to dynamically build the expression tree. The problem occur when i try to aggregate expressions. I readed few articles on building Expressions trees with very complex logic using Parameters and generics, well for my case i'm not sure to have the need to go as much in complexity.
Thanks in advance for your advices and help.
CodePudding user response:
The simplest option would be to change your methods - your BuildSearchExpression method would return the List<Expression<Func<EntityObject, bool>>>, and the GetObjectsAsync method would simply loop through the list and pass each filter to the Where method. (Chaining multiple calls to Where is equivalent to combining the filters with AndAlso.)
var query = _context.EntityObjects.Include(x => x.Child).AsQueryable();
foreach (Expression<Func<EntityObject, bool>> filter in exprBuilderList)
{
query = query.Where(filter);
}
If you really want to combine all of the filters into a single predicate, you'll find that the parameter for each filter is a different object. You'll need an ExpressionVisitor to replace those parameters with a single ParameterExpression instance:
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;
}
With that in place, you can combine your filters:
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;
