Генерація мапінгу через 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 < 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


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.