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