DEV Community

Cover image for Why I Don't Use AutoMapper in .Net
Grant Riordan
Grant Riordan

Posted on

Why I Don't Use AutoMapper in .Net

TL;DR

I dislike Automapper as a tool to auto-map objects from one to another. So I performed some benchmark tests around performance and memory usage between AutoMapper and manually mapping an object to a DTO (Data Transfer Object with fewer properties, normally used for API / UI usage).

Results found that the manual mapping technique was substantially faster, and more performant than that of AutoMapper.

Read on for:

Ok, so as a .Net developer, or any other developer for that matter you may use a Mapping library/tool, to help you map some object to another.

However, I've seen them being used for every mapping; mapping extremely small and easy objects to another form of DTO (Data Transfer Object), which is very inefficient.

I dislike automated / self-configured mapping tools like this for multiple reasons.

Cons (In My Opinion)

Performance Overhead: AutoMapper can introduce performance overhead compared to manual object mapping, especially for complex mappings or large datasets.

Learning Curve: For new or unfamiliar developers there is certainly a learning curve with using AutoMapper, from simple configuration profiles to custom resolvers. It's worth knowing it may take your developers a little while to learn or become familiar with it.

Complexity: In some cases, AutoMapper might introduce unnecessary complexity, especially for simpler projects where manual object mapping might suffice.

Maintenance: While AutoMapper can simplify object mapping in the short term, it adds a dependency to your project that needs to be maintained over time, including keeping up with updates and potential breaking changes.

It also means there is a reliance on another third-party library within your application, that if updated, or features are deprecated may have a detrimental impact on your code base.

AutoMapper relies on reflection to dynamically discover and map properties between source and destination types based on configured mappings.

Reflection is a feature in .NET that allows you to inspect and manipulate types, methods, properties, and other members of managed code assemblies at runtime. For those Javascript developers, it's a bit like using Object.Values, or Object.Keys to find out what properties an object has and use the key/value from dynamic objects.

As a result, we have more risks/concerns:

Reflection Overhead: Using Reflection can be slower compared to direct property access, as it involves dynamic discovery of types and members at runtime, which adds some overhead.

Dynamic Mapping: AutoMapper performs dynamic mapping between source and destination types based on configured mappings. This dynamic nature introduces additional processing overhead compared to manual mapping.

Configuration Processing: AutoMapper requires configuration, typically performed during application startup. This configuration involves parsing and processing mapping configurations, which can add yet another overhead, especially if you have a large number of mappings or profiles.

Object Creation and Initialisation: AutoMapper creates and initializes destination objects based on source objects and mapping configurations.

This process involves instantiating objects, setting properties, and potentially invoking constructors or property setters, which adds some overhead compared to direct object creation.

Optimisations and Features: AutoMapper provides various features and optimisations to handle complex mapping scenarios, such as custom type converters, value resolvers, and nested mappings. While these features enhance flexibility and convenience, they can also contribute to increased processing overhead! It's one more thing it has to load into memory, interpret and apply.

Getting to the Pros

As I want you to make your mind up here, I can't miss out that there are however some positives:

Productivity: AutoMapper simplifies the process of mapping objects between different layers of an application, saving developers time and reducing boilerplate code. As it uses reflection, sometimes you don't even have to manually map the column names. If both objects have the same Property name at the same level of the class tree, it will automatically map the like-named properties to each other.

Multiple lines of code can be avoided with a simple mapping function of one object to another, rather than manually listing all properties and mapping them to another object's properties etc.

Reduced Code Duplication: With AutoMapper, you can define mapping configurations once and reuse them throughout your application, reducing code duplication and promoting maintainability.

This kind of thing is really useful where say you wish to apply some manipulation of the properties, for example, formatting, translating, or something. You could simply create a CustomTranslationResolver, which will take the original value, run it through a translator, and map it to the DTO, removing the necessity of mapping, and then translating.

Customization: It allows for custom mapping configurations, enabling developers to handle complex mappings.

Note: For more junior developers this can sometimes be cumbersome or convoluted.

Integration with Dependency Injection: Many mapping libraries, including AutoMapper, integrate seamlessly with popular dependency injection frameworks like .NET Core's built-in DI, facilitating easier management of object mappings within dependency injection containers.

For example within a.Net8 Console Application using the Microsoft.Extensions.Hosting package, simply call

    var builder = Host.CreateApplicationBuilder();
    builder.Services.AddAutoMapper(typeof(Program));
Enter fullscreen mode Exit fullscreen mode

Wait Those Pro's Sound Great: In Reality Is It That Bad Then?

So ok, you may be looking at those positives list and thinking well they're all valid points. Doesn't sound that bad to me.

Well, this is where I took it upon myself to prove out my theory on how "bad" it was.

Hypothesis

I believe AutoMapper will run slower, and be less performant than mapping a simple Customer database object to a DTO object.

Method of Test

  • Create 2 identical class types with the same properties, just different Class names, to help differentiate between results and arrangement.

  • Create 2 helper methods,
    1 which will instantiate the auto-mapper customer object, and then utilise AutoMapper to map the DB object, to the DTO.

    1 which will simply instantiate the DB Customer, it will then map this to a new DTO object

Both Methods will use the same values for all properties, to avoid introducing any anomalies in data size.

the BenchmarkDotNet package will be used to perform the benchmarks and testing. This library will run the methods multiple times and report back a mean value of execution time.

The Code

Use the following classes, utils and helpers to run the benchmarks yourself.

Pre-Requisites:

  • .Net 6+ Dotnet Console Application (Will need to Convert code to non-simple setup for < .Net 6 Console Applications i.e using Program => Main methods)

  • Run dotnet add package AutoMapper, dotnet add package BenchmarkDotNet and dotnet add package Microsoft.Extensions.Hosting to install required NuGet packages.

Program.cs:


using AutoMapper_Test;
using BenchmarkDotNet.Running;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder();
builder.Services.AddAutoMapper(typeof(Program));

var app = builder.Build();

BenchmarkRunner.Run<Helper>();
Enter fullscreen mode Exit fullscreen mode

IDbCustomer:

namespace AutoMapper_Test;

public interface ICustomer
{
    int Id { get; set; }
    string FirstName { get; set; }

    string LastName { get; set; }

    string Email { get; set; }

    Address? Address { get; set; }

    DateTime DoB { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

DBCustomer:

namespace AutoMapper_Test;

public class DbCustomer : ICustomer
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public string Email { get; set; }

    public Address? Address { get; set; }

    public DateTime DoB { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

ICustomerDto

namespace AutoMapper_Test;

public interface ICustomerDto
{
    int Id { get; set; }
    string FullName { get; set; }
    string Email { get; set; }
    DateTime DoB { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

CustomerDto:

namespace AutoMapper_Test;

public class CustomerDto : ICustomerDto
{
    public int Id { get; set; }
    public string FullName { get; set; }
    public string Email { get; set; }
    public DateTime DoB { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Address:

namespace AutoMapper_Test;

public class Address
{
    public string Street { get; set; }
    public string House { get; set; }
    public string PostalCode { get; set; }
    public string City { get; set; }
    public string Country { get; set; }

    public string FullAddress => $"{House} {Street} {City} {Country} {PostalCode}";
}
Enter fullscreen mode Exit fullscreen mode

MappingProfile:

using AutoMapper;
using AutoMapper_Test;

public class MappingProfile : Profile
 {
     public MappingProfile()
     {
         CreateMap<DbCustomer, CustomerDto>()
             .ForMember(dest => dest.FullName, opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"));
     }
 }
Enter fullscreen mode Exit fullscreen mode

MappingExtensions

namespace AutoMapper_Test;

public static class MapExtensions
{
    public static ICustomerDto ToDto(this ICustomer customer)
    {
        return new CustomerDto()
        {
            Id = customer.Id,
            FullName = $"{customer.FirstName} {customer.LastName}",
            Email = customer.Email,
            DoB = customer.DoB
        };
    }
}

Enter fullscreen mode Exit fullscreen mode

BenchMark Helper:

using AutoMapper;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Order;

namespace AutoMapper_Test;

[MemoryDiagnoser]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class Helper
{
    [Benchmark]
    public void AutomapperTest()
    {
        var configuration = new MapperConfiguration(cfg => { cfg.AddProfile<MappingProfile>(); });

        var mapper = new Mapper(configuration);

        var autoMappedCustomer = new DbCustomer
        {
            FirstName = "Grant",
            LastName = "Riordan",
            Address = new Address()
            {
                House = "123", City = "Balamory", Country = "Scotland", Street = "Balamory Way", PostalCode = "BA12 8SC"
            },
            DoB = new DateTime()
        };

        var mappedDto = mapper.Map<CustomerDto>(autoMappedCustomer);

        Console.WriteLine(mappedDto.FullName);
    }

    [Benchmark]
    public void ManualTest()
    {
        var manualCustomer = new DbCustomer()
        {
            FirstName = "Grant",
            LastName = "Riordan",
            Address = new Address()
            {
                House = "123", City = "Balamory", Country = "Scotland", Street = "Balamory Way", PostalCode = "BA12 8SC"
            },
            DoB = new DateTime()
        };
        var manualMappedDto = manualCustomer.ToDto();
        Console.WriteLine(manualMappedDto.FullName);
    }
}
Enter fullscreen mode Exit fullscreen mode

Rundown of the code:

We have a CustomerDto object (smaller object for transfering relevant data), as well as a DbCustomer object (which would represent our domain model (what would be loaded from a Database for example).

The Helper class, is where our benchmark tests will lay. This class is prefixed with some Benchmark attributes to specify what and how I want to see results,

Each method under test, is then marked with the[Benchmark] attribute which will allow the library to pick these methods up as methods to be tested against.

Each method will be tested mulitple times to gather a mean average time to execute, and collect data on how much memory was used during execution.

First run dotnet build -c Release in your project directory, followed by dotnet <path to your build dll

example: dotnet <projectDirectory/bin/Release/net8.0/AutoMapper_Test.dll

Results

| Method         | Mean      | Error    | StdDev   | Gen0    | Gen1   | Gen2   | Allocated |
|--------------- |----------:|---------:|---------:|--------:|-------:|-------:|----------:|
| ManualTest     |  10.93 us | 0.269 us | 0.794 us |  0.0305 |      - |      - |     216 B |
| AutomapperTest | 534.15 us | 2.931 us | 2.742 us | 11.7188 | 5.8594 | 0.9766 |   75833 B |

Enter fullscreen mode Exit fullscreen mode

As you can see the manual version of this functionality is vastly more performant.

With 10.93 microseconds (0.01093ms) vs 534.15 microseconds (0.53415ms)

I mean yes we're still talking sub 1 second, but it's a big difference with > 500ms difference in operation time.

In addition to this Automapper uses ~352x more memory being used than the manual variation. On such a basic mapping where only 4-5 properties are included. Imagine the impact when working with a much more elaborate object.

Let's add the Address object as a smaller more complex object and see and look at the difference.

Test 2: Including Address Object

// CustomerWithAddressDto
namespace AutoMapper_Test;

public class CustomerWithAddressDto : ICustomerWithAddressDto
{
    public int Id { get; set; }
    public string FullName { get; set; }
    public string Email { get; set; }
    public DateTime DoB { get; set; }
    public Address Address { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
// add the following method to the MappingExtensions.cs

public static ICustomerWithAddressDto ToDtoWithAddress(this ICustomer customer)
    {
        return new CustomerWithAddressDto()
        {
            Id = customer.Id,
            FullName = $"{customer.FirstName} {customer.LastName}",
            Email = customer.Email,
            DoB = customer.DoB,
            Address = customer.Address
        };
    }
Enter fullscreen mode Exit fullscreen mode
// add this mapping to the MappingProfile
   CreateMap<DbCustomer, CustomerWithAddressDto>()
             .ForMember(dest => dest.FullName, opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"));
Enter fullscreen mode Exit fullscreen mode
// Add the following two benchmark tests to your Helper.cs file

 [Benchmark]
    public void ManualTest_WithAddress()
    {
        var manualCustomer = new DbCustomer()
        {
            FirstName = "Grant",
            LastName = "Riordan",
            Address = new Address()
            {
                House = "123", City = "Balamory", Country = "Scotland", Street = "Balamory Way", PostalCode = "BA12 8SC"
            },
            DoB = new DateTime()
        };
        var manualMappedDto = manualCustomer.ToDtoWithAddress();
        Console.WriteLine($"{manualMappedDto.FullName} {manualMappedDto.Address?.Street}");
    }

 [Benchmark]
    public void AutomapperTest_WithAddress()
    {
        var configuration = new MapperConfiguration(cfg => { cfg.AddProfile<MappingProfile>(); });

        var mapper = new Mapper(configuration);

        var autoMappedCustomer = new DbCustomer
        {
            FirstName = "Grant",
            LastName = "Riordan",
            Address = new Address()
            {
                House = "123", City = "Balamory", Country = "Scotland", Street = "Balamory Way", PostalCode = "BA12 8SC"
            },
            DoB = new DateTime()
        };
Enter fullscreen mode Exit fullscreen mode

Re-run the benchmark tests as before, compiling and then running the dll with dotnet command.

| Method                     | Mean       | Error     | StdDev    | Gen0    | Gen1   | Gen2   | Allocated |
|--------------------------- |-----------:|----------:|----------:|--------:|-------:|-------:|----------:|
| ManualTest                 |   9.881 us | 0.0707 us | 0.0662 us |  0.0305 |      - |      - |     216 B |
| ManualTest_WithAddress     |  11.474 us | 0.0864 us | 0.0766 us |  0.0458 |      - |      - |     304 B |
| AutomapperTest             | 489.031 us | 2.1267 us | 1.8853 us | 13.6719 | 6.8359 | 0.9766 |   87530 B |
| AutomapperTest_WithAddress | 538.434 us | 2.6644 us | 2.3619 us | 13.6719 | 6.8359 | 2.9297 |   91504 B |

Enter fullscreen mode Exit fullscreen mode

Again, the results are conclusive, mapping it manually will result in a faster mapping. I mean, the difference between mapping the Address and not mapping the Address is negligible.

Conclusion

I'd like to point out that this is a very basic example, and with larger more complex objects Automapper may be more enticing than right custom mappers. However I find the points I've made still stand, and I like to have control over my codebase, and also be able to debug issues easier. Having that level of abstraction via AutoMapper makes it more difficult to pin point problems, and debug code.

Top comments (0)