Le visiteur d'expression appelle uniquement VisitParameter pour certaines expressions lambda

c# expression-trees lambda

Question

Je veux pouvoir utiliser des méthodes d'extension imbriquées pour faire la projection d'entités dans EF aux modèles de vue correspondants. (voir ma question précédente Projection d'entités uniques dans EF avec des méthodes d'extension pour plus de détails sur ce que je fais).

Selon cette question, j'ai construit un attribut pour remplacer une méthode d'extension dans un arbre d'expression par un lambda pour pouvoir le faire. Il prend les arguments de la méthode extentsion et les remplace lorsque VisitParameter est appelé (je ne sais pas s'il existe un moyen de remplacer les paramètres en ligne dans LambdaExpression).

Cela fonctionne bien pour quelque chose comme ça:

entity => new ProfileModel
{
    Name = entity.Name  
}

Et je peux voir que le visiteur d’expression remplace le paramètre entity de LambdaExpression par le paramètre correct de la méthode d’extension args

Cependant, lorsque je le change en quelque chose de plus imbriqué,

entity => new ProfileModel
{
    SomethingElses = entity.SomethingElses.AsQueryable().ToViewModels()
}

alors je reçois:

Le paramètre 'entity' n'était pas lié dans l'expression de requête LINQ to Entities spécifiée.

De plus, VisitParameter dans mon expression visiteur ne semble pas du tout être appelé avec le paramètre 'entity'.

C'est comme si je n'utilisais pas du tout mon visiteur pour le deuxième Lambda, mais je ne sais pas pourquoi.

Comment puis-je remplacer correctement le paramètre dans le cas des deux types d'expressions lambda?

Mon visiteur ci-dessous:

    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        bool expandNode = node.Method.GetCustomAttributes(typeof(ExpandableMethodAttribute), false).Any();
        if (expandNode && node.Method.IsStatic)
        {
            object[] args = new object[node.Arguments.Count];
            args[0] = _provider.CreateQuery(node.Arguments[0]);

            for (int i = 1; i < node.Arguments.Count; i++)
            {
                Expression arg = node.Arguments[i];
                args[i] = (arg.NodeType == ExpressionType.Constant) ? ((ConstantExpression)arg).Value : arg;
            }
            return ((IQueryable)node.Method.Invoke(null, args)).Expression;
        }
        var replaceNodeAttributes = node.Method.GetCustomAttributes(typeof(ReplaceInExpressionTree), false).Cast<ReplaceInExpressionTree>();
        if (replaceNodeAttributes.Any() && node.Method.IsStatic)
        {
            var replaceWith = node.Method.DeclaringType.GetMethod(replaceNodeAttributes.First().MethodName).Invoke(null, null);
            if (replaceWith is LambdaExpression)
            {
                RegisterReplacementParameters(node.Arguments.ToArray(), replaceWith as LambdaExpression);
                return Visit((replaceWith as LambdaExpression).Body);
            }
        }
        return base.VisitMethodCall(node);
    }
    protected override Expression VisitParameter(ParameterExpression node)
    {
        Expression replacement;
        if (_replacements.TryGetValue(node, out replacement))
            return Visit(replacement);
        return base.VisitParameter(node);
    }
    private void RegisterReplacementParameters(Expression[] parameterValues, LambdaExpression expressionToVisit)
    {
        if (parameterValues.Length != expressionToVisit.Parameters.Count)
            throw new ArgumentException(string.Format("The parameter values count ({0}) does not match the expression parameter count ({1})", parameterValues.Length, expressionToVisit.Parameters.Count));
        foreach (var x in expressionToVisit.Parameters.Select((p, idx) => new { Index = idx, Parameter = p }))
        {
            if (_replacements.ContainsKey(x.Parameter))
            {
                throw new Exception("Parameter already registered, this shouldn't happen.");
            }
            _replacements.Add(x.Parameter, parameterValues[x.Index]);
        }
    }

Exemple de code de repro complet ici: https://github.com/lukemcgregor/ExtensionMethodProjection

Modifier:

J'ai maintenant un article de blog ( référentiels Composable - Extensions d'imbrication ) et un paquetage nuget pour vous aider à imbriquer des méthodes d'extension dans linq

Réponse acceptée

La première chose à retenir est que lors de l'analyse des nœuds, nous allons essentiellement à l'envers:

entity => new ProfileModel
{
    SomethingElses = entity.SomethingElses.AsQueryable().ToViewModels()
}

Ici, nous traitons ToViewModels() , puis AsQueryable() , puis SomethingElses et enfin entity . Puisque nous constatons que cette entity n’est jamais analysée ( VisitParameter ), cela signifie que quelque chose dans notre chaîne a arrêté le parcours de l’arbre.

Nous avons deux coupables ici:

VisitMethodCall() (AsQueryable et ToViewModels) et VisitMemberAccess() (SomethingElses)

Nous ne VisitMemberAccess pas VisitMemberAccess , le problème doit donc se trouver dans VisitMethodCall

Nous avons trois points de sortie pour cette méthode:

return ((IQueryable)node.Method.Invoke(null, args)).Expression;

return Visit((replaceWith as LambdaExpression).Body);

return base.VisitMethodCall(node);

La première ligne renvoie une expression mot pour mot et arrête la poursuite du parcours de l’arbre. Cela signifie que les nœuds descendants ne seront jamais visités - car nous disons que le travail est essentiellement effectué. Que ce soit un comportement correct ou non dépend vraiment de ce que vous voulez réaliser avec le visiteur.

Changer le code en

return Visit(((IQueryable)node.Method.Invoke(null, args)).Expression);

Cela signifie que nous traversons cette expression (potentiellement nouvelle!). Cela ne garantit pas que nous allons visiter les noeuds corrects (par exemple, cette expression peut être complètement indépendant de l'original) - mais cela ne signifie pas que si cette nouvelle expression contenait une expression de paramètre, que l'expression du paramètre serait rendu correctement.


Réponse populaire

Je pense que vous avez trop compliqué. Voir le visiteur:

public class CustomerVM { }
public class Customer {}

public class ReplaceMethodAttribute: Attribute
{
    public string ReplacementMethodName {get; private set;}
    public ReplaceMethodAttribute(string name)
    {
        ReplacementMethodName = name;
    }
}

public static class Extensions
{
    public static CustomerVM ToCustomerVM(Customer customer)
    {
        throw new NotImplementedException();
    }
    [ReplaceMethod("Extensions.ToCustomerVM")]
    public static CustomerVM ToVM(this Customer customer)
    {
        return Extensions.ToCustomerVM(customer);
    }
}

public class ReplaceMethodVisitor: ExpressionVisitor
{
    protected override Expression VisitMethodCall(MethodCallExpression exp)
    {
        var attr = exp.Method.GetCustomAttributes(typeof(ReplaceMethodAttribute), true).OfType<ReplaceMethodAttribute>().FirstOrDefault();
        if (attr != null)
        {
            var parameterTypes = exp.Method.GetParameters().Select(i => i.ParameterType).ToArray();
            var mi = GetMethodInfo(attr.ReplacementMethodName, parameterTypes);
            return Visit(Expression.Call(mi, exp.Arguments));
        }
        return base.VisitMethodCall(exp);
    }

    private MethodInfo GetMethodInfo(string name, Type[] argumentTypes)
    {
        // enhance with input checking
        var lastDot = name.LastIndexOf('.');
        var type = name.Substring(0, lastDot);
        var methodName = name.Substring(lastDot);
        return this.GetType().Assembly.GetTypes().Single(x => x.FullName == type).GetMethod(methodName, argumentTypes); // this might need adjusting if types are in different assembly
    }

}



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