Click here to Skip to main content
14,430,298 members

HigLabo.Mapper (Zero Configuraition, Full customization, Easy to use) and inside of creating mapping library

Rate this:
5.00 (6 votes)
Please Sign up or sign in to vote.
5.00 (6 votes)
13 Dec 2016CPOL
This article (and GitHub source code) describe how to design mapping rule by test case, and how to write il code to achieve good performance.

1. Introduction

There are many Mapper library found in the world. AutoMapper, TinyMapper, Mapster, AgileMapper, ExpressMapper, FastMapper...etc. I found that other library has 3 missing feature.

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

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

You can see the reproduct code of these problem at
https://github.com/higty/higlabo/tree/master/HigLabo.Test/HigLabo.Mapper.PerformanceTest/NotSupportedTest/NotSupportedTest.cs

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

How it works?

HigLabo.Mapper is using IL generator under the food. It generate code to map object. You can see all generate logic at ObjectMapConfig class of HigLabo.Mapper.
https://github.com/higty/higlabo/blob/master/HigLabo.Mapper/Core/ObjectMapConfig.cs

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

2. Zero Configuration

Other some existing library 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 require to call Bind method in multi thread environment.

TinyMapper.Bind<Customer, CustomerDTO>();

If you have 100 domain object in your applicatoin, it takes a lot 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 required any initialization code. Just you have to do is to call map method.

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

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

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

HigLabo.Mapper will map same name property automatically and handle child object and child collections. You can change all mapping rule 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 in later PropertyMapRule chapter.

3. Mapping specification

This chapter, I'll explain spec about type mapping rule. I defined these rule by test code. You can see these test code at https://github.com/higty/higlabo/blob/master/HigLabo.Test/HigLabo.Mapper.Test/TestCase/ObjectMapConfigTest.cs.

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 at these method.

  • ObjectMapConfig_Map_Object_Object
  • ObjectMapConfig_Map_ValueType_Object
  • ObjectMapConfig_Map_Object_ValueType

3.2 Null handling when source is null

MapOrNull method return null when source is null. If source is not null, call Map. You can see this spec as test code at these 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 at these 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 3 ways as below.

  1. Do nothing. Keep target proeprty is 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 at these method.

  • 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 at these method.

  • ObjectMapConfig_Map_Dictionary_Object
  • ObjectMapConfig_Map_Object_Dictionary

3.6 Handling when that source property value can not 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 at these method.

  • 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 at these method.

  • ObjectMapConfig_Map_Dictionary_Encoding

Later chapter, I'll explain custom TypeConverter.

3.8 Dynamic object

HigLabo.Mapper support dynamic object. You can see this spec as test code at these method.

  • ObjectMapConfig_Map_DynamicObject_Object

3.9 IDataReader object

HigLabo.Mapper support 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 at these method.

  • ObjectMapConfig_Map_IDataReader_Object_With_PostAction

3.10 List to List

List property need 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 elements.

To manage these mapping configuration, 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 at these method.

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

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

  • ObjectMapConfig_Map_Flatten

3.12 Custom conversion

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

  • 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 at these method.

  • ObjectMapConfig_AddPostAction_Enum
  • ObjectMapConfig_RemovePropertyMap
  • ObjectMapConfig_RemoveAllPropertyMap

You can replace all logic by call 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 at these method.

  • ObjectMapConfig_ReplacePropertyMap

5. TypeConverter

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

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

6. PropertyMappingRule

As you can see in early chapter, you can use HigLabo.Mapper without configuration. But if it is not suite 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 at these method.

  • 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 at these method.

  • ObjectMapConfig_CustomDictionaryMappingRule

8. Extension methods

You can use Map extension method against object.

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

All extension method is declared in ObjectMapExtensions class. These extension method 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 create new object, another one deep copy.

10. Performance comparison

Performance is critical for mapper library. Mapper is tendecy to used in a hot path (such as foreach statement to process collection from databases) in your application. Unfortunately, HigLabo.Mapper does not faster than other project. I created performance test project at https://github.com/higty/higlabo/tree/master/HigLabo.Test/HigLabo.Mapper.PerformanceTest

Here is a test code for performance by BenchmarkDotNet.
https://github.com/higty/higlabo/blob/master/HigLabo.Test/HigLabo.Mapper.PerformanceTest/PerformanceTest/MapperPerformanceTest.cs

All entity classes are here.
https://github.com/higty/higlabo/blob/master/HigLabo.Test/HigLabo.Mapper.PerformanceTest/PerformanceTest/Entity.cs

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, you would better 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 cause 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 use ILGenerator. The main methods are CreatePropertyMaps, CreateMethod.CreatePropertyMaps method create mapping information by reflection. Mapping is based on PropertyMappingRule and DictionaryMappingRule. ObjectMapConfig manage these classes that you can set by PropertyMappingRules, DictionaryMappingRules property.

CreateMethod generate 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 childe object's property map.It is slow...
target.P1 = source.P1.Map(target.P1);

CreateMethod has four parameters. First parameter is ObjectMapConfig. Second parameter is source object and Third is target object. 4th parameter is MappingContext that has some information to prevent from infinite loop.

I created il code for each target types. 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 very straight forward way. Perhaps you can easily read my code than other library.

After copied 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.

11. Help!!

Any help is great, I really apreciate for your help. I searched to improve Map method call performance but I could not found a way. It is not fast, but never StackoverflowException, List private setter available, Dictionary (DataReader also) mapping, easy full customization by AddPostAction.

I'm glad to use my library and all your feedback or star by GitHub repository. I continue my effort to reach AutoMapper feature or TinyMapper performance.

Thank you for reading my article.

History

2016.12.07 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
Web Developer
Japan Japan
I'm Working at Software Company in Tokyo.

Comments and Discussions

 
QuestionVote of 5, for ease of configuration Pin
longnights14-Dec-16 16:21
Memberlongnights14-Dec-16 16:21 
AnswerRe: Vote of 5, for ease of configuration Pin
Higty15-Dec-16 4:28
MemberHigty15-Dec-16 4:28 

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 6 Dec 2016

Tagged as

Stats

11.1K views
2 bookmarked