在相同属性上将一个对象映射到另一个对象的表达式

时间:2017-10-22 08:37:00

标签: c# linq lambda expression-trees dynamicmethod

我试图通过此代码使用Expression创建一个简单的映射器:

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

}

对我来说一切似乎都没问题,但是当我试图测试它时,我只得到一个空引用异常。测试对象和方法:

public class UserEntity {

    public string Name { get; set; }
    public string Family { get; set; }
    public int Age { get; set; }
    public string Nickname { get; set; }

}

public class UserModel {

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public string Nickname { get; set; }

}

public static class CallTest {

    public static void Call() {
        var entity = new UserEntity {
            Name="Javad",
            Family="Amiry",
            Age = 25,
            Nickname = "my nickname is here",
        };
        var model = new UserModel();

        var map1 = new PropertyMap {
            SourceProperty = entity.GetType().GetProperty("Age"),
            TargetProperty = model.GetType().GetProperty("Age"),
        };
        var map2 = new PropertyMap {
            SourceProperty = entity.GetType().GetProperty("Nickname"),
            TargetProperty = model.GetType().GetProperty("Nickname"),
        };

        var action = MyUtility.BuildMapAction<UserEntity, UserModel>(new[] {map1, map2});
        action(entity, model); // here I get the error System.NullReferenceException: 'Object reference not set to an instance of an object.'
    }

}

你知道那里发生了什么吗?我错过了什么?

注意:我无法使用第三方映射器(如AutoMapper)

1 个答案:

答案 0 :(得分:6)

问题是由这一行引起的:

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

使用的Expression.Block重载的第一个参数表示块的局部变量。通过传递lambda参数,您只需定义2个本地未分配变量,从而在执行时定义NRE。你可以通过检查VS locals / watch窗口中的lambda表达式DebugView来看到,在你的示例调用中看起来像这样:

.Lambda #Lambda1<System.Action`2[ConsoleApp3.UserEntity,ConsoleApp3.UserModel]>(
    ConsoleApp3.UserEntity $source,
    ConsoleApp3.UserModel $target) {
    .Block(
        ConsoleApp3.UserEntity $source,
        ConsoleApp3.UserModel $target) {
        .If (
            $source != null & .Call $source.get_Age() .As System.Object != null
        ) {
            .Call $target.set_Age((System.Int32).Call $source.get_Age())
        } .Else {
            .Default(System.Void)
        };
        .If (
            $source != null & .Call $source.get_Nickname() .As System.Object != null
        ) {
            .Call $target.set_Nickname((System.String).Call $source.get_Nickname())
        } .Else {
            .Default(System.Void)
        }
    }
}

请注意在块内重新定义sourcetarget

使用正确的重载后:

Expression blockExp = Expression.Block(statements);

视图现在是这样的:

.Lambda #Lambda1<System.Action`2[ConsoleApp3.UserEntity,ConsoleApp3.UserModel]>(
    ConsoleApp3.UserEntity $source,
    ConsoleApp3.UserModel $target) {
    .Block() {
        .If (
            $source != null & .Call $source.get_Age() .As System.Object != null
        ) {
            .Call $target.set_Age((System.Int32).Call $source.get_Age())
        } .Else {
            .Default(System.Void)
        };
        .If (
            $source != null & .Call $source.get_Nickname() .As System.Object != null
        ) {
            .Call $target.set_Nickname((System.String).Call $source.get_Nickname())
        } .Else {
            .Default(System.Void)
        }
    }
}

并且NRE已经消失。

那是关于原始问题的。但生成的代码看起来很丑陋而且不是最理想的。源对象null检查可以围绕整个块,并且只有在需要时才能执行类型转换和值null检查。作为奖励,这是我写它的方式:

public static Action<TSource, TTarget> BuildMapAction<TSource, TTarget>(IEnumerable<PropertyMap> properties)
{
    var source = Expression.Parameter(typeof(TSource), "source");
    var target = Expression.Parameter(typeof(TTarget), "target");

    var statements = new List<Expression>();
    foreach (var propertyInfo in properties)
    {
        var sourceProperty = Expression.Property(source, propertyInfo.SourceProperty);
        var targetProperty = Expression.Property(target, propertyInfo.TargetProperty);
        Expression value = sourceProperty;
        if (value.Type != targetProperty.Type)
            value = Expression.Convert(value, targetProperty.Type);
        Expression statement = Expression.Assign(targetProperty, value);
        // for class/interface or nullable type
        if (!sourceProperty.Type.IsValueType || Nullable.GetUnderlyingType(sourceProperty.Type) != null)
        {
            var valueNotNull = Expression.NotEqual(sourceProperty, Expression.Constant(null, sourceProperty.Type));
            statement = Expression.IfThen(valueNotNull, statement);
        }
        statements.Add(statement);
    }

    var body = statements.Count == 1 ? statements[0] : Expression.Block(statements);
    // for class.interface type
    if (!source.Type.IsValueType)
    {
        var sourceNotNull = Expression.NotEqual(source, Expression.Constant(null, source.Type));
        body = Expression.IfThen(sourceNotNull, body);
    }

    // not sure about the need of this
    if (body.CanReduce)
        body = body.ReduceAndCheck();
    body = body.ReduceExtensions();

    var lambda = Expression.Lambda<Action<TSource, TTarget>>(body, source, target);

    return lambda.Compile();
}

生成更多C#外观代码:

.Lambda #Lambda1<System.Action`2[ConsoleApp3.UserEntity,ConsoleApp3.UserModel]>(
    ConsoleApp3.UserEntity $source,
    ConsoleApp3.UserModel $target) {
    .If ($source != null) {
        .Block() {
            $target.Age = $source.Age;
            .If ($source.Nickname != null) {
                $target.Nickname = $source.Nickname
            } .Else {
                .Default(System.Void)
            }
        }
    } .Else {
        .Default(System.Void)
    }
}