Skip to content

Repository files navigation

Mapster - AutoMapper

Introduction

This is an analysis of the use of the two most famous mapping tools: Automapper and Mapster. We will show some benchmarks by using BenchmarkDotNet.

Automapper

Once you have added AutoMapper Nuget Package (ATM v12.0.0), instantiate automapper in few steps:

IMapperautomapper=newMapper(newMapperConfiguration(z =>z.AddProfile(newAutomapperProfile())));classAutomapperProfile:Profile{publicAutomapperProfile(){CreateMap<Portfolio,DtoPortfolio>();}}varpDto=automapper.Map<DtoPortfolio>(p);

This is the simplest case where p and pDTO have the same properties. If pDTO has a property that we want to map but the name is different from the source (p), we need is some way how to indicate this rule.

publicclassPortfolioProfile:Profile{publicPortfolioProfile(){CreateMap<Portfolio,DtoPortfolio>().ForMember(dest =>dest.DtoId, opt =>opt.MapFrom(src =>src.Id)).ForMember(dest =>dest.DtoCode, opt =>opt.MapFrom(src =>src.Code)).ForMember(dest =>dest.DtoName, opt =>opt.MapFrom(src =>src.Name)).ForMember(dest =>dest.DtoType, opt =>opt.MapFrom(src =>src.Type)).ForMember(dest =>dest.DtoStatus, opt =>opt.MapFrom(src =>src.Status));}}

A custom profile is used to map each prop from the source to destination we want to obtain and pass it to the MapperConfiguration:

IMapperautomapper=newMapper(newMapperConfiguration(z =>z.AddProfile(newPortfolioProfile())));

Mapster

Mapster is relatively new and the github page shows some benchmarks compared with Automapper.

In order to use Mapster it is enough to use this simple code:

varpDto=p.Adapt<DtoPortfolio>();

and Mapster will map the Dto automatically. But this is a simple case with one-to-one mapping. For the case with adapter the code remains basically the same but we need to add AdaptMember as annotation in the props we want to map.

publicclassDtoPortfolio{[AdaptMember("Id")]publicintDtoId{get;set;}[AdaptMember("Code")]publicstringDtoCode{get;set;}[AdaptMember("name")]publicstringDtoName{get;set;}[AdaptMember("Type")]publicstringDtoType{get;set;}[AdaptMember("Status")]publicstringDtoStatus{get;set;}}

MapsterCodeGen

Mapster offers a chance to generate the Dto dynamically at build time. This we will see later, offers great performances. In order to generate automatically the Dto, it is needed to add these lines to csproj file:

<TargetName="Mapster"><Exec WorkingDirectory="$(ProjectDir)"Command="dotnet build"/><Exec WorkingDirectory="$(ProjectDir)"Command="dotnet tool restore"/><Exec WorkingDirectory="$(ProjectDir)"Command="dotnet mapster model -a &quot;$(TargetDir)$(ProjectName).dll&quot;"/><Exec WorkingDirectory="$(ProjectDir)"Command="dotnet mapster extension -a &quot;$(TargetDir)$(ProjectName).dll&quot;"/><Exec WorkingDirectory="$(ProjectDir)"Command="dotnet mapster mapper -a &quot;$(TargetDir)$(ProjectName).dll&quot;"/></Target>

and this for the clean up:

<ItemGroup><Generated Include="**\*.g.cs"/></ItemGroup><Target Name="CleanGenerated"><Delete Files="@(Generated)"/></Target>

In this way, the generated files will be cleaned by executing the command:

dotnetmsbuild-t:CleanGenerated

and with this command the files will be generated:

dotnetbuild-t:Mapster

When we will run the build the Dto with .g.cs extension will be generated.

Mapping scenarios to test

The benchmarks will consider two cases:

  • a simple case for a one-to-one mapping fields with a small and a great number of fields as subcases. We should also consider cases:

    • with adapter: the mapping rule maps the field with a different name. A custom adapter rule should be provided to indicate which field should be mapped to.
      int a => int x
      string b => string y
      date c => date z
      
    • without adapter: the mapping rule maps the field with the same name. The mapping tool does it for you out of the box. No extra code is required.
      int a => int a
      string b => string b
      date c => date c
      
  • unflattened: the mapping for nested onjects like this:

     ```
    int x0 => int x1
    class a0: => class a1:
    { {
    string b0; string b1;
    date c0; date c1;
    } }
    date y0 => date y1;
    ```
    

