0

I am using AutoMapper 6.2.2, I have two source models that share an Id property:

using System.Diagnostics;
using AutoMapper;

public class Outer
{
    public int Id { get; set; }
    public string Foo { get; set; }
    public Inner Bar { get; set; }
}
public class Inner
{
    public int Id { get; set; }
    public string Baz { get; set; }
    public string Qux { get; set; }
    public string Bof { get; set; }
}
public class FlatDto
{
    public int Id { get; set; }
    public string Foo { get; set; }
    public string Baz { get; set; }
    public string Qux { get; set; }
    public string Bof { get; set; }
}
public class AutoMapperProfile : Profile
{
    public AutoMapperProfile()
    {
        this.CreateMap<Outer, FlatDto>()
            .ForMember(dst => dst.Id, opt => opt.MapFrom(s => s.Id))
            .ForMember(dst => dst.Foo, opt => opt.MapFrom(s => s.Foo))
            .ForMember(dst => dst.Baz, opt => opt.MapFrom(s => s.Bar.Baz))
            .ForMember(dst => dst.Qux, opt => opt.MapFrom(s => s.Bar.Qux))
            .ForMember(dst => dst.Bof, opt => opt.MapFrom(s => s.Bar.Bof));
    }
}
class Program
{
    static void Main(string[] args)
    {
        Outer model = new Outer
        {
            Id = 1,
            Foo = "FooString",
            Bar = new Inner
            {
                Id = 2,
                Baz = "BazString",
                Qux = "QuxString",
                Bof = "BofString"
            }
        };

        var config = new MapperConfiguration(cfg => cfg.AddProfiles(typeof(Program).Assembly));
        config.AssertConfigurationIsValid();
        IMapper mapper = new Mapper(config);

        FlatDto dto = mapper.Map<Outer, FlatDto>(model);
        Trace.Assert(model.Id == dto.Id);
        Trace.Assert(model.Foo == dto.Foo);
        Trace.Assert(model.Bar.Baz == dto.Baz);
        Trace.Assert(model.Bar.Qux == dto.Qux);
        Trace.Assert(model.Bar.Bof == dto.Bof);
    }
}

I want FlatDto.Id to come from Outer and the other parameters all by name. AutoMapper's convention in this case is pretty clear however I cannot modify these properties. It's currently mapped explicitly with ForMember for every dest property. The solution for a similar question actually is even longer.

Does a more elegant solution exist for this case where both models contain several fields and only one overlaps and requires explicit handling?

Ritmo2k
  • 993
  • 6
  • 17

1 Answers1

-1

The simplest solution (without changing code even you modified Outer/Inner in the future) is:

Mapper.Initialize(c =>
{
    c.CreateMap<Inner, FlatDto>();
    c.CreateMap<Outer, FlatDto>().BeforeMap((s, t) => Mapper.Map(s.Bar, t));
});

Be aware that:

  1. You need to change if you are using instance mappers instead of static .Map method of "global" mapper.

  2. Properties with same name in both Inner and Outer will be mapped twice, and the Outer has a higher priority, be careful with possible side effects.

EDIT Since you are using instance mappers and profiles, the instance IMapper can't be accessed inside a profile, we need to register the mappings dynamic. The following code, just like the code snippet in the question, essentially uses .ForMember, with the arguments built in dynamic expressions.

class TestProfile : Profile
{
    public TestProfile()
    {
        BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;
        Func<PropertyInfo, bool> filter = p => p.CanRead && p.CanWrite;

        var outerProperties = typeof(Outer).GetProperties(flags).Where(filter).ToDictionary(p => p.Name);
        var innerProperties = typeof(Inner).GetProperties(flags).Where(filter);
        var mappingProperties = innerProperties.Where(p => !outerProperties.ContainsKey(p.Name));

        //code above gets the properties of Inner that needs to be mapped

        var outerParameter = Expression.Parameter(typeof(Outer));
        var accessBar = Expression.Property(outerParameter, nameof(Outer.Bar));
        var map = CreateMap<Outer, FlatDto>();
        var mapExp = Expression.Constant(map);

        foreach (var property in mappingProperties)
        {
            var accessProperty = Expression.MakeMemberAccess(accessBar, property);
            var funcType = typeof(Func<,>).MakeGenericType(typeof(Outer), property.PropertyType);
            var funcExp = Expression.Lambda(funcType, accessProperty, outerParameter);
            //above code builds s => s.Bar.Qux

            var configType = typeof(IMemberConfigurationExpression<,,>).MakeGenericType(typeof(Outer), typeof(FlatDto), typeof(object));
            var configParameter = Expression.Parameter(configType);
            var mapFromMethod = configType
                .GetMethods()
                .Single(m => m.Name == "MapFrom" && m.IsGenericMethod)
                .MakeGenericMethod(property.PropertyType);
            var invokeMapFrom = Expression.Call(configParameter, mapFromMethod, funcExp);
            var configExp = Expression.Lambda(typeof(Action<>).MakeGenericType(configType), invokeMapFrom, configParameter);
            //above code builds opt => opt.MapFrom(s => s.Bar.Qux)

            var forMemberMethod = map.GetType()
                .GetMethods()
                .Single(m => m.Name == "ForMember" && !m.IsGenericMethod);
            var invokeForMember = Expression.Call(mapExp, forMemberMethod, Expression.Constant(property.Name), configExp);
            //above code builds map.ForMember("Qux", opt => opt.MapFrom(s => s.Bar.Qux))

            var configAction = Expression.Lambda<Action>(invokeForMember);
            configAction.Compile().Invoke();
        }
    }
}

Looks very huge code but in fact you can(and should) put the get property/method snippet somewhere else, the foreach loop itself uses them to build an expression to invoke. It's quite clean and effective.

Cheng Chen
  • 42,509
  • 16
  • 113
  • 174