Генерация маппинга через t4 шаблоны
Здравствуйте! Наш проект уже достиг такой стадии когда встал вопрос об оптимизации производительности. После анализа слабых мест, одно из возможных путей для оптимизации был способ избавления от 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 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 { 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"; // суффиксы классов, которые можно игнорировать при сравнении имен классов.
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