0

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; }
}
x19
  • 8,277
  • 15
  • 68
  • 126
  • Maybe I don't understand exactly what you're trying to do, but why not `string.Split()`? Why Expressions? – gunr2171 Nov 20 '22 at 23:42
  • If this is for an AND condition, you can just call `.Where` for each fragment. If you need to combine OR expressions, you can do it like https://gist.github.com/SamWM/2029839#file-linqextensionmethods-cs-L13. I would only build an expression from scratch if you need to compare a property by name. – Jeremy Lakeman Nov 20 '22 at 23:48
  • @gunr2171: I modified my question. I hope this is now more clear. BaseSpecifcation accept only Expression> – x19 Nov 21 '22 at 00:02
  • Probably I would rethink your approach. How is your proposed solution supposed to know which string variable in the expression to split (you have 2, one is the `.Name`)? If you really want to re-write expression at runtime, look at https://stackoverflow.com/q/11159697/1462295 and similar. Otherwise, it looks like you have access to `ProductsSpecification` constructor. I would add a method / factory class / something to change how the `BaseSpecifcation` constructor is called, and you can split your string at that point to pass in all the `.Contains(s1) || .Contains(s2) || ...` . – BurnsBA Nov 21 '22 at 15:45
  • Left a few unstated assumptions in my comment, mainly there's a limit to the number of substrings you want to potentially search for, so supporting a short explicit list is not so hard. – BurnsBA Nov 21 '22 at 15:50
  • There is no limit to number of substring. It's dynamic. – x19 Nov 21 '22 at 17:15

1 Answers1

0

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!

x19
  • 8,277
  • 15
  • 68
  • 126