Mutation de l'arbre d'expression d'un prédicat pour cibler un autre type

c# expression-trees lambda linq

Question

Intro

Dans l'application sur laquelle je travaille actuellement, il existe deux types d'objet métier: le type "ActiveRecord" et le type "DataContract". Ainsi, par exemple, il y aurait:

namespace ActiveRecord {
    class Widget {
        public int Id { get; set; }
    }
}

namespace DataContract {
    class Widget {
        public int Id { get; set; }
    }
}

La couche d'accès à la base de données s'occupe de la traduction entre les familles: vous pouvez lui indiquer de mettre à jour un DataContract.Widget et elle créera comme par magie un ActiveRecord.Widget avec les mêmes valeurs de propriété et l'enregistre à la place.

Le problème est apparu lors de la tentative de refactorisation de cette couche d'accès à la base de données.

Le problème

Je souhaite ajouter des méthodes telles que les suivantes à la couche d'accès à la base de données:

namespace ActiveRecord {
    class Widget {
        public int Id { get; set; }
    }
}

namespace DataContract {
    class Widget {
        public int Id { get; set; }
    }
}

Ce qui précède est une méthode "get" d'usage général simple avec un prédicat personnalisé. Le seul point intéressant est que je passe une arborescence d'expression au lieu d'une lambda car, à l'intérieur de IDbAccessLayer un IQueryable<ActiveRecord.Widget> ; pour le faire efficacement (pensez à LINQ to SQL), je dois passer un arbre d’expression afin que cette méthode le demande.

Le problème: le paramètre doit être transformé comme par magie d'une Expression<Func<DataContract.Widget, bool>> à une Expression<Func<ActiveRecord.Widget, bool>> .

Tentative de solution

Ce que je voudrais faire dans GetMany c'est:

namespace ActiveRecord {
    class Widget {
        public int Id { get; set; }
    }
}

namespace DataContract {
    class Widget {
        public int Id { get; set; }
    }
}

Cela ne fonctionnera pas car dans un scénario typique, par exemple si:

namespace ActiveRecord {
    class Widget {
        public int Id { get; set; }
    }
}

namespace DataContract {
    class Widget {
        public int Id { get; set; }
    }
}

... l'arbre des expressions contient une instance MemberAccessExpression qui a une propriété de type MemberInfo qui décrit DataContract.Widget.Id . Il existe également des occurrences ParameterExpression dans l’arbre des expressions et dans sa collection de paramètres ( predicate.Parameters ) qui décrivent DataContract.Widget ; tout cela entraînera des erreurs puisque le corps interrogeable ne contient pas ce type de widget mais plutôt ActiveRecord.Widget .

Après une recherche un peu, j'ai trouvé System.Linq.Expressions.ExpressionVisitor (sa source peut être trouvée ici dans le contexte d'un System.Linq.Expressions.ExpressionVisitor pratique), qui offre un moyen pratique de modifier un arbre d'expressions. Dans .NET 4, cette classe est incluse.

Armé de cela, j'ai implémenté un visiteur. Ce simple visiteur ne s'occupe que de changer les types dans les accès membres et les expressions de paramètre, mais c'est assez de fonctionnalités pour fonctionner avec le prédicat w => w.Id == 0 .

namespace ActiveRecord {
    class Widget {
        public int Id { get; set; }
    }
}

namespace DataContract {
    class Widget {
        public int Id { get; set; }
    }
}

Avec ce visiteur, GetMany devient:

namespace ActiveRecord {
    class Widget {
        public int Id { get; set; }
    }
}

namespace DataContract {
    class Widget {
        public int Id { get; set; }
    }
}

Résultats

La bonne nouvelle est que lambda est bien construit. La mauvaise nouvelle est que cela ne fonctionne pas. ça m'explose lorsque j'essaie de l'utiliser, et les messages d'exception ne sont vraiment d'aucune aide.

J'ai examiné le lambda que mon code produit et un lambda codé en dur avec la même expression; ils se ressemblent exactement. J'ai passé des heures dans le débogueur à essayer de trouver une différence, mais je ne peux pas.

Lorsque le prédicat est w => w.Id == 0 , lambda ressemble exactement à referenceLambda . Mais ce dernier fonctionne avec, par exemple, IQueryable<T>.Where , alors que le premier ne le fait pas; J'ai essayé ceci dans la fenêtre immédiate du débogueur.

Je devrais également mentionner que lorsque le prédicat est w => true , tout fonctionne correctement. Par conséquent, je suppose que je ne fais pas assez de travail chez le visiteur, mais je ne trouve pas d’autres pistes à suivre.

Solution finale

Après avoir pris en compte les bonnes réponses au problème (deux d’entre elles ci-dessous; une courte, une avec code), le problème a été résolu. J'ai mis le code avec quelques notes importantes dans une réponse séparée pour éviter que cette longue question ne devienne encore plus longue.

Merci à tous pour vos réponses et commentaires!

Réponse acceptée

Il semble que vous générez l'expression de paramètre deux fois, dans VisitMember () ici:

var converted = Expression.MakeMemberAccess(
    base.Visit(node.Expression),
    activeRecordType.GetProperty(node.Member.Name));

... puisque base.Visit () aboutira dans VisitParameter j'imagine, et dans GetMany () lui-même:

var converted = Expression.MakeMemberAccess(
    base.Visit(node.Expression),
    activeRecordType.GetProperty(node.Member.Name));

Si vous utilisez une expression ParameterExpression dans le corps, il doit s'agir de la même instance (et pas uniquement du même type et du même nom) que celle déclarée pour Lambda. J'ai déjà eu des problèmes avec ce genre de scénario, bien que je pense que le résultat était que je n'étais tout simplement pas capable de créer l'expression, cela ferait simplement une exception. Dans tous les cas, vous pouvez essayer de réutiliser l'instance de paramètre pour voir si cela vous aide.


Réponse populaire

Il s’est avéré que la difficulté réside simplement dans le fait que les instances ParameterExpression présentes dans l’arbre d’expression du nouveau lambda doivent être identiques à celles qui sont transmises dans le IEnumerable<ParameterExpression> de Expression.Lambda .

Notez que dans TransformPredicateLambda je donne t => typeof(TNewTarget) comme fonction "convertisseur de type"; En effet, dans ce cas particulier, nous pouvons supposer que tous les paramètres et les accès membres seront de ce type spécifique. Les scénarios plus avancés peuvent nécessiter une logique supplémentaire.

Le code:

internal class DbAccessLayer {
    private static Expression<Func<TNewTarget, bool>> 
    TransformPredicateLambda<TOldTarget, TNewTarget>(
    Expression<Func<TOldTarget, bool>> predicate)
    {
        var lambda = (LambdaExpression) predicate;
        if (lambda == null) {
            throw new NotSupportedException();
        }

        var mutator = new ExpressionTargetTypeMutator(t => typeof(TNewTarget));
        var explorer = new ExpressionTreeExplorer();
        var converted = mutator.Visit(predicate.Body);

        return Expression.Lambda<Func<TNewTarget, bool>>(
            converted,
            lambda.Name,
            lambda.TailCall,
            explorer.Explore(converted).OfType<ParameterExpression>());
    }


    private class ExpressionTargetTypeMutator : ExpressionVisitor
    {
        private readonly Func<Type, Type> typeConverter;

        public ExpressionTargetTypeMutator(Func<Type, Type> typeConverter)
        {
            this.typeConverter = typeConverter;
        }

        protected override Expression VisitMember(MemberExpression node)
        {
            var dataContractType = node.Member.ReflectedType;
            var activeRecordType = this.typeConverter(dataContractType);

            var converted = Expression.MakeMemberAccess(
                base.Visit(node.Expression), 
                activeRecordType.GetProperty(node.Member.Name));

            return converted;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            var dataContractType = node.Type;
            var activeRecordType = this.typeConverter(dataContractType);

            return Expression.Parameter(activeRecordType, node.Name);
        }
    }
}

/// <summary>
/// Utility class for the traversal of expression trees.
/// </summary>
public class ExpressionTreeExplorer
{
    private readonly Visitor visitor = new Visitor();

    /// <summary>
    /// Returns the enumerable collection of expressions that comprise
    /// the expression tree rooted at the specified node.
    /// </summary>
    /// <param name="node">The node.</param>
    /// <returns>
    /// The enumerable collection of expressions that comprise the expression tree.
    /// </returns>
    public IEnumerable<Expression> Explore(Expression node)
    {
        return this.visitor.Explore(node);
    }

    private class Visitor : ExpressionVisitor
    {
        private readonly List<Expression> expressions = new List<Expression>();

        protected override Expression VisitBinary(BinaryExpression node)
        {
            this.expressions.Add(node);
            return base.VisitBinary(node);
        }

        protected override Expression VisitBlock(BlockExpression node)
        {
            this.expressions.Add(node);
            return base.VisitBlock(node);
        }

        protected override Expression VisitConditional(ConditionalExpression node)
        {
            this.expressions.Add(node);
            return base.VisitConditional(node);
        }

        protected override Expression VisitConstant(ConstantExpression node)
        {
            this.expressions.Add(node);
            return base.VisitConstant(node);
        }

        protected override Expression VisitDebugInfo(DebugInfoExpression node)
        {
            this.expressions.Add(node);
            return base.VisitDebugInfo(node);
        }

        protected override Expression VisitDefault(DefaultExpression node)
        {
            this.expressions.Add(node);
            return base.VisitDefault(node);
        }

        protected override Expression VisitDynamic(DynamicExpression node)
        {
            this.expressions.Add(node);
            return base.VisitDynamic(node);
        }

        protected override Expression VisitExtension(Expression node)
        {
            this.expressions.Add(node);
            return base.VisitExtension(node);
        }

        protected override Expression VisitGoto(GotoExpression node)
        {
            this.expressions.Add(node);
            return base.VisitGoto(node);
        }

        protected override Expression VisitIndex(IndexExpression node)
        {
            this.expressions.Add(node);
            return base.VisitIndex(node);
        }

        protected override Expression VisitInvocation(InvocationExpression node)
        {
            this.expressions.Add(node);
            return base.VisitInvocation(node);
        }

        protected override Expression VisitLabel(LabelExpression node)
        {
            this.expressions.Add(node);
            return base.VisitLabel(node);
        }

        protected override Expression VisitLambda<T>(Expression<T> node)
        {
            this.expressions.Add(node);
            return base.VisitLambda(node);
        }

        protected override Expression VisitListInit(ListInitExpression node)
        {
            this.expressions.Add(node);
            return base.VisitListInit(node);
        }

        protected override Expression VisitLoop(LoopExpression node)
        {
            this.expressions.Add(node);
            return base.VisitLoop(node);
        }

        protected override Expression VisitMember(MemberExpression node)
        {
            this.expressions.Add(node);
            return base.VisitMember(node);
        }

        protected override Expression VisitMemberInit(MemberInitExpression node)
        {
            this.expressions.Add(node);
            return base.VisitMemberInit(node);
        }

        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            this.expressions.Add(node);
            return base.VisitMethodCall(node);
        }

        protected override Expression VisitNew(NewExpression node)
        {
            this.expressions.Add(node);
            return base.VisitNew(node);
        }

        protected override Expression VisitNewArray(NewArrayExpression node)
        {
            this.expressions.Add(node);
            return base.VisitNewArray(node);
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            this.expressions.Add(node);
            return base.VisitParameter(node);
        }

        protected override Expression VisitRuntimeVariables(RuntimeVariablesExpression node)
        {
            this.expressions.Add(node);
            return base.VisitRuntimeVariables(node);
        }

        protected override Expression VisitSwitch(SwitchExpression node)
        {
            this.expressions.Add(node);
            return base.VisitSwitch(node);
        }

        protected override Expression VisitTry(TryExpression node)
        {
            this.expressions.Add(node);
            return base.VisitTry(node);
        }

        protected override Expression VisitTypeBinary(TypeBinaryExpression node)
        {
            this.expressions.Add(node);
            return base.VisitTypeBinary(node);
        }

        protected override Expression VisitUnary(UnaryExpression node)
        {
            this.expressions.Add(node);
            return base.VisitUnary(node);
        }

        public IEnumerable<Expression> Explore(Expression node)
        {
            this.expressions.Clear();
            this.Visit(node);
            return expressions.ToArray();
        }
    }
}



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