AutoMapper Madness - Getting Started in Your C# Application

August 10, 2022#Software Development
Article
Author image.

Scott DePouw, Senior Consultant

Over the years, one of the tools I end up setting up / using / maintaining a lot has been AutoMapper. For the uninitiated, AutoMapper is a tool used to do a routine operation in a robust, testable manner: Copying data from one kind of object to another. Their web site has an abundance of information on how to use it, and won’t be the subject of this series of blog posts. What I’ll be focusing on is how I’ve grown to use AutoMapper and what I feel is an optimal way within a C# application, allowing me to write tests with minimal duplication.

A Note on Code Examples

For the bulk of code examples, I’ll be using AutoMapper alongside these technologies:

  • A .NET 6 application
  • Autofac as my IoC container of choice
  • XUnit as my unit testing tool
  • Moq for unit test mocking

The AutoMapper-specific code will more-or-less be universal if used in older versions of .NET, but the examples will be specifically tuned for what I mentioned above. (Of particular note, file scoped namespaces.)

AutoMapper Setup in Solution

In these steps, I’ll be creating my static factory AutoMapper instance class for use in the application, defining what my mappings are (via Profile classes), and using an AutoMapper Autofac Module class so that I can resolve instances of IMapper throughout my application.

Factory Class

This is pretty straightforward. After I install the AutoMapper NuGet package, I define a static class AutoMapperConfig with a single method Initialize(), that takes no parameters and returns an instance of IMapper. It can be called by any code (both application-initialization code and test code, which is important for later!)

using AutoMapper;

namespace MyApp.Core.AutoMapper;

public static class AutoMapperConfig
{
    public static IMapper Initialize()
    {
        // Profile classes are in the same Core project of my application.
        // MapperConfiguration will grab all my Profiles from the Core assembly, and use them to create my Mapper class.
        var coreAssembly = typeof(AutoMapperConfig).Assembly;
        
        return new MapperConfiguration(mc => mc.AddMaps(coreAssembly)).CreateMapper();
    }
}

You can have any number of Profile classes defined in your application. We’ll dive into a real Profile definition in future posts, but for now just assume that AutoMapperConfig returns an IMapper instance with Profiles defined in it.

Autofac Module

I love Autofac Modules. They allow me to compartmentalize IoC configuration in a piecemeal fashion, which makes maintaining them much easier. It’s beyond the scope of this post to delve into the details, but the short version is, much like AutoMapper’s MapperConfiguration class, I can scoop up all defined Modules in an assembly (or assemblies) and don’t have to hardcode all of them within the application’s startup code. I’m free to create as many Modules as I wish, and they’ll all get registered within my application’s startup.

One such module example is the one I use for AutoMapper explicitly: It configures IMapper to be available as a singleton instance, using the result of AutoMapperConfig.Initialize() as the object to return.

using Autofac;

namespace MyApp.Core.AutoMapper;

public class AutoMapperModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterInstance(AutoMapperConfig.Initialize()).SingleInstance();
    }
}

Like with any dependency, IMapper gets injected into a class that needs AutoMapper via the constructor. And when it does, it’ll return the singleton I’ve defined above.

How I Use and Don’t Use AutoMapper

I’ve used AutoMapper a lot over the years, and have developed best practices for myself in how to take advantage of it, and, perhaps more importantly, how to not use it. I’ll walk through both examples quickly.

  • I will use AutoMapper to perform basic property mappings between e.g. a domain object class Foo and a DTO class FooDTO
    • One-to-one property mappings I wish to keep are left alone
    • Properties with different names will be configured to get mapped (e.g. “Name” to “FullName”)
    • Sensitive data in the domain can be ignored (e.g. database-level IDs)
  • I will do basic null-replacement or other default case logic
    • One example would be replacing null strings with empty strings
    • I do this with caution though and stick to basic conditionals, without having to delve into complex multi-result situations, etc.
  • I will not use AutoMapper as object factory classes
    • A common code smell in your AutoMapper Profile class is if you find yourself making private methods to handle a mapping scenario
    • These definitions should exist outside AutoMapper, as AutoMapper should be nothing but simple mappings from one class to another
using AutoMapper;

namespace MyApp.Core.AutoMapper;

public class MyProfile : Profile
{
    public MyProfile()
    {
        CreateMap<Foo, FooDTO>()
            .ForMember(dest => dest.ComplexProp, opt => opt.MapFrom(src => MapComplexProp(src)));
    }
    
    private Bar MapComplexProp(Foo src)
    {
        // Code smell! Logic that should be tested independently of AutoMapper shouldn't be here!
    }
}
  • If I require more complex logic than simple mappings in order to create an instance of a new object, I’ll create a factory class tuned for that object
    • This allows me to more-easily unit test this business logic, without having to navigate through AutoMapper to do it
    • This also enables me to mock the factory instance, when unit testing the class using it. As you’ll discover in the next section, I don’t mock IMapper!
using AutoMapper;

namespace MyApp.Core;

public class BarFactory : IBarFactory
{
    public Bar Create(Foo src)
    {
        // Create complex Bar here, using Foo src.
        // You can even inject an `IMapper` here, as AutoMapper can still map Foo to Bar to take care of all the simple cases.
        // The important thing is that any complex work take place within this factory.
    }
}

Why I Don’t Overcomplicate AutoMapper Logic

Unlike most dependencies for a given class, when I test said class I do not mock IMapper, injecting the real one instead.

using AutoMapper;
using MyApp.Core.AutoMapper;

namespace MyApp.UnitTests.ExampleClassTests;

public class GetFooShould
{
    // Call the same class real code uses for Dependency Injection!
    // This assures we're always on the same page, where test code uses the same mappings defined for real code.
    private readonly ExampleClass _myClass = new(AutoMapperConfig.Initialize());
}

I do this because AutoMapper is solely used to translate one class instance to another. Often this occurs at the very end of a method using it, to shape a return value.

public FooDTO DoSomething()
{
    // Testable business logic and such that gets an instance of Foo goes here.
    return _mapper.Map<FooDTO>(myFoo);
}

If I were to mock IMapper here, then I would either have to configure the mock to always “pass through” its inputs and map it manually myself in the test, or configure it to return a test-defined object. As you can imagine, that wouldn’t make for a very good test suite for DoSomething(), as everything would be ignored and I’d be returning what I mocked!

In my experience it’s best to just let AutoMapper be called for real, so that you get your real code’s results when you’re trying to test it. This is the primary reason I opt to keep AutoMapper as simple as possible. It’s Single Responsibility is to translate instances of one type of object into another, with no extreme mutations or decision-making going on.

Of course, no rule or practice applies to 100% of situations, and I definitely still mock IMapper from time to time. In some cases the reuslt of an AutoMapper call is irrelevant to a class under test or mocking/stubbing a result from it doesn’t matter. But for the most part, since my AutoMapper mappings are defined as simple translations, I can just let it be.

Wrap-Up

In this post I’ve outlined how I set up AutoMapper in a new C# application in such a way to make it easy to inject in the application and in test code. I hope you’ve gained some insight into how I develop and maintain AutoMapper, why I don’t overcomplicate mapping logic, and how I handle the IMapper dependencies when trying to put classes under test.

The next post in this series on AutoMapper will delve into how I test my AutoMapper configurations and mappings. Until next time!


Copyright © 2024 NimblePros - All Rights Reserved