A better way to use AutoMapper to flatten nested objects?

前端 未结 7 1623
执笔经年
执笔经年 2020-12-28 15:02

I have been flattening domain objects into DTOs as shown in the example below:

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


        
相关标签:
7条回答
  • 2020-12-28 15:24

    Not sure if this adds value to the previous solutions, but you could do it as a two-step mapping. Be careful to map in correct order if there are naming conflicts between the parent and child (last wins).

            Mapper.CreateMap<Root, Flattened>();
            Mapper.CreateMap<Nested, Flattened>();
    
            var flattened = new Flattened();
            Mapper.Map(root, flattened);
            Mapper.Map(root.TheNestedClass, flattened);
    
    0 讨论(0)
  • 2020-12-28 15:25

    I much prefer avoiding the older Static methods and do it like this.

    Place our mapping definitions into a Profile. We map the Root first, and then apply the mappings of the Nested afterwards. Note the use of the Context.

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

    The advantage of defining both the mapping from Root to Flattened and Nested to Flatterned is that you retain full control over the mapping of the properties, such as if the destination property name is different or you want to apply a transformation etc.

    An XUnit test:

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

    By adding AutoMapper's serviceCollection.AddAutoMapper() from the AutoMapper.Extensions.Microsoft.DependencyInjection nuget package to your start up, the Profile will be picked up automatically, and you can simply inject IMapper into wherever you are applying the mapping.

    0 讨论(0)
  • 2020-12-28 15:30

    In the latest version of AutoMapper, there's a naming convention you can use to avoid multiple .ForMember statements.

    In your example, if you update your Flattened class to be:

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

    You can avoid the use of the ForMember statement:

    Mapper.CreateMap<Root, Flattened>();
    

    Automapper will (by convention) map Root.TheNestedClass.ANestedProperty to Flattened.TheNestedClassANestedProperty in this case. It looks less ugly when you're using real class names, honest!

    0 讨论(0)
  • 2020-12-28 15:31

    To improve upon another answer, specify MemberList.Source for both mappings and set the nested property to be ignored. Validation then passes OK.

    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);
    
    0 讨论(0)
  • 2020-12-28 15:33

    I wrote extension method to solve similar problem:

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

    So in your case it can be used like this:

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

    IgnoreAllNonExisting() is from here.

    Though it's not universal solution it should be enough for simple cases.

    So,

    1. You don't need to follow flattening convention in destination's properties
    2. Mapper.AssertConfigurationIsValid() will pass
    3. You can use this method in non-static API (a Profile) as well
    0 讨论(0)
  • 2020-12-28 15:38

    I created a simple example using AutoMappers new naming convention rules to map from flattened to nested objects, hope this helps

    https://dotnetfiddle.net/i55UFK

    0 讨论(0)
提交回复
热议问题