Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

HigLabo.Mapper (Zero Configuration, Full Customization, Easy to Use) and Inside of Creating Mapping Library

0.00/5 (No votes)
5 Jul 2020 1  
Design mapping rule by test case and write IL code for good performance
This article (and GitHub source code) describes how to design mapping rule by test case, and how to write IL code to achieve good performance.

1. Introduction

There are many Mapper libraries found in the world - AutoMapper, TinyMapper, Mapster, AgileMapper, ExpressMapper, FastMapper, etc. I found that other libraries have 3 missing features:

  1. Cause StackoverflowException with recursive referenced object
  2. List property that has private setter
  3. Dictionary to target object

All libraries cause StackOverflowException when processing recursive object. Even though List with private setter is very common class design, some libraries do not copy element to List private setter. Some libraries do not map from Dictionary to object.

You can see the reproduct code of these problems here.

So I started trying to create another maping library for my study. It is my hobby project. But I want to share my experience in this work for other developers which may help them. You can get HigLabo.Mapper from Nuget.

How It Works?

HigLabo.Mapper is using IL generator under the food. It generates code to map object. You can see all generate logic at ObjectMapConfig class of HigLabo.Mapper.

CreateMapPropertyMethod method creates actual IL code for each type mapping. I'll describe these internal logic spec later chapter.

2. Zero Configuration

Some other existing libraries need configuration for mapping. I try to reduce configuration on HigLabo.Mapper. As a result, zero configuration is achieved (for 80% general usage). In a while, full customization by configuration for other cases (20% specific usage).

In general cases, AutoMapper required Initialize mapping between two types like this:

AutoMapper.Mapper.Initialize(config =>
{
    config.CreateMap<Customer, CustomerDTO>();
    config.CreateMap<Address, AddressDTO>();
});

And call map method:

var customerDto = AutoMapper.Mapper.Map<CustomerDTO>(customer);

TinyMapper also requires to call Bind method in multi thread environment.

TinyMapper.Bind<Customer, CustomerDTO>();

If you have 100 domain objects in your application, it takes a lot of time to write initialization code.

AutoMapper.Mapper.Initialize(config =>
{
    config.CreateMap<Customer, CustomerDTO>();
    //So many class mapping code...
    config.CreateMap<Address, AddressDTO>();
});

HigLabo.Mapper does not require any initialization code. All you have to do is call map method.

var customerDto = config.Map(customer, new CustomerDTO());

You can call by extension method like this by using extension method. Inside the extension method, ObjectMapConfig.Current is used.

var customerDto = customer.Map(new CustomerDTO());

HigLabo.Mapper will map the same name property automatically and handle child object and child collections. You can change all mapping rules by PropertyMapRule class like prefix, suffix, underscore or other. You can also do custom mapping like ignore some property or different property name map by calling RemovePropertyMap and AddPostAction method. I'll explain it later in PropertyMapRule chapter.

3. Mapping Specification

In this chapter, I'll explain spec about type mapping rule. I defined this rule by test code. You can see this test code
here.

3.1 Basic Mapping for Same Property Names

By default, HigLabo.Mapper will map same name properties.You can see this spec as test code in these methods.

  • ObjectMapConfig_Map_Object_Object
  • ObjectMapConfig_Map_ValueType_Object
  • ObjectMapConfig_Map_Object_ValueType

3.2 Null Handling When Source Is Null

MapOrNull method returns null when source is null. If source is not null, call Map. You can see this spec as test code in this method:

  • ObjectMapConfig_MapOrNull

3.3 Null Handling When Source Property Is Null

If source property is null (class or Nullable<>), we want to set target property to null. You can see this spec as test code in this method:

  • ObjectMapConfig_Map_Object_Object_SetNullablePropertyToNull

3.4 Nullable Handling (Target Property is Null)

Other case is target property type is class (except string) but source property value could not convert to target property type. In this case, we handle it in three ways as listed below:

  1. Do nothing. Keep target property as null.
  2. If target type has default constructor, it will create new object to target property and call map method between source property value and this new object on target property.
  3. Copy reference of source property value to target property (if source type is assignable to target type).

