Test Driven AutoMapper with .NET Core

July 18, 2019
Written by

VNcMVOXYfSc2DCoiaq5heVIF6fTmdIPVXG81WxYpM5QG06S-df8fuYYbNjgavC69zPz6U9afsp2RIjnxL0VA0T3M5TwyCqdIpmgXGStZp7NvzSP0utazOyORagpt1LutXk1m21bH

If you use data in a project then you have most likely had to map one model to another.  Whether you've done that in the constructor, a dedicated method, or used a mapper of some sort, it can be repetitive and tedious.

AutoMapper, a library for .NET written by Jimmy Bogard, has been around for a while.  It had a revamp to work with .NET Core and Dependency Injection but can still feel a little bit like magic.

I use AutoMapper for an internal Twilio tool.  I struggled to create valid maps that work without requiring me to map everything manually.

I found the best way to configure AutoMapper is by writing tests and lettingAutoMapper tell me exactly what I need to configure. Let me show you how that works.

I have created a solution in this repository that has the AutoMapper NuGet package already installed and all the basic plumbing included using Dependency Injection.  You can follow along with the code in the complete branch or actively code along using the incomplete code in the master branch.

If you do wish to code along, the project is using .NET Core SDK version 2.2.104. But anything above version 2.2 should work, even .NET Core 3 previews.  You can change the version of .NET used by updating the global.json file in the route of the solution.

You can check which versions of the .NET Core SDK you have installed using the following command in the console:

> dotnet --list-sdks

If you would like to see a full integration of Twilio APIs in a .NET Core application then checkout this free 5-part video series I created. It's separate from this blog post tutorial but will give you a full run down of many APIs at once.

Start with the tests

The solution has a test project. In the file named TestDrivenAutoMapping.Tests/MappingsTests.cs you should see the following code:


   [TestFixture]
   public class MappingsTests
   {

       [OneTimeSetUp]
       [Obsolete]
       public void Init()
       {
           Mapper.Initialize(cfg =>
           {
               cfg.AddProfile<Mappings>();
           });
       }


      [Test]
      public void Map_Should_HaveValidConfig()
      {
          Mapper.AssertConfigurationIsValid();
      }
...

I use NUnit as my testing framework, but you can use one of your choosing. The key line in the above code is the Mapper.AssertConfigurationIsValid(); method call.  This is what will help us to create the correct mappings.

The file we will complete our mappings in is the TestDrivenAutoMapping.Common/Mappings/Mappings.cs file and should have the following content in the default constructor of the Mappings class:

public Mappings()
{
    CreateMap<PersonViewModel, Person>();
    CreateMap<Person, PersonViewModel>();
...
}

AutoMapper follows conventions and therefore, this class must inherit from Profile.  The two private methods found in the same class are custom Type Convertors that convert a string to DateTime and vice versa.  We won't need to worry about these as they are just another type of mapping.

 Our main goal is to map Person to PersonViewModel.

Visual representation of Person and Person ViewModel and their properties

If the two models contained the same fields and properties, the code in Mapping.cs would be sufficient and our Mapper.AssertConfigurationIsValid(); test method call in would pass. 

We could simplify it even further by consolidating the two mappings into one line of code: CreateMap<PersonViewModel, Person>().ReverseMap();

However, our models are more complicated. We have some nested models which make it slightly more interesting, and also have some view specific content, public IList<string> Jobs { get; set; }, in the ViewModel that is of no relevance to the domain model.

If we run the tests now, either from within your IDE or in the console using dotnet test`, we will receive a load of errors.  However, it's these errors that will assist us with our correct mappings.

The first two errors we receive are:

PersonViewModel -> Person (Destination member list)
TestDrivenAutoMapping.Common.ViewModels.PersonViewModel -> TestDrivenAutoMapping.Common.Models.Person (Destination member list)

Unmapped properties:
AddressId
Address
JobId
Job
==============================================================
Person -> PersonViewModel (Destination member list)
TestDrivenAutoMapping.Common.Models.Person -> TestDrivenAutoMapping.Common.ViewModels.PersonViewModel (Destination member list)

Unmapped properties:
HouseName
JobViewModel
Jobs

Mapping from the errors

The errors tell us we have four unmapped properties when mapping from PersonViewModel to Person and vice versa.

We can add these mappings directly to the TestDrivenAutoMapping.Common/Mappings/Mappings.cs file as shown below:


CreateMap<PersonViewModel, Person>()
               .ForMember(e => e.Job, opts => opts.MapFrom(src => src.JobViewModel))
               .ForMember(e => e.Address, opts => opts.MapFrom(model => model))
               .ForMember(e => e.Id, opts => opts.Ignore())
               .ForMember(e => e.JobId, opts => opts.Ignore())
               .ForMember(e => e.AddressId, opts => opts.Ignore());

CreateMap<Person, PersonViewModel>()
               .ForMember(e => e.HouseName, opts => opts.MapFrom(src => src.Address.HouseName))
               .ForMember(e => e.JobViewModel, opts => opts.MapFrom(src => src.Job))
               .ForMember(e => e.Jobs, opts => opts.Ignore());

We have either mapped or chosen to ignore the properties that were mentioned in the test error.  You may notice that, for some of the properties, such as .ForMember(e => e.Job, opts => opts.MapFrom(src => src.JobViewModel)), we have specified that this is mapping between two models.  Therefore we will need to declare this mapping like so:


CreateMap<JobViewModel, Job>();

Now, when we run the tests again, we have the following error:

JobViewModel -> Job (Destination member list)
TestDrivenAutoMapping.Common.ViewModels.JobViewModel -> TestDrivenAutoMapping.Common.Models.Job (Destination member list)

Unmapped properties:
Id

 As JobViewModel doesn't have a property called Id we will want to just ignore this.  Update the code like so:


CreateMap<JobViewModel, Job>()
               .ForMember(e => e.Id, opts => opts.Ignore())
               .ReverseMap();

This time it's a fairly simple mapping and we can use the ReverseMap() feature.  

We also have the following mapping: .ForMember(e => e.Address, opts => opts.MapFrom(model => model)) on the CreateMap<PersonViewModel, Person>() map.  This requires us to define a mapping between Person and Address.  We also need to define a mapping between the PersonViewModel and Address to complete the reverse mapping.  Add the following mapping to the Mappings class:

CreateMap<Person, Address>()
               .ForMember(e => e.HouseName, opts => opts.MapFrom(src => src.Address.HouseName));

CreateMap<PersonViewModel, Address>()
               .ForMember(e => e.HouseName, opts => opts.MapFrom(src => src.HouseName));

If we run the tests now, all should pass and we have only the custom mappings that we required in our Mappings class.

AutoMapper is a fantastic tool for speeding up development and reducing the amount of boilerplate code you need to write.  I hope that approaching it in this manner helps speed up your development even more.

If you have any questions or further ideas on this approach please reach out to me on one of the following channels: