0

In my web page, there are multiple textboxes where users can enter complex queries. A complex query can contain any of the following Parameters:

  1. XX* that matches everything starting with XX
  2. *XX for everything ending in XX
  3. *XX* for everything containing XX
  4. XX123-XX129 for everthing matching the inclusive range from XX123 to XX129
  5. XX444 for an exact individual value
  6. ... any comma separated combination of any/all of the above

Implementing that is not my problem; my problem is implementing it for multiple values in a reuseable manner.

The sample below filters Items on the Item.Value property.

public static IQueryable<Item> WithMatchingItemValues(this IQueryable<Item> items,
    IEnumerable<Parameter> itemValues)
{
    var parameters = (itemValues ?? Enumerable.Empty<Parameter>()).ToList();
    if (parameters.IsEmpty()) return items;

    var (wildCards, exactMatches) = parameters.SplitOnWildCards();

    var predicate = PredicateBuilder.New<Item>(); // https://github.com/scottksmith95/LINQKit

    wildCards.ForEach(wc =>
    {
        switch (wc)
        {
            case WildCardStartsWith startsWith:
                predicate = predicate.Or(s => s.Value.ToUpper().StartsWith(startsWith.ToString()));
                break;
            case WildCardContains contains:
                predicate = predicate.Or(s => s.Value.ToUpper().Contains(contains.ToString()));
                break;
            case WildCardEndsWith endsWith:
                predicate = predicate.Or(s => s.Value.ToUpper().EndsWith(endsWith.ToString()));
                break;
        }
    });

    if (exactMatches.Any())
        predicate = predicate.Or(s => exactMatches.Select(p => p.Value).Contains(s.Value.ToUpper()));

    return items.AsExpandableEFCore().Where(predicate);
}

How can I refactor this so I can "pass in" the Item.Value to the method, so I can also pass in Item.PartNumber or Item.Foo without having to duplicate all this code for every property I want to filter? I can't just pass in Item.Value... that's just a string, and won't work in the lambda statements.

Scott Baker
  • 10,013
  • 17
  • 56
  • 102

1 Answers1

1

Write your method to take an ExpressionLambda that represents the field reference:

public static IQueryable<Item> WithMatchingItemValues(this IQueryable<Item> items,
    IEnumerable<Parameter> itemValues,
    Expression<Func<Item,string>> field)

Then in the code that needs to refer to the field, use LINQKit's Invoke method:

case WildCardStartsWith startsWith:
    predicate = predicate.Or(s => field.Invoke(s).ToUpper().StartsWith(startsWith.ToString()));

Finally, use LINQKit's Expand method to inline expand the field references, or use AsExpandable as you have on the data source:

if (exactMatches.Any())
    predicate = predicate.Or(s => exactMatches.Select(p => p.Value).Contains(field.Invoke(s).ToUpper()));

return items.AsExpandableEFCore().Where(predicate);
NetMage
  • 26,163
  • 3
  • 34
  • 55
  • I'm getting an error on that last line's `predicate.Expand()` method call: Cannot convert from `System.Linq.Expressions.Expression` to `System.Linq.Expressions.Expression>` ... where'd I go wrong here... – Scott Baker Feb 07 '20 at 21:23
  • @ScottBaker Not sure - since you have `AsExpandable` you don't need `Expand()` so I removed it from my answer... – NetMage Feb 07 '20 at 21:33