【问题标题】:Flatten nested object展平嵌套对象
【发布时间】:2018-10-24 18:25:07
【问题描述】:

我正在使用 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 的解决方案实际上更长。

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

【问题讨论】:

  • 那么你想要InnerOuter 里面吗?你想用FlatDto做什么?
  • 源模型是外部模型和内部模型,我将它们展平为 FlatDto 的一个实例。
  • 如果你能提供一个minimal reproducible example 带有样本输入和基于这些样本输入的预期输出,那就太棒了。
  • this。您链接到的问题中也有类似的东西。
  • 问题已更新为包含一个工作示例。

标签: c# automapper


【解决方案1】:

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

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

请注意:

  1. 如果您使用实例映射器而不是“全局”映射器的静态.Map 方法,则需要进行更改。

  2. InnerOuter 中同名的属性会被映射两次,Outer 的优先级更高,小心可能产生的副作用。

编辑由于您使用的是实例映射器和配置文件,因此无法在配置文件中访问实例IMapper,我们需要动态注册映射。下面的代码,就像问题中的代码 sn-p 一样,本质上使用了.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 属性/方法 sn-p 放在其他地方,foreach 循环本身使用它们来构建要调用的表达式。它非常干净有效。

【讨论】:

  • 如何使用实例 API 在配置文件中执行此操作?
猜你喜欢
  • 2017-07-02
  • 2020-12-22
  • 2021-10-19
  • 2020-06-23
  • 2012-05-29
  • 2017-10-19
  • 2019-04-14
  • 2020-03-02
相关资源
最近更新 更多