Search code examples
c#linqlambdarefactoringlinqkit

How can I Refactor This To Pass in the "Property to Use" in this Lambda to Make This Method Reusable?


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.


Solution

  • 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);