Field mapping using LINQ expressions

May 31, 2011 at 2:33 PM

Hi,

Our current architecture has a lot of DTO classes that should be mapped. Now this is achieved via manual mapping and I'm going to involve automatic mapping library here. But our DTO's have different names, so we cannot use automatic mapping. Custom field mapping using field names as strings is the way I would like to avoid.

Why not to involve something like this to emit mapper:

class FieldMapper<T1, T2>
{
    public FieldMapper<T1, T2> SetMatch(Expression<Func<T1, object>> field1,
                                        Expression<Func<T2, object>> field2)
    {
        // TODO: parse expressions, get field names and add match pattern

        return this; // To support method chaining
    }
}

Then we could use it following way:

 

var fieldMatcher =
    new FieldMapper<MyClass1, MyClass2>()
        .SetMatch(a => a.Field, b => b.TheField)
        .SetMatch(a => a.TimeStmp, b => b.TimeStamp);

class MyClass1
{
    public int Field { get; set; }
    public DateTime TimeStmp { get; set; }
}

class MyClass2
{
    public int TheField { get; set; }
    public DateTime TimeStamp { get; set; }
}


 


Jun 6, 2011 at 4:25 PM

I too would see this as a very valuable feature, I know you can expand on your matches example from the documentation, but that is not type safe, and I would prefer some sort of Fluent API, like suggested above

Jul 13, 2011 at 2:35 PM
Edited Jul 13, 2011 at 2:36 PM

check this out, I wrote something like this and this could be a part of EM as you said. I think it solves problem of mapping members using expressions :)

 

 

    public class ExpressionsMapConfig<TFrom, TTo> : DefaultMapConfig
    {
        public class MemberMapper
        {
            private Expression<Func<string, string, bool>> mappingExpression = (f, t) => false;

            public Func<string, string, bool> GetMappingExpression()
            {
                return mappingExpression.Compile();
            }

            public MemberMapper Members(Expression<Func<TFrom, object>> field1, Expression<Func<TTo, object>> field2)
            {
                if (field1 == null)
                {
                    throw new ArgumentNullException("field1", "field1 is null.");
                }
                if (field2 == null)
                {
                    throw new ArgumentNullException("field2", "field2 is null.");
                }

                string field1Name, field2Name;

                field1Name = GetMemberName(field1);
                field2Name = GetMemberName(field2);

                var parameters = mappingExpression.Parameters;

                var fParameterExpression = parameters[0];
                var tParameterExpression = parameters[1];

                var newBody = Expression.Or(
                    mappingExpression.Body,
                    Expression.And(
                        Expression.Equal(fParameterExpression, Expression.Constant(field1Name)),
                        Expression.Equal(tParameterExpression, Expression.Constant(field2Name))
                    )
                );

                mappingExpression = Expression.Lambda<Func<string, string, bool>>(newBody, fParameterExpression, tParameterExpression);

                return this;
            }
        }

        private static string GetMemberName(LambdaExpression lambda)
        {
            if (lambda == null)
            {
                throw new ArgumentNullException("lambda", "lambda is null.");
            }
            string memberName;

            Expression lambdaBody = lambda.Body;

            if (lambdaBody.NodeType == ExpressionType.Convert)
            {
                var convert = (UnaryExpression)lambdaBody;
                lambdaBody = convert.Operand;
            }

            if (lambdaBody.NodeType != ExpressionType.MemberAccess)
            {
                throw new ArgumentException("MemberExpression required in lambda body", "lambda");
            }

            memberName = ((MemberExpression)lambdaBody).Member.Name;

            return memberName;
        }

        public ExpressionsMapConfig<TFrom, TTo> MatchMembers(Action<MemberMapper> map)
        {
            if (map == null)
            {
                throw new ArgumentNullException("map", "map is null.");
            }

            var mapper = new MemberMapper();

            map(mapper);

            return (ExpressionsMapConfig<TFrom, TTo>)this.MatchMembers(mapper.GetMappingExpression());
        }
    }
    public class EmitMapperExtensionsTest
    {

        public void Test()
        {
            var newDefaultMapConfig = new ExpressionsMapConfig<MyClass1, MyClass2>();

            newDefaultMapConfig.MatchMembers(map => map
                .Members(f => f.MyProperty1, t => t.CoolStuffWithMapping)
                .Members(f => f.MyProperty2, t => t.MultipleMembersUsingExpressions)
            );

            MyClass1 input = new MyClass1() { MyProperty1 = 5, MyProperty2 = "aaaa" };

            var output = EmitMapper.ObjectMapperManager.DefaultInstance.GetMapper<MyClass1, MyClass2>(newDefaultMapConfig).Map(input);

            if (input.MyProperty1 != output.CoolStuffWithMapping)
            {
                throw new Exception("Error");
            }
            if (input.MyProperty2 != output.MultipleMembersUsingExpressions)
            {
                throw new Exception("Error");
            }
        }


        public class MyClass1
        {
            public int MyProperty1 { get; set; }
            public string MyProperty2 { get; set; }
        }
        public class MyClass2
        {
            public int CoolStuffWithMapping { get; set; }
            public string MultipleMembersUsingExpressions { get; set; }

        }
    }

Jul 17, 2011 at 3:25 PM

Here is one more AutoMapper-like solution (you can use more comlex mapping, not only field to field mapping):

 

    public class ExtDefaultMapConfig<TSrc, TDst> : DefaultMapConfig
    {
        private readonly Dictionary<stringFunc<TSrc, object>> _properties = new Dictionary<stringFunc<TSrc, object>>();
 
        public ExtDefaultMapConfig<TSrc, TDst> ForMember(string property, Func<TSrc, object> func)
        {
            if (!_properties.ContainsKey(property))
                _properties.Add(property, func);
            return this;
        }
 
        public ExtDefaultMapConfig<TSrc, TDst> ForMember(Expression<Func<TDst, object>> dstMember, Func<TSrc, object> func)
        {
            var prop = ReflectionHelper.FindProperty(dstMember);
            return ForMember(prop.Name, func);
        }
 
        public ExtDefaultMapConfig<TSrc, TDst> Ignore(Expression<Func<TDst, object>> dstMember)
        {
            var prop = ReflectionHelper.FindProperty(dstMember);
            IgnoreMembers<TSrc, TDst>(new[] { prop.Name });
            return this;
        }
 
        public override IMappingOperation[] GetMappingOperations(Type from, Type to)
        {
            var list = new List<IMappingOperation>();
            list.AddRange(base.GetMappingOperations(from, to));
            list.AddRange(
                    FilterOperations(
                        from,
                        to,
                        ReflectionUtils.GetPublicFieldsAndProperties(to)
                        .Where(f => _properties.ContainsKey(f.Name))
                        .Select(
                            m =>
                            (IMappingOperation)new DestWriteOperation
                            {
                                Destination = new MemberDescriptor(m),
                                Getter =
                                    (ValueGetter<object>)
                                    (
                                        (value, state) =>
                                            {
                                                Debug.WriteLine(string.Format("Mapper: getting value of field or property {0}", m.Name));
                                                return ValueToWrite<object>.ReturnValue(_properties[m.Name]((TSrc) value));
                                            }
                                    )
                            }
                        )
                    )
                );
 
            return list.ToArray();
        }
    }

    class ReflectionHelper
    {
        public static MemberInfo FindProperty(LambdaExpression lambdaExpression)
        {
            Expression expression = lambdaExpression;
            bool flag = false;
            while (!flag)
            {
                switch (expression.NodeType)
                {
                    case ExpressionType.Convert:
                        expression = ((UnaryExpression)expression).Operand;
                        break;
                    case ExpressionType.Lambda:
                        expression = ((LambdaExpression)expression).Body;
                        break;
                    case ExpressionType.MemberAccess:
                        MemberExpression memberExpression = (MemberExpression)expression;
                        if (memberExpression.Expression.NodeType != ExpressionType.Parameter && memberExpression.Expression.NodeType != ExpressionType.Convert)
                            throw new ArgumentException(string.Format("Expression '{0}' must resolve to top-level member.", lambdaExpression), "lambdaExpression");
                        return memberExpression.Member;
                    default:
                        flag = true;
                        break;
                }
            }
            return null;
        }
 
        public static object GetValue(string property, object obj)
        {
            PropertyInfo pi = obj.GetType().GetProperty(property);
            return pi.GetValue(obj, null);
        }
    }   

 Tests:

    [TestClass]
    public class ExtDefaultMapConfigTest
    {
        const string SoMuch = "So much?";
        const string ItSnOtEnought = "It's not enought :(";
        const string Yes = "Yes";
        const string No = "No";
 
        private static ObjectMapperManager MapperManager
        {
            get { return ObjectMapperManager.DefaultInstance;}
        }
 
        [TestMethod]
        public void Mapping1Test()
        {
            ExtDefaultMapConfig<Entity1DtoEntity1> extd =
                    new ExtDefaultMapConfig<Entity1DtoEntity1>()
                        .ForMember(e => e.Money, p => p.Money > 100000 ? SoMuch : ItSnOtEnought)
                        .ForMember(e => e.Created, p => p.Created.ToShortDateString())
                        .ForMember("ChildID", p => p.Child.ID)
                        .ForMember("ChildName", p => p.Child.Name.ToUpper())
                        .ForMember("ChildIsActive", p => p.Child.IsActive ? Yes: No);
 
            DateTime created = new DateTime(2011, 1, 1);
            Entity1 entity = new Entity1
                                 {
                                     ID = 1,
                                     Name = "Entity1",
                                     Money = 100001,
                                     Created = created,
                                     Child = new ChildEntity1
                                                 {
                                                     ID = 2,
                                                     Name = "ChildEntity1",
                                                     IsActive = true
                                                 }
                                 };
 
            var mapper = MapperManager.GetMapper<Entity1DtoEntity1>(extd);
            DtoEntity1 dto = mapper.Map(entity);
 
            Assert.AreEqual(entity.ID, dto.ID);
            Assert.AreEqual(entity.Name, dto.Name);
            Assert.AreEqual(SoMuch, dto.Money);
            Assert.AreEqual(created.ToShortDateString(), dto.Created);
            Assert.AreEqual(entity.Child.ID, dto.ChildID);
            Assert.AreEqual(entity.Child.Name.ToUpper(), dto.ChildName);
            Assert.AreEqual(Yes, dto.ChildIsActive);
 
        }
    }

public class Entity1     {         public int ID { getset; }         public string Name { getset; }         public double Money { getset; }         public DateTime Created { getset; }         public ChildEntity1 Child { getset; }     }
    public class ChildEntity1
    {
        public int ID { getset; }
        public string Name { getset; }
        public bool IsActive { getset; }
    } 
    public class DtoEntity1
    {
        public int ID { getset; }
        public string Name { getset; }
        public string Money { getset; }
        public string Created { getset; }
 
        public int ChildID { getset; }
        public string ChildName { getset; }
        public string ChildIsActive { getset; }
 
    }