DEV Community

Cover image for Avoid breaking changes with AutoMapper's simple and awesome Custom Value Resolvers
Eliza Lus
Eliza Lus

Posted on • Updated on

Avoid breaking changes with AutoMapper's simple and awesome Custom Value Resolvers

Are you using AutoMapper to map model properties between your APIs? Do you need to adjust your models to match the changing domain logic and structure? And do you want to avoid breaking the application while doing so (without creating several mapping patches like in the picture above)? Then AutoMapper's Custom Value Resolvers are for you! Keep reading!

Background - description of our application

The architecture of our application is made of several services.

In every backend service, there are three main layers:

  • Api layer - responsible for communication with other APIs, validates incoming requests and models, maps Api models to Core models.
  • Core layer - contains models used in the rest of the solution.
  • Infrastructure layer - stores data in the database, contains mapping of Core and external models.

AutoMapper is used to map models across the layers, certain properties can be ignored or modified accordingly with the domain logic. For instance, if you add a new property you can keep mapping as it is, our Api, Core and Storage User models can be mapped in a couple of lines of code:

public class User {
    public Guid Id {get; set;}
    public string LastName {get; set;}
    public string Nationality {get; set;}
}
Enter fullscreen mode Exit fullscreen mode
public ApiModelMappingProfile()
    {
        CreateMap<ApiModel.User, CoreModel.User>().IncludeAllDerived();
    }
Enter fullscreen mode Exit fullscreen mode

Modifying the Api model while other APIs's models are not yet updated

At some point, it was necessary to not only store the Nationality of a user, but also to use the nationality's country code somewhere else in the application.
Instead of a primitive type string, we needed to create a model with Nationality name and its country code:

public class Country
{
    public string Name {get; set;}
    public string Code {get; set;}
}
Enter fullscreen mode Exit fullscreen mode
public class User {
    public Guid Id {get; set;}
    public string LastName {get; set;}
    public string Nationality {get; set;}
    //new property added
    public Country SecondNationality {get; set;}
    }
Enter fullscreen mode Exit fullscreen mode

The Core model was to only contain the SecondNationality property. Other APIs that still used the Nationality could derive it from the SecondNationality.Name:

 CreateMap<CoreModel.User, ApiModel.User>()
        .IncludeAllDerived()
        .ForMember(dest => dest.Nationality,
                    opt =>
                    {
                        opt.MapFrom((src, dest) => dest.Nationality =
                        {
                            src.SecondNationality.Name
                        });
                    });
Enter fullscreen mode Exit fullscreen mode

Eventually, all usages of Nationality were to be replaced with the new SecondNationality, at which point it would be renamed back to "Nationality", allowing us to make breaking changes without a lockstep deploy of our internal services. However, during the change it turned out that User should be able to have multiple nationalities. Yet another property was added: Nationalities - an Enumerable of Country.
And unfortunately, the first change was never fully finalized. We ended up with models with three properties of different type denominating the same value for the end user.
As a result, at a certain point, different APIs used Nationality, SecondNationality, Nationalities or a mix of these in their User models:

public class User {
    public Guid Id {get; set;}
    public string LastName {get; set;}
    public string Nationality {get; set;}
    public Country SecondNationality {get; set;}
    public List<Country> Nationalities {get; set;}
}
Enter fullscreen mode Exit fullscreen mode

We decided to clean up the models, remove the older properties and use only the Nationalities property.
But how to do it without breaking the whole application? Some APIs will send in requests with Nationality, other with SecondNationality and others with Nationalities or with a mix of those.
All these properties will have to saved as a single property - Nationalities.

Here is where AutoMapper's Custom Value Resolvers come in handy!

Custom Value Resolvers

The Resolver class has a single method Resolve which takes the source model, the destination model and the destination model's property as arguments.
It checks the source value, and resolves it to match the type of the destination member.
The below Resolver method can resolve value to be saved in the Core model, whether the source model contain SecondNationality or Nationalities:


