Здравствуйте! Наш проект уже достиг такой стадии когда встал вопрос об оптимизации производительности. После анализа слабых мест, одно из возможных путей для оптимизации был способ избавления от 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;
}
Пример использования
Для того чтобы использовать наш маппинг нужно:
- Взять 2 файла шаблона из нашего проекта: MapHelper.tt и VisualStudioHelper.tt
- Создать 2 атрибута Map и MapIgnore, можно скопировать наши, и необязательно использовать одни и те же для разных проектов, главное чтобы назывались одинаково.
- Создать свой файл шаблона 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"; // суффиксы классов, которые можно игнорировать при сравнении имен классов.
MapHelper.DOSkipAttribute = false;
MapHelper.DTOSkipAttribute = false; // Флаг, который позволяет игнорировать атрибут [Map] и искать во всех классах проекта, а настройки брать с другой стороны.
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