¿Hay alguna razón particular por la que el expansor de LinqKit no pueda recoger expresiones de los campos?

c# closures expression-trees lambda linqkit

Pregunta

Estoy usando la biblioteca LinqKit que permite combinar expresiones sobre la marcha.

Esta es una felicidad pura para escribir la capa de acceso a datos de Entity Framewok porque, opcionalmente, se pueden reutilizar y combinar varias expresiones, lo que permite un código legible y eficiente.

Considere la siguiente pieza 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 proyecta Message en MessageView ( MessageView los detalles para mayor claridad).

Ahora, el código de acceso a datos puede usar esta expresión para obtener un 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()
        )
    );

Esto es hermoso porque la misma expresión se puede reutilizar para obtener una lista de mensajes también:

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 error extraño cuando este código se bloqueó en la llamada Expand() .
El error decía:

No se puede convertir el objeto de tipo System.Linq.Expressions.FieldExpression para escribir System.Linq.Expressions.LambdaExpression .

Solo después de un tiempo me di cuenta de que todo funciona cuando se hace referencia a una expresión en una variable local antes de 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 código funciona como se espera.

Mi pregunta es:

¿Hay alguna razón específica por la que LinqKit no reconozca Invoke en expresiones almacenadas en campos? ¿Es solo una omisión por parte del desarrollador, o hay alguna razón importante por la que las expresiones deben almacenarse primero en las variables locales?

Esta pregunta probablemente puede responderse mirando el código generado y verificando las fuentes de LinqKit. Sin embargo, pensé que tal vez alguien relacionado con el desarrollo de LinqKit podría responder esta pregunta.

Gracias.

Respuesta aceptada

Descargué el código fuente y traté de analizarlo. ExpressionExpander no permite hacer referencia a expresiones que están almacenadas en variables que no sean constantes. Espera la expresión de que se está Invoke método Invoke para que haga referencia al objeto representado por ConstantExpression , no a otro MemberExpression .

Por lo tanto, no podemos proporcionar nuestra expresión reutilizable como referencia a ningún miembro de la clase (incluso campos públicos, no propiedades). El acceso de miembros object.member1.member2 (como object.member1.member2 ... etc) tampoco es compatible.

Pero esto puede solucionarse atravesando la expresión inicial y extrayendo recíprocamente los valores de los subcampos.

He reemplazado el código del método TransformExpr de ExpressionExpander clase ExpressionExpander 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 mencioné anteriormente (recursivamente atravesando el árbol) se realiza por nosotros mediante el compilador de ExpressionTree :)


Respuesta popular

He creado una versión mejorada de respuesta de micrófono :

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;

La principal ventaja es la eliminación de DynamicInvoke que tiene grandes gastos generales y llamadas Invoke solo cuando realmente lo necesito.



Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow