AutoMapper Madness - ReverseMap and Config Validation

December 12, 2022#Software Development
Article
Author image.

Scott DePouw, Senior Consultant

This is part of my blog post series on AutoMapper.

This was something I stumbled across in my work lately, which spurred this unexpected fourth part to this series! ReverseMap(), useful as it is, has a caveat or two when trying to use it. While it works just fine when working with simple types that require little configuration, wanting to configure your maps and reverse maps separately can cause some unexpected behavior when trying to validate configurations.

Configuring Reverse Mappings

As it turns out, you can chain configurations together after you call ReverseMap(), in order to configure said reverse mapping! Let’s set up a model, a DTO, and a basic AutoMapper Profile class to do this:

namespace MyApp.Core;

public class Foo
{
  public Guid Id { get; set; }
  
  public string Some { get; set; }
  public int Common { get; set; }
  public string Properties { get; set; }
  
  // No DTOExclusive Property
}
namespace MyApp.Core;

public class FooDTO
{
  // No Id
  
  public string Some { get; set; }
  public int Common { get; set; }
  public string Properties { get; set; }

  public string DTOExclusiveProperty { get; set; }
}
namespace MyApp.Core.AutoMapperProfiles;

public class FooProfile : Profile
{
  CreateMap<Foo, FooDTO>().ReverseMap();
}

Running Some Tests

With our code in place, we’ll whip up a few quick unit tests to assert that we can call AutoMapper to map between Foo and FooDTO in either direction, and assert that our AutoMapper configuration is valid (i.e. that of the types we told AutoMapper to map, we didn’t miss out on any properties).

using AutoMapper;
using MyApp.Core;
using MyApp.Core.AutoMapperProfiles;
using Xunit;

namespace MyApp.UnitTests;

public class AutoMapper
{
    private readonly MapperConfiguration _config = new(cfg => cfg.AddProfile(new FooProfile()));

    [Fact] public void MustBeValid() => _config.AssertConfigurationIsValid();
    [Fact] public void SupportsMappingFooToFooDTO() => _config.CreateMapper().Map<FooDTO>(new Foo());
    [Fact] public void SupportsMappingFooDTOToFoo() => _config.CreateMapper().Map<Foo>(new FooDTO());
}

If we run this immediately, all three tests fail. If we add the mappings to our FooProfile as defined above, two tests pass but MustBeValid fails:

Unmapped members were found. Review the types and members below.
Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type
For no matching constructor, add a no-arg ctor, add optional arguments, or map all of the constructor parameters
====================================
Foo -> FooDTO (Destination member list)
MyApp.Core.Foo -> MyApp.Core.FooDTO (Destination member list)

Unmapped properties:
DTOExclusiveProperty

   at MyApp.UnitTests.AutoMapper.MustBeValid() in C:\dev\MyApp\src\MyApp.UnitTests\AutoMapper.cs:line 12

That’s good, right? We didn’t tell AutoMapper what to do with the destination member property of DTOExclusiveProperty when going from Foo to FooDTO, and the reverse with Foo.Id!

But wait, where’s the unmapped property Id? It’s not in the test result! Indeed, if we fix the listed unmapped property without fixing the reverse map, our tests all pass. Oops!

CreateMap<Foo, FooDTO>()
  .ForMember(dest => dest.DTOExclusiveProperty, opt => opt.Ignore())
  .ReverseMap();

ReverseMap Configs Are Not Tested By AssertConfigurationIsValid()!

Turns out, reverse maps aren’t validated or checked by AutoMapper! This is by design with the author himself, Jimmy Bogard.

ReverseMap is different, it assumes you’re literally flattening and un-flattening. For un-flattening, you’d want to assert that the source side is all mapped, not the destination. Reverse map doesn’t assume you want to assert anything, since you’ve already validated the Source -> Destination mapping.

In short, ReverseMap is now “special” and not merely a short cut for two CreateMap calls.

As he points out, ReverseMap() is not syntax sugar over two distinct CreateMap() calls, and so its behavior is different than expected.

Fixing Our Code to Validate All the Things

So what should we do to make sure we don’t accidentally skip any mappings? We have two possible solutions.

Solution 1: Create Two Distinct Mappings

Rather than lumping our two maps together with a ReverseMap(), let’s create two separate mapping setups instead.

CreateMap<Foo, FooDTO>();
CreateMap<FooDTO, Foo>();

Upon running the tests, we now get the expected error when AssertConfigurationIsValid() fails, reporting two properties instead of one.

...

Unmapped properties:
DTOExclusiveProperty
====================================
FooDTO -> Foo (Destination member list)
MyApp.Core.FooDTO -> MyApp.Core.Foo (Destination member list)

Unmapped properties:
Id

...

Then when we fix our mappings, all tests pass!

CreateMap<Foo, FooDTO>().ForMember(dest => dest.DTOExclusiveProperty, opt => opt.Ignore());
CreateMap<FooDTO, Foo>().ForMember(dest => dest.Id, opt => opt.Ignore());

Solution 2: ValidateMemberList()

We can alter how AutoMapper validates properties for reverse maps by calling ValidateMemberList():

CreateMap<Foo, FooDTO>()
  .ReverseMap()
  .ValidateMemberList(MemberList.Source);

This will make our unit test correctly fail, reporting both properties, as in the first solution. And like in the first solution, if we provide proper mappings, the test passes.

CreateMap<Foo, FooDTO>()
  .ForMember(dest => dest.DTOExclusiveProperty, opt => opt.Ignore())
  .ReverseMap().ValidateMemberList(MemberList.Source)
    .ForMember(dest => dest.Id, opt => opt.Ignore());

You can even bake this extra step into an extension method!

public static class MappingExpressionExtensions
{
  /// <summary>
  /// Creates mapping from <typeparamref name="TDestination" /> to <typeparamref name="TSource"/>,
  /// enabling validation so that <see cref="MapperConfiguration.AssertConfigurationIsValid"/>
  /// correctly identifies any missed reverse mappings.
  /// </summary>
  public static IMappingExpression<TDestination, TSource> CreateValidatedReverseMap<TSource, TDestination>(
    this IMappingExpression<TSource, TDestination> mappingExpression)
  {
    return mappingExpression.ReverseMap().ValidateMemberList(MemberList.Source);
  }
}

// Back in our AutoMapper Profile
CreateMap<Foo, FooDTO>()
  .ForMember(dest => dest.DTOExclusiveProperty, opt => opt.Ignore())
  .CreateValidatedReverseMap()
    .ForMember(dest => dest.Id, opt => opt.Ignore());

Which is Better?

That’s up to you! You can potentially have less clutter by continuing to use ReverseMap(), or you can explicitly define every mapping that is different when coming instead of going by using two separate CreateMap() calls. Creating two separate calls is the more “pure” solution, that AutoMapper seems more geared towards. While tweaking ReverseMap() behavior does work, it does go against what Jimmy said, in that ReverseMap() isn’t intended to be a shortcut to a second CreateMap() call. Ultimately, the choice is yours!

When to Use ReverseMap() and Wrap-Up

With all of that said, when should we use ReverseMap() in the intended fashion? Essentially, use it when you can configure the initial mapping, and when the reverse mapping requires no customization of its own. If, for example, we had no properties unique between Foo and FooDTO, or if FooDTO had an extra property that the Foo to FooDTO mapping could be configured against, we’d be perfectly fine using ReverseMap(). Its intended use is for simple “do exactly what we did before, only in reverse” and not “when we reverse, go down this other mapping path.”

The goal at the end of the day is to make sure that our mappings are always properly validated so that we don’t potentially leave unmapped properties in our code, whether coming or going. Whichever solution we end up using will assure that the convenient method we’re using for AutoMapper to validate itself — AssertConfigurationIsValid() — is always getting the job done.


Copyright © 2024 NimblePros - All Rights Reserved