2011-06-03 6 views
25

Estoy usando LinqKit biblioteca que permite combinar expresiones sobre la marcha.¿Existe algún motivo particular por el que el expansor de LinqKit no pueda recoger expresiones de los campos?

Esto es una bendición pura para escribir la capa de acceso a datos de Entity Framewok porque varias expresiones pueden reutilizarse y combinarse opcionalmente, lo que permite un código legible y eficiente.

Considerar siguiente fragmento de código:

private static readonly Expression<Func<Message, int, MessageView>> _selectMessageViewExpr = 
    (Message msg, int requestingUserId) => 
     new MessageView 
     { 
      MessageID = msg.ID, 
      RequestingUserID = requestingUserId, 
      Body = (msg.RootMessage == null) ? msg.Body : msg.RootMessage.Body, 
      Title = ((msg.RootMessage == null) ? msg.Title : msg.RootMessage.Title) ?? string.Empty 
     }; 

Declaramos una expresión que se proyecta hacia MessageMessageView (He quitado los detalles para mayor claridad).

Ahora, el código de acceso de datos puede utilizar esta expresión para obtener mensaje individual:

var query = CompiledQueryCache.Instance.GetCompiledQuery(
    "GetMessageView", 
    () => CompiledQuery.Compile(
     _getMessagesExpr 
      .Select(msg => _selectMessageViewExpr.Invoke(msg, userId)) // re-use the expression 
      .FirstOrDefault((MessageView mv, int id) => mv.MessageID == id) 
      .Expand() 
     ) 
    ); 

Esta es bella porque la misma expresión se puede reutilizar para obtener una lista de mensajes, así:

var query = CompiledQueryCache.Instance.GetCompiledQuery(
    "GetMessageViewList", 
    () => CompiledQuery.Compile(
     BuildFolderExpr(folder) 
      .Select(msg => _selectMessageViewExpr.Invoke(msg, userId)) 
      .OrderBy(mv => mv.DateCreated, SortDirection.Descending) 
      .Paging() 
      .Expand() 
     ), 
    folder 
    ); 

Como puede ver, la expresión de proyección se almacena en _selectMessageViewExpr y se utiliza para crear varias consultas diferentes.

Sin embargo, pasé mucho tiempo rastreando un extraño error donde este código se bloqueó en Expand() llamada.
El error dice:

Unable to cast object of type System.Linq.Expressions.FieldExpression to type System.Linq.Expressions.LambdaExpression .

Es sólo después de un tiempo me di cuenta de que todo funciona cuando la expresión se hace referencia en una variable local antes de ser llamado Invoke en:

var selector = _selectMessageViewExpr; // reference the field 

var query = CompiledQueryCache.Instance.GetCompiledQuery(
    "GetMessageView", 
    () => CompiledQuery.Compile(
     _getMessagesExpr 
      .Select(msg => selector.Invoke(msg, userId)) // use the variable 
      .FirstOrDefault((MessageView mv, int id) => mv.MessageID == id) 
      .Expand() 
     ) 
    ); 

Este el código funciona como se espera

Mi pregunta es:

Is there any specific reason why LinqKit doesn't recognize Invoke on expressions stored in fields? Is it just an omission by developer, or is there some important reason why expressions need to be stored in local variables first?

Esta pregunta probablemente pueden ser respondidas por mirar el código generado y comprobación de las fuentes LinqKit, sin embargo pensé que tal vez alguien relacionada con el desarrollo LinqKit podría responder a esta pregunta.

Gracias.

+0

¿Encontró una respuesta a esto? –

+0

@Lawrence: no, todavía tengo curiosidad. Actualmente uso la solución descrita. –

+0

Acabo de toparme con esto en mi proyecto. Por lo que puedo decir mirando el origen de LinqKit, parece ser porque ExpressionExpander no está programado para manejar campos de propiedad. No sabe cómo llamar a 'get' ya que su funcionamiento se basa en la reflexión. Estoy tratando de encontrar una solución que no marque este – Charles

Respuesta

25

He descargado el código fuente y he intentado analizarlo. ExpressionExpander no permite hacer referencia a expresiones que están almacenadas en variables que no sean constantes. Se espera que la expresión que se llama al método Invoke haga referencia al objeto representado por ConstantExpression, no a otro MemberExpression.

Así que no podemos proporcionar nuestra expresión reutilizable como referencia a cualquier miembro de la clase (incluso campos públicos, no propiedades). El acceso de miembro de anidación (como object.member1.member2 ... etc) tampoco es compatible.

Pero esto se puede solucionar atravesando la expresión inicial y extrayendo recursivamente los valores de los subcampos.

he sustituido TransformExpr código del método de ExpressionExpander clase para

var lambda = Expression.Lambda(input); 
object value = lambda.Compile().DynamicInvoke(); 

if (value is Expression) 
    return Visit((Expression)value); 
else 
    return input; 

y funciona ahora.

En esta solución, todo lo que he mencionado antes (de forma recursiva atravesar árbol) está hecho por nosotros por ExpressionTree compilador :)

+0

Una gran respuesta. No esperaba que esto fuera respondido en absoluto, sinceramente. Gracias. –

+0

Como nota al margen, realmente no necesita JS para formatear, solo asegúrese de familiarizarse con Markdown. Puedes ver mi edición, para empezar. ¡Gracias de nuevo! –

+0

Gracias :) Debo decir que no revisé todos los casos y árboles de expresión, así que no puedo garantizar que funcione bien en todo momento. Tal vez sea necesario hacer nuevos ajustes. El código de LinqKit es bastante complicado :) – Mic

9

creé mejorada versión de Mic answer:

if (input == null) 
    return input; 

var field = input.Member as FieldInfo; 
var prope = input.Member as PropertyInfo; 
if ((field != null && field.FieldType.IsSubclassOf(typeof(Expression))) || 
    (prope != null && prope.PropertyType.IsSubclassOf(typeof(Expression)))) 
    return Visit(Expression.Lambda<Func<Expression>>(input).Compile()()); 

return input; 

principal ventaja es la eliminación DynamicInvoke que tienen big overhead y llamando al Invoke solo cuando realmente lo necesito.

Cuestiones relacionadas