Costruisci un albero di espressioni per LINQ usando List .Contains metodo

c# entity-framework expression-trees linq

Domanda

Problema

Sto lavorando al refactoring di alcune query LINQ per diversi report nella nostra applicazione Web e sto tentando di spostare alcuni predicati di query duplicati nei loro metodi di esenzione IQueryable modo che possiamo riutilizzarli per questi report e i report in futuro. Come probabilmente puoi dedurre, ho già rifattorizzato il predicato per i gruppi, ma il predicato per i codici mi sta dando problemi. Questo è un esempio di uno dei metodi di rapporto che ho finora:

Metodo DAL:

public List<Entities.QueryView> GetQueryView(Filter filter)
{
    using (var context = CreateObjectContext())
    {
        return (from o in context.QueryViews
                    where (!filter.FromDate.HasValue || o.RepairDate >= EntityFunctions.TruncateTime(filter.FromDate))
                    && (!filter.ToDate.HasValue || o.RepairDate <= EntityFunctions.TruncateTime(filter.ToDate))
                    select o)
                .WithCode(filter)
                .InGroup(filter)
                .ToList();
    }
}

Estensione IQueryable :

public static IQueryable<T> WithCode<T>(this IQueryable<T> query, Filter filter)
{
    List<string> codes = DAL.GetCodesByCategory(filter.CodeCategories);

    if (codes.Count > 0)
        return query.Where(Predicates.FilterByCode<T>(codes));

    return query;
}

Predicato:

public static Expression<Func<T, List<string>, bool>> FilterByCode<T>(List<string> codes)
{
    // Method info for List<string>.Contains(code).
    var methodInfo = typeof(List<string>).GetMethod("Contains", new Type[] { typeof(string) });

    // List of codes to call .Contains() against.
    var instance = Expression.Variable(typeof(List<string>), "codes");

    var param = Expression.Parameter(typeof(T), "j");
    var left = Expression.Property(param, "Code");
    var expr = Expression.Call(instance, methodInfo, Expression.Property(param, "Code"));

    // j => codes.Contains(j.Code)
    return Expression.Lambda<Func<T, List<string>, bool>>(expr, new ParameterExpression[] { param, instance });
}

Il problema che sto avendo è Queryable.Where non accetta un tipo di Expression<Func<T, List<string>, bool> . L'unico modo in cui posso pensare di creare questo predicato in modo dinamico è di usare due parametri, che è la parte che mi sta davvero facendo impazzire.

Quello che non sto comprendendo è il seguente metodo funziona. Posso passare l'espressione lambda esatta che sto cercando di creare dinamicamente e filtra correttamente i miei dati.

public List<Entities.QueryView> GetQueryView(Filter filter)
{
    // Get the codes here.
    List<string> codes = DAL.GetCodesByCategory(filter.CodeCategories);

    using (var context = CreateObjectContext())
    {
        return (from o in context.QueryViews
                    where (!filter.FromDate.HasValue || o.RepairDate >= EntityFunctions.TruncateTime(filter.FromDate))
                    && (!filter.ToDate.HasValue || o.RepairDate <= EntityFunctions.TruncateTime(filter.ToDate))
                    select o)
                .Where(p => codes.Contains(p.Code)) // This works fine.
                //.WithCode(filter)
                .InGroup(filter)
                .ToList();

        }

    }

Domande

  1. Posso implementare il mio proprio Queryable.Where sovraccarico? Se è così, è anche fattibile?
  2. Se un sovraccarico non è possibile, esiste un modo per costruire dinamicamente il predicato p => codes.Contains(p.Code) senza utilizzare due parametri?
  3. C'è un modo più semplice per farlo? Mi sento come se mi mancasse qualcosa.

Risposta accettata

  1. È possibile creare il proprio metodo di estensione, denominarlo Where , accettare un IQueryable<T> , restituire un IQueryable<T> e, in caso contrario, emulare la forma dei metodi LINQ. Non sarebbe un metodo LINQ, ma sarebbe simile a uno. Ti scoraggio dal scrivere un metodo del genere semplicemente perché probabilmente confonderebbe gli altri; anche se si desidera creare un nuovo metodo di estensione, utilizzare un nome non utilizzato in LINQ per evitare confusione. In breve, fai quello che stai facendo ora, crea nuove estensioni senza nominarle Where . Se volessi davvero nominarne uno Where se nulla ti fermasse.

  2. Certo, basta usare un lambda:

    public static Expression<Func<T, bool>> FilterByCode<T>(List<string> codes)
        where T : ICoded //some interface with a `Code` field
    {
        return p => codes.Contains(p.Code);
    }
    

    Se davvero non è possibile che le entità implementino un'interfaccia (suggerimento: quasi certamente lo si può fare), il codice sembrerebbe identico al codice che si ha, ma usando l'elenco che si passa come una costante piuttosto che come un nuovo parametro:

    public static Expression<Func<T, bool>> FilterByCode<T>(List<string> codes)
    {
        var methodInfo = typeof(List<string>).GetMethod("Contains", 
            new Type[] { typeof(string) });
    
        var list = Expression.Constant(codes);
    
        var param = Expression.Parameter(typeof(T), "j");
        var value = Expression.Property(param, "Code");
        var body = Expression.Call(list, methodInfo, value);
    
        // j => codes.Contains(j.Code)
        return Expression.Lambda<Func<T, bool>>(body, param);
    }
    

    Incoraggerei vivamente l'uso del precedente metodo; questo metodo perde la sicurezza di tipo statico ed è più complesso e quindi più difficile da mantenere.

    Un'altra nota, il commento che hai nel tuo codice: // j => codes.Contains(j.Code) non è accurato. In realtà, l' aspetto di lambda è: (j, codes) => codes.Contains(j.Code); che è in realtà notevolmente diverso.

  3. Vedi la prima metà del # 2.



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é