The performance of mappers in C#

This past week I published a video about auto mappers in C#, and even though it lasted 20 minutes it seems I didn't cover everything expected. Several people asked me for comparisons with other mappers or additional features, so we're going to cover everything here, and we'll also check benchmarks in each case. 

 

 

1 - What is a benchmark? 

The first thing I want to explain is what a benchmark is. I already have a detailed post about benchmarks in c#, but a quick recap never hurts. 

A benchmark is a performance comparison. Here we can consider performance as including various factors like memory usage, or the time it takes to complete. 

 

In .NET we do benchmarks with the benchmarkdotnet library (link) which is very comprehensive and allows you to include detailed information. For example, if you only want to know how much memory is being allocated, you can check just the memory. If you want to know how long it takes, you can check only the time, etc. 

For our example, we'll be checking both memory and time. 

 

To use the library, you have to use the [Benchmark] attribute on the method or methods whose performance you want to check. If you want to check several, these methods must be in the same class, so it's common to create a benchmark class where you call everything:

[MemoryDiagnoser]
[KeepBenchmarkFiles(false)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByMethod)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class MapperBenchmark
{
    private readonly AccountInformationDto _accountDto = MapperHelper.BuildDefaultDto();
    private readonly AutomapperExample _automapperExample = new AutomapperExample();
    private readonly ManualMapperExample _manualMapperExample = new ManualMapperExample();
    private readonly MapperlyExample _mapperlyExample = new MapperlyExample();
    private readonly MapsterExample _mapsterExample = new MapsterExample();

    [Benchmark]
    public void Automapper()
    {
        _ = _automapperExample.Execute(_accountDto);
    }

    [Benchmark]
    public void Manual()
    {
        _ = _manualMapperExample.Execute(_accountDto);
    }

    [Benchmark]
    public void Mapperly()
    {
        _ = _mapperlyExample.Execute(_accountDto);
    }

    [Benchmark]
    public void Mapster()
    {
        _ = _mapsterExample.Execute(_accountDto);
    }
}

And as I mentioned earlier, we have this class and then in our application we must call the benchmark runner, passing the benchmark class as the type.

BenchmarkRunner.Run<MapperBenchmark>();

Another thing to consider is that when you run the application to do the benchmarks you have to do it in release mode, since running in debug mode would not be realistic compared to production.

 

 

2 - Comparing the different .NET mappers

As I mentioned a moment ago, this post comes in response to comments on the video about mappers, so I won't go into detail again about how each one works. What I will do is explain the example.

For our example, we will use a mapping between a DTO and an Entity. The DTO is simple but contains a list with a type that has another object inside, simply because people in the previous video asked about this. Here is the structure: 

public class AccountInformationDto
{
    public int AccountId { get; set; }
    public List<UserDetailsDto> Users { get; set; }
}

public class UserDetailsDto
{
    public string UserName { get; set; }
    public ContactDetails ContactDetails { get; set; }
}

public class UserDetailsEntity
{
    public string UserName { get; set; }
    public string Email { get; set; }
}

The entity is very similar, but with a change: in our UserEntity we don't have contact information, instead the email goes directly on the user. This means we need to do what is called a "flatten" in English. Most importantly, the default configuration of the mappers won't work for this object, so we'll look at the specific configuration in this post. 

public class AccountInformationDetailsEntity
{
    public int AccountId { get; set; }
    public List<UserDetailsEntity> Users { get; set; }
}

public class ContactDetails
{
    public string Email { get; set; }
    public string PhoneNumber { get; set; }
}

Still, it is a small object, so it will be mapped quickly.

Alright, let's get to it.

 

 

2.1- Mapping lists with automapper

We already saw in the previous post how we can map objects. In this case we're going to do it with a list. To the configuration we had before, we add the parent object and that's it. 

public class AutomapperExample
{
    private readonly IMapper _mapper;

    public AutomapperExample()
    {
        MapperConfiguration config = new(cfg =>
        {
            cfg.CreateMap<UserDetailsDto, UserDetailsEntity>()
                .ForMember(destination => destination.Email,
                    options => options
                        .MapFrom(source => source.ContactDetails.Email));

            cfg.CreateMap<AccountInformationDto, AccountInformationDetailsEntity>()
                .ForMember(destination => destination.Users,
                    options => options.MapFrom(a => a.Users));
        });

        _mapper = new Mapper(config);
    }
    
    public AccountInformationDetailsEntity Execute(AccountInformationDto accountInfoDto)
        => _mapper.Map<AccountInformationDetailsEntity>(accountInfoDto);
}

This tells us that we have to map basically every object we need, and with automatic mapping it is enough as long as the names aren't different.

And then we call the mapping just like we did in the other post, _mapper.Map<T>(obj).

Note: With AutoMapper we can create what are called profiles, a way to store all the configuration in one place, which is pretty nice.

 

2.2 - Mapping lists with mapperly

The second library we saw was mapperly, and we saw a regular example. Now let's create a scenario with the object I explained earlier. 

We already know we have to create a partial class for the mapping, so we do the same and include the parent class. Then, for the flatten, we have to include the configuration to indicate what goes where, and that's it:

[Mapper]
public partial class AccountInformationMapper
{   
    public partial AccountInformationDetailsEntity ToEntityMapperly(AccountInformationDto dto);
    
    [MapProperty(nameof(@UserDetailsDto.ContactDetails.Email), nameof(@UserDetailsEntity.Email))]
    public partial UserDetailsEntity ToEntity(UserDetailsDto dto);
}

public class MapperlyExample
{
    private readonly AccountInformationMapper _mapper = new();
    
    public AccountInformationDetailsEntity Execute(AccountInformationDto accountInfoDto)
        => _mapper.ToEntityMapperly(accountInfoDto);
}

Mapperly uses code generation under the hood, and it is smart enough to know that it has to use the method we created when mapping the users list.

 

 

2.3 - Mapping lists with mapster

In the original post, I didn't include this mapper. The reason is that I have never used it, and you know I usually only explain libraries, patterns, etc. I've actually worked with, or unless there are exceptions, like this one. So for me, this implementation is new.

As with any other mapper, if the property names are the same in both objects, you don't have to add any extra configuration.

 

But you do need to if the names are different, as in our case, where we're flattening one of the objects: 

 

public class MapsterExample
{
    public MapsterExample()
    {
        TypeAdapterConfig<UserDetailsDto, UserDetailsEntity>
            .NewConfig()
            .Map(dest => dest.Email, src => src.ContactDetails.Email);
    }

    public AccountInformationDetailsEntity Execute(AccountInformationDto accountInfoDto)
        => accountInfoDto.Adapt<AccountInformationDetailsEntity>();
}

To invoke the mapping, you just use the Adapt method.

 

One thing I noticed is that the configuration is done with TypeAdapterConfig, which means it can be done anywhere in the application and will affect all uses. But as a first impression, I found it very simple to understand.

 

 

2.4 - Mapping lists manually

When you map manually, there are several ways to do it. What I like is using a static class with a static method called .ToXXXX where XXXX is what you're converting to, in my case, ToEntity:

public class ManualMapperExample
{
    public AccountInformationDetailsEntity Execute(AccountInformationDto accountInfoDto)
        => accountInfoDto.ToEntity();
}

public static class UserManualMapper
{
    public static AccountInformationDetailsEntity ToEntity(this AccountInformationDto dto)
    {
        return new AccountInformationDetailsEntity()
        {
            AccountId = dto.AccountId,
            Users = dto.Users.Select(ToEntity).ToList()
        };
    }

    public static UserDetailsEntity ToEntity(this UserDetailsDto dto)
        => new UserDetailsEntity()
        {
            UserName = dto.UserName,
            Email = dto.ContactDetails.Email
        };
}

 

 

3 - The moment of truth: Which mapper is the best? 

Before presenting the results, I want to pause briefly to say the following: It doesn't matter how you create the mapping, whether with a library or manually, but what you MUST always do is have tests that cover that mapping.

 

They don't have to be unit tests. You can just do an integration test and test each output if you want. Of course, it's easier to do a unit test for this, but either way, what I mean is, you have to test each assignment, otherwise problems will come up later on. 

 

And well, these are the results for an AccountInformationDto object with 100k users

Benchmarking automappers

  • In the table, the second column is the time taken and the last one is the allocated memory. In all columns, lower is better.

 

Honestly, I didn't expect these results at all. Well, I did expect Automapper to be the slowest, but then, I thought manual mapping would be faster than the other two. 

 

It is true, though, that the difference is minimal, still, it surprises me.

Even so, I personally prefer to use manual mapping, meaning building the mapping myself, since in my opinion it's easier to read and if we have slightly more complex configuration, understand, although of course, with these data I can't really argue much, except in terms of readability, with anyone who wants to use Mapster or Mapperly. 

 

 

This post was translated from Spanish. You can see the original one here.
If there is any problem you can add a comment bellow or contact me in the website's contact form

Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2025 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café