Come applicare un filtro in un'espressione LINQ a SQL solo se i risultati esistono quando il filtro viene applicato?

c# expression-trees linq linq-to-sql

Domanda

Ho una funzione che mi piacerebbe trasformare in un'espressione LINQ to SQL, ma non riesco a capire come. Questa funzione viene chiamata da una query LINQ, una volta per ogni riga nel set di risultati. Il productAreaId che viene passato può o non può fare riferimento a dati validi, quindi devo controllare, e quindi filtrare solo per productAreaId se esistono delle righe dopo aver applicato il filtro:

//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;
}

Non voglio farlo in questo modo. Ho bisogno di una funzione che restituisca un'espressione senza codice arbitrario, quindi sarà componibile. La funzione precedente viene visualizzata solo perché è l'unico modo in cui so come incapsulare questo in una funzione.

Mi piacerebbe fare qualcosa di simile, con l'inventato "ApplyFilterIfAnyResultExists" sostituito con qualcosa che funziona davvero:

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

C'è un modo per applicare quel tipo di filtro all'interno di un'espressione LINQ a SQL? Se no, qualche suggerimento?

Grazie!
Roy

EDIT: Questa è la query principale come vorrei che guardasse:

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()
    };

Vale anche la pena notare che gli ordini e i clienti si trovano in due diversi database sul server del database, ma sono entrambi referenziati dallo stesso DataContext qui.

Risposta accettata

Forse qualcosa del genere:

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() 
    };

Risposta popolare

Per il tuo metodo originale, questo potrebbe funzionare meglio:

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;
}

Questo rende così stai facendo solo un singolo roundtrip del database. Se viene fornito l'ID dell'area del prodotto, si restituiranno gli ordini nei seguenti casi:

  • nessuno degli ordini nella query originale ha quell'ID area, o
  • questo ordine ha quell'ID area

Rende la query più complessa, quindi proverei un po 'per vedere se ti dà davvero dei guadagni in termini di prestazioni.

Questo non si tradurrà molto bene nella funzione che stai suggerendo, ma se condividi più informazioni su come viene chiamato questo codice, potrei probabilmente darti alcuni consigli su come evitare di chiamare questa funzione più di 20 volte.

modificare

Qualcosa di simile dovrebbe funzionare:

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()
    };


Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché
Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché