Reduced AutoMapper- Auto-Map Objects 180% Faster - CodeProject

:

Introduction

From the AutoMapper CodePlex Web Page, we can see that AutoMapper is an object-object mapper. Object-object mapping works by transforming an input object of one type into an output object of a different type. It has a large amount of settings that sometimes are hard to setup. In my projects, I needed to auto-map simple objects that don’t have collection properties, only a big tree of custom property types - TestCase object that has a property of a type TestStep and so on. Also, there are rare cases in which the AutoMapper is not working. So, I created ReducedAutoMapper which is only 150 lines of code but it runs 80% faster than AutoMapper.

Reduced AutoMapper Explained

The primary goal of the object-object mappers is to map object A to object B.

Original Object Type - Not Serializable

public class FirstObject
{
    public FirstObject()
    {
    }

    public string FirstName { get; set; }
    public string SecondName { get; set; }
    public string PoNumber { get; set; }
    public decimal Price { get; set; }
    public DateTime SkipDateTime { get; set; }
    public SecondObject SecondObjectEntity { get; set; }
    public List<SecondObject> SecondObjects { get; set; }
    public List<int> IntCollection { get; set; }
    public int[] IntArr { get; set; }
    public SecondObject[] SecondObjectArr { get; set; }
}

Destination Object- Serializable (identical properties, only serialization attributes added)

[DataContract]
public class MapFirstObject
{
    [DataMember]
    public string FirstName { get; set; }
    [DataMember]
    public string SecondName { get; set; }
    [DataMember]
    public string PoNumber { get; set; }
    [DataMember]
    public decimal Price { get; set; }
    [DataMember]
    public MapSecondObject SecondObjectEntity { get; set; }

    public MapFirstObject()
    {
    }
}

The first step in the object-object mapper is to register the relations between the Original and Destination objects.

private Dictionary<object, object> mappingTypes;
public Dictionary<object, object> MappingTypes
{
    get
    {
        return this.mappingTypes;
    }
    set
    {
        this.mappingTypes = value;
    }
}

public void CreateMap<TSource, TDestination>()
    where TSource : new()
    where TDestination : new()
{
    if (!this.MappingTypes.ContainsKey(typeof(TSource)))
    {
        this.MappingTypes.Add(typeof(TSource), typeof(TDestination));
    }
}

To accomplish the task, the class contains mappingTypes Dictionary which stores the relations between the original and destination types. Through the generic method CreateMap the types are added to the dictionary.

Sample Registration

ReducedAutoMapper.Instance.CreateMap<FirstObject, MapFirstObject>();

How the Main AutoMapping Algorithm Works?

In its core, the ReducedAutoMapper heavily uses Reflection to get the information related to the auto-mapped objects.

public TDestination Map<TSource, TDestination>(
    TSource realObject, 
    TDestination dtoObject = default (TDestination), 
    Dictionary<object, object> alreadyInitializedObjects = null,
    bool shouldMapInnerEntities = true)
    where TSource : class, new()
    where TDestination : class, new()
{
    if (realObject == null)
    {
        return null;
    }
    if (alreadyInitializedObjects == null)
    {
        alreadyInitializedObjects = new Dictionary<object, object>();
    }
    if (dtoObject == null)
    {
        dtoObject = new TDestination();
    }

    var realObjectType = realObject.GetType();
    PropertyInfo[] properties = realObjectType.GetProperties();
    foreach (PropertyInfo currentRealProperty in properties)
    {
        PropertyInfo currentDtoProperty = dtoObject.GetType().GetProperty(currentRealProperty.Name);
        if (currentDtoProperty == null)
        {
            ////Debug.WriteLine("The property {0} was not found 
            ////in the DTO object in order to be mapped. 
            /// Because of that we skip to map it.", currentRealProperty.Name);
        }
        else
        {
            if (this.MappingTypes.ContainsKey
            (currentRealProperty.PropertyType) && shouldMapInnerEntities)
            {
                object mapToObject = this.mappingTypes[currentRealProperty.PropertyType];
                var types = new Type[] { currentRealProperty.PropertyType, (Type)mapToObject };
                MethodInfo method = GetType().GetMethod("Map").MakeGenericMethod(types);
                var realObjectPropertyValue = currentRealProperty.GetValue(realObject, null);
                var objects = new object[]
                {
                    realObjectPropertyValue,
                    null,
                    alreadyInitializedObjects,
                    shouldMapInnerEntities
                };
                if (objects != null && realObjectPropertyValue != null)
                {
                    if (alreadyInitializedObjects.ContainsKey
                    (realObjectPropertyValue) && currentDtoProperty.CanWrite)
                    {
                        // Set the cached version of the same object (optimization)
                        currentDtoProperty.SetValue(dtoObject, alreadyInitializedObjects
				[realObjectPropertyValue]);
                    }
                    else
                    {
                        // Add the object to cached objects collection.
                        alreadyInitializedObjects.Add(realObjectPropertyValue, null);
                        // Recursively call Map method again to get the new proxy object.
                        var newProxyProperty = method.Invoke(this, objects);
                        if (currentDtoProperty.CanWrite)
                        {
                            currentDtoProperty.SetValue(dtoObject, newProxyProperty);
                        }

                        if (alreadyInitializedObjects.ContainsKey(realObjectPropertyValue) 
                        && alreadyInitializedObjects[realObjectPropertyValue] == null)
                        {
                            alreadyInitializedObjects[realObjectPropertyValue] = newProxyProperty;
                        }
                    }
                }
                else if (realObjectPropertyValue == null && currentDtoProperty.CanWrite)
                {
                    // If the original value of the object was null set null to the destination property.
                    currentDtoProperty.SetValue(dtoObject, null);
                }
            }
            else if (!this.MappingTypes.ContainsKey(currentRealProperty.PropertyType))
            {
                // If the property is not custom type just set normally the value.
                if (currentDtoProperty.CanWrite)
                {
                    currentDtoProperty.SetValue
			(dtoObject, currentRealProperty.GetValue(realObject, null));
                }
            }
        }
    }

    return dtoObject;
}

