Expression permettant de mapper un objet sur un autre sur les mêmes propriétés

c# dynamicmethod expression-trees lambda linq

Question

J'essaie de créer un mappeur simple en utilisant Expression avec ce code:

public static class MyUtility {

    public static Action<TSource, TTarget> BuildMapAction<TSource, TTarget>(IEnumerable<PropertyMap> properties) {

        var sourceInstance = Expression.Parameter(typeof(TSource), "source");
        var targetInstance = Expression.Parameter(typeof(TTarget), "target");

        var statements = BuildPropertyGettersSetters(sourceInstance, targetInstance, properties);

        Expression blockExp = Expression.Block(new[] { sourceInstance, targetInstance }, statements);

        if (blockExp.CanReduce)
            blockExp = blockExp.ReduceAndCheck();
        blockExp = blockExp.ReduceExtensions();

        var lambda = Expression.Lambda<Action<TSource, TTarget>>(blockExp, sourceInstance, targetInstance);

        return lambda.Compile();
    }

    private static IEnumerable<Expression> BuildPropertyGettersSetters(
        ParameterExpression sourceInstance,
        ParameterExpression targetInstance,
        IEnumerable<PropertyMap> properties) {

        var statements = new List<Expression>();

        foreach (var property in properties) {

            // value-getter
            var sourceGetterCall = Expression.Call(sourceInstance, property.SourceProperty.GetGetMethod());
            var sourcePropExp = Expression.TypeAs(sourceGetterCall, typeof(object));

            // value-setter
            var targetSetterCall =
                    Expression.Call(
                        targetInstance,
                        property.TargetProperty.GetSetMethod(),
                        Expression.Convert(sourceGetterCall, property.TargetProperty.PropertyType)
                        );
            var refNotNullExp = Expression.ReferenceNotEqual(sourceInstance, Expression.Constant(null));
            var propNotNullExp = Expression.ReferenceNotEqual(sourcePropExp, Expression.Constant(null));
            var notNullExp = Expression.And(refNotNullExp, propNotNullExp);
            var ifExp = Expression.IfThen(notNullExp, targetSetterCall);

            statements.Add(ifExp);
        }

        return statements;
    }

}

Tout me semble correct, mais lorsque j'essaie de le tester, j'obtiens simplement une exception de référence nulle. Les objets de test et la méthode:

public static class MyUtility {

    public static Action<TSource, TTarget> BuildMapAction<TSource, TTarget>(IEnumerable<PropertyMap> properties) {

        var sourceInstance = Expression.Parameter(typeof(TSource), "source");
        var targetInstance = Expression.Parameter(typeof(TTarget), "target");

        var statements = BuildPropertyGettersSetters(sourceInstance, targetInstance, properties);

        Expression blockExp = Expression.Block(new[] { sourceInstance, targetInstance }, statements);

        if (blockExp.CanReduce)
            blockExp = blockExp.ReduceAndCheck();
        blockExp = blockExp.ReduceExtensions();

        var lambda = Expression.Lambda<Action<TSource, TTarget>>(blockExp, sourceInstance, targetInstance);

        return lambda.Compile();
    }

    private static IEnumerable<Expression> BuildPropertyGettersSetters(
        ParameterExpression sourceInstance,
        ParameterExpression targetInstance,
        IEnumerable<PropertyMap> properties) {

        var statements = new List<Expression>();

        foreach (var property in properties) {

            // value-getter
            var sourceGetterCall = Expression.Call(sourceInstance, property.SourceProperty.GetGetMethod());
            var sourcePropExp = Expression.TypeAs(sourceGetterCall, typeof(object));

            // value-setter
            var targetSetterCall =
                    Expression.Call(
                        targetInstance,
                        property.TargetProperty.GetSetMethod(),
                        Expression.Convert(sourceGetterCall, property.TargetProperty.PropertyType)
                        );
            var refNotNullExp = Expression.ReferenceNotEqual(sourceInstance, Expression.Constant(null));
            var propNotNullExp = Expression.ReferenceNotEqual(sourcePropExp, Expression.Constant(null));
            var notNullExp = Expression.And(refNotNullExp, propNotNullExp);
            var ifExp = Expression.IfThen(notNullExp, targetSetterCall);

            statements.Add(ifExp);
        }

        return statements;
    }

}

Avez-vous une idée de ce qui se passe là-bas? Qu'est-ce qui m'a manqué?


REMARQUE : je ne peux pas utiliser de mappeurs tiers (comme AutoMapper)

Réponse acceptée

Le problème est causé par cette ligne:

Expression blockExp = Expression.Block(new[] { sourceInstance, targetInstance }, statements);

Le premier argument de la surcharge Expression.Block utilisée représente les variables locales du bloc. En y passant les paramètres lambda, vous définissez simplement 2 variables locales non attribuées, d’où le NRE au moment de l’exécution. Vous pouvez voir cela en examinant l'expression lambda DebugView dans la fenêtre locale / montre de VS, qui ressemble à ceci dans votre exemple d'appel:

Expression blockExp = Expression.Block(new[] { sourceInstance, targetInstance }, statements);

Notez la redéfinition de la source et de la target à l'intérieur du bloc.

Après avoir utilisé la surcharge correcte:

Expression blockExp = Expression.Block(new[] { sourceInstance, targetInstance }, statements);

la vue est maintenant comme ça:

Expression blockExp = Expression.Block(new[] { sourceInstance, targetInstance }, statements);

et le NRE est parti.

Cela concernait le problème initial. Mais le code généré semble moche et sous-optimal. La vérification null de l'objet source peut entourer l'ensemble du bloc et les vérifications de conversion de type et de valeur ne peuvent être effectuées que lorsque cela est nécessaire. En prime, voici comment je l'écrirais:

Expression blockExp = Expression.Block(new[] { sourceInstance, targetInstance }, statements);

qui génère un code plus C #:

Expression blockExp = Expression.Block(new[] { sourceInstance, targetInstance }, statements);



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