Здравствуйте! Наш проект уже достиг такой стадии когда встал вопрос об оптимизации производительности. После анализа слабых мест, одно из возможных путей для оптимизации был способ избавления от AutoMapper’а, он хоть и не является самым тормозным местом, но является тем местом, которое мы можем улучшить. AutoMapper используется у нас для маппинга DO объектов в DTO объекты для передачи через WCF сервис. Вручную написанный метод с созданием нового объекта и копированием полей работает быстрее. Писали маппинг вручную — безрадостная рутина, часто были ошибки, забытые поля, забытые новые поля, поэтому решили написать генерацию маппинга через t4 шаблоны.

По сути нам надо было сверить список пропертей и типов, и написать копирование, но не всё так гладко в датском королевстве.

Для того чтобы связать два класса, был добавлен атрибут [Map]. В конфигурировании шаблона прописывались 2 проекта в которых надо было искать классы с этим атрибутом. Классы связывались в пары по имени, у DTO классов отрезался суффикс “Dto”, если был. Но в некоторых случаях все равно надо было связывать разноименные классы, в атрибут был добавлен параметр Name.

    [Map(Name = "NewsCategory")]
    public class CategoryDto

Маппинг генерируется в виде методов расширения. Вроде всё хорошо, поля копируются. Но всё равно остается много ручной работы. DTO и DO объекты имеют внутри себя другие объекты и коллекции, их приходится маппить вручную, хоть и с помощью сгенерированных нами методов. У многих полей имена совпадают, а соответствие типов лежит в коллекции связей, которую мы уже составили.
Маппинг был расширен до автоматического маппинга вложенных объектов и коллекций. А действие атрибута [Map] было расширено до пропертей, чтобы можно было их маппить с не совпадающими именами.
Пример получившегося кода.

        public static DataTransferObject.CategoryDto MapToDto (this DataObjects.NewsCategory item)
        {  
            if (item == null) return null;            
            var itemDto = new DataTransferObject.CategoryDto ();
                itemDto.NewsCategoryId = item.NewsCategoryId;
                itemDto.Name = item.Name;
                itemDto.ParentCategory = item.ParentCategory.MapToDto();
                itemDto.ChildCategories = item.ChildCategories.Select(x => x.MapToDto());
            return itemDto;
        }

А для совсем сложных случаев было добавлено поле Function в атрибут, и при генерации маппинга — текст из этого поля просто вставлялся в код. Также был добавлен атрибут [MapIgnore]

        [Map(Function="itemDto.Status = item.Status.ToString()") ]
        public string Status { get; set; }

Дальнейшие усложнения были вызваны необходимостью маппить DTO объекты на View модели уже в WPF приложении клиента.
Вместо поля Function были введены 2 поля FunctionTo и FunctionFrom для того, чтобы кастомный маппинг в обе стороны можно было прописать только в одном атрибуте, чтобы не конфликтовал маппинг DO-DTO и DTO-ViewModel.
Маппинг ObservableRangeCollection через ReplaceRange

Финальный пример классов
namespace DataTransferObject
{
    [Map]
    public class NewsDto
    {
        public Guid? NewsId { get; set; }
        public string Title { get; set; }
        public string Anounce { get; set; }
        public string Text { get; set; }
        public string Status { get; set; }
        public CategoryDto Category { get; set; }
        public DateTime Created { get; set; }
        public string Author { get; set; }
        public IEnumerable<string> Tags { get; set; }
    }
}

namespace DataObjects
{
    [Map]
    public class News
    {
        public Guid NewsId { get; set; }
        public string Title { get; set; }
        public string Anounce { get; set; }
        public string Text { get; set; }
        [Map(FunctionFrom = "itemDto.Status = item.Status.ToString()", FunctionTo = "item.Status = (DataObjects.Attributes.StatusEnum) System.Enum.Parse(typeof(DataObjects.Attributes.StatusEnum), itemDto.Status)")]
        public StatusEnum Status { get; set; }
        public NewsCategory Category { get; set; }
        public DateTime Created { get; set; }
        [Map(FunctionFrom = "itemDto.Author = item.Author.Login")]
        public User Author { get; set; }
        [Map(Name = "Tags", FunctionFrom = "itemDto.Tags = item.NewsToTags.Select(p => p.Tag.Name)")]
        public IEnumerable<NewsToTags> NewsToTags { get; set; }
    }
}

