I made the world’s fastest Mapper library in C # (3 to 10 times faster than AutoMapper etc.)

9 minute read

Overview

The English article posted to Code Project will be translated into Japanese and shared on Qiita.
https://www.codeproject.com/Articles/5275388/HigLabo-Mapper-Creating-Fastest-Object-Mapper-in-t

I made an object mapper called HigLaboMapper. I tried to make it with the goal of being the fastest in the world because I wanted to make it.
It uses ExpressionTree to implement a much faster implementation than the existing mapper libraries AutoMapper, ExpressMapper, AgileMapper, FastMapper, and Mapster. As a result, it is currently the fastest in the world in test results by Benchmark DotNet.
In addition, there is no need to write unnecessary setting code because it can be used without initial setting. You can also customize the mapping rules in an intuitive way.

introduction

I created an object mapper with il code 4 years ago. I decided to re-implement it in July, so I rewrote HigLabo.Mapper with Expression Tree. As a result, we were able to significantly improve performance. As a result of performance testing, it is the fastest in the world as of August 2020. It took about 10 days to create. The 10-day commit log
https://github.com/higty/higlabo.netstandard/tree/master/HigLabo.Mapper.
It is in.

It’s also a good idea to make it and keep it buried, so I will share it and contribute to the C # and .NET communities.

The source code is
https://github.com/higty/higlabo/tree/master/NetStandard/HigLabo.Mapper.
It is in.

You can also get it from Nuget with HigLabo.Mapper.

How to use

Install HigLabo.Mapper from Nuget. Please install version 3.0 or later.

Import the namespace.

using HigLabo.Core

The Map extension method is now available.

var a1 = new Address(); //your POCO class.
var a2 = a1.Map(new Address());

HigLabo.Mapper also supports Dictionary to Object mapping.

var d = new Dictionary<String, String>(); 
d["Name"] = "Bill";
var person = d.Map(new Person());
//person.Name is "Bill"

Object to Dictionary mapping is supported as well.

var p = new Person(); 
p.Name = "Bill";
var d = p.Map(new Dictionary<String, String>);
//d["Name"] is "Bill"

HigLabo.Mapper is designed to be intuitive and easy to use.

Comparison with other mappers

In this section, we will explain the differences from other mappers. The table of contents is as follows.

  1. Performance
  2. Initial setting
  3. Customize
  4. Multiple settings

performance

Performance is very important in the mapper library. Since the mapping process is often executed in a hot code path, such as inside a loop, it tends to have a large impact on performance.

Below is a summary of the performance test results.
・ 3-4 times faster than AutoMapper (POCO object without collection property)
10% -20% faster than Mapster (POCO objects without collection properties)
7x-10x faster than AgileMapper, FastMapper, TinyMapper (POCO objects without collection properties)
3 times faster than AutoMapper (POCO object with collection properties)
10 times faster than Mapster (POCO object with collection properties)
・ 10 to 20 times faster than AgileMapper, FastMapper, TinyMapper (POCO object with collection property)

The test result is as follows. HigLaboObjectMapper_XXX is the new version, HigLaboObjectMapConfig_XXX is the old version.
HigLabo.Mapper.PerformanceTestResult_Mini1.png

The POCO class used for the test is as follows.

public class Address
{
    public int Id { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
    public AddressType AddressType { get; set; }
}

public class AddressDTO
{
    public int Id { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
    public AddressType AddressType { get; set; } = AddressType.House;
}
public struct GpsPosition
{
    public double Latitude { get; private set; }
    public double Longitude { get; private set; }

    public GpsPosition(double latitude, double longitude)
    {
        this.Latitude = latitude;
        this.Longitude = longitude;
    }
}

public class Customer
{
    public Int32? Id { get; set; }
    public String Name { get; set; }
    public Address Address { get; set; }
    public Address HomeAddress { get; set; }
    public Address[] AddressList { get; set; }
    public IEnumerable<Address> WorkAddressList { get; set; }
}

public class CustomerDTO
{
    public Int32? Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
    public AddressDTO HomeAddress { get; set; }
    public AddressDTO[] AddressList { get; set; }
    public List<AddressDTO> WorkAddressList { get; set; }
    public String AddressCity { get; set; }
}

I tested with 4 patterns.

// 1. POCO class without collection property to same class.
XXX.Map(new Address(), new Address());
// 2. POCO class without collection property to other class.
XXX.Map(new Address(), new AddressDTO());
// 3. POCO class that has collection property map to same class.
XXX.Map(new Customer(), new Customer());
// 4. POCO class that has collection property map to other class.
XXX.Map(new Customer(), new CustomerDTO());

As the result of the attached image, HigLabo.Mapper is the fastest in all cases.
The test code is here.
https://github.com/higty/higlabo/tree/master/NetStandard/HigLabo.Test/HigLabo.Mapper.PerformanceTest

Initial setting

Some mappers require initial setup before use.

var configuration = new AutoMapper.MapperConfiguration(config => {
    config.CreateMap<Building, Building>();
    config.CreateMap<TreeNode, TreeNode>();
});

This is the AutoMapper configuration code. If the class to be mapped becomes 1000 or so, this work is very tedious and time-consuming.

Similarly, TinyMapper requires the following setting code.

TinyMapper.Bind<Park, Park>();
TinyMapper.Bind<Customer, CustomerDTO>();
TinyMapper.Bind<Dictionary<String, String>, Building>();

HigLabo.Mapper does not require the above configuration code. The man-hours for writing the setting code can be reduced to zero.

Customize

Sometimes in the mapper library you want to customize the mapping rules. Customizing AutoMapper requires a very complicated and confusing description.
For example, let’s compare with the mapping rules on this page.
https://stackoverflow.com/questions/50964757/delegating-member-mapping-to-child-object-with-automapper
In AutoMapper

class Source {
  public int Id {get;set;}
  public int UseThisInt {get;set;}
  public InnerType Inner {get;set;}
  // other properties that the Destination class is not interested in
}
class InnerType {
  public int Id {get;set;}
  public int Height {get;set;}
  // more inner properties
}
class Destination {
  public int Id {get;set;}
  public int UseThisInt {get;set;}
  public int Height {get;set;}
  // more inner properties that should map to InnerType
}

//So many configuration and complicated....
Mapper.Initialize(cfg => {
    cfg.CreateMap<source, destination="">();
    cfg.CreateMap<innertype, destination="">();
});
var dest = Mapper.Map<destination>(src);
Mapper.Map(src.Inner, dest);
Mapper.Initialize(cfg => {
        cfg.CreateMap<source, destination="">()AfterMap
                     ((src, dest) => Mapper.Map(src.Inner, dest));
        cfg.CreateMap<innertype, destination="">();
    });
var dest = Mapper.Map<destination>(src);

You need to be familiar with AutoMapper rules (Mapper.Initialize, ForMember, CreateMap, AfterMap, etc.).

Customization with HigLabo.Mapper is very easy.

c.AddPostAction<Source, Destination>((s, d) =>
{
    d.Id = s.Inner.Id;
    //Set Inner object property to Destination object     
    s.Inner.Map(d); 
});

HigLabo.Mapper calls this lambda expression after the default mapping process is done. This allows you to override the default mapping.

If you want to completely replace the mapping process, use the ReplaceMap method.

c.ReplaceMap<Source, Destination>((s, d) =>
{
    //Set all map with your own.
    d.Id = s.Inner.Id;
    //Set Inner object property to Destination object
    s.Inner.Map(d);
});
//You can call Map method.
var source = new Source();
var destination = new Destination();
source.Map(distination); //Above lambda will be called.

This method is very simple and intuitively easy to understand. You can use it if you have knowledge of C # lambda. If you’re using C # to some extent, you already have some knowledge of lambda expressions, so no additional knowledge is needed.

You can easily add conversion processing.

c.AddPostAction<Person, PersonVM>((s, d) =>
{
    d.BMI = CalculateBMI(s.Height, s.Weight);
});

Mapping by conditional branching is also easy.

c.AddPostAction<Employee, EmployeeVM>((s, d) =>
{
    if (s.EmployeeType == EmployeeType.Contract)
    {
        d.Property1 = someValue1;
    }
    else
    {
        d.Property1 = someValue2;
    }
});

Another useful point is that it is very easy to debug. Breakpoints can be set inside the lambda expression passed to the AddPostAction and ReplaceMap methods for debugging.

You can also customize the property mapping.

class Person
{
    public string Name { get; set; }
    public string Position_Name { get; set; }
}
class PersonModel
{
    public string Name { get; set; }
    public string PositionName { get; set; }
}

var mapper = HigLabo.Core.ObjectMapper.Default;
mapper.CompilerConfig.PropertyMatchRule = 
       (sourceType, sourceProperty, targetType, targetProperty) 
{
    if (sourceType == typeof(Person) && targetType == typeof(PersonModel))
    {
        return sourceProperty.Name.Replace("_", "") == targetProperty.Name;
    }
    return false;
};

Multiple settings

HigLabo.Mapper allows you to create multiple instances of the ObjectMapper class.

var om1 = new ObjectMapper();
om1.AddPostAction<Address, Address>((s, d) =>
{
    //Custom map rule
});

var om2 = new ObjectMapper();
om2.AddPostAction<Address, Address>((s, d) => 
{
   //Another Custom map rule 
});

var a = new Address();
var a1 = om1.Map(a, new Address());
var a2 = om1.Map(a, new Address());

The Map extension methods declared in the ObjectMapperExtensions class actually use an instance of the ObjectMapper.Default property.

using System;

namespace HigLabo.Core
{
    public static class ObjectMapperExtensions
    {
        public static TTarget Map<TSource, TTarget>(this TSource source, TTarget target)
        {
            return ObjectMapper.Default.Map(source, target);
        }
        public static TTarget MapOrNull<TSource, TTarget>
               (this TSource source, Func<TTarget> targetConstructor)
            where TTarget : class
        {
            return ObjectMapper.Default.MapOrNull(source, targetConstructor);
        }
        public static TTarget MapOrNull<TSource, TTarget>(this TSource source, TTarget target)
            where TTarget : class
        {
            return ObjectMapper.Default.MapOrNull(source, target);
        }
        public static TTarget MapFrom<TTarget, TSource>(this TTarget target, TSource source)
        {
            return ObjectMapper.Default.Map(source, target);
        }
    }
}

You can create multiple instances and set mapping rules for each instance. It is also possible to encapsulate the application mapping rules.

public static class ObjectMapperExtensions
{
    public static void Initialize(this ObjectMapper mapper)
    {
        mapper.AddPostAction<Address, Address>((s, d) =>
        {
            //Your mapping rule.
        });
        mapper.AddPostAction<Address, Address>((s, d) =>
        {
            //Another your mapping rule.
        });
    }
}

//And call it on Application initialization process.
ObjectMapper.Default.Initialize();

About mapping test cases

The mapping test cases are below.
https://github.com/higty/higlabo/tree/master/NetStandard/HigLabo.Test/HigLabo.Mapper.Test
All test cases from previous versions have passed except one test case.
(* Dictionary custom mapping is not supported in the new version)

Deep Dive into expression tree

The test cases are below.
https://github.com/higty/higlabo/tree/master/NetStandard/HigLabo.Test/HigLabo.Mapper.Test
In this, the expression tree code generated in the ObjectMapper_Map_ValueType_ValueType test case looks like the following.

.Lambda #Lambda1<System.Func`3[System.Object,System.Object,HigLabo.Mapper.Test.Vector2]>(
    System.Object $sourceParameter,
    System.Object $targetParameter) {
    .Block(
        HigLabo.Mapper.Test.Vector2 $source,
        HigLabo.Mapper.Test.Vector2 $target) {
        $source = .Unbox($sourceParameter);
        $target = .Unbox($targetParameter);
        .Call $target.set_X($source.X);
        .Call $target.set_Y($source.Y);
        $target
    }
}

The following Map Action Func is generated in the mapping from Address to Address DTO.

.Lambda #Lambda1<System.Func`4[System.Object,System.Object,HigLabo.Core.ObjectMapper+MapContext,HigLabo.Mapper.PerformanceTest.AddressDTO]>(
    System.Object $sourceParameter,
    System.Object $targetParameter,
    HigLabo.Core.ObjectMapper+MapContext $context) {
    .Block(
        HigLabo.Mapper.PerformanceTest.Address $source,
        HigLabo.Mapper.PerformanceTest.AddressDTO $target) {
        $source = $sourceParameter .As HigLabo.Mapper.PerformanceTest.Address;
        $target = $targetParameter .As HigLabo.Mapper.PerformanceTest.AddressDTO;
        .Call $target.set_Id($source.Id);
        .Call $target.set_City($source.City);
        .Call $target.set_Country($source.Country);
        .Call $target.set_AddressType($source.AddressType);
        $target
    }
}

You can see that the code is generated with almost no waste. As you can see from the code, it will be harder to generate the fastest code than HigLabo.Mapper. It may be faster if you use Span etc.

The code blocks in these expression trees are compiled, converted to Func <Object, Object, TTarget> and stored in a private _MapActionList field. From the second time onward, the compiled Func <Object, Object, TTarget> is used, so there is no compilation overhead. The lambda passed in AddPostAction will generate a new Func so that it will be called after this Func. You can use the ReplaceMap method to replace this Func.

Summary

I hope this library is useful for those who want to speed up the mapping process in C #, reduce the code for configuration, and customize it more easily. Feel free to comment on GitHub if you have any problems.

bonus

Those who are interested in creating high-performance applications will also find the following articles helpful.
I tried to create task management-tools and services to improve from beginner level-
Basic knowledge of computer science and its practice that you should know for improving performance
Technology and knowledge you need to know to improve the performance of web applications
I made the world’s fastest Mapper library in C # (3 to 10 times faster than AutoMapper etc.)
Functional UI design for programmers
Advanced technical articles for becoming a world-class engineer
Azure Application Architecture Guide

This is the original article ↓
Basic knowledge of computer science and its practice to improve performance

In addition, this article introduces how to make a Saas application using this knowledge.