更好的使用AutoMapper扁平化嵌套对象的方法?

32

我一直在将领域对象展开为DTO,如下所示的示例:

public class Root
{
    public string AParentProperty { get; set; }
    public Nested TheNestedClass { get; set; }
}

public class Nested
{
    public string ANestedProperty { get; set; }
}

public class Flattened
{
    public string AParentProperty { get; set; }
    public string ANestedProperty { get; set; }
}

// I put the equivalent of the following in a profile, configured at application start
// as suggested by others:

Mapper.CreateMap<Root, Flattened>()
      .ForMember
       (
          dest => dest.ANestedProperty
          , opt => opt.MapFrom(src => src.TheNestedClass.ANestedProperty)
       );

// This is in my controller:
Flattened myFlattened = Mapper.Map<Root, Flattened>(myRoot);

我查看了很多示例,目前似乎这是展开嵌套层次结构的方法。但是,如果子对象有许多属性,则此方法并没有节省太多编码。

我找到了这个例子:

http://consultingblogs.emc.com/owainwragg/archive/2010/12/22/automapper-mapping-from-multiple-objects.aspx

但它需要映射对象的实例,由Map()函数所需,这在我理解中无法与配置文件一起使用。

我是AutoMapper的新手,想知道是否有更好的方法来做到这一点。


8个回答

21

我更喜欢避免使用较旧的静态方法,而是像这样做。

将我们的映射定义放入一个Profile中。我们首先映射根,然后再应用嵌套的映射。请注意上下文的使用。

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<Root, Flattened>()
            .AfterMap((src, dest, context) => context.Mapper.Map(src.TheNestedClass, dest));
        CreateMap<Nested, Flattened>();
    }
}

定义从RootFlattened的映射和从NestedFlattened的映射的好处在于您可以完全掌控属性的映射,例如目标属性名称不同或者您想应用转换等。

XUnit测试:

[Fact]
public void Mapping_root_to_flattened_should_include_nested_properties()
{
    // ARRANGE
    var myRoot = new Root
    {
        AParentProperty = "my AParentProperty",
        TheNestedClass = new Nested
        {
            ANestedProperty = "my ANestedProperty"
        }
    };

    // Manually create the mapper using the Profile
    var mapper = new MapperConfiguration(cfg => cfg.AddProfile(new MappingProfile())).CreateMapper();

    // ACT
    var myFlattened = mapper.Map<Root, Flattened>(myRoot);

    // ASSERT
    Assert.Equal(myRoot.AParentProperty, myFlattened.AParentProperty);
    Assert.Equal(myRoot.TheNestedClass.ANestedProperty, myFlattened.ANestedProperty);
}

通过在启动中添加AutoMapper.Extensions.Microsoft.DependencyInjection nuget包中的serviceCollection.AddAutoMapper(),将自动获取Profile,并且您可以在应用映射的任何地方简单地注入IMapper。


AfterMap 不再接受上下文。我们可能需要使用 IMappingAction: https://docs.automapper.org/en/stable/Before-and-after-map-actions.html - David Liang

17
在最新版本的AutoMapper中,有一种命名约定可以使用,以避免多个 .ForMember 语句。
在您的示例中,如果您将 Flattened 类更新为:
public class Flattened
{
    public string AParentProperty { get; set; }
    public string TheNestedClassANestedProperty { get; set; }
}

您可以避免使用 ForMember 语句:

Mapper.CreateMap<Root, Flattened>();

在这种情况下,Automapper 将(按照惯例)将 Root.TheNestedClass.ANestedProperty 映射到 Flattened.TheNestedClassANestedProperty。当你使用真实的类名时,它看起来不那么丑陋,诚实地说!


12
以Automapper为基础命名我的视图模型属性并不是我想做的事情。我感谢这个答案,但是这种解决方案的副作用会让我使用原来问题中的技术。 - Choco

5

还有两种可能的解决方案:

Mapper.CreateMap<Nested, Flattened>()
    .ForMember(s=>s.AParentProperty, o=>o.Ignore());
Mapper.CreateMap<Root, Flattened>()
    .ForMember(d => d.ANestedProperty, o => o.MapFrom(s => s.TheNestedClass));

另一种方法如下,但它不能通过Mapper.AssertConfigurationIsValid()。
Mapper.CreateMap<Nested, Flattened>()
//.ForMember map your properties here
Mapper.CreateMap<Root, Flattened>()
//.ForMember... map you properties here
.AfterMap((s, d) => Mapper.Map(s.TheNestedClass, d));