First, it gets the properties of the source object.

var realObjectType = realObject.GetType();
PropertyInfo[] properties = realObjectType.GetProperties();

Next, it iterates through them. If a property with the same name is not present in the destination object, it is skipped. If there is and it is not our custom class (it is a System class like - string, int, DateTime), its value is set to the original’s property one.

else if (!this.MappingTypes.ContainsKey(currentRealProperty.PropertyType))
{
    // If the property is not custom type just set normally the value.
    if (currentDtoProperty.CanWrite)
    {
        currentDtoProperty.SetValue(dtoObject, currentRealProperty.GetValue(realObject, null));
    }
}

If the type of the property is a custom type, and it is not present in the dictionary, it is not auto-mapped.

Otherwise, for the new value of the destination object to be calculated, we use reflection to call recursively the generic Map method.

There is an optimization if the values of the inner property types are already calculated. When a registered destination type is calculated, its value is placed in the alreadyInitializedObjects collection and the method Map is not called recursively afterwards.

If you need to auto-map collection of objects, you can use the third method of the ReducedAutoMapper class - MapList.

public List<TDestination> MapList<TSource, TDestination>
(List<TSource> realObjects, Dictionary<object, object> alreadyInitializedObjects = null)
    where TSource : class, new()
    where TDestination : class, new()
{
    List<TDestination> mappedEntities = new List<TDestination>();
    foreach (var currentRealObject in realObjects)
    {
        TDestination currentMappedItem = this.Map<TSource, 
	TDestination>(currentRealObject, alreadyInitializedObjects: alreadyInitializedObjects);
        mappedEntities.Add(currentMappedItem);
    }

    return mappedEntities;
}

Compare AutoMapper with ReducedAutoMapper

I created a simple console application where I initialized enormous objects with more than 1000 properties. The number of the created objects is 100000.

Above, you can find the first source class- FirstObject. Below, you can find the other two.

SecondObject

public class SecondObject
{
    public SecondObject(string firstNameS, string secondNameS, string poNumberS, decimal priceS)
    {
        this.FirstNameS = firstNameS;
        this.SecondNameS = secondNameS;
        this.PoNumberS = poNumberS;
        this.PriceS = priceS;
        ThirdObject1 = new ThirdObject();
        ThirdObject2 = new ThirdObject();
        ThirdObject3 = new ThirdObject();
        ThirdObject4 = new ThirdObject();
        ThirdObject5 = new ThirdObject();
        ThirdObject6 = new ThirdObject();
    }

    public SecondObject()
    {
    }

    public string FirstNameS { get; set; }
    public string SecondNameS { get; set; }
    public string PoNumberS { get; set; }
    public decimal PriceS { get; set; }
    public ThirdObject ThirdObject1 { get; set; }
    public ThirdObject ThirdObject2 { get; set; }
    public ThirdObject ThirdObject3 { get; set; }
    public ThirdObject ThirdObject4 { get; set; }
    public ThirdObject ThirdObject5 { get; set; }
    public ThirdObject ThirdObject6 { get; set; }
}

ThirdObject

{
    public ThirdObject()
    {
    }

    public DateTime DateTime1 { get; set; }
    public DateTime DateTime2 { get; set; }
    public DateTime DateTime3 { get; set; }
//.. it contains 996 properties more
    public DateTime DateTime1000 { get; set; }
}

The code below tests the ReducedAutoMapper with 100000 objects.

public class Program
{
    static void Main(string[] args)
    {
        Profile("Test Reduced AutoMapper 10 Runs 10k Objects", 10, () => MapObjectsReduceAutoMapper());
        System.Console.ReadLine();
    }

    static void Profile(string description, int iterations, Action actionToProfile)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < iterations; i++)
        {
            actionToProfile();
        }
        watch.Stop();
        System.Console.WriteLine(description);
        System.Console.WriteLine("Total: {0:0.00} ms ({1:N0} ticks) (over {2:N0} iterations)",
            watch.ElapsedMilliseconds, watch.ElapsedTicks, iterations);
        var avgElapsedMillisecondsPerRun = watch.ElapsedMilliseconds / iterations;
        var avgElapsedTicksPerRun = watch.ElapsedMilliseconds / iterations;
        System.Console.WriteLine("AVG: {0:0.00} ms ({1:N0} ticks) (over {2:N0} iterations)",
            avgElapsedMillisecondsPerRun, avgElapsedTicksPerRun, iterations);
    }

    static void MapObjectsReduceAutoMapper()
    {
        List<FirstObject> firstObjects = new List<FirstObject>();
        List<MapFirstObject> mapFirstObjects = new List<MapFirstObject>();

        ReducedAutoMapper.Instance.CreateMap<FirstObject, MapFirstObject>();
        ReducedAutoMapper.Instance.CreateMap<SecondObject, MapSecondObject>();
        ReducedAutoMapper.Instance.CreateMap<ThirdObject, MapThirdObject>();
        for (int i = 0; i < 10000; i++)
        {
            FirstObject firstObject =
                new FirstObject(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), (decimal)12.2, DateTime.Now,
                    new SecondObject(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), (decimal)11.2));
            firstObjects.Add(firstObject);
        }
        foreach (var currentObject in firstObjects)
        {
            MapFirstObject mapSecObj = ReducedAutoMapper.Instance.Map<FirstObject, MapFirstObject>(currentObject);
            mapFirstObjects.Add(mapSecObj);
        }
    }
}

Results

The code below tests the AutoMapper with 100000 objects.

public class Program
{
    static void Main(string[] args)
    {
        Profile("Test Original AutoMapper 10 Runs 10k Objects", 10, () => MapObjectsAutoMapper());
        System.Console.ReadLine();
    }

    static void Profile(string description, int iterations, Action actionToProfile)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < iterations; i++)
        {
            actionToProfile();
        }
        watch.Stop();
        System.Console.WriteLine(description);
        System.Console.WriteLine("Total: {0:0.00} ms ({1:N0} ticks) (over {2:N0} iterations)",
            watch.ElapsedMilliseconds, watch.ElapsedTicks, iterations);
        var avgElapsedMillisecondsPerRun = watch.ElapsedMilliseconds / iterations;
        var avgElapsedTicksPerRun = watch.ElapsedMilliseconds / iterations;
        System.Console.WriteLine("AVG: {0:0.00} ms ({1:N0} ticks) (over {2:N0} iterations)",
            avgElapsedMillisecondsPerRun, avgElapsedTicksPerRun, iterations);
    }

    static void MapObjectsAutoMapper()
    {
        List<FirstObject> firstObjects = new List<FirstObject>();
        List<MapFirstObject> mapFirstObjects = new List<MapFirstObject>();

        AutoMapper.Mapper.CreateMap<FirstObject, MapFirstObject>();
        AutoMapper.Mapper.CreateMap<SecondObject, MapSecondObject>();
        AutoMapper.Mapper.CreateMap<ThirdObject, MapThirdObject>();
        for (int i = 0; i < 10000; i++)
        {
            FirstObject firstObject =
                new FirstObject(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), (decimal)12.2, DateTime.Now,
                    new SecondObject(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), (decimal)11.2));
            firstObjects.Add(firstObject);
        }
        foreach (var currentObject in firstObjects)
        {
            MapFirstObject mapSecObj = AutoMapper.Mapper.Map<FirstObject, MapFirstObject>(currentObject);
            mapFirstObjects.Add(mapSecObj);
        }
    }
}

Results

As you can see from the results above, the ReducedAutoMapper performed >180% better thanAutoMapper.

So Far in the C# Series

1. Implement Copy Paste C# Code
2. MSBuild TCP IP Logger C# Code
3. Windows Registry Read Write C# Code
4. Change .config File at Runtime C# Code
5. Generic Properties Validator C# Code
6. Reduced AutoMapper- Auto-Map Objects 180% Faster
7. 7 New Cool Features in C# 6.0
8. Types Of Code Coverage- Examples In C#
9. MSTest Rerun Failed Tests Through MSTest.exe Wrapper Application
10. Hints For Arranging Usings in Visual Studio Efficiently
11. 19 Must-Know Visual Studio Keyboard Shortcuts – Part 1
12. 19 Must-Know Visual Studio Keyboard Shortcuts – Part 2
13. Specify Assembly References Based On Build Configuration in Visual Studio
14. Top 15 Underutilized Features of .NET
15. Top 15 Underutilized Features of .NET Part 2
16. Neat Tricks for Effortlessly Format Currency in C#
17. Assert DateTime the Right Way MSTest NUnit C# Code
18. Which Works Faster- Null Coalescing Operator or GetValueOrDefault or Conditional Operator
19. Specification-based Test Design Techniques for Enhancing Unit Tests
20. Get Property Names Using Lambda Expressions in C#
21. Top 9 Windows Event Log Tips Using C#

 

If you enjoy my publications, feel free to SUBSCRIBE
Also, hit these share buttons. Thank you!

Source Code

 

The post- Reduced AutoMapper- Auto-Map Objects 80% Faster appeared first on Automate The Planet.

All images are purchased from DepositPhotos.com and cannot be downloaded and used for free. License Agreement