How to apply a filter in a LINQ to SQL expression only if results exist when the filter is applied?

c# expression-trees linq linq-to-sql

Question

I have a function I'd like to transform into a LINQ to SQL expression, but I can't figure out how. This function is called from within a LINQ query, once for each row in the result set. The productAreaId being passed in may or may not refer to valid data, so I have to check, and then only filter by productAreaId if any rows exist after applying the filter:

//Forgive the contrived example...
public static IQueryable<Order> GetOrders(int orderNumber, int? productAreaId, 
    OSDataContext db)
{
    var orders = db.Orders.Where(o => o.OrderNumber == orderNumber &&
        o.Group.GroupTypeId != (int)GroupTypeId.INTERNAL &&
        !o.Deleted);

    if (productAreaId != null)
    {
        var orders2 = orders.Where(o => o.ProductAreaId == productAreaId);
        if (orders2.Any()) return orders2;
    }
    return orders;
}

I don't want to do it like this. I need a function that returns an expression without arbitrary code, so it will be composable. The above function is only displayed because it's the only way I know how to encapsulate this in a function.

I'd like to do something like this, with the contrived "ApplyFilterIfAnyResultExists" replaced with something that actually works:

public static Expression<Func<Order,bool>> 
    GetOrdersExpr(int orderNumber, int? productAreaId)
{
    return o => o.OrderNumber == orderNumber && 
        o.Group.GroupTypeId != (int)GroupTypeId.INTERNAL &&
        !o.Deleted && (productAreaId == null || 

            //Making up a fictional function. Is there a real way to do this?
            o.ApplyFilterIfAnyResultExists(row => 
                row.ProductAreaId == productAreaId)
        );
}

Is there any way to apply that kind of filter within a LINQ to SQL expression? If not, any suggestions?

Thank you!
Roy

EDIT: This is the main query as I'd like it to look:

var customerData = 
    from c in db.Customers
    select new 
    {
        id = c.Id,
        name = c.Name,

        lastOrder =
            db.Orders
            .Where(GetOrdersExpr(c.LastOrderNumber, 
                c.PreferredProductAreaId))
            .FirstOrDefault(),

        allOrders = c.OrderForms
            .Select(form => 
                db.Orders
                .Where(GetOrdersExpr(form.OrderNumber,
                    c.PreferredProductAreaId))
                .FirstOrDefault()
            )
            .Where(o => o != null)

        //How lastOrder used to be queried
        //lastOrder =
        //    GetOrders(c.LastOrderNumber, c.PreferredProductAreaId, db)
        //    .FirstOrDefault()
    };

It's also worth noting that Orders and Customers are in two different databases on the database server, but they're both referenced from the same DataContext here.

Accepted Answer

Maybe something like this:

var customerData = 
    from c in db.Customers
    let orders = db.Orders.Where(o => o.OrderNumber == c.orderNumber &&
        o.Group.GroupTypeId != (int)GroupTypeId.INTERNAL &&
        !o.Deleted)
    let orders2 = orders.Where(o => o.ProductAreaId == c.productAreaId)
    select new 
    {
        id = c.Id,
        name = c.Name,
        lastOrder = c.productAreaId != null && orders2.Any() ?
            orders2.FirstOrDefault() :
            orders.FirstOrDefault() 
    };

Popular Answer

For your original method, this might work better:

public static IQueryable<Order> GetOrders(int orderNumber, int? productAreaId, 
    OSDataContext db)
{
    var orders = db.Orders.Where(o => o.OrderNumber == orderNumber &&
        o.Group.GroupTypeId != (int)GroupTypeId.INTERNAL &&
        !o.Deleted);
    if(productAreaId != null)
    {
        orders = orders.Where(
            o => !orders.Any(o2 => o2.ProductAreaId == productAreaId) ||
                    o.ProductAreaId == productAreaId);
    }
    return orders;
}

This makes it so you're only doing a single database roundtrip. If the product area ID is provided, you will return orders where either:

  • none of the orders in the original query have that area ID, or
  • this order does have that area ID

It does make the query more complex, so I'd test it a bit to see if it really gives you any performance gains.

This won't translate very well to the function that you're suggesting, but if you share more information about how this code is getting called, I could probably give you some advice on how to avoid calling this function 20+ times.

Edit

Something like this should work:

var customerData = 
    from c in db.Customers
    let productAreaId = c.PreferredProductAreaId
    let orders = 
        db.Orders
        .Where(o => o.OrderNumber == c.LastOrderNumber &&
            o.Group.GroupTypeId != (int)GroupTypeId.INTERNAL &&
            !o.Deleted)
        .OrderBy(o => o.Date)
    let lastOrderInProductArea = productAreaId != null
        ? orders.FirstOrDefault(o => o.ProductAreaId == productAreaId)
        : null
    select new 
    {
        id = c.Id,
        name = c.Name,
        lastOrder = lastOrderInProductArea != null
            ? lastOrderInProductArea
            : orders.FirstOrDefault()
    };



Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why