1
不错的方法;可惜在此配置上调用 Mapper.AssertConfigurationIsValid(); 会失败并出现两个错误(创建了两个映射,但都没有完全覆盖目标类型的属性)。 - Cristian Diaconescu
6
如果嵌套类中有很多字段,我不确定这样做如何解决问题?如果Flattened对象中有多个属性应该从Nested对象中映射,那么OP希望避免添加多个"For destination Member"语句。 - Peter McEvoy
这只是一种替代命名约定的方法。并不是每次都有可能将属性名称更改/重构以符合AutoMapper的命名约定。 - Alex M
第二遍使用 .AfterMap 步骤正是我所需要的 - 谢谢! - jocull

4

不确定这是否为之前的解决方案增加了价值,但是您可以将其作为两步映射来完成。如果父项和子项之间存在名称冲突,请注意按正确顺序进行映射(后者优先)。

        Mapper.CreateMap<Root, Flattened>();
        Mapper.CreateMap<Nested, Flattened>();

        var flattened = new Flattened();
        Mapper.Map(root, flattened);
        Mapper.Map(root.TheNestedClass, flattened);

2
为了改进另一个答案,需要在两个映射中指定MemberList.Source并设置嵌套属性为被忽略。这样验证就能够通过了。
Mapper.Initialize(cfg =>
{
    cfg.CreateMap<SrcNested, DestFlat>(MemberList.Source);
    cfg.CreateMap<SrcRoot, DestFlat>(MemberList.Source)
        .ForSourceMember(s => s.Nested, x => x.Ignore())
        .AfterMap((s, d) => Mapper.Map(s.Nested, d));
});

Mapper.AssertConfigurationIsValid();

var dest = Mapper.Map<SrcRoot, DestFlat>(src);

1

我编写了扩展方法来解决类似的问题:

public static IMappingExpression<TSource, TDestination> FlattenNested<TSource, TNestedSource, TDestination>(
    this IMappingExpression<TSource, TDestination> expression,
    Expression<Func<TSource, TNestedSource>> nestedSelector,
    IMappingExpression<TNestedSource, TDestination> nestedMappingExpression)
{
    var dstProperties = typeof(TDestination).GetProperties().Select(p => p.Name);

    var flattenedMappings = nestedMappingExpression.TypeMap.GetPropertyMaps()
                                                    .Where(pm => pm.IsMapped() && !pm.IsIgnored())
                                                    .ToDictionary(pm => pm.DestinationProperty.Name,
                                                                    pm => Expression.Lambda(
                                                                        Expression.MakeMemberAccess(nestedSelector.Body, pm.SourceMember),
                                                                        nestedSelector.Parameters[0]));

    foreach (var property in dstProperties)
    {
        if (!flattenedMappings.ContainsKey(property))
            continue;

        expression.ForMember(property, opt => opt.MapFrom((dynamic)flattenedMappings[property]));
    }

    return expression;
}

在你的情况下,它可以这样使用:

var nestedMap = Mapper.CreateMap<Nested, Flattened>()
                      .IgnoreAllNonExisting();

Mapper.CreateMap<Root, Flattened>()
      .FlattenNested(s => s.TheNestedClass, nestedMap);

IgnoreAllNonExisting() 来自 这里

尽管它不是通用解决方案,但对于简单情况应该足够了。

因此,

  1. 您无需遵循目标属性中的展开约定
  2. Mapper.AssertConfigurationIsValid() 将通过
  3. 您也可以在非静态 API(即 Profile)中使用此方法

2
我无法在最新版本的AutoMapper中使其工作。MappingExpression.TypeMap不再可用。 - Rudey

0

我使用AutoMapper的新命名约定规则创建了一个简单的示例,用于从扁平对象映射到嵌套对象,希望这能帮到你。

https://dotnetfiddle.net/i55UFK


0
对于那些偶然来到这里寻找一种同时映射嵌套对象属性并利用属性映射的方法的人,我希望你能从以下内容中获得有价值的东西。
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class NestedSourceMemberAttribute : Attribute, IMemberConfigurationProvider
{
    public string? PropertyName { get; }

    public NestedSourceMemberAttribute(params string[] nestedParams) {
        PropertyName = string.Join('.', nestedParams);
    }

    public void ApplyConfiguration(IMemberConfigurationExpression memberConfigurationExpression)
    {
        memberConfigurationExpression.MapFrom(PropertyName);
    }
}

使用方法如下:
    [NestedSourceMember(nameof(ParentEntity.NestedEntity), nameof(NestedEntity.Property))]
    public string? PropertyValue { get; set; }

利用params string[]可以实现任意深度的映射,只需继续添加所需深度的参数即可。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接