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.
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.
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 Mapster
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.
Hi there
Great article, thank you! The second title “Custom Property or Member Mapping With AutoMapper” should be Mapster not AutoMapper…
Have a great day!
Hello Michael. Thank you for the comment and the suggestion. Yeah, it was a mistake from our side and now it is fixed. If you can’t see it corrected, just CTRL+F5 the page. Thanks one more time.
I *strongly* advise against using mappers.
If its complicated, then a hand coded mapper is better as it’s much more explicit and easier to test.
If it’s simple, then a hand coded mapper is simple.
Even better, use read only interfaces then you don’t have to map at all.
Hi Simon. Well, you are not the first one I met who is against mappers, that’s for sure 🙂 Just sometimes, you have to write a big code sausage to create a mapping you need, and mappers do that in a cleaner way (of course, we will not argue what sausages those mappers have behind the scene 🙂 )
It’s a shame this again is a very shallow comparison against AutoMapper. I’ve been using AutoMapper extensively for over 10 years and have read many comparisons and rants about how bad or slow or whatever it is, but I’ve yet to see a single article mention the projection capabilities of AutoMapper in such a comparison. Of course not everyone will use those, but in my opinion that’s a hugely important feature. AutoMapper als supports conventions for repeated behavior, performs null conditional mappings out of the box and has mapping validation. These are features that matter a lot in the long run. Performance is something you can buy, maintainability isn’t.
Hi Michael. I am sorry, but you got it all wrong. This article has nothing to do with any rant against Automapper. This article is mainly about how to do same things with one and another, and then just in the last sections we used benchmark to compare those operations. From that performance test, we just interpreted results. You won’t read anywhere in the article that we say: “don’t use Automapper, use mapster always”, or anything similar. Again, in the last section, we just interpreted the benchmark results. Moreover, we have very popular ASP.NET Core book package, and we use Automapper for mapping options there, because we find it a very useful tool.
I’m not saying your post is a rant, I’m saying it’s a shallow comparison and missing details that matter in the long run. It’s like comparing a SUV and a true offroader and saying they both offer a good height to get into the car and stop at that. What good is a comparison on the outer most perspective on anything?
Ok, I can accept your opinion, but simply don’t agree. Are there so many more functionalities in both? YES. But the author covered the actions that are commonly used in most of the projects and presented the way of doing the same operations in both libraries and I think he did a great job there. Lastly, those operations are simply compared using the benchmark, nothing more than that. We can argue here whether should we cover more operations, or shouldn’t we use these ones, but I simply disagree that the article is a shallow comparison. Again, I completely respect your opinion, as long as we agree this article is not a rant 🙂
Then we agree. I mistakenly referred to you as the author, but that doesn’t change the message. It’s not just about covering more operations, it’s about delivering perspective, clarifying purpose. Do the explained properties of both projects matter? I don’t think so, they’re what most basic mappers will support. Is the performance important? Not hugely, as long as one isn’t terribly slow (and even then, purpose can still be more important). Anyone that actually needs to consider performance should benchmark their own use of either solution anyway, a sample benchmark doesn’t provide guarantees for real world usage.
The thing you can however hold me accountable for is that I don’t have any experience with Mapster, so I can’t tell if it offers any of the features that I think are important when choosing a mapping framework, unfortunately the author isn’t shining any light on that either.
Well, I am not the author, but the site is mine, so I feel about each comment as if I wrote the article 🙂 🙂 Anyway, since I don’t have any experience with Mapster as well, from my perspective, this was/is a great comparison between the two in how the same things could be executed in both tools. I find it pretty informative but of course, we don’t have to agree on this.
Nice and simple comparison of 2 most popular .NET mapping libraries! Could you review and benchmark MappingGenerator commercial library to see how it compares with this two?
Thanks
Hi Branislav. Thanks for the comment. About that tool you suggested, we didn’t use it at all, but if any of our authors did, we will see to make some comparisons.
One aspect you skipped is how to create a reverse map on objects which require a custom field mapping. I believe with AutoMapper, it would be:
cfg.CreateMap<User, UserDto>()
.ForMember(
dest => dest.FullName,
config => config.MapFrom(src => $”{src.FirstName} {src.LastName}”
))
.ReverseMap()
.ForMember(
dest => dest.FirstName,
config => config.MapFrom(src => src.FullName.SubString(0, src.FullName.IndexOf(‘ ‘))))
.ForMember(
dest => dest.LastName,
config => config.MapFrom(src => src.FullName.SubString(src.FullName.IndexOf(‘ ‘)+1)))
I’m not sure how to do it in Mapster.