Question

In the past I've dealt with optional search criteria by dynamically adding filters to a Linq query like this:

public IEnumerable<Customer> FindCustomers(string name)
{
    IEnumerable<Customer> customers = GetCustomers();
    var results = customers.AsQueryable();
    if (name != null)
    {
        results = results.Where(customer => customer.Name == name);
    }
    results = results.OrderBy(customer => customer.Name);
    return results;
}

or similarly using predicates, where you basically just move the lambda from the Where into a Func<> (or Expression<Func<>> if using LinqToEntities), like this:

public IEnumerable<Customer> FindCustomers(string name)
{
    Func<Customer, bool> searchPredicate = customer => true;
    if (name != null)
    {
        searchPredicate = customer => customer.Name == name;
    }

    IEnumerable<Customer> customers = GetCustomers();
    var results = customers
        .Where(searchPredicate)
        .OrderBy(customer => customer.Name);
    return results;
}

However I can't figure out how to do something similar when the Where clause is buried somewhere in a nested subquery. Consider the following (made up) scenario:

public class Customer
{
    public string Name;
    public int MaxOrderItemAmount;
    public ICollection<Order> PendingOrders;
    public ICollection<OrderItem> FailedOrderItems;
    public ICollection<Order> CompletedOrders;
}

public class Order
{
    public int Id;
    public ICollection<OrderItem> Items;
}

public class OrderItem
{
    public int Amount;
}

public IEnumerable<OrderItem> FindInterestingOrderItems(
    bool onlyIncludePendingItemsOverLimit)
{
    var customers = GetCustomersWithOrders();

    // This approach works, but yields an unnecessarily complex SQL
    // query when onlyIncludePendingItemsOverLimit is false
    var interestingOrderItems = customers
        .SelectMany(customer => customer.PendingOrders
            .SelectMany(order => order.Items
                .Where(orderItem => onlyIncludePendingItemsOverLimit == false
                    || orderItem.Amount > customer.MaxOrderItemAmount))
            .Union(customer.FailedOrderItems)
        );

    // Instead I'd like to dynamically add the Where clause only if needed:
    Func<OrderItem, bool> pendingOrderItemPredicate = orderItem => true;
    if (onlyIncludePendingItemsOverLimit)
    {
        pendingOrderItemPredicate =
            orderItem => orderItem.Amount > customer.MaxOrderItemAmount;
       // PROBLEM: customer not defined here  ^^^
    }

    interestingOrderItems = customers
        .SelectMany(customer => customer.PendingOrders
            .SelectMany(order => order.Items
                .Where(pendingOrderItemPredicate)
            .Union(customer.FailedOrderItems)))
        .OrderByDescending(orderItem => orderItem.Amount);

    return interestingOrderItems;
}

Obviously I can't just move the lambda to a Func<> this time because it contains a reference to a variable (customer) defined by a higher-level part of the query. What am I missing here?

Était-ce utile?

La solution

This works: building the predicate in a separate function, passing the "customer" value from the higher-level lambda as a parameter.

publicvoid FindInterestingOrderItems(bool onlyIncludePendingItemsOverLimit)
{
    var customers = GetCustomersWithOrders();
    var interestingOrderItems = customers
        .SelectMany(customer => customer.PendingOrders
            .SelectMany(order => order.Items
                .Where(GetFilter(customer, onlyIncludePendingItemsOverLimit))
            .Union(customer.FailedOrderItems)))
        .OrderByDescending(orderItem => orderItem.Amount);
}

private Func<OrderItem, bool> GetFilter(Customer customer, bool onlyIncludePendingItemsOverLimit)
{
    if (onlyIncludePendingItemsOverLimit)
    {
        return orderItem => orderItem.Amount > customer.MaxOrderItemAmount;
    }
    else
    {
        return orderItem => true;
    }
}
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top