Passa il parametro expression come argomento a un'altra espressione

c# entity-framework expression expression-trees lambda

Domanda

Ho una query che filtra i risultati:

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 sto usando il parametro q per abbinare una proprietà a una proprietà dal parametro qpi . Poiché il filtro verrà utilizzato in più punti, sto tentando di riscrivere la clausola where in un albero di espressioni che assomiglierebbe a qualcosa del genere:

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 implementerei questa funzione? O dovrei usare un approccio diverso nel complesso?

Risposta accettata

Se ho capito bene, vuoi riutilizzare un albero di espressioni all'interno di un altro, e comunque permetti al compilatore di fare tutta la magia di costruire l'albero delle espressioni per te.

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.

Innanzitutto cambierei il metodo che fa sì che la parte riutilizzabile sia un metodo statico che restituisce la tua espressione (come suggerito da mr100):

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

Il wrapping sarebbe stato 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!");
  }

Quindi lo srotolamento avverrebbe 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.

La tua risoluzioneQuota visitatore sarebbe simile a:

    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à. 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.

Per prima cosa creiamo un visitatore, che sostituirà tutti i parametri con le espressioni che specifichi.

    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 tornare alle nostre invocazioni ResolveQuoteVisitor e 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. Lo useresti 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 tu possa fare qui molto più riusabilità, con la definizione di espressioni anche a livelli più alti.

Potresti anche fare un ulteriore passo avanti e definire ResolveQuotes su IQueryable, e visitare semplicemente IQueryable.Expression e creare un nuovo IQUeryable usando 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 lontano, come sostituire 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 albero di espressione riutilizzabile simile.

Spero che aiuti :)

Disclaimer: Ricorda di non copiare mai il codice incolla da nessuna parte alla produzione senza capire cosa 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. Inoltre, ricorda 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 consente di usare anche più flessibilità: P


Risposta popolare

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. Ciò che è possibile ottenere è tenere da qualche parte l'intera espressione da Seleziona 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 quindi puoi passarlo per selezionare come argomento:

_context.Context.Quotes.Select(selectExp);

rendendolo così riutilizzabile. Se desideri avere una query riutilizzabile:

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

Quindi prima dovresti creare un metodo diverso per tenerlo:

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

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



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é