Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Expression to mapping one object to another on same-properties

I'm trying to create a simple mapper using Expression by this 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;
    }

}

Everything seems OK to me, but when I'm trying to test it, I just get a null-reference exception. The test objects and method:

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.'
    }

}

Do you have any idea what's going on there? What I missed?


NOTE: I cannot use third-party mappers (like AutoMapper)

like image 981
amiry jd Avatar asked Feb 09 '26 19:02

amiry jd


1 Answers

The issue is caused by this line:

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

The first argument of the used Expression.Block overload represents the local variables of the block. By passing the lambda parameters there you just define 2 local unassigned variables, hence the NRE at execution time. You can see that by examining the lambda expression DebugView in VS locals/watch window, which in your sample call looks something like this:

.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)
        }
    }
}

Note the redefinition of source and target inside the block.

After using the correct overload:

Expression blockExp = Expression.Block(statements);

the view is now like this:

.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)
        }
    }
}

and the NRE is gone.

That was regarding the original issue. But the generated code looks ugly and suboptimal. The source object null check can be surrounding the whole block and the type conversion and value null checks can be performed only when needed. As a bonus, here is how I would write it:

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

which generates a more C# looking code:

.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)
    }
}
like image 123
Ivan Stoev Avatar answered Feb 12 '26 14:02

Ivan Stoev



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!