Click here to Skip to main content
14,602,044 members

HigLabo.Mapper, Creating Fastest Object Mapper in the world with Expression Tree in 10 Days

Rate this:
5.00 (6 votes)
Please Sign up or sign in to vote.
5.00 (6 votes)
4 Aug 2020CPOL
Fastest mapper in the world, easy to Use, ZERO configuration, FULL customization
HigLabo.Mapper is an object mapper. Faster than AutoMapper, Mapster, ExpressMapper, AgileMapper, etc. You can use without configuration, so it saves your time. And you can customize mapping rule in a very intuitive way.

Introduction

Four years ago, I created the initial version of object mapper with IL code. Just a few weeks ago, I decided to reimplement HigLabo.Mapper with Expression Tree and achieve huge performance improvement. It scored fastest in the world currently. You can see my 10 days commit log at https://github.com/higty/higlabo.netstandard/tree/master/HigLabo.Mapper.

I share my code and library to contribute to the C# and .NET community.

The source code can be found at https://github.com/higty/higlabo/tree/master/NetStandard/HigLabo.Mapper.

You can use from Nuget.

HigLabo.Mapper (version 3.0.2)

Background

Now a days, whether you are a developer of Web, Desktop, Mobile, you may use some of POCO, DTO object. And you must transfer property value from object to object. Object mapper is useful for this situation. This time, the goal of my reimplementation is as listed below:

  1. Fastest in the world
  2. Zero configuration for productivity
  3. Full customization for various usecase
  4. Multiple mapping rule for an application

How to Use?

You can get HigLabo.Mapper (version 3.0.0 or later) from Nuget package. (Old HigLabo.Mapper is moved to HigLabo.Mapper.ObjectMapConfig package.)

Add using directive in your application:

using HigLabo.Core;

Now you can use Map extension method like this:

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

HigLabo.Mapper 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 also supported.

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

I design HigLabo.Mapper easy to use.

Comparison to Other Mappers

In this chapter, I will explain the difference to other mapper libraries. This is a summary of comparison.

  1. Performance
  2. Initial Configuration
  3. Customization
  4. Multiple Settings

Performance!!!

It is important for mapper library because it has a tendency to be used in deeper place like inside loop. The summary of performance test is here.

  • 3x-4x faster than AutoMapper (for POCO that does not have collection property)
  • 10%-20% faster than Mapster (for POCO that does not have collection property)
  • 7x-10x faster than AgileMapper, FastMapper, TinyMapper (for POCO that does not have collection property) 
  • 3x faster than AutoMapper (for POCO that has collection property)
  • 10x faster than Mapster (for POCO that has collection property)
  • 10x-20x faster than AgileMapper, FastMapper, TinyMapper (for POCO that has collection property)

Here is a performance test result with BenchmarkDotNet. HigLaboObjectMapper_XXXX is the result of  new HigLabo.Mapper.

Image 1

Here is a class that is used for performance test.

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 4 types of mapping like this:

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 you can see, all 4 cases HigLabo.Mapper is fastest.

You can see the entire test code at https://github.com/higty/higlabo/tree/master/NetStandard/HigLabo.Test/HigLabo.Mapper.PerformanceTest.

Initial Configuration

Some of Mapper requires initial configuration like this:

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

This is AutoMapper configuration code. If you have thousands of classes to map with, it is quite boring to create this mapping configuration code. TinyMapper also requires configuration.

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

HigLabo.Mapper does not require any configuration. You can use it out of the box.

Customization

Sometimes, you want to customize the mapping rule against your POCO object. AutoMapper is so complicated to customize mapping rule. I compare AutoMapper and HigLabo.Mapper with example of this page.

https://stackoverflow.com/questions/50964757/delegating-member-mapping-to-child-object-with-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);

If you use AutoMapper, you must know about the AutoMapper library like Mapper.Initialize, ForMember, CreateMap, AfterMap, etc.

HigLabo.Mapper can customize like this:

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

HigLabo.Mapper simply calls this lambda after mapping completed when you call Map method. So, you can overwrite default mapping rule.

You can completely replace mapping rule with ReplaceMap method like this:

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.

It is simple and no additional knowledge is required. You can only know about C# lambda that you already use.

You can easily add convert logic.

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

You can also use conditional property map.

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

Another advantage of this design is that you can easily debug to your code. You can set break point on lambda inside AddPostAction, ReplaceMap method.

You can also customize property mapping rule like this:

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

You can create multiple instances of 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());

Map extension method is declared ObjectMapperExtensions class. This method simply calls the method of ObjectMapper.Default instance.

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);
        }
    }
}

So, you can create a new instance and add new rule and use it.

You can encapsulate the initalization code like this:

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();

Test Cases for Mapping

I created all test cases here:

I tested all cases that the previous version supported except 1 case for Dictionary custom mapping.

But I think that some of rare case that casse exception may be included because this is the initial release. If you encountered an exception, please feel free to ask me on CodeProject or GitHub issue.

Deep Dive Into Generated Expression Tree

Test case is here.

https://github.com/higty/higlabo/tree/master/NetStandard/HigLabo.Test/HigLabo.Mapper.Test

ObjectMapper_Map_ValueType_ValueType test case will generate the below code with expression tree.

.Block() {
    .Call $target.set_X($source.X);
    .Call $target.set_Y($source.Y);
    $target
}

Address to AddressDTO will generate this expression tree as MapAction Func.

.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
    }
}

ObjectMapper_Map_CollectionElementCreateMode_CollectionElementCreateMode_NewObject will generate the below code:

.Block() {
    .Call $target.set_Value($source.Value);
    .Call $target.set_Name($source.Name);
    .Call $target.set_Int32($source.Int32);
    .Call $target.set_Int32Nullable($source.Int32Nullable);
    .Call $target.set_Int32_Nullable($source.Int32_Nullable);
    .Call $target.set_Int32NullableToInt32($source.Int32NullableToInt32);
    .Call $target.set_DateTime($source.DateTime);
    .Call $target.set_DateTimeNullable($source.DateTimeNullable);
    .Call $target.set_Decimal($source.Decimal);
    .Call $target.set_DecimalNullable($source.DecimalNullable);
    .Call $target.set_DayOfWeek($source.DayOfWeek);
    .Call $target.set_DayOfWeekNullable($source.DayOfWeekNullable);
    .Call $target.set_GuidNullable($source.GuidNullable);
    .If ($source.MapPoint == null) {
        .Call $target.set_MapPoint(.Default(HigLabo.Mapper.Test.MapPoint))
    } .Else {
        .Block() {
            .If ($target.MapPoint == null) {
                $target.MapPoint = .New HigLabo.Mapper.Test.MapPoint()
            } .Else {
                .Default(System.Void)
            };
            .Call ($target.MapPoint).set_Longitude(($source.MapPoint).Longitude);
            .Call ($target.MapPoint).set_Latitude(($source.MapPoint).Latitude)
        }
    };
    .Call $target.set_VectorToNullable($source.VectorToNullable);
    .Call $target.set_Vector2($source.Vector2);
    .If ($source.ParentUser == null) {
        .Call $target.set_ParentUser(.Default(HigLabo.Mapper.Test.User))
    } .Else {
        .Block() {
            .If ($target.ParentUser == null) {
                $target.ParentUser = .New HigLabo.Mapper.Test.User()
            } .Else {
                .Default(System.Void)
            };
            .Call ($target.ParentUser).set_Value(($source.ParentUser).Value);
            .Call ($target.ParentUser).set_Name(($source.ParentUser).Name);
            .Call ($target.ParentUser).set_Int32(($source.ParentUser).Int32);
            .Call ($target.ParentUser).set_Int32Nullable(($source.ParentUser).Int32Nullable);
            .Call ($target.ParentUser).set_Int32_Nullable(($source.ParentUser).Int32_Nullable);
            .Call ($target.ParentUser).set_Int32NullableToInt32
                                      (($source.ParentUser).Int32NullableToInt32);
            .Call ($target.ParentUser).set_DateTime(($source.ParentUser).DateTime);
            .Call ($target.ParentUser).set_DateTimeNullable
                                      (($source.ParentUser).DateTimeNullable);
            .Call ($target.ParentUser).set_Decimal(($source.ParentUser).Decimal);
            .Call ($target.ParentUser).set_DecimalNullable
                                      (($source.ParentUser).DecimalNullable);
            .Call ($target.ParentUser).set_DayOfWeek(($source.ParentUser).DayOfWeek);
            .Call ($target.ParentUser).set_DayOfWeekNullable
                                      (($source.ParentUser).DayOfWeekNullable);
            .Call ($target.ParentUser).set_GuidNullable(($source.ParentUser).GuidNullable);
            .If (($source.ParentUser).MapPoint == null) {
                .Call ($target.ParentUser).set_MapPoint(.Default(HigLabo.Mapper.Test.MapPoint))
            } .Else {
                .Block() {
                    .If (($target.ParentUser).MapPoint == null) {
                        ($target.ParentUser).MapPoint = .New HigLabo.Mapper.Test.MapPoint()
                    } .Else {
                        .Default(System.Void)
                    }
                }
            };
            .Call ($target.ParentUser).set_VectorToNullable
                                       (($source.ParentUser).VectorToNullable);
            .Call ($target.ParentUser).set_Vector2(($source.ParentUser).Vector2);
            .If (($source.ParentUser).ParentUser == null) {
                .Call ($target.ParentUser).set_ParentUser(.Default(HigLabo.Mapper.Test.User))
            } .Else {
                .Block() {
                    .If (($target.ParentUser).ParentUser == null) {
                        ($target.ParentUser).ParentUser = .New HigLabo.Mapper.Test.User()
                    } .Else {
                        .Default(System.Void)
                    }
                }
            };
            .If (($source.ParentUser).Dictionary == null) {
                .Call ($target.ParentUser).set_Dictionary
                (.Default(System.Collections.Generic.Dictionary`2
                  [System.String,System.String]))
            } .Else {
                .Block() {
                    .If (($target.ParentUser).Dictionary == null) {
                        ($target.ParentUser).Dictionary = 
                        .New System.Collections.Generic.Dictionary`2
                         [System.String,System.String]()
                    } .Else {
                        .Default(System.Void)
                    }
                }
            }
        }
    };
    .If ($source.Dictionary == null) {
        .Call $target.set_Dictionary(.Default(System.Collections.Generic.Dictionary`2
                                     [System.String,System.String]))
    } .Else {
        .Block() {
            .If ($target.Dictionary == null) {
                $target.Dictionary = .New System.Collections.Generic.Dictionary`2
                                     [System.String,System.String]()
            } .Else {
                .Default(System.Void)
            }
        }
    };
    .If ($target.Users == null) {
        .Call $target.set_Users(.New System.Collections.Generic.List`1
                                [HigLabo.Mapper.Test.User]())
    } .Else {
        .Default(System.Void)
    };
    .Block(
        HigLabo.Mapper.Test.User $sourceElement,
        HigLabo.Mapper.Test.User $targetElement,
        System.Int32 $i) {
        $i;
        .If ($source.Users == null) {
            .Return end { }
        } .Else {
            .Default(System.Void)
        };
        $i = 0;
        .Loop  {
            .Block() {
                .If (($source.Users).Count <= $i) {
                    .Break endLoop { }
                } .Else {
                    .Default(System.Void)
                };
                $sourceElement = ($source.Users).Item[$i];
                $targetElement = .New HigLabo.Mapper.Test.User();
                .Call ($target.Users).Add($targetElement);
                $i += 1
            }
        }
        .LabelTarget endLoop:;
        .Label
        .LabelTarget end:
    };
    .If ($source.Tags != null) {
        .Block(
            System.String $sourceElement,
            System.String $targetElement,
            System.Int32 $i,
            System.String[] $arrayMember) {
            $i;
            $i = 0;
            $arrayMember;
            $arrayMember = .NewArray System.String[($source.Tags).Length];
            .Loop  {
                .Block() {
                    .If (($source.Tags).Length <= $i) {
                        .Break endLoop { }
                    } .Else {
                        .Default(System.Void)
                    };
                    $sourceElement = ($source.Tags)[$i];
                    $targetElement = $sourceElement;
                    $arrayMember[$i] = $targetElement;
                    $i += 1
                }
            }
            .LabelTarget endLoop:;
            $target.Tags = $arrayMember
        }
    } .Else {
        .Default(System.Void)
    };
    .If ($source.Timestamp != null) {
        .Block(
            System.Byte $sourceElement,
            System.Byte $targetElement,
            System.Int32 $i,
            System.Byte[] $arrayMember) {
            $i;
            $i = 0;
            $arrayMember;
            $arrayMember = .NewArray System.Byte[($source.Timestamp).Length];
            .Loop  {
                .Block() {
                    .If (($source.Timestamp).Length <= $i) {
                        .Break endLoop { }
                    } .Else {
                        .Default(System.Void)
                    };
                    $sourceElement = ($source.Timestamp)[$i];
                    $targetElement = $sourceElement;
                    $arrayMember[$i] = $targetElement;
                    $i += 1
                }
            }
            .LabelTarget endLoop:;
            $target.Timestamp = $arrayMember
        }
    } .Else {
        .Default(System.Void)
    };
    $target
}

These blocks will be compiled to lambda func of Func<TSource, TTarget, MapContext, TTarget>.

And saved private _MapActionList field. Of course, this compile process is only once when first time and reuse Func that saved _MapActionList. AddPostAction will combine this Func and your Lambda, ReplaceMap will replace this Func.

Summary

I hope this library helps someone's work and makes it easy to focus on your product.

History

  • 1st August, 2020: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Higty
CEO TinyBetter, Inc
Japan Japan
I'm a CEO of TinyBetter, Inc in Japan.

Comments and Discussions

 
QuestionUpdated documentation? Pin
Martin Hart Turner4-Aug-20 1:52
MemberMartin Hart Turner4-Aug-20 1:52 
AnswerRe: Updated documentation? Pin
Higty5-Aug-20 2:57
MemberHigty5-Aug-20 2:57 
QuestionConverting to version 3 Pin
Martin Hart Turner4-Aug-20 1:51
MemberMartin Hart Turner4-Aug-20 1:51 
AnswerRe: Converting to version 3 Pin
Higty4-Aug-20 4:52
MemberHigty4-Aug-20 4:52 
GeneralRe: Converting to version 3 Pin
Martin Hart Turner5-Aug-20 2:26
MemberMartin Hart Turner5-Aug-20 2:26 
Questionerror in compiling .NET Framework solution Pin
BillWoodruff3-Aug-20 0:39
mveBillWoodruff3-Aug-20 0:39 
AnswerRe: error in compiling .NET Framework solution Pin
Higty3-Aug-20 10:41
MemberHigty3-Aug-20 10:41 
QuestionCool stuff! Pin
LightTempler1-Aug-20 10:22
MemberLightTempler1-Aug-20 10:22 
AnswerRe: Cool stuff! Pin
Higty2-Aug-20 4:41
MemberHigty2-Aug-20 4:41 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 1 Aug 2020

Stats

5.2K views
7 bookmarked