32

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

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

I have looked at a number of examples, and so far this seems to be the way to flatten a nested hierarchy. If the child object has a number of properties, however, this approach doesn't save much coding.

I found this example:

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

but it requires instances of the mapped objects, required by the Map() function, which won't work with a profile as I understand it.

I am new to AutoMapper, so I would like to know if there is a better way to do this.

user20358
  • 14,182
  • 36
  • 114
  • 186
John
  • 511
  • 1
  • 5
  • 10

8 Answers8

21

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.

Lee Oades
  • 1,638
  • 17
  • 24
  • `AfterMap` doesn't take the context anymore. We might have to use `IMappingAction`: https://docs.automapper.org/en/stable/Before-and-after-map-actions.html – David Liang Oct 10 '22 at 15:35
17

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!

superjos
  • 12,189
  • 6
  • 89
  • 134
Jag
  • 1,840
  • 1
  • 17
  • 25
  • 12
    naming my view model properties based on the use of Automapper is not something I would like to do. I appreciated the answer but the side effects of the solution would have me use the technique in the original question. – Choco Oct 18 '17 at 02:56
5

2 more possible solutions:

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

An alternative approach would be the below, but it would not pass the 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));
Alex M
  • 2,410
  • 1
  • 24
  • 37
  • 1
    Nice approach; too bad calling `Mapper.AssertConfigurationIsValid();` on this configuration fails with two errors (there are two maps created, none of which completely covers the destination type's properties) – Cristian Diaconescu Jun 17 '15 at 15:14
  • 6
    not sure how this solves the problem if there are a lot of fields in the nested class? The OP wants to avoid adding mulitple "For destination Member" statements if there are multiple properties in the Flattened object that should map from the Nested object. – Peter McEvoy Sep 23 '15 at 12:35
  • It is just an alternative to naming conventions approach. Not all the time one has a possibility to change/refactor property names all the way down to conform to naming conventions of AutoMapper. – Alex M Sep 25 '15 at 08:04
  • The 2nd pass with an `.AfterMap` step is what I needed -- thanks! – jocull Feb 10 '17 at 20:35
4

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);
toree
  • 399
  • 2
  • 8
2

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);
Mark Toman
  • 3,090
  • 2
  • 17
  • 18
1

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
Community
  • 1
  • 1
Alexey Merson
  • 414
  • 5
  • 12
  • 2
    I can't get this to work using the newest version of AutoMapper. MappingExpression.TypeMap isn't available anymore. – Rudey Jan 07 '18 at 17:01
0

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

bturner1273
  • 660
  • 7
  • 18
0

For anyone who stumbles here looking for a way to map Nested object properties while also leveraging Attribute Mapping, I am hoping you are able to pull something valuable from the following:

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

Usage is as follows:

    [NestedSourceMember(nameof(ParentEntity.NestedEntity), nameof(NestedEntity.Property))]
    public string? PropertyValue { get; set; }

Leveraging the params string[] allows for really any depth of mapping, just continue to add the params the deeper you need to go.

vandsh
  • 1,329
  • 15
  • 12