展平嵌套对象

时间:2018-05-15 02:14:16

标签: c# automapper

我使用的是AutoMapper 6.2.2,我有两个共享Id属性的源模型:

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

我希望FlatDto.Id来自Outer,其他参数全部来自名称。在这种情况下,AutoMapper的惯例非常清楚,但我无法修改这些属性。它目前针对每个dest属性显式映射ForMember。类似question的解决方案实际上甚至更长。

对于这种情况,是否存在更优雅的解决方案,其中两个模型都包含多个字段且只有一个重叠且需要显式处理?

1 个答案:

答案 0 :(得分:-1)

最简单的解决方案(即使您将来修改了Outer / Inner也没有更改代码)是:

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

请注意:

  1. 如果您使用实例映射器而不是静态.Map方法&#34; global&#34;则需要更改映射器。

  2. InnerOuter中具有相同名称的属性将被映射两次,Outer具有更高的优先级,请注意可能的副作用。

  3. 编辑由于您使用的是实例映射器和配置文件,因此无法在配置文件中访问实例IMapper,我们需要注册动态映射。以下代码,就像问题中的代码片段一样,基本上使用.ForMember,并在动态表达式中构建参数。

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

    看起来非常庞大的代码,但事实上你可以(并且应该)将get属性/方法片段放在其他地方,foreach循环本身使用它们来构建一个表达式来调用。它非常干净有效。