public class NationalitiesResolverForUser : IValueResolver<ApiModel.User, CoreModel.User, IEnumerable<CoreModel.Country>>
{
    public IEnumerable<CoreModel.Country> Resolve(ApiModel.User source,
        CoreModel.User destination, IEnumerable<CoreModel.Country> destMember,
        ResolutionContext context)
    {
        CoreModel.Country[] resolved = { };

        if (source.Nationalities != null)
        {
            resolved = source.Nationalities
                .Select(Country => new CoreModel.Country(Country.Code, Country.Name)).ToArray();
        }
        else if (source.SecondNationality != null)
        {
            resolved = new []
            {
                new CoreModel.Country(source.SecondNationality.Code, source.SecondNationality.Name)
            };
        }

        return resolved;
    }
Enter fullscreen mode Exit fullscreen mode

The Resolver can be called in the Api Mapping profile like this:

CreateMap<ApiModel.User, CoreModel.User>()
            .ForMember(dest => dest.Nationalities,
                opt =>
                {
                    opt.MapFrom<NationalitiesResolverForUser>();
                });
            .ReverseMap().IncludeAllDerived();
Enter fullscreen mode Exit fullscreen mode

Because some APIs still use only SecondNationality and other use Nationalities, we need to map the core's Nationalities list to both properties.
We can use more value resolvers:

SecondNationality resolver:

public class SecondNationalityResolver : IValueResolver<CoreModel.User,
    ApiModel.User, ApiModel.Country>
{
    public ApiModel.Country Resolve(CoreModel.User source,
        ApiModel.User destination, ApiModel.Country destMember,
        ResolutionContext context)
    {
        var firstNationality = source.Nationalities?.FirstOrDefault();

        return firstNationality == null
            ? null
            : new ApiModel .Country(firstNationality?.Code, firstNationality?.Naam,
                firstNationality?.WeergaveNaam);
    }
}
Enter fullscreen mode Exit fullscreen mode

Nationalities resolver:

public class NationalitiesResolver : IValueResolver<CoreModel.User, ApiModel.User, IEnumerable<ApiModel.Country>>
{
    public IEnumerable<ApiModel.Country> Resolve(CoreModel.User source, ApiModel.User destination, IEnumerable<ApiModel.Country> destMember, ResolutionContext context)
    {
        return source.Nationalities
            ?.Select(country => new ApiModel.Country(country.Code, country.Naam, country.WeergaveNaam)).ToArray();
    }
}
Enter fullscreen mode Exit fullscreen mode

The Resolvers are then called in the mapper for the respective properties:


CreateMap<CoreModel.User, ApiModel.User>()
            .IncludeAllDerived()
            .ForMember(dest => dest.SecondNationality,
                opt =>
                {
                    opt.MapFrom<SecondNationalityResolver>();
                })
            .ForMember(dest => dest.Nationalities, opt =>
            {
                opt.MapFrom<NationalitiesResolver>();
            })
            .ForMember(dest => dest.Nationality, opt => opt.Ignore());
    }
Enter fullscreen mode Exit fullscreen mode

Conclusion

Having these Custom Value Resolvers on the Core layer right in front of the infrastructure layer assures that whatever User model is sent from anywhere in the application, the correct value will be stored in the database. It also assures that whatever needs to be queried from the database, the resulting Api model will have all correct and consistent values.

Thanks to this, we can easily start removing old properties from all the APIs and their models without the danger of breaking the application at any point.

After all the APis are updated, the resolvers can be deleted. Then, we can happily get back to the standard AutoMapper mapping of:

CreateMap<ApiModel.User, CoreModel.User>().IncludeAllDerived();
Enter fullscreen mode Exit fullscreen mode

For more details about Resolvers checkout the documentation.

Photo by Tim Mossholder on Unsplash

Top comments (0)