c# - significado - ¿Hay alguna razón particular por la que el expansor de LinqKit no pueda recoger expresiones de los campos?
expansores numero 4 (2)
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 escribirSystem.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.
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
:)
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.