Y at-il une raison particulière pour que l’extenseur de LinqKit ne puisse pas récupérer les expressions dans les champs?

c# closures expression-trees lambda linqkit

Question

J'utilise la bibliothèque LinqKit qui permet de combiner des expressions à la volée.

C’est un pur bonheur pour l’écriture de la couche d’accès aux données d’Entity Framewok car plusieurs expressions peuvent éventuellement être réutilisées et combinées, ce qui permet à la fois un code lisible et efficace.

Considérons le morceau de code suivant:

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

Nous déclarons une expression qui projette Message sur MessageView (j'ai supprimé les détails pour plus de clarté).

Maintenant, le code d'accès aux données peut utiliser cette expression pour obtenir un message individuel:

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

C'est beau parce que la même expression peut être réutilisée pour obtenir une liste de messages:

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

Comme vous pouvez le constater, l'expression de projection est stockée dans _selectMessageViewExpr et est utilisée pour créer plusieurs requêtes différentes.

Cependant, j'ai passé beaucoup de temps à rechercher une erreur étrange dans laquelle ce code était bloqué lors de l'appel de Expand() .
L'erreur a dit:

Impossible de System.Linq.Expressions.FieldExpression objet de type System.Linq.Expressions.FieldExpression en type System.Linq.Expressions.LambdaExpression .

Ce n'est qu'après un certain temps que j'ai compris que tout fonctionnait lorsque expression est référencée dans une variable locale avant d'appeler Invoke sur :

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

Ce code fonctionne comme prévu.

Ma question est:

Existe-t-il une raison spécifique pour laquelle LinqKit ne reconnaît pas Invoke sur les expressions stockées dans des champs? S'agit-il d'une simple omission du développeur ou existe-t-il une raison importante pour laquelle les expressions doivent d'abord être stockées dans des variables locales?

Vous pouvez probablement répondre à cette question en consultant le code généré et en vérifiant les sources LinqKit. Cependant, je pensais que quelqu'un lié au développement de LinqKit pourrait peut-être répondre à cette question.

Merci.

Réponse acceptée

J'ai téléchargé le code source et essayé de l'analyser. ExpressionExpander ne permet pas de référencer des expressions stockées dans des variables autres que constantes. Il s'attend à ce que expression soit Invoke méthode Invoke pour faire référence à un objet représenté par ConstantExpression et non par un autre MemberExpression .

Nous ne pouvons donc pas fournir notre expression réutilisable comme référence à un membre de la classe (même les champs publics, pas les propriétés). L'imbrication des membres (comme object.member1.member2 ... etc) n'est pas prise en charge également.

Mais cela peut être corrigé en parcourant l'expression initiale et en extrayant de manière récurrente les valeurs des sous-champs.

J'ai remplacé le code de la méthode TransformExpr de la classe ExpressionExpander par

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

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

et cela fonctionne maintenant.

Dans cette solution, tout ce que j'ai mentionné précédemment (arborescence récursive) est effectué pour nous par le compilateur ExpressionTree :)


Réponse populaire

J'ai créé une version améliorée 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;

Le principal avantage est la suppression de DynamicInvoke qui ont une charge importante et d'appeler Invoke uniquement lorsque j'en ai vraiment besoin




Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi