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:
- Cause
StackoverflowException
with recursive referenced object List
property that has private
setter 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>();
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:
- Do nothing. Keep target property as
null
. - 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. - 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:
- map collection element or not
- 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;
}
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;
}
}
}
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