Пример сгенерированного кода
        public static DataTransferObject.NewsDto MapToDto (this DataObjects.News item)
        {  
            if (item == null) return null;
            
            var itemDto = new DataTransferObject.NewsDto ();
                itemDto.NewsId = item.NewsId;
                itemDto.Title = item.Title;
                itemDto.Anounce = item.Anounce;
                itemDto.Text = item.Text;
                itemDto.Status = item.Status.ToString();
                itemDto.Category = item.Category.MapToDto();
                itemDto.Created = item.Created;
                itemDto.Author = item.Author.Login;
                itemDto.Tags = item.NewsToTags.Select(p => p.Tag.Name);

            return itemDto;
        }

        public static DataObjects.News MapFromDto (this DataTransferObject.NewsDto itemDto)
        {  
            if (itemDto == null) return null;
            
            var item = new DataObjects.News ();
                item.NewsId = itemDto.NewsId.HasValue ? itemDto.NewsId.Value : default(System.Guid);
                item.Title = itemDto.Title;
                item.Anounce = itemDto.Anounce;
                item.Text = itemDto.Text;
                item.Status = (DataObjects.Attributes.StatusEnum) System.Enum.Parse(typeof(DataObjects.Attributes.StatusEnum), itemDto.Status);
                item.Category = itemDto.Category.MapFromDto();
                item.Created = itemDto.Created;
            
            return item;
        }
        
        public static DataTransferObject.CategoryDto MapToDto (this DataObjects.NewsCategory item)
        {  
            if (item == null) return null;
            
            var itemDto = new DataTransferObject.CategoryDto ();
                itemDto.NewsCategoryId = item.NewsCategoryId;
                itemDto.Name = item.Name;
                itemDto.ParentCategory = item.ParentCategory.MapToDto();
                itemDto.ChildCategories = item.ChildCategories.Select(x => x.MapToDto());

            return itemDto;
        }

        public static DataObjects.NewsCategory MapFromDto (this DataTransferObject.CategoryDto itemDto)
        {  
            if (itemDto == null) return null;
            
            var item = new DataObjects.NewsCategory ();
                item.NewsCategoryId = itemDto.NewsCategoryId;
                item.Name = itemDto.Name;
                item.ParentCategory = itemDto.ParentCategory.MapFromDto();
                if(itemDto.ChildCategories != null) item.ChildCategories.ReplaceRange(itemDto.ChildCategories.Select(x => x.MapFromDto()));
            
            return item;
        }


Пример использования


Для того чтобы использовать наш маппинг нужно:
  1. Взять 2 файла шаблона из нашего проекта: MapHelper.tt и VisualStudioHelper.tt
  2. Создать 2 атрибута Map и MapIgnore, можно скопировать наши, и необязательно использовать одни и те же для разных проектов, главное чтобы назывались одинаково.
  3. Создать свой файл шаблона t4, добавить в него наши шаблоны и прописать настройки маппинга (пример).


Настройки
    MapHelper.DoProjects.Add("DataObject"); // список проектов, где искать DO объекты 
    MapHelper.DtoProjects.Add("DataTransferObject"); // список проектов, где искать DTO объекты 
    MapHelper.MapExtensionClassName = "MapExtensionsViewModel"; // имя класса с методами расширений, для избежания конфликтов.
    MapHelper.MapAttribute = "Map";
    MapHelper.MapIgnoreAttribute = "MapIgnore"; // имена атрибутов, тоже для избежания конфликтов, если на одних и тех же классах используется несколько маппингов.
    MapHelper.DtoSuffix = "Dto";
    MapHelper.DoSuffix = "ViewModel"; // суффиксы классов, которые можно игнорировать при сравнении имен классов.

VisualStudioHelper.tt

Этот файл был найден мной давно в просторах интернета, содержит полезные функции для работы со структурой проекта в Visual Studio, постепенно дополнялся и улучшался.
В частности для текущей задачи были добавлены методы:

public List GetClassesByAttributeName(string attributeName, string projectName) — получение списка классов в проекте по имени атрибута.

public List GetAttributesAndPropepertiesCollection(CodeElement element) — получение списка аттрибутов у класса или метода или проперти с распарсеными значениями полей и параметров если есть.

public bool IsExistSetterInCodeProperty(CodeProperty codeproperty)
public bool IsExistGetterInCodeProperty(CodeProperty codeproperty)

проверка на наличие сетера и гетера у проперти.

Сейчас создание маппинга происходит легко, а использование ещё легче
        var dto = item.MapToDto()

Буду рад если кому пригодится. GitHub

Комментарии (23)


  1. Akit
    23.06.2015 22:06

    Зачем вообще использовать два набора объектов? Не проще ли обойтись одним?


    1. KvanTTT
      24.06.2015 00:35
      +1

      Это нерукопожато, т.к. это разные сущности, не все свойства DO должны быть записаны в DTO, может возникнуть путаница.


    1. dj_raphael Автор
      24.06.2015 01:25
      +3

      DO объекты и другие серверные могут иметь циклические ссылки, бизнес логику, и вообще плохо поддаются сериализации в лоб. А DTO — это вообще считай структуры, только объявлены как классы. К тому же могут иметь структуру отличную от DO объектов. Структуру удобную для бизнес процессов через API и ничего лишнего. С ними меньше путаницы.


      1. Throwable
        24.06.2015 11:42

        Не спорю, что DO-DTO правильная связка. Однако:
        — связка имеет смысл только в случае remote interface, особенно если он публичный. Локальные бизнес процессы неплохо работают напрямую с DO.
        — структура DTO, мепящего один и тот же DO, может меняться для разных операций. Приходится писать разные мепперы для одного и того же DO.
        — в некоторых случаях приходится писать обратный меппер DTO->DO
        — в 90% случаев структура DO и DTO практически идентична (не считая ссылок), а количество полей запросто может достигать нескольких десятков.
        — писать и поддерживать меппинги — достаточно нудная и ресурсозатратная задача, если в проекте нет отдельной специально обученной обезьяны, пишущей и тестирующей меппинги. При использовании различных ухищрений и библиотек как правило теряется typesafe, и в разы усложняется поддержка (модификации DO, анализ изменений, тестирование, etc...)

        Поэтому в большинстве случаев обходятся одними DO. Обычно любая ORM проксирует ссылки, а при сериализации они не резольвятся. Таким образом можно вручную контролировать возвращаемый подграф. Некоторые ORM позволяют въявную ограничивать список полей для запрашиваемого объекта (остальные либо проксируются для ленивой инициализации, либо тупо устанавливаются в null).
        Можно также использовать смешанную схему: DTO, содержащие DO.

        P.S. DO не должно иметь бизнес логику, это противоречит идее DO. DO — это такие же структуры, только с connected state.


        1. Razaz
          24.06.2015 12:27

          С ормами всегда есть нюансы. Самое простое — List в EF. Мапить влоб не очень красиво, а вот отдельная модель уровня хранения данных решает эту проблему. Плюс с теми же Dto можно развесить доп. логику на базовый класс, которая не касается DO.


        1. Nagg
          24.06.2015 12:32

          >>DO не должно иметь бизнес логику, это противоречит идее DO. DO — это такие же структуры, только с connected state.
          Кто сказал? Вот к примеру Фаулер сказал, что анемичная доменная модель — анти-паттерн: www.martinfowler.com/bliki/AnemicDomainModel.html


  1. Nagg
    23.06.2015 22:53

    Хорошо когда узкое место в перфомансе — AutoMapper, а не кривая архитектура :-).

    Идеальная архитетура выглядет как-то так:[/sarcasm]
    EmployeePersistenceObj -> EmployeeDomainObj -> EmployeeDto — [transport] — EmployeeDto -> EmployeeClientDomainObj -> EmployeeViewModel


    1. Nagg
      23.06.2015 23:02

      PS: говорят EmitMapper хорош, генерит эти штуки сам в рантайме (эмитит IL).


      1. dj_raphael Автор
        24.06.2015 01:43

        Emit как то пропустил, но синтаксис у него примерно такой же как и AutoMapper'a. А AutoMapper ещё плох том что джуниоры имею тенденцию писать в его правилах бизнес логику.


        1. lexxpavlov
          25.06.2015 22:59

          есть старая хабрастатья со сравнением AutoMapper, BLToolkit и EmitMapper. По той статье EmitMapper рвёт на два порядка! Но статья старая, может, за эти пять лет AutoMapper тоже научился этим заклинаниям…


          1. KvanTTT
            26.06.2015 10:19

            Сейчас посмотрел на EmitMapper… ну что сказать, проект застрял в 2010 году. И что-то мне подсказывает, что и производительность AutoMapper с того времени выросла в разы.


  1. mird
    24.06.2015 00:00

    Очень интересуют ответы на три вопроса:
    1. На сколько процентов уменьшилось время выполнения маппинга?
    2. Сколько времени в среднем занимает получение DO объекта из бд и отправка DTO объекта в вебсервис
    3. Сколько человеко-часов было потрачено на разработку вашего маппинга на Т4 шаблонах?


    1. dj_raphael Автор
      24.06.2015 01:37
      +1

      1. На сколько процентов сейчас не скажу, а абсолютно на 120 миллисекунд насколько я помню по профайлеру. На мало процентов скорее всего, но как я писал это не самое узкое место.
      2. В среднем 100-500 миллисекунд.
      3. Около месяца работы джуниора под моим руководством,
      примерно: джуниор — 160, синьор — 8.
      Но работа по оптимизации ведется по всем направлениям, просто тут была возможность что то сделать, отдали её джуниору. И в результате этой работы получился продукт который можно выделить отдельно.


  1. KvanTTT
    24.06.2015 00:05

    Познавательная вещь. Из плюсов еще можно отметить отсутствие зависимости от мэпперов (AutoMapper, EmitMapper и т.д.).


    1. mayorovp
      24.06.2015 08:54
      +1

      С появлением nuget отсутствие зависимости уже не является таким большим плюсом…


  1. KvanTTT
    26.06.2015 10:12
    +1

    Возникла идея. А может ли с помощью шаблонов генерить еще и dto классы?


    1. dj_raphael Автор
      26.06.2015 10:46
      +1

      Можно, но тогда где то понадобится ещё и описать структуру этих dto классов, а где лучше описать структуру классов как не в самих классах? Структура dto у нас довольно сильно отличается от структуры DO.


      1. KvanTTT
        26.06.2015 11:17

        Это да. Но вот у меня был кейс, когда существовало только много DO объектов, а сгенерировать и подправить DTO (DAO) для них было бы быстрее и менее утомительно, чем создать их с нуля самому.


  1. KvanTTT
    27.06.2015 16:32

    А какие преимущества вашего генератора по сравнению с существующими? Занимались исследованием?
    Сейчас погуглил немного и нашел по DTO T4 вот что:


    Ну на самом деле из всех мне ваш больше всего понравился.


    1. dj_raphael Автор
      27.06.2015 17:06
      +1

      Честно. мы не искали. Задача простая, мелкая подходит для обучения и к тому же с пользой основному проекту.
      Большая куча зайцев одним выстрелом. И нам нужен был маппинг для себя.
      А по Вашему списку:
      1. Чисто генерация dto по образу и подобию do не интересует. У нас DTO сильно отличается от DO
      2. dto-gen — выглядит похоже, но опять генерация dto, похоже без кастомизации и маппинг только на свои объекты.
      3. T4DtoBuilder — странный способ использовать method chain вместо object initializer'a
      4. entitiestodtos — вообще расширение для студии, стараюсь ставить поменьше.

      В общем одним из этих проектов можно сгенерировать DTO, а нашим сгенерировать маппинг, и получится то, что Вы хотите!


    1. dj_raphael Автор
      27.06.2015 17:14
      +1

      а насчёт преимущества, наверно кастомизация, можно сгенерировать метод любой сложности.


  1. Shablonarium
    05.07.2015 19:01

    А вы проводили замеры по скорости маппинга между прямым копированием объектов, библиотекой AutoMapper и каким-то более быстрым маппером? Если мне не изменяет память, EmitMapper работает в сотни раз быстрее, чем AutoMapper и почти сравним с ручным копированием.


  1. dj_raphael Автор
    06.07.2015 12:02

    Как я уже писал EmitMapper я пропустил. А с автомаппером есть разница. Чужие обзоры посмотрел потом.