2015-04-04 15 views
7

Ho una domanda che filtra i risultati:parametro espressione Passo come argomento di un'altra espressione

public IEnumerable<FilteredViewModel> GetFilteredQuotes() 
{ 
    return _context.Context.Quotes.Select(q => new FilteredViewModel 
    { 
     Quote = q, 
     QuoteProductImages = q.QuoteProducts.SelectMany(qp => qp.QuoteProductImages.Where(qpi => q.User.Id == qpi.ItemOrder)) 
    }); 
} 

nella clausola WHERE che sto usando il parametro q per abbinare una proprietà nei confronti di un immobile dal parametro qpi. Perché il filtro sarà utilizzato in diversi luoghi che sto cercando di riscrivere la clausola dove un albero di espressione, che sarebbe simile a qualcosa di simile:

public IEnumerable<FilteredViewModel> GetFilteredQuotes() 
{ 
    return _context.Context.Quotes.Select(q => new FilteredViewModel 
    { 
     Quote = q, 
     QuoteProductImages = q.QuoteProducts.SelectMany(qp => qp.QuoteProductImages.AsQueryable().Where(ExpressionHelper.FilterQuoteProductImagesByQuote(q))) 
    }); 
} 

In questa query il parametro q viene utilizzato come parametro per la funzione:

public static Expression<Func<QuoteProductImage, bool>> FilterQuoteProductImagesByQuote(Quote quote) 
{ 
    // Match the QuoteProductImage's ItemOrder to the Quote's Id 
} 

Come implementare questa funzione? O dovrei usare un approccio diverso nel complesso?

risposta

8

Se ho capito bene, si desidera riutilizzare un albero di espressioni all'interno di un altro, e comunque consentire al compilatore di fare tutta la magia di costruire l'albero delle espressioni per voi.

Questo è effettivamente possibile, e l'ho fatto in molte occasioni.

Il trucco è quello di avvolgere la parte riutilizzabile in una chiamata al metodo, quindi, prima di applicare la query, scartarla.

prima cosa vorrei cambiare il metodo che ottiene la parte riutilizzabile di essere un metodo statico di restituire l'espressione (come MR100 suggerito):

public static Expression<Func<Quote,QuoteProductImage, bool>> FilterQuoteProductImagesByQuote() 
{ 
    return (q,qpi) => q.User.Id == qpi.ItemOrder; 
} 

Wrapping potrebbe essere fatto con:

public static TFunc AsQuote<TFunc>(this Expression<TFunc> exp) 
    { 
     throw new InvalidOperationException("This method is not intended to be invoked, just as a marker in Expression trees!"); 
    } 

Poi Avvolgerebbe sarebbe accaduto in:

public static Expression<TFunc> ResolveQuotes<TFunc>(this Expression<TFunc> exp) 
    { 
     var visitor = new ResolveQuoteVisitor(); 
     return (Expression<TFunc>)visitor.Visit(exp); 
    } 

Ovviamente la parte più interessante accade nel visitatore. Quello che devi fare è trovare i nodi che sono chiamate di metodo al tuo metodo AsQuote, e quindi sostituire l'intero nodo con il corpo della tua espressione lambda. Il lambda sarà il primo parametro del metodo.

tuo resolveQuote visitatore sarà simile:

private class ResolveQuoteVisitor : ExpressionVisitor 
    { 
     public ResolveQuoteVisitor() 
     { 
      m_asQuoteMethod = typeof(Extensions).GetMethod("AsQuote").GetGenericMethodDefinition(); 
     } 
     MethodInfo m_asQuoteMethod; 
     protected override Expression VisitMethodCall(MethodCallExpression node) 
     { 
      if (IsAsquoteMethodCall(node)) 
      { 
       // we cant handle here parameters, so just ignore them for now 
       return Visit(ExtractQuotedExpression(node).Body); 
      } 
      return base.VisitMethodCall(node); 
     } 

     private bool IsAsquoteMethodCall(MethodCallExpression node) 
     { 
      return node.Method.IsGenericMethod && node.Method.GetGenericMethodDefinition() == m_asQuoteMethod; 
     } 

     private LambdaExpression ExtractQuotedExpression(MethodCallExpression node) 
     { 
      var quoteExpr = node.Arguments[0]; 
      // you know this is a method call to a static method without parameters 
      // you can do the easiest: compile it, and then call: 
      // alternatively you could call the method with reflection 
      // or even cache the value to the method in a static dictionary, and take the expression from there (the fastest) 
      // the choice is up to you. as an example, i show you here the most generic solution (the first) 
      return (LambdaExpression)Expression.Lambda(quoteExpr).Compile().DynamicInvoke(); 
     } 
    } 

Ora siamo già a metà strada attraverso. Quanto sopra è sufficiente, se non hai alcun parametro sul tuo lambda. Nel tuo caso lo fai, quindi vuoi davvero sostituire i parametri del tuo lambda con quelli dell'espressione originale. Per questo, io uso l'espressione invoke, dove ottengo i parametri che voglio avere nel lambda.

Prima consente di creare un visitatore, che sostituirà tutti i parametri con le espressioni specificate.

private class MultiParamReplaceVisitor : ExpressionVisitor 
    { 
     private readonly Dictionary<ParameterExpression, Expression> m_replacements; 
     private readonly LambdaExpression m_expressionToVisit; 
     public MultiParamReplaceVisitor(Expression[] parameterValues, LambdaExpression expressionToVisit) 
     { 
      // do null check 
      if (parameterValues.Length != expressionToVisit.Parameters.Count) 
       throw new ArgumentException(string.Format("The paraneter values count ({0}) does not match the expression parameter count ({1})", parameterValues.Length, expressionToVisit.Parameters.Count)); 
      m_replacements = expressionToVisit.Parameters 
       .Select((p, idx) => new { Idx = idx, Parameter = p }) 
       .ToDictionary(x => x.Parameter, x => parameterValues[x.Idx]); 
      m_expressionToVisit = expressionToVisit; 
     } 

     protected override Expression VisitParameter(ParameterExpression node) 
     { 
      Expression replacement; 
      if (m_replacements.TryGetValue(node, out replacement)) 
       return Visit(replacement); 
      return base.VisitParameter(node); 
     } 

     public Expression Replace() 
     { 
      return Visit(m_expressionToVisit.Body); 
     } 
    } 

Ora possiamo avanzare di nuovo alla nostra ResolveQuoteVisitor, e invocazioni hanlde correttamente:

 protected override Expression VisitInvocation(InvocationExpression node) 
     { 
      if (node.Expression.NodeType == ExpressionType.Call && IsAsquoteMethodCall((MethodCallExpression)node.Expression)) 
      { 
       var targetLambda = ExtractQuotedExpression((MethodCallExpression)node.Expression); 
       var replaceParamsVisitor = new MultiParamReplaceVisitor(node.Arguments.ToArray(), targetLambda); 
       return Visit(replaceParamsVisitor.Replace()); 
      } 
      return base.VisitInvocation(node); 
     } 

Questo dovrebbe fare tutto il trucco. Si potrebbe usarlo come:

public IEnumerable<FilteredViewModel> GetFilteredQuotes() 
    { 
     Expression<Func<Quote, FilteredViewModel>> selector = q => new FilteredViewModel 
     { 
      Quote = q, 
      QuoteProductImages = q.QuoteProducts.SelectMany(qp => qp.QuoteProductImages.Where(qpi => ExpressionHelper.FilterQuoteProductImagesByQuote().AsQuote()(q, qpi))) 
     }; 
     selector = selector.ResolveQuotes(); 
     return _context.Context.Quotes.Select(selector); 
    } 

Naturalmente penso che si può fare molto di più qui riutilizzabilità, con definizione delle espressioni anche su un livelli più alti.

Si potrebbe anche fare un passo avanti, e definire una ResolveQuotes sulla IQueryable, e basta visitare l'IQueryable.Expression e creando un nuovo IQueryable utilizzando il provider originale e l'espressione risultato, ad esempio:

public static IQueryable<T> ResolveQuotes<T>(this IQueryable<T> query) 
    { 
     var visitor = new ResolveQuoteVisitor(); 
     return query.Provider.CreateQuery<T>(visitor.Visit(query.Expression)); 
    } 

In questo modo è possibile allineare la creazione dell'albero dell'espressione. Potresti anche andare oltre, sostituendo il provider di query predefinito per ef e risolvere le virgolette per ogni query eseguita, ma potrebbe andare troppo lontano: P

Puoi anche vedere come questo si tradurrebbe in qualsiasi espressione riutilizzabile simile alberi.

Spero che questo aiuta :)

responsabilità: Ricorda mai copiare incollare il codice da qualsiasi luogo alla produzione senza capire ciò che fa. Non ho incluso molti errori di gestione qui, per mantenere il codice al minimo. Inoltre non ho controllato le parti che usano le tue classi se dovessero compilare. Inoltre, non assumo alcuna responsabilità per la correttezza di questo codice, ma penso che la spiegazione dovrebbe essere sufficiente, per capire cosa sta succedendo e correggerlo se ci sono problemi con esso. Ricorda anche che questo funziona solo per i casi, quando hai una chiamata al metodo che produce l'espressione.Presto scriverò un post sul blog basato su questa risposta, che ti permetterà di usare anche più flessibilità: P

+0

Ok, sono impressionato, ha funzionato perfettamente. Questo è sicuramente molto utile e cercherò di renderlo più generico, così posso usarlo in più occasioni. –

2

L'implementazione a modo tuo causerà un'eccezione generata dal parser ef linq-to-sql. All'interno della query linq si richiama la funzione FilterQuoteProductImagesByQuote, che viene interpretata come espressione Invoke e non può essere semplicemente analizzata in sql. Perché? Generalmente perché da SQL non esiste la possibilità di invocare il metodo MSIL. L'unico modo per passare un'espressione alla query è archiviarlo come espressione> oggetto al di fuori della query e quindi passarlo al metodo Where. Non puoi farlo come al di fuori della query che non avrai lì Quote oggetto. Questo implica che generalmente non puoi ottenere ciò che volevi. Che cosa si può forse raggiungere è quello di tenere da qualche parte intera espressione da Selezionare in questo modo:

Expression<Func<Quote,FilteredViewModel>> selectExp = 
    q => new FilteredViewModel 
    { 
     Quote = q, 
     QuoteProductImages = q.QuoteProducts.SelectMany(qp => qp.QuoteProductImages.AsQueryable().Where(qpi => q.User.Id == qpi.ItemOrder))) 
    }; 

E poi si può passare per selezionare come argomento:

_context.Context.Quotes.Select(selectExp); 

rendendo così riutilizzabile. Se si desidera avere interrogazione riutilizzabile:

qpi => q.User.Id == qpi.ItemOrder 

Quindi prima si dovrebbe creare metodo diverso per tenerlo:

public static Expression<Func<Quote,QuoteProductImage, bool>> FilterQuoteProductImagesByQuote() 
{ 
    return (q,qpi) => q.User.Id == qpi.ItemOrder; 
} 

Applicazione di esso alla tua richiesta principale sarebbe possibile, tuttavia abbastanza difficile e difficile da leggere in quanto richiederà la definizione di tale query con l'uso della classe Expression.

+0

Grazie per la tua risposta chiara! Ho provato a costruire manualmente l'albero delle espressioni ma mi imbatto nel problema dove non faccio t avere accesso al parametro ** q ** e ridefinirlo non è consentito. Potrei costruire l'intera query (non solo la clausola where) ma non ne vale la pena poiché la query effettiva che dovrei costruire è piuttosto ampia e complessa. Invece mi limiterò a riutilizzare e scriverò la stessa query più volte. –

+0

Ho anche provato a compilare manualmente quella query e l'ho quasi finito, ma sembrava molto complesso e quindi sarebbe stato difficile da mantenere, quindi sono giunto alla conclusione che non ti interesserebbe vederlo dato che non fornisce alcun reale beneficio. Durante il lavoro con Ef purtroppo sono arrivato alla conclusione che in alcune situazioni dovevamo concordare la duplicazione del codice. – mr100

Problemi correlati