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
Ok, sono impressionato, ha funzionato perfettamente. Questo è sicuramente molto utile e cercherò di renderlo più generico, così posso usarlo in più occasioni. –