Статьи

Уменьшенный AutoMapper: Auto-Map объектов на 80 процентов быстрее

Из AutoMapper CodePlex веб — страницы, мы можем видеть , что AutoMapper является объектно-объект картографа. Сопоставление объектов и объектов осуществляется путем преобразования входного объекта одного типа в выходной объект другого типа. Он имеет большое количество настроек, которые иногда очень сложно настроить. В моих проектах мне нужно было автоматически сопоставлять простые объекты, которые не имеют свойств коллекции, только большое дерево пользовательских типов свойств — объект TestCase, который имеет свойство типа TestStep и так далее. Кроме того, в редких случаях AutoMapper не работает. Итак, я создал ReducedAutoMapper,  который содержит всего 150 строк кода, но работает на 80% быстрее, чем AutoMapper .

Уменьшенный AutoMapper объяснил

Основная цель сопоставителей объект-объект состоит в том, чтобы сопоставить объект A с объектом B.

Исходный тип объекта — не сериализуемый

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

Объект назначения — Сериализуемый (идентичные свойства, добавлены только атрибуты сериализации)

[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()
    {
    }
}

Первый шаг в объектно-объекте Mapper является регистром в отношениях между оригиналом и целевыми объектами.

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

Чтобы выполнить задачу, класс содержит mappingTypesDictionary,  который хранит отношения между исходным и целевым типами. Через универсальный метод CreateMap типы добавляются в словарь.

Образец регистрации

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

 Как работает основной алгоритм AutoMapping?

По своей сути, ReducedAutoMapper активно использует Reflection для получения информации, связанной с автоматически отображаемыми объектами.

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

Сначала он получает свойства исходного объекта.

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

Затем он проходит через них. Если свойство с тем же именем отсутствует в целевом объекте, оно пропускается. Если есть и это не наш пользовательский класс (это системный класс like-string, int, DateTime), его значение устанавливается равным свойству оригинала.

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

Если тип свойства является пользовательским и отсутствует в   словаре , оно не сопоставляется автоматически .

В противном случае, чтобы рассчитать новое значение целевого объекта, мы используем рефлексию для рекурсивного вызова универсального метода Map .

Существует оптимизация, если значения внутренних типов свойств уже рассчитаны. Когда вычисляется зарегистрированный тип назначения, его значение помещается в   коллекцию ужеInitializedObjects, и метод Map впоследствии не вызывается рекурсивно.

Если вам нужно коллекция автоматического отображения объектов вы можете использовать третий метод ReducedAutoMapper  классового  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;
}

 Сравните AutoMapper с ReducedAutoMapper

Я создал простое консольное приложение, в котором инициализировал действительно большие объекты с более чем 1000 свойствами. Количество созданных объектов составляет 100000 .

Выше вы можете найти первый исходный класс — FirstObject . Ниже вы можете найти два других.

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

Приведенный ниже код проверяет ReducedAutoMapper с 100000 объектов.

public class Program
{
    static void Main(string[] args)
    {
        DateTime startTime = DateTime.Now;
        List<FirstObject> firstObjects = new List<FirstObject>();
        List<MapFirstObject> mapFirstObjects = new List<MapFirstObject>();

        // REDUCED AUTO MAPPER TEST ------------------------------------------------------------
        ReducedAutoMapper.Instance.CreateMap<FirstObject, MapFirstObject>();
        ReducedAutoMapper.Instance.CreateMap<SecondObject, MapSecondObject>();
        ReducedAutoMapper.Instance.CreateMap<ThirdObject, MapThirdObject>();
        for (int i = 0; i < 100000; 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);
            System.Console.WriteLine("Object created: {0}", i);
        }
        for (int i = 0; i < firstObjects.Count - 1; i++)
        {
            MapFirstObject mapSecObj = ReducedAutoMapper.Instance.Map<FirstObject, MapFirstObject>(firstObjects[i]);
            mapFirstObjects.Add(mapSecObj);
            System.Console.WriteLine("Map Object: {0}", i);
        }

        DateTime endTime = DateTime.Now;
        System.Console.WriteLine("Finish for {0}", endTime - startTime);
        System.Console.WriteLine();
    }
}

Полученные результаты

ReducedAutoMapper 100000 Тестирование объектов

Код ниже тестирует AutoMapper с 100000 объектов.

public class Program
{
    static void Main(string[] args)
    {
        DateTime startTime = DateTime.Now;
        List<FirstObject> firstObjects = new List<FirstObject>();
        List<MapFirstObject> mapFirstObjects = new List<MapFirstObject>();

        // AUTO MAPPER TEST ------------------------------------------------------------
        AutoMapper.Mapper.CreateMap<FirstObject, MapFirstObject>();
        AutoMapper.Mapper.CreateMap<SecondObject, MapSecondObject>();
        AutoMapper.Mapper.CreateMap<ThirdObject, MapThirdObject>();
        for (int i = 0; i < 1000; 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);
            System.Console.WriteLine("Object created: {0}", i);
        }
        for (int i = 0; i < firstObjects.Count; i++)
        {
            MapFirstObject mapSecObj = AutoMapper.Mapper.Map<FirstObject, MapFirstObject>(firstObjects[i]);
            mapFirstObjects.Add(mapSecObj);
            System.Console.WriteLine("Map Object: {0}", i);
        }

        DateTime endTime = DateTime.Now;
        System.Console.WriteLine("Finish for {0}", endTime - startTime);
        System.Console.WriteLine();
    }
}

Полученные результаты

AutoMapper 100000 Тестирование объектов

Как вы можете видеть из приведенных выше результатов, ReducedAutoMapper производится 82% лучше , чем AutoMapper .

Вы можете скачать полный исходный код из моего репозитория GIT HUB —  https://github.com/angelovstanton/Projects/tree/master/AAngelov.Utilities/AAngelov.Utilities.Test