⬆️ ⬇️

Generating mapping via t4 templates



Hello! Our project has already reached such a stage when there was a question about performance optimization. After analyzing the weak points, one of the possible ways to optimize was the way to get rid of AutoMapper, although it is not the most stopping place, it is the place that we can improve. We use AutoMapper for mapping DO objects into DTO objects for transmission via WCF service. A hand-written method of creating a new object and copying fields is faster. We wrote the mapping manually - a bleak routine, often there were errors, forgotten fields, forgotten new fields, so we decided to write the mapping generation through t4 templates.



In fact, we had to check the list of types and types, and write a copy, but not everything is so smooth in the Danish kingdom.



In order to link the two classes, the attribute [Map] was added. In the configuration of the template, 2 projects were written in which it was necessary to look for classes with this attribute. Classes were paired by name, while the DTO class had the “Dto” suffix cut off, if there was one. But in some cases, it was still necessary to bind the opposite classes, the Name parameter was added to the attribute.



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


Mapping is generated as extension methods. It seems all is well, the fields are copied. But still there is a lot of manual work. DTO and DO objects have other objects and collections inside themselves, they have to be mapped manually, albeit with the help of methods generated by us. Many fields have the same name, and the type matching is in the collection of links that we have already made.

Mapping has been extended to automatically mapping nested objects and collections. And the effect of the attribute [Map] was extended to prop, so that you can map them with different names.

An example of the resulting code.

')

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


And for quite complex cases, the Function field was added to the attribute, and when generating a mapping, the text from this field was simply inserted into the code. Also added attribute [MapIgnore]



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


Further complications were caused by the need to map DTO objects on the View model already in the WPF client application.

Instead of the Function field, 2 FunctionTo and FunctionFrom fields were entered so that custom mapping in both directions can be registered only in one attribute, so that DO-DTO and DTO-ViewModel do not conflict.

Mapping ObservableRangeCollection via ReplaceRange



Final class example
 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; } } } 


Sample generated code
  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; } 




Usage example



In order to use our mapping you need:

  1. Take 2 template files from our project: MapHelper.tt and VisualStudioHelper.tt
  2. Create 2 attributes Map and MapIgnore, you can copy ours, and not necessarily use the same for different projects, the main thing that called the same.
  3. Create your own template file t4, add our templates to it and set the mapping settings ( example ).




Settings
  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

This file was found by me a long time ago on the Internet, contains useful functions for working with the project structure in Visual Studio, and was gradually supplemented and improved.

In particular, methods have been added for the current task:



public List GetClassesByAttributeName (string attributeName, string projectName) - get the list of classes in the project by the attribute name.



public List GetAttributesAndPropepertiesCollection (CodeElement element) - get a list of attributes from a class or method or process it with parsed values ​​of fields and parameters, if any.



public bool IsExistSetterInCodeProperty (CodeProperty codeproperty)

public bool IsExistGetterInCodeProperty (CodeProperty codeproperty)

check for the presence of a setter and a hetero in a propert.



Now creating mapping is easy, and using is even easier.

  var dto = item.MapToDto() 


I would be glad if anyone come in handy. Github

Source: https://habr.com/ru/post/260963/



All Articles