By default, if target property is null, create new object if object has default constructor. You can change this behavior by setting NullPropertyMapMode property of ObjectMapConfig. You can see this spec as test code in these methods.

  • ObjectMapConfig_Map_NullProperty_NewObject
  • ObjectMapConfig_Map_NullProperty_DeepCopy

3.5 Between Dictionary and Object mapping

If source is Dictionary, we want to map by indexer to target object. And we also want to map from object to Dictionary. You can see this spec as test code in these methods:

  • ObjectMapConfig_Map_Dictionary_Object
  • ObjectMapConfig_Map_Object_Dictionary

3.6 Handling When That Source Property Value Cannot Be Converted to Target Property Type

For example, source value is "abc" and target type is int, we want to fail map. Other case is from decimal to int. Actually in these cases, we want to do nothing for target property. You can see this spec as test code in these methods:

  • ObjectMapConfig_Map_Dictionary_Object_Convert_Failure
  • ObjectMapConfig_Map_FromDecimalToInt32

3.7 Encoding Object

You want to convert string like "UTF-8" to Encoding object. I create this feature achieved by TypeConverter object. You can see this spec as test code in this method:

  • ObjectMapConfig_Map_Dictionary_Encoding

In a later chapter, I'll explain custom TypeConverter.

3.8 Dynamic Object

HigLabo.Mapper supports dynamic object. You can see this spec as test code in this method:

  • ObjectMapConfig_Map_DynamicObject_Object

3.9 IDataReader Object

HigLabo.Mapper supports IDataReader object. You can pass IDataReader object to Map method. Inside Map method, IDataReader will be converted to Dictionary and call Map method. You can map each database column to property name by AddPostAction, such as database_id column to DatabaseID property. You can see this spec as test code in this method:

  • ObjectMapConfig_Map_IDataReader_Object_With_PostAction

3.10 List to List

List property needs some consideration. We must consider about:

  1. map collection element or not
  2. map collection element by create new object or copy reference for each element

To manage these mapping configurations, you set CollectionElementMapMode property of ObjectMapConfig class. If CollectionElementMapMode is None, any element is not copied to target property. If CollectionElementMapMode is NewObject, create new object and call Map method. If CollectionElementMapMode is DeepCopy, reference is copied to target collection. You can see this spec as test code in these methods:

  • ObjectMapConfig_Map_List_List
  • ObjectMapConfig_Map_List_List_ValueType
  • ObjectMapConfig_Map_List_NullableList
  • ObjectMapConfig_Map_List_ReadonlyList
  • ObjectMapConfig_Map_ListProperty
  • ObjectMapConfig_Map_CollectionElement_NewObject
  • ObjectMapConfig_Map_CollectionElement_NewObject_NoDefaultConstructor
  • ObjectMapConfig_Map_CollectionElement_Reference
  • ObjectMapConfig_MapCollection_CollectionElement_DeepCopy
  • ObjectMapConfig_Map_NullListProperty_NewObject
  • ObjectMapConfig_Map_NullListProperty_DeepCopy_AddElement

3.11 Flatten Mapping

Flatten mapping is supported. You can flatten from source object to target object like this:

var config = new ObjectMapConfig();
config.AddPostAction<User, UserFlatten>((source, target) =>
{
    source.Vector2.Map(target);
    source.MapPoint.Map(target);
});

Looks like it is easy to understand compared to other library. If you use AutoMapper, you must know about CreateMap, ForMember, ResolveUsing, etc. Just you must know its source is User and target is UserFlatten and just write usual C# code to flatten object. You can see this spec as test code in this method.

  • ObjectMapConfig_Map_Flatten

3.12 Custom Conversion

You can add common custom conversion when you convert source object to target types. If default conversion is failed, your custom conversion will be tried to convert source object. You can see this spec as test code in these methods:

  • ObjectMapConfig_AddPostAction_EnumNullable
  • ObjectMapConfig_AddPostAction_Encoding
  • ObjectMapConfig_AddPostAction_Collection

If you find missing pattern, please send me a test code on GitHub.

4. Full Customization

You can customize all behavior by using AddPostAction and RemovePropertyMap. You can change map rule by calling AddPostActionMethod. If you want to convert your custom logic, you write code like this:

config.AddPostAction<String, DayOfWeek>((source, target) =>
{
    return DayOfWeekConverter(source) ?? target;
});
config.AddPostAction<Person1, Person2>((source, target) =>
{
    target.FullName = source.FamilyName + " " + source.FirstName;
});

You can ignore default property mapping by call RemovePropertyMap method. It would be better to use nameof keyword rather than string to protect runtime error when you change property name.

config.RemovePropertyMap<User, User>
(nameof(User.DecimalNullable), "DateTimeNullable", "DayOfWeekNullable");

You can see this spec as test code in these methods:

  • ObjectMapConfig_AddPostAction_Enum
  • ObjectMapConfig_RemovePropertyMap
  • ObjectMapConfig_RemoveAllPropertyMap

You can replace all logic by calling ReplacePropertyMap method by passing your custom action against source and target. That's all and it is easy to understand than other library.

You can see this spec as test code in these methods:

  • ObjectMapConfig_ReplacePropertyMap

5. TypeConverter

TypeConverter is declared in HigLabo.Core.
https://github.com/higty/higlabo/blob/master/NetStandard/HigLabo.Core/Core/TypeConverter.cs

TypeConverter class handles all primitive class and Encoding object to convert from object to target types. You can customize by inheriting from TypeConverter object and override method. And set your custom TypeConverter class to ObjectMapConfig.TypeConverter property.

6. PropertyMappingRule

As you can see in the earlier chapter, you can use HigLabo.Mapper without configuration. But if it does not suit your requirement, you can customize property mapping rule. If you want to map all Value property to target property by using PropertyNameMappingRule, you can use PrefixPropertyMappingRule, SuffixPropertyMappingRule to map prefix, suffix properties. You can see this spec as test code in these methods:

  • PropertyNameMappingRule_Failure
  • ObjectMapConfig_SuffixPropertyMappingRule
  • ObjectMapConfig_IgnoreUnderscorePropertyMappingRule
  • ObjectMapConfig_CustomPropertyMappingRule
  • ObjectMapConfig_CustomPropertyMappingRule_AddPostAction

7. DictionaryMappingRule

If you would like to customize mapping rule between Dictionary(Dictionary<String, String> or Dictionary<String, Object>) and object, you can do it by using DictionaryMappingRules property of ObjectMapConfig object. You can assign dictionary key and property name by DictionaryKeyMappingRule object to customize your own mapping rule. And you also add your custom DictionaryMappingRule object to map Dictionary and Object. You can see this spec as test code in this method:

  • ObjectMapConfig_CustomDictionaryMappingRule

8. Extension Methods

You can use Map extension method against object:

var target = source.Map(new TTarget());

All extension methods are declared in ObjectMapExtensions class. These extension methods use ObjectMapConfig. Current object to map source and target object.

9. Multiple Map Rule

You can create multiple ObjectMapConfig instance. You can set NullPropertyMapMode, CollectionElementMapMode for each object, such as first one creates new object, another one deep copy.

10. Performance Comparison

Performance is critical for mapper library. Mapper has a tendency to used in a hot path (such as foreach statement to process collection from databases) in your application. Unfortunately, HigLabo.Mapper is not faster than other project. I created a performance test project here.

Here is a test code for performance by BenchmarkDotNet.

All entity classes are here.

Here is a test result for basic 1000 times loop.

            Method |          Mean |     StdDev |    Gen 0 | Allocated |
------------------ |-------------- |----------- |--------- |---------- |
 HigLaboMapperTest | 2,107.1849 us | 24.4056 us |  83.3333 | 497.55 kB |
    TinyMapperTest |   657.8088 us |  8.1006 us |  67.1875 | 273.55 kB |
    AutoMapperTest |   752.8322 us |  3.1900 us |  53.5156 | 229.55 kB |
       MapsterTest |   774.9905 us |  7.5189 us |  63.8021 | 261.55 kB |
   AgileMapperTest | 1,065.5490 us | 14.5767 us | 223.1771 | 825.57 kB |
 ExpressMapperTest | 1,325.6581 us | 15.3520 us |  88.5417 | 393.54 kB |
    FastMapperTest | 1,481.9169 us | 15.6490 us | 199.7396 | 757.57 kB |

If you need ultimately speed, it would be better for you to select TinyMapper. I found that TinyMapper is fastest. As you can see, HigLabo.Mapper is not fast due to prevent from StackoverflowException by calling Map method recursively. Map method call causes 1ms slower performance penalty, but it is necessary. Other library may compile inline to child object and that may cause StackoverflowException.

11. Deep Dive to Internal Code

HigLabo.Mapper uses ILGenerator. The main methods are CreatePropertyMaps, CreateMethod.CreatePropertyMaps methods which create mapping information by reflection. Mapping is based on PropertyMappingRule and DictionaryMappingRule. ObjectMapConfig manages these classes that you can set by PropertyMappingRules, DictionaryMappingRules property.

CreateMethod generates IL code from collection of PropertyMappingRule and DictionaryMappingRule. The basic implementation of map code is like below. (Conceptual code)

source.P1 --> target.P1;
source.P1 --> target["P1"];
source["P1"] --> target.P1;
source["P1"] --> target["P1"];
context --> MappingContext.
******************************************************
if (typeof(source) == typeof(target))
{
    target.P1 = source.P1;
}
else if (Use TypeConverter for primitive types)
{
    var converted = converter.ToXXX(source.P1);
    if (converted != null)
    {
        target.P1 = converted;
        return;
    }
}
else
{
    target.P1 = source["P1"];
    return;
}
//Null property handling...
if (target property is Class)
{
    switch (context.NullPropertyMapMode)
    {
        case NullPropertyMapMode.NewObject: target.P1 = new XXX(); break;
        case NullPropertyMapMode.CopyReference:
        {
            if (typeof(source) inherit from typeof(parent))
            {
                target.P1 = source.P1;
            }
            break;
        }
    }
    if (source type is IEnumerable and target type is ICollection)
    {
        switch (context.CollectionElementMapmode)
        {
            case CollectionElementMapmode.NewObject: this.MapElement(source, target); break;
            case CollectionElementMapmode.CopyReference: 
                                     this.MapDeepCopy(source, target); break;
        }
    }
}
//for child object's property map.It is slow...
target.P1 = source.P1.Map(target.P1);

CreateMethod has four parameters. The first parameter is ObjectMapConfig. Second parameter is source object and the third is target object. The fourth parameter is MappingContext that has some information to prevent from infinite loop.

I created il code for each target type. Types are String, Encoding, Int32, Guid, Boolean, etc. Nullable<>, Collection, Array... etc.

If source property type is string and target property type is string, IL code is like below:

var sourceGetMethod = sourceProperty.PropertyInfo.GetGetMethod();
var targetSetMethod = targetProperty.PropertyInfo.GetSetMethod();

il.Emit(OpCodes.Ldarg, 2);
il.Emit(OpCodes.Ldarg, 1);
il.Emit(OpCodes.Callvirt, sourceGetMethod);
il.Emit(OpCodes.Callvirt, targetSetMethod);

Other type il code is a very straight forward way. Perhaps you can easily read my code than other library.

After copying value from source property to target property, call Map method if target type is class (Complex type) to copy child properties. I add constraint to generic Map_XXX method to avoid performance penalty. But still slow to call Map method. It is why I could not reach other library's performance.

History

  • 6th July, 2020: Update for .NET Core
  • 7th December, 2016: Initial post

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here