expresiones c# entity-framework lambda expression expression-trees

expresiones - lambda c#



Pase el parámetro de expresión como argumento a otra expresión (2)

Tengo una consulta que filtra los resultados:

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

En la cláusula where estoy usando el parámetro q para hacer coincidir una propiedad con una propiedad del parámetro qpi . Debido a que el filtro se usará en varios lugares, estoy tratando de reescribir la cláusula where en un árbol de expresiones que se vería así:

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

En esta consulta, el parámetro q se usa como parámetro de la función:

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

¿Cómo implementaría esta función? ¿O debería usar un enfoque diferente?


Implementar esto a tu manera provocará una excepción lanzada por el analizador ef linq-to-sql. Dentro de su consulta de linq, invoca la función FilterQuoteProductImagesByQuote: esto se interpreta como expresión de Invocación y simplemente no se puede analizar en sql. ¿Por qué? Generalmente porque desde SQL no hay posibilidad de invocar el método MSIL. La única forma de pasar la expresión para consultar es almacenarlo como Expresión> objeto fuera de la consulta y luego pasarlo al método Where. No puede hacer esto porque fuera de la consulta no tendrá allí el objeto Citar. Esto implica que generalmente no puedes lograr lo que quieres. Lo que posiblemente puede lograr es mantener en alguna parte la expresión completa de Select como esta:

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

Y luego puede pasarlo para seleccionar como argumento:

_context.Context.Quotes.Select(selectExp);

por lo tanto, es reutilizable. Si desea tener una consulta reutilizable:

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

Entonces primero deberías crear un método diferente para mantenerlo:

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

Sería posible aplicarlo a su consulta principal, aunque es bastante difícil y difícil de leer, ya que requerirá definir esa consulta con el uso de la clase Expresión.


Si entiendo correctamente, quiere reutilizar un árbol de expresiones dentro de otro, y aún permitir que el compilador haga toda la magia de construir el árbol de expresiones para usted.

Esto es realmente posible, y lo he hecho en muchas ocasiones.

El truco es envolver su parte reutilizable en una llamada a método, y luego, antes de aplicar la consulta, desenvolverla.

Primero cambiaría el método que hace que la parte reutilizable sea un método estático que devuelva tu expresión (como se sugirió mr100):

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

El envoltorio se haría 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!"); }

Luego, el desenvolvimiento ocurriría en:

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

Obviamente, la parte más interesante ocurre en el visitante. Lo que necesita hacer es encontrar nodos que son llamadas a métodos para su método AsQuote, y luego reemplazar todo el nodo con el cuerpo de su lambdaexpression. La lambda será el primer parámetro del método.

Su visitante resolveQuote se vería así:

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

Ahora ya estamos a mitad de camino. Lo anterior es suficiente, si no tienes parámetros en tu lambda. En tu caso lo haces, así que realmente quieres reemplazar los parámetros de tu lambda por los de la expresión original. Para esto, uso la expresión de invocación, donde obtengo los parámetros que quiero tener en la lambda.

Primero permitamos crear un visitante, que reemplazará todos los parámetros con las expresiones que especifique.

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

Ahora podemos avanzar de nuevo a nuestro ResolveQuoteVisitor y realizar invocaciones correctamente:

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

Esto debería hacer todo el truco. Lo usarías como:

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

Por supuesto, creo que puedes hacer aquí mucho más reutilizables, con expresiones definitorias incluso en niveles más altos.

Incluso podría ir un paso más allá y definir un ResolveQuotes en IQueryable, y simplemente visitar IQueryable.Expression y crear un nuevo Iqueryable utilizando el proveedor original y la expresión resultante, por ejemplo:

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

De esta manera puede alinear la creación del árbol de expresiones. Incluso podría ir tan lejos, anular el proveedor de consultas predeterminado para ef y resolver citas para cada consulta ejecutada, pero eso podría ir demasiado lejos: P

También puede ver cómo esto se traduciría en realidad en árboles de expresiones reutilizables similares.

Espero que esto ayude :)

Descargo de responsabilidad: Recuerde que nunca copie el código de pegado desde cualquier lugar a la producción sin entender lo que hace. No incluí mucho manejo de errores aquí, para mantener el código al mínimo. Tampoco revisé las partes que usan tus clases si compilaran. Tampoco me responsabilizo por la exactitud de este código, pero creo que la explicación debería ser suficiente para comprender lo que está sucediendo y solucionarlo si hay algún problema. También recuerde, esto solo funciona para casos, cuando tiene una llamada de método que produce la expresión. Pronto escribiré una publicación de blog basada en esta respuesta, que también te permite usar más flexibilidad: P