Search code examples
c#automapperautomapper-3

Automapper - efficient mapping & check mapping between two specific types


In my application I need to do many mappings (Domain objects, DTOs, ViewModels etc.) between different pair of objects. I use AutoMapper heavily for the purpose.

So I have a common Mapper class which has methods to map different objects. As Mappings are persisted in the application, I do all my CreateMap() in the static constructor so that the mappings are created only once and are ready by the time I might use them. The class looks like this

public class SomeMapper
{
    static SomeMapper()
    {
        Mapper.CreateMap<SomeType1, SomeOtherType1>();
        Mapper.CreateMap<SomeType2, SomeOtherType2>();
        Mapper.CreateMap<SomeType3, SomeOtherType3>();
        //...
    }

    public SomeOtherType1 MapSomeHierarchy1(SomeType1 source) 
    { //... }

    public SomeOtherType2 MapSomeHierarchy2(SomeType2 source)
    { //... }
}

Question 1: What can be a better way to create the mappings? (better in any way - performance, semantics, standard practices etc.)

Question 2: This code is also used in a Console application. In a particular run, it'll not need all the mappings. So, rather than creating all the mappings eagerly, can I create the map at run time if it does not exist already? Something like

public SomeOtherTypeN MapSomeHierarchyN(SomeTypeN source)
{
    if (!AlreadyMapped(SomeTypeN, SomeOtherTypeN))
        Mapper.CreateMap<SomeTypeN, SomeOtherTypeN>();
    return Mapper.Map<SomeOtherTypeN>(source);
}

Is there a simple way to implement the method AlreadyMapped() ?


Solution

  • As you've said, mappings need only be created once during the lifecycle of the application. There are two main changes I would recommend:

    Split your mappings into Profiles

    These smaller units, and can be unit tested individually, so you can ensure all destination properties are either automatically mapped, explicitly mapped or ignored.

    public class MyProfile : Profile 
    {
        protected override void Configure()
        {
            // Note, don't use Mapper.CreateMap here!
            CreateMap<SomeType1, SomeOtherType1>();
        }
    }
    

    You then load individual profiles, allowing you to define these closer to where they are used in modular applications.

    Mapper.AddProfile<MyProfile>();
    

    Profiles can be tested individually:

    Mapper.AssertConfigurationIsValid<MyProfile>();
    

    I typically include a unit test with every profile - this way if your source or destination objects change in a way which break your mapping, you'll know about it immediately.

    Create Mappings at Startup

    While technically you can create mappings at any point during your application's lifecycle, AutoMapper makes various optimisations if you tell it that you're done. Some of these are essential if you perform any complex mappings with inheritance. Rather than creating mappings on the fly:

    Mapper.CreateMap<SomeType1, SomeOtherType1>();
    Mapper.AddProfile<MyProfile>();
    

    You should load these using Mapper.Initialize instead:

    Mapper.Initialize(cfg =>
    {
        cfg.CreateMap<SomeType1, SomeOtherType1>();
        cfg.AddProfile<MyProfile>();
    });
    

    If you absolutely must add mappings on the fly, then you can force AutoMapper to perform its optimisations again after adding mappings using Mapper.Configuration.Seal().

    Finally, if you're using an IoC container, then you can combine these two techniques by registering all of your Profiles in your AutoMapper container, then using this to locate and register them. Here's an example using Autofac:

    // Register Components...
    ContainerBuilder builder = new ContainerBuilder();
    builder.RegisterType<MyProfile>().As<Profile>();
    // This could also be done with assembly scanning...
    builder.RegisterAssemblyTypes(typeof<MyProfile>.Assembly).As<Profile>();
    
    // Build container...
    var container = builder.Build();
    
    // Configure AutoMapper
    var profiles = container.Resolve<IEnumerable<Profile>>();
    Mapper.Initialise(cfg => 
    {
        foreach (var profile in profiles)
        {
            cfg.AddProfile(profile);
        }
    });
    

    Regarding your second question, if you follow my point about creating mappings at startup then you won't need this, but defining a mapping which already exists overwrites the previous mapping, so shouldn't have any effect.