Here a summary of different cases:

BenchMarkDotNet (Basecode)
TypeDescriptionAutomapperMapsterMapsterCodeGen
Simplewith Adapter
SimpleNo Adapter
Bigwith Adapter
BigNo Adapter
UnflattenedNested objects, no Adapter

Test cases

Because there are different cases and combinations, by showing all these results in one shot is not helpful, so we go step by step.

Simple case

The simple case regards a one-to-one mapping with a Dto with just five fields:

publicclassDtoPortfolio{publicintId{get;set;}publicstringCode{get;set;}publicstringName{get;set;}publicstringType{get;set;}publicstringStatus{get;set;}}

Big case

publicclassDtoPortfolio{publicintId{get;set;}publicstringCode{get;set;}publicstringName{get;set;}publicstringType{get;set;}publicstringStatus{get;set;}
...publicdecimalProp101{get;set;}
...publicdecimalProp150{get;set;}publicstringProp1{get;set;}
...publicstringProp50{get;set;}publicDateTimeProp151{get;set;}
...publicDateTimeProp200{get;set;}}

Unflattened case

The unflattened case regards a mapping with nested objects:

publicclassDtoPortfolioUnflattened{[Key]publicintId{get;set;}publicstringCode{get;set;}publicstringName{get;set;}publicstringType{get;set;}publicstringStatus{get;set;}publicDtoStringPropertiesUnflattenedGroupStringProperties{get;set;}publicDtoIntPropertiesUnflattenedGroupIntProperties{get;set;}publicDtoDecimalPropertiesUnflattenedGroupDecimalProperties{get;set;}publicDtoDateTimePropertiesUnflattenedGroupDateTimeProperties{get;set;}}// 50 decimal fieldspublicclassDtoDecimalPropertiesUnflattened{publicdecimalProp101{get;set;}
...publicdecimalProp150{get;set;}}// 50 int fieldspublicclassDtoIntPropertiesUnflattened{publicintProp51{get;set;}
...publicintProp100{get;set;}}// 50 string fieldspublicclassDtoStringPropertiesUnflattened{publicstringProp1{get;set;}
...publicstringProp50{get;set;}}// 50 datetime fieldspublicclassDtoDateTimePropertiesUnflattened{publicDateTimeProp151{get;set;}
...publicDateTimeProp200{get;set;}}

BenchmarkDotNet results

Now let us go to run the benchmarks for these cases by using AutoMapper and Mapster.

The benchmark considers a list of portfolios of 10, 100 and 1000 size. How to do that?

By adding an annotation on the size property, we can parameterize the benchmark. So for each type of benchmark we will consider a list portfolios with length 10, 100 or 1000.

[Params(10,100,1000)]publicintnumElements{get;set;}

Simple Portfolio

Simple Portfolio - With Adapter - 10 elements

MethodMeanAllocated
MapsterCodeGen403.8 ns1.14 KB
Mapster434.7 ns1.14 KB
AutoMapper974.8 ns1.17 KB

Simple Portfolio - No Adapter - 10 elements

MethodMeanAllocated
MapsterCodeGen417.7 ns1.14 KB
Mapster529.1 ns1.14 KB
AutoMapper996.8 ns1.17 KB

Simple Portfolio - With Adapter - 100 elements

MethodMeanAllocated
MapsterCodeGen3,788.3 ns10.98 KB
Mapster4,110.2 ns10.98 KB
AutoMapper9,651.9 ns11.25 KB

Simple Portfolio - No Adapter - 100 elements

MethodMeanAllocated
MapsterCodegen3,862.9 ns10.98 KB
Mapster4,481.8 ns10.98 KB
AutoMapper10,086.9 ns11.25 KB

Simple Portfolio - With Adapter - 1000 elements

MethodMeanAllocated
MapsterCodeGen37,456.1 ns109.42 KB
Mapster40,872.3 ns109.42 KB
AutoMapper99,565.3 ns112.05 KB

Simple Portfolio - No Adapter - 1000 elements

MethodMeanAllocated
MapsterCodeGen38,104.4 ns109.42 KB
Mapster44,300.2 ns109.42 KB
AutoMapper98,356.1 ns112.05 KB

In the mapping one-to-one for a list of portfolios with the same allocation memory Mapster CodeGen is more performing. Futhermore, as we can see, there is no much difference between Adapter and No Adapter scenario. The use of Adapter then is clearly suggested only for situations where the Dto is already defined and we cannot create from scratch.

Big Portfolio

MethodnumElementsMeanAllocated
MapsterCodeGenNoAdapter10104.9 μs18.17 KB
MapsterCodeGenWithAdapter10105.3 μs18.17 KB
MapsterWithAdapter10106.6 μs36.3 KB
MapsterNoAdapter10107.0 μs36.3 KB
AutoMapperNoAdapter10117.8 μs36.3 KB
AutoMapperWithAdapter10118.2 μs36.3 KB
MethodnumElementsMeanAllocated
MapsterCodeGenNoAdapter1001,054.0 μs181.3 KB
MapsterCodeGenWithAdapter1001,063.9 μs181.3 KB
MapsterWithAdapter1001,070.7 μs362.55 KB
MapsterNoAdapter1001,079.6 μs362.55 KB
AutoMapperNoAdapter1001,154.2 μs362.55 KB
AutoMapperWithAdapter1001,193.3 μs362.55 KB
MethodnumElementsMeanAllocated
MapsterCodeGenWithAdapter100010,186.8 μs1812.57 KB
MapsterWithAdapter100010,775.2 μs3625.07 KB
MapsterNoAdapter100010,782.8 μs3625.07 KB
MapsterCodeGenNoAdapter100010,903.7 μs1812.57 KB
AutoMapperNoAdapter100011,498.7 μs3625.06 KB
AutoMapperWithAdapter100012,082.7 μs3625.06 KB

Unflattened Portfolio - No Adapter

MethodnumElementsMeanAllocated
Mapster10109.1 μs38.17 KB
MapsterCodeGen10109.5 μs38.17 KB
AutoMapper10126.6 μs38.17 KB
Mapster1001,086.9 μs381.3 KB
MapsterCodeGen1001,093.7 μs381.3 KB
AutoMapper1001,255.0 μs381.3 KB
Mapster100010,978.2 μs3812.56 KB
MapsterCodeGen100011,067.4 μs3812.56 KB
AutoMapper100012,680.4 μs3812.56 KB

Also in this case Mapster confirms a better performance than AutoMapper with the same allocation memory.

Can we do better?

This is a question that we should ask always to ourselves, we cannot never say never... From C# 9 we can use record types.

C# 9 introduces records, a new reference type that you can create instead of classes or structs. C# 10 adds record structs so that you can define records as value types. Records are distinct from classes in that record types use value-based equality. Two variables of a record type are equal if the record type definitions are identical, and if for every field, the values in both records are equal. Two variables of a class type are equal if the objects referred to are the same class type and the variables refer to the same object. Value-based equality implies other capabilities you'll probably want in record types. The compiler generates many of those members when you declare a record instead of a class. The compiler generates those same methods for record struct types.

and here:

Beginning with C# 9, you use the record keyword to define a reference type that provides built-in functionality for encapsulating data. C# 10 allows the record class syntax as a synonym to clarify a reference type, and record struct to define a value type with similar functionality. You can create record types with immutable properties by using positional parameters or standard property syntax.

What happen by using a record struct istead of a class? We are telling to the compiler to generate value based type instead of reference types.

As we can see we have a better performance and less memory allocation at the same time.

AutoMapper With Adapter - comparing with struct

MethodnumElementsMeanAllocated
AutoMapperNoAdapterRecord10970.3 ns608 B
AutoMapperWithAdapter10974.8 ns1168 B
AutoMapperWithAdapterRecord10979.9 ns608 B
AutoMapperNoAdapter10996.8 ns1168 B
MethodnumElementsMeanAllocated
AutoMapperNoAdapterRecord1009,478.7 ns5648 B
AutoMapperWithAdapterRecord1009,503.1 ns5648 B
AutoMapperWithAdapter1009,651.9 ns11248 B
AutoMapperNoAdapter10010,086.9 ns11248 B
MethodnumElementsMeanAllocated
AutoMapperWithAdapterRecord100095,940.8 ns56048 B
AutoMapperNoAdapterRecord100096,556.7 ns56048 B
AutoMapperNoAdapter100098,356.1 ns112048 B
AutoMapperWithAdapter100099,565.3 ns112048 B

An interesting case

Here we wil now consider a more complex case. Let's suppose we need to do some elaborations from our model, for example:

```
int a => int x
string b string c => string y = b + c + d
string d date e => date z
```

We will see that in this case the performance are not good and we will explain why. In particular starting from the second simple case (205 fields) we will take in consideration the following elaborations in the mapping:

  • StringProperties is a concatenation of strings operation by using the strings props
  • IntProperties: calculates min(), max() and avg() from a list in integers props
  • DecimalProperties: calculates min(), max() and avg() from a list in decimals props
  • GroupDateTimeProperties: calculates min() and max() date from a list of dates from the dates props

In this case the Dto will be:

publicclassDtoPortfolio{publicintDtoId{get;set;}publicstringDtoCode{get;set;}publicstringDtoName{get;set;}publicstringDtoType{get;set;}publicstringDtoStatus{get;set;}publicStringPropertiesGroupStringProperties{get;set;}publicIntPropertiesGroupIntProperties{get;set;}publicDecimalPropertiesGroupDecimalProperties{get;set;}publicDateTimePropertiesGroupDateTimeProperties{get;set;}}publicclassStringProperties{publicstringvalue{get;set;}publicintnumWords{get;set;}publicintlength{get;set;}}publicclassIntProperties{publicintminValue{get;set;}publicintmaxValue{get;set;}publicdoubleavgValue{get;set;}}publicclassDecimalProperties{publicdecimalminValue{get;set;}publicdecimalmaxValue{get;set;}publicdecimalavgValue{get;set;}}publicclassDateTimeProperties{publicDateTimeminValue{get;set;}publicDateTimemaxValue{get;set;}}

Automapper configuration

When a more complex mapping is needed (because you know, life is not easy sometimes :-)), Automapper helps us on this by using a resolver. A custom resolver is created by implementing IValueResolver<in TSource, in TDestination, TDestMember> for each operation we need, with:

- TSource: type of source
- TDestination: type of destination
- TDestMember: type of destination field
publicclassPortfolioResolver:IValueResolver<Portfolio,DtoPortfolio,DecimalProperties>,IValueResolver<Portfolio,DtoPortfolio,StringProperties>,IValueResolver<Portfolio,DtoPortfolio,DateTimeProperties>,IValueResolver<Portfolio,DtoPortfolio,IntProperties>{publicDecimalPropertiesResolve(Portfoliosource,DtoPortfoliodestination,DecimalPropertiesdestMember,ResolutionContextcontext){returnnewDecimalProperties{//do your operations};}publicStringPropertiesResolve(Portfoliosource,DtoPortfoliodestination,StringPropertiesdestMember,ResolutionContextcontext){returnnewStringProperties(){//do your operations };}publicDateTimePropertiesResolve(Portfoliosource,DtoPortfoliodestination,DateTimePropertiesdestMember,ResolutionContextcontext){returnnewDateTimeProperties{//do your operations };}publicIntPropertiesResolve(Portfoliosource,DtoPortfoliodestination,IntPropertiesdestMember,ResolutionContextcontext){returnnewIntProperties{//do your operations };}}

Then adding to the existing profile:

publicclassPortfolioProfile:Profile{publicPortfolioProfileBig(){CreateMap<Portfolio,DtoPortfolio>().ForMember(dest =>dest.DtoId, opt =>opt.MapFrom(src =>src.Id)).ForMember(dest =>dest.DtoCode, opt =>opt.MapFrom(src =>src.Code)).ForMember(dest =>dest.DtoName, opt =>opt.MapFrom(src =>src.Name)).ForMember(dest =>dest.DtoType, opt =>opt.MapFrom(src =>src.Type)).ForMember(dest =>dest.DtoStatus, opt =>opt.MapFrom(src =>src.Status));.ForMember(dest =>dest.GroupDecimalProperties, src =>src.MapFrom<PortfolioResolver>()).ForMember(dest =>dest.GroupStringProperties, src =>src.MapFrom<PortfolioResolver>()).ForMember(dest =>dest.GroupIntProperties, src =>src.MapFrom<PortfolioResolver>()).ForMember(dest =>dest.GroupDateTimeProperties, src =>src.MapFrom<PortfolioResolver>());}}

Mapster

For the complex case we can map the elaboration into the mapping, ElaborationInMapping or outside the mapping, ElaborationOutsideMapping.

Elaboration-In-Mapping

We need to extend the Adapt instruction with a TypeAdapterConfig parameter in this way:

varpDto=p.Adapt<DtoPortfolio>(GetTypeAdapterConfig());

where GetTypeAdapterConfig() define our custom mapping in a similar way it has been done for PortfolioResolver for AutoMapper

publicstaticTypeAdapterConfigGetTypeAdapterConfig(){varconfig=newTypeAdapterConfig();config.NewConfig<Portfolio,DtoPortfolio>().Map(dest =>dest.DtoId, src =>src.Id).Map(dest =>dest.DtoCode, src =>src.Code).Map(dest =>dest.DtoName, src =>src.Name).Map(dest =>dest.DtoType, src =>src.Type).Map(dest =>dest.DtoStatus, src =>src.Status).Map(dest =>dest.GroupStringProperties, src =>CalculateStringOperation(...)).Map(dest =>dest.GroupIntProperties, src =>CalculateIntOperation(...)).Map(dest =>dest.GroupDecimalProperties, src =>CalculateDecimalOperation(...)).Map(dest =>dest.GroupDateTimeProperties, src =>CalculateDateTimeOperation(...));returnconfig;}privatestaticStringPropertiesCalculateStringOperation(...){//do your operations }privatestaticDateTimePropertiesCalculateDateTimeOperation(...){//do your operations }privatestaticIntPropertiesCalculateIntOperation(...){//do your operations }privatestaticDecimalPropertiesCalculateDecimalOperation(...){//do your operations }

Elaboration-Outside-Mapping

This is just a different way where the elaboration is done in the classical way without using any mapping. This we will see has a different performance that makes it useful to add in out tests:

varpDto=p.Adapt<DtoPortfolio>();pDto.GroupDateTimeProperties=CalculateDateTimeOperation(...);pDto.GroupDecimalProperties=CalculateDecimalOperation(p);pDto.GroupIntProperties=CalculateIntOperation(...);pDto.GroupStringProperties=CalculateStringOperation(...);

Benchmarks

The benchmarks will show results for 10, 100 and 500 iterations:

MethodnumElementsMeanAllocated
MapsterOutsideMapping10139.3 us78.41 KB
AutoMapperPortfolio10140.6 us79.35 KB
MapsterInMapping10117,229.9 us8173.44 KB
MethodnumElementsMeanAllocated
MapsterOutsideMapping1001,384.0 us783.64 KB
AutoMapperPortfolio1001,417.8 us793.04 KB
MapsterInMapping1001,136,756.6 us81724.42 KB
MethodnumElementsMeanAllocated
MapsterOutsideMapping5006,898.0 us3918.02 KB
AutoMapperPortfolio5007,035.1 us3965.02 KB
MapsterInMapping5005,701,285.5 us408468.32 KB

There is an interesting point to put in evidence here. Even if the mapster-outside-mapping has the best performance, this means that the mapping is not useful at all in this case. This is not because the mapping does not work but we are not using the tool in the right way. We are using the mapping to resolve a problem where mapping does not fits for this. In order to obtain some calculated values this is to be done at model level, before the mapping level. Probably in this case we need to do some changes in the model, not in the Dto.

About

Mapster and Automapper performance analysis

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages