Search code examples
c#entity-frameworklambdadynamic-expression

create a lambda expression tree, Expression<Func<T, bool>>, dynamically that performs an Or-based, And-based predicate


I have a search string that can be "sub1 sub2 sub3" and I want to write a proper Expression<Func<T, bool>> that can find "sub1", "sub2", "sub3" and "sub1 sub2 sub3" in the x.Name

In the other hand I want to modify x.Name.ToLower().Contains(productParams.Search) for my purpose. Now I can search the term "sub1 sub2 sub3". However, I want to search for sub-strings as well.

my expectation for the search is: "sub1" || "sub2" || "sub3" || "sub1 sub2 sub3"

productParams.Search = "sub1 sub2 sub3"

How can do it?

public class ProductsSpecification : BaseSpecifcation<Product>
{
   public ProductsSpecification(ProductSpecParams productParams) : base(x =>
      (string.IsNullOrEmpty(productParams.Search) || 
      x.Name.ToLower().Contains(productParams.Search)) &&
      (!productParams.BrandId.HasValue || x.ProductBrandId == productParams.BrandId))
}

BaseSpecifcation:

public class BaseSpecifcation<T> : ISpecification<T>
{
    public BaseSpecifcation(Expression<Func<T, bool>> criteria)
    {
        Criteria = criteria;
    }

    public Expression<Func<T, bool>> Criteria { get; }
}

Solution

  • First of all, I created a helper class for generating predicates:

     public static class PredicateBuilder
        {
            public static Expression<Func<T, bool>> True<T>() { return f => true; }
            public static Expression<Func<T, bool>> False<T>() { return f => false; }
    
            public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
            {
                var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
                return Expression.Lambda<Func<T, bool>> (Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters);
            }
    
            public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
            {
                var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
                return Expression.Lambda<Func<T, bool>> (Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters);
            }
        }
    

    I created a private method in ProductsSpecification and used my class helper:

    private Expression<Func<Product, bool>> CreateProductFilter(ProductSpecParams productParams)
            {
                Expression<Func<Product, bool>> pr = PredicateBuilder.True<Product>(); // pr.Body.ToString() is "True"
    
                if (!string.IsNullOrEmpty(productParams.Search) && !string.IsNullOrEmpty(productParams.Search.Trim()))
                {
                    var searchValue = productParams.Search.Trim().ToLower();
                    pr = pr.And(a => a.Name.ToLower().Contains(searchValue));
    
                    foreach (var term in productParams.Search.ToLower().Split(' '))
                    {
                        string temp = term.Trim();
                        pr = pr.Or(a => a.Name.ToLower().Contains(temp));
                    }
                }
                if (productParams.BrandId.HasValue)
                {
                    pr = pr.And(p => p.ProductBrandId == productParams.BrandId);
                }
                
    
                if (pr.Body.ToString() == "True")
                {
                    return null;
                }
    
                return pr;
            }
    

    I modified the construcor of ProductsSpecification:

    public ProductsSpecification(ProductSpecParams productParams) : base()
        {
            Criteria = CreateProductFilter(productParams);
    
            // rest of the code
            
        }
    

    Now, the filter works well!