In this article, we are going to compare the popular object mapping libraries AutoMapper and Mapster. Later, we’ll do a performance benchmark to find the most performant one.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in.

Why Compare AutoMapper vs Mapster?

AutoMapper is one of the popular object-object mapping libraries with over 296 million NuGet package downloads. It was first published in 2011 and its usage is growing ever since.

Mapster is an emerging alternative to AutoMapper which was first published in 2015 and has over 7.4 million NuGet package downloads. Although the download count is nowhere near AutoMapper, it promises better performance and a lower memory footprint than other mapping libraries.

Based on the commit history of both AutoMapper and Mapster, we can know that both projects are actively maintained.

Apart from that, they both provide configuration options for simple to more advanced mapping scenarios.

If you’re interested to learn more about these libraries, check out our in-depth articles on AutoMapper and Mapster.

Let’s get started and find out how Mapster compares to AutoMapper for some of the most common object-to-object mapping scenarios.

Simple Type Mapping

In this case, both the source and the destination types have similar properties. The property names are always the same. However, we may use different data types for the properties.

In order to demonstrate it, let’s create a simple source type User:

public class User
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public bool IsActive { get; set; }
    public string Email { get; set; } = null!;
    public DateTime CreatedAt { get; set; }
}

And then, let’s create our destination type UserDto:

public class UserDto
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public bool IsActive { get; set; }
    public string Email { get; set; } = null!;
    public string CreatedAt { get; set; } = null!;
}

Here, all the properties are similar to the source type except CreatedAt property. The CreatedAt property has string as the destination type whereas DateTime is the source type. In situations like this, the mapping libraries do the implicit type casting.

Both libraries support the type casting between the primitive types.

Simple Type Mapping With AutoMapper

To use AutoMapper, we first need to create the IMapper object. There are multiple ways to create it. One way is to use the MapperConfiguration class:

var mapper = new MapperConfiguration(cfg => cfg.CreateMap<User, UserDto>()).CreateMapper();

This way, we need to specify the source and the destination type as type params to the CreateMap<TSource, TDestination>() method. In our case, User is the source type and UserDto is the destination type.

The other way to do the mapper configuration is to use the profiles. We can learn more about this way in this article.

Before going ahead, let’s create our source object using the User type:

var source = new User
{
    Id = 1,
    Name = "User 1",
    Email = "[email protected]",
    IsActive = true,
    CreatedAt = DateTime.Now
};

Once we have the source object and the IMapper instance in place, we can now easily map to the destination object:

UserDto destination = mapper.Map<UserDto>(source);

In this case, we need to pass the source object as the parameter and specify the destination type as a generic type param to the Map<TDestination>(object source) method.

If we inspect the values of the destination variable, all the values of the properties in the source type are correctly mapped to the destination type properties:

{"Id":1,"Name":"User 1","IsActive":true,"Email":"[email protected]","CreatedAt":"01-09-2022 21:53:57"}

The CreatedAt property is also mapped by implicit type casting from DateTime to string.

Simple Type Mapping With Mapster

It’s even simpler in Mapster:

UserDto destination = source.Adapt<UserDto>();

We can directly invoke the Adapt<TDestination>(this object source) extension method on the source object where we must pass the destination type UserDto as the generic type parameter.

But, if we want to map to the existing destination object, we can do so:

var destination = new UserDto();
source.Adapt(destination);

Mapster provides other ways as well like query.ProjectToType<Dest>(), IMapper instance for dependency injection and a few others.

Now, if we inspect the values of the destination object, the mapping is as expected:

{"Id":1,"Name":"User 1","IsActive":true,"Email":"[email protected]","CreatedAt":"01-09-2022 21:53:57"}

Nested Type Mapping

In this case, we need to map nested objects. To illustrate this, let’s create one more type called Address:

public class Address
{
    public string AddressLine1 { get; set; } = null!;
    public string AddressLine2 { get; set; } = null!;
    public string City { get; set; } = null!;
    public string State { get; set; } = null!;
    public string Country { get; set; } = null!;
    public string ZipCode { get; set; } = null!;
}

Let’s use the Address type as one of the properties in User class:

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; } = null!;
    public string LastName { get; set; } = null!;
    public string Email { get; set; } = null!;
    public Address Address { get; set; } = null!;
}

In nested or complex type mapping, the mapping libraries need to map all the types at any level in its way. In this instance, it needs to map both User and Address to their respective target types.

Let’s assume we have UserDto and AddressDto types as their destination.

Nested Type Mapping With AutoMapper

In AutoMapper, we need to create the mapping configuration for all the types to map to their destination types:

var mapper = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<User, UserDto>();
        cfg.CreateMap<Address, AddressDto>();
    })
    .CreateMapper();

Here, we created the mapping for both User and Address to their destination types UserDto and AddressDto.

However, there’s no change in the actual mapping:

UserDto destination = mapper.Map<UserDto>(source);

This command will map both the first level (UserDto) and the second level inside the UserDto (AddressDto).

Nested Type Mapping With Mapster

In Mapster, since the source and the destination types have exactly the same properties then there’s no change and a simple command is sufficient:

UserDto destination = source.Adapt<UserDto>();

List or Array Mapping

In this scenario, we might want to map one list or array into another. To do this, we just need to specify the destination type param as List<TDestination>, TDestination[] or something similar. Other than that, we don’t need any special configuration for this to work.

List or Array Mapping With AutoMapper

In AutoMapper, we still need to specify the mapping configuration for the individual types:

var mapper = new MapperConfiguration(cfg => cfg.CreateMap<User, UserDto>()).CreateMapper();
var sourceList = new List<User>() { ... };
List<UserDto> destinationList = mapper.Map<List<UserDto>>(sourceList);

If we look closely, we provided the destination type param as List<UserDto> to the Map<TDestination>() method.

List or Array Mapping With Mapster

In Mapster, we can directly invoke the Adapt() method while providing the destination type param as List<UserDto>:

List<UserDto> destinationList = sourceList.Adapt<List<UserDto>>();

Custom Property or Member Mapping

In this case, we want to map our custom properties in the source and the destination types.

Let’s say our destination type has FullName property:

public class UserDto
{
    public string FullName { get; set; } = null!;
}

But our source type only has FirstName and LastName property:

public class User
{
    public string FirstName { get; set; } = null!;
    public string LastName { get; set; } = null!;
}

Now the mapping library will not know which property to map due to the name mismatch. Hence we need to explicitly tell the library of our custom mapping.

Custom Property or Member Mapping With AutoMapper

In AutoMapper, we call the ForMember fluent method on the CreateMap() method to specify our indented mapping:

var mapper = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<User, UserDto>()
        .ForMember(
            dest => dest.FullName,
            config => config.MapFrom(src => $"{src.FirstName} {src.LastName}"
            ));
});

Custom Property or Member Mapping With AutoMapper

In Mapster, we need to use the TypeAdapterConfig static class to do our custom property mapping:

TypeAdapterConfig<User, UserDto>
    .NewConfig()
    .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}");

Here, we need to specify the source type as the first type param and the destination type as the second type param. Hence we say TypeAdapterConfig<User, UserDto>. The NewConfig() fluent method creates a new mapping configuration for the source and destination types while dropping any other configurations for those types.

Object Flattening

In object flattening, we can map the nested properties to the top-level properties using a simple naming convention.

For instance, the Address.ZipCode nested property from the User source type can be mapped to AddressZipCode property in the UserDto destination type:

public class User
{
    public Address Address { get; set; } = null!;
}

public class Address
{
    public string ZipCode { get; set; } = null!;
}

public class UserDto
{
    public string AddressZipCode { get; set; } = null!;
}

Another way is to have a method prefixed with Get followed by the destination property name. For instance, GetFullName() method will be mapped to FullName property:

public class User
{
    public string FirstName { get; set; } = null!;
    public string LastName { get; set; } = null!;
    public string GetFullName() => $"{FirstName} {LastName}";
}

public class UserDto
{
    public string FullName { get; set; } = null!;
}

We can use this feature in both AutoMapper and Mapster by default.

Reverse Mapping and Unflattening

The object unflattening is just the opposite. We can also call it reverse mapping because we do the mapping from the destination to the source object.

Reverse Mapping and Unflattening With AutoMapper

In AutoMapper, to achieve both reverse mapping and unflattening, we need to call the ReverseMap() method in the mapper configuration:

var mapper = new MapperConfiguration(cfg => cfg.CreateMap<User, UserDto>().ReverseMap()).CreateMapper();

Reverse Mapping and Unflattening With Mapster

In Mapster, reverse mapping is termed as “two ways”. As the term suggests, we need to call the TwoWays() method in the type adapter config:

TypeAdapterConfig<User, UserDto>
    .NewConfig()
    .TwoWays()
    .Map(dest => dest.EmailAddress, src => src.Email);

This will do both reverse mapping and unflattening. Any mapping followed by the TwoWays() method will be used in both directions.

Attribute Mapping

So far, we saw the fluent configuration for different scenarios. We can use attributes as well to achieve the same functionality.

Both AutoMapper and Mapster provide attributes to do the custom mapping.

Attribute Mapping With AutoMapper

In AutoMapper, we can use attributes like AutoMap, Ignore, ReverseMap, SourceMember and so on:

public class User
{
    public DateTime CreatedAt { get; set; }
}

[AutoMap(typeof(User))]
public class UserDto
{
    [SourceMember("CreatedAt")]
    public string CreatedDate { get; set; } = null!;
}

Here we used the AutoMap attribute on the destination type to specify the source mapping type. And then, we used the SourceMember type to map to the source type’s property.

In order for this to work, we need to use the AddMaps() method in the mapper configuration which takes the assembly of the source and destination types as parameter:

var mapper = new MapperConfiguration(cfg => cfg.AddMaps(typeof(User).Assembly)).CreateMapper();

When we perform the mapping, the end result will be the same as the fluent configuration.

Attribute Mapping With Mapster

In Mapster, we can use attributes like AdaptTo, AdaptFrom, AdaptTwoWays, AdaptMember and more to do the mapping for us.

public class User
{
    public DateTime CreatedAt { get; set; }
}

public class UserDto
{
    [AdaptMember("CreatedAt")]
    public string CreatedDate { get; set; } = null!;
}

In this case, we used the AdaptMember attribute to specify the source type’s property.

Dependency Injection

Both libraries provide support for dependency injection.

Dependency Injection With AutoMapper

In AutoMapper, we need to install another NuGet package AutoMapper.Extensions.Microsoft.DependencyInjection.

And then we can simply call the AddAutoMapper() method on the IServiceCollection object:

services.AddAutoMapper(typeof(Profile1), typeof(Profile2) /*, ...*/);

We need to pass the assemblies as parameters to identify the AutoMapper profiles.

Dependency Injection With Mapster

Similarly, in Mapster as well, we need to install another NuGet package Mapster.DependencyInjection.

And then we need to register TypeAdapterConfig object as a singleton and register ServiceMapper class for IMapper interface with any lifetime:

services.AddSingleton(TypeAdapterConfig.GlobalSettings);
services.AddScoped<IMapper, ServiceMapper>();

In both the libraries, we can use the IMapper interface in the constructor to use the mapper object:

public class SampleService {
    private readonly IMapper _mapper;

    public SampleService(IMapper mapper) {
        _mapper = mapper;
    }
}

Performance Benchmark of AutoMapper and Mapster

In order to do the performance benchmark, let’s use the BenchmarkDotNet library available in .NET.

Before we go ahead with the tests, let’s look at our benchmark setup:

[Benchmark(Description = "AutoMapper_SimpleMapping")]
public void AutoMapperSimpleObjectMapping()
{
    for (var i = 0; i < _size; i++)
    {
        AutoMapperSimpleTypeMapping.Map(_simpleObjectSource[i]);
    }
}

The AutoMapperSimpleObjectMapping() method will perform the object mapping for N number of items based on the _size field. In this case, the method will do the mapping using AutoMapper for a simple mapping scenario.

Let’s find out what does the AutoMapperSimpleTypeMapping.Map() method does:

public class AutoMapperSimpleTypeMapping
{
    public static IMapper Mapper = new MapperConfiguration(cfg => cfg.CreateMap<User, UserDto>()).CreateMapper();

    public static UserDto Map(User source)
    {
        var destination = Mapper.Map<UserDto>(source);

        return destination;
    }
}

The Map() method in AutoMapperSimpleTypeMapping class will do the actual object-object mapping. It takes the source object as a parameter and returns the destination object after performing the object mapping. We also created the IMapper instance required for this particular mapping scenario.

Similarly, we’ve created the classes and methods for each scenario for both AutoMapper and Mapster.

Let’s look at how we generate the object source:

public class SimpleTypeMappingDataGenerator
{
    public static List<User> GetSources(int count = 1000)
    {
        var faker = new Faker<User>()
            .Rules((f, o) =>
            {
                o.Id = f.Random.Number();
                o.Name = f.Name.FullName();
                o.Email = f.Person.Email;
                o.IsActive = f.Random.Bool();
                o.CreatedAt = DateTime.Now;
            });
        return faker.Generate(count);
    }
}

The GetSources() method will produce the required number of source objects for the mapping. As the source object will differ for each scenario, we’ve created similar data generator classes for each scenario.

We generate the source objects before performing the actual benchmark:

[GlobalSetup(Targets = new[] { nameof(AutoMapperSimpleObject), nameof(MapsterSimpleObject) })]
public void SetupDataSourceForSimpleTypeMapping()
{
    _simpleObjectSource = SimpleTypeMappingDataGenerator.GetSources(_size).ToArray();
}

Finally, let’s set the _size field’s value to 1,000 and then run the benchmark:

|                           Method |      Mean |    Error |    StdDev |    Median | Allocated |
|--------------------------------- |----------:|---------:|----------:|----------:|----------:|
|         AutoMapper_SimpleMapping | 327.11 us | 5.718 us |  7.826 us | 326.73 us |    109 KB |
|            Mapster_SimpleMapping | 250.13 us | 4.795 us |  4.924 us | 250.81 us |    109 KB |
|    AutoMapper_ListOrArrayMapping | 245.02 us | 4.628 us |  6.018 us | 245.66 us |    133 KB |
|       Mapster_ListOrArrayMapping | 234.22 us | 4.441 us | 10.204 us | 237.02 us |    125 KB |
|         AutoMapper_NestedMapping | 149.77 us | 5.923 us | 16.510 us | 141.89 us |    117 KB |
|            Mapster_NestedMapping |  68.51 us | 1.096 us |  1.500 us |  68.22 us |    117 KB |
|      AutoMapper_FlattenedMapping | 162.06 us | 2.773 us |  2.316 us | 161.89 us |    137 KB |
|         Mapster_FlattenedMapping |  86.60 us | 1.700 us |  1.590 us |  86.26 us |    137 KB |
| AutoMapper_CustomPropertyMapping | 226.93 us | 8.919 us | 26.299 us | 222.56 us |     90 KB |
|    Mapster_CustomPropertyMapping |  49.07 us | 2.350 us |  6.928 us |  49.17 us |     39 KB |
|        AutoMapper_ReverseMapping | 150.28 us | 3.718 us | 10.726 us | 145.84 us |    117 KB |
|           Mapster_ReverseMapping |  55.12 us | 2.155 us |  6.286 us |  53.76 us |     55 KB |
|      AutoMapper_AttributeMapping | 351.38 us | 6.153 us |  5.755 us | 348.70 us |    117 KB |
|         Mapster_AttributeMapping | 274.45 us | 5.443 us |  6.883 us | 271.15 us |    117 KB |

From the result, we can see that Mapster is almost 1-4 times faster than AutoMapper. In a few cases, the memory footprint is almost the same for both AutoMapper and Mapster. But, Mapster breaks the tie by performing faster in those cases.

In the end, we come to know that Mapster is a better option when performance is crucial. However, for non-performance critical applications, AutoMapper will do just fine.

Conclusion

In this article, we have compared the usage of AutoMapper vs Mapster for some of the common object-object mapping scenarios. Finally, we did a performance benchmark to find the most optimal one. From the benchmark result, we came to know that Mapster performs better than AutoMapper in almost every scenario. In addition to the performance, Mapster scores in ease of use as well.

If you want to learn about all the available options, please check out the official documentation of AutoMapper and Mapster.