Признайтесь, и вы тоже. Наверное, нет более противоречивой библиотеки в мире .NET, чем AutoMapper. Тысячи проектов его используют, а разработчики этих проектов страдают. Страдают, когда нужно быстро ответить на вопрос: «Откуда, %$&дь, тут взялось это значение?!». Ты наводишься на setter, нажимаешь на Alt+F7 и видишь то самое Usage of 'Property.set' were not found:

А мы точно знаем, что кто-то задаёт это свойство.
А мы точно знаем, что кто-то задаёт это свойство.

Еще раз выругавшись, ты понимаешь, что это свойство устанавливает AutoMapper, идёшь искать конфигурацию…

Знакомо? Пора с этим заканчивать.

TL;DR

Устанавливай плагин AutoMapper.FindUsage для Rider/Resharper, он встраивается в стандартный Find Usages (Alt+F7) и показывает AutoMapper-маппинги как обычные usages — как будто никакой магии нет.

Стандартное окно результатов поиска Find Usages по свойству UserDto.Name показывает ссылку на UserModel.Name.
Стандартное окно результатов поиска Find Usages по свойству UserDto.Name показывает ссылку на UserModel.Name.

Как всё начиналось

Добавляя AutoMapper в проект, ты всегда идешь на сделку с совестью. Да, ты сэкономишь массу времени на генерации boilerplate-кода, новые свойства будут маппиться автоматически. Но с другой стороны, ты получишь «разрыв» потока данных в проекте, когда ручеёк данных внезапно теряет прослеживаемость, т.к. копирование значений происходит где-то за кулисами через рефлексию.

На прошлой работе я принёс AutoMapper в проект для упрощения конвертации различных моделей. И мы начали очень плотно его использовать. До сих пор считаю, что это моё худшее инженерное решение за всю карьеру.

Периодически, в разных проектах я опять начинал встречаться с этой проблемой. В фоне я думал о том, можно ли это как-то решить.

Анализатор

Поднабравшись опыта с написанием Roslyn-анализаторов для поиска всяких проблемных мест в коде, я решил, что и для проблемы AutoMapper можно применить этот же инструмент.

Опущу разные нюансы разработки и скажу, что у меня получилось найти все соответствия моделей и их свойств, которые описываются через метод CreateMap<TSource, TDestination>() и как-то их подсветить:

Предупреждения компилятора.
Предупреждения компилятора.

Но тут я столкнулся с фундаментальными ограничениями анализатора: они, как ни странно, анализируют код и могут его менять. Они не привязаны к какой-то среде разработки и не могут дать команду IDE «а перейди-ка вот сюда» хоть даже и знают, куда именно. Всё что они могут — вывести WARN при компиляции или показать какую-то подсказку в IDE. Не очень удобно.

Одним из половинчатых решений было добавление комментария к свойству, где указывается источник данных:

Комментарии в xml-формате, которые позволят быстро найти источник.
Комментарии в xml-формате, которые позволят быстро найти источник.

Но это опять же неудобно: нужно править код, засорять его комментариями, поддерживать их. Анализаторы работают в рамках компилятора и не имеют доступа к API IDE для навигации. Нужен доступ к API IDE — значит, нужен плагин.

Плагин

Функция навигации по коду — это возможность IDE, в котором ты этот код смотришь. Поэтому верным решением будет написать плагин.

Стоит сказать, что Rider/Resharper не использует для анализа кода Roslyn. У него своя собственная модель работы с исходным кодом — Rider.SDK. В целом это логично, Resharper появился намного раньше. Но подход там похож. Ещё из удивительного — все плагины пишутся под классический .net 4.7.2.

Для старта разработки нужно скачать шаблон, на основе которого будет создан новый плагин. Вместе с шаблоном поставляется полный набор инструментов для тестирования и деплоя на основе Gradle. Это сильно облегчает жизнь. Например, одной командой можно запустить тестовый Rider, в котором будет твой плагин, и можно будет проверить его работоспособность. Так же есть команда, которая собирает тебе готовый пакет локально или сразу отправляет его в Marketplace JetBrains.

Стоит отметить, что разрабатывать плагин оказалось намного сложнее, чем roslyn-анализатор. Намного меньше примеров, документации. Различные ИИ помощники теряются и генерируют нерабочий код. Пришлось много изучать код Rider.SDK, различных базовых классов.

Провозившись пару вечеров, я добился нужного мне поведения: ставишь курсор на set, открываешь контекстное меню, видишь, откуда приходят данные, и можешь перейти в один клик туда:

Навигация через контекстное меню.
Навигация через контекстное меню.

Если разработчик вызывает контекстное меню, проверяем, что в данный момент мы находимся именно в setter'е какого-то свойства и запускается поиск регистраций маппинга (метод CreateMap()), далее извлекается тип TSource, находится свойство с таким же именем и добавляется новый пункт в контекстное меню:

Отображение нескольких маппингов для одного свойства.
Отображение нескольких маппингов для одного свойства.

Ну а навигация — это уже дело техники, ничего сложного. Так родилась первая версия плагина, которую я опубликовал в JetBrains Marketplace. Порекламировал среди коллег, попользовался сам, собрал первый фидбэк. Но уже тогда я понимал, что это не конечный результат, который я хочу получить.

Как видите, поиск осуществляется «на лету» при открытии контекстного меню. Это не самое хорошее решение, но оно стабильно работает на достаточно больших проектах. Тем не менее, это можно отнести к недостаткам. Так же, поиск через контекстное меню — не самый прямой путь. Обычно мы ищем связи через тот самый механизм Find Usages.

Инкрементальный анализ

Поиск маппинга в момент вызова может сказываться на производительности. Более оптимальный путь — найти все маппинги заранее, сложить их в кэш и обновлять его при изменении файла. Rider.SDK предоставляет приятную абстракцию для таких сценариев — SimpleICache<T>, где T — будет наш список маппингов. В этом классе Rider/Resharper будут вызывать метод Build при обновлении файла, а мы сможем накапливать наш кэш:

public class AutoMapperMappingCache : SimpleICache<List<SerializableMapping>>
{
    public override object Build(IPsiSourceFile sourceFile, bool isPreParent)
    {
        // анализируем файл, возвращаем маппинги
    }
}

Дальше наши маппинги сохраняются в памяти и на диске, что позволяет не анализировать весь проект на старте. Сериализацию маппингов делаем вручную:

SerializableMapping.cs
public class SerializableMapping
{
    public string SourceTypeClrName;
    public string DestinationTypeClrName;
    public List<string> IgnoredProperties;
    public bool HasReverseMap;

    public void Write(UnsafeWriter writer)
    {
        writer.WriteString(SourceTypeClrName);
        writer.WriteString(DestinationTypeClrName);
        writer.WriteInt32(IgnoredProperties.Count);
        foreach (var prop in IgnoredProperties) 
            writer.WriteString(prop);
        writer.WriteBoolean(HasReverseMap);
    }

    public static SerializableMapping Read(UnsafeReader reader)
    {
        var mapping = new SerializableMapping
        {
            SourceTypeClrName = reader.ReadString(),
            DestinationTypeClrName = reader.ReadString()
        };
        var count = reader.ReadInt32();
        mapping.IgnoredProperties = new List<string>(count);
        for (var i = 0; i < count; i++) 
            mapping.IgnoredProperties.Add(reader.ReadString());
        mapping.HasReverseMap = reader.ReadBoolean();
        return mapping;
    }
}

Теперь, когда нам нужно быстро найти соответствующий маппинг, нам не придётся обходить заново весь проект, а только пройтись по сохранённому ранее кэшу:

public IEnumerable<(IPsiSourceFile, SerializableMapping)> GetMappingsForType(string typeClrName)
{
    foreach (var (sourceFile, mappings) in Map)
    {
        if (mappings == null) continue;
        foreach (var mapping in mappings)
        {
            if (mapping.SourceTypeClrName == typeClrName || mapping.DestinationTypeClrName == typeClrName)
            {
                yield return (sourceFile, mapping);
            }
        }
    }
}

Find Usages

Мне так же хотелось сделать работу с AutoMapper более явной, как будто нет той самой закулисной магии и значение прилетает в свойство одного класса из свойства другого класса. Сделать так же, как обычно это происходит без него: ты находишь нужное свойство, нажимаешь Find Usages и идёшь дальше по этой цепочке. Поэтому я стал искать варианты, как встроить свою логику в стандартный механизм Rider/Resharper.

Оказывается, такое тоже возможно.

Для этого нам нужно реализовать свой кастомный механизм поиска, унаследованный от фабрикиDomainSpecificSearcherFactoryBase и пару вспомогательных классов:

Схема обработки запроса от пользователя.
Схема обработки запроса от пользователя.

Всё это вылилось в текущую версию плагина.

Как это работает

Давайте для примера рассмотрим такой код:

using AutoMapper;
using Microsoft.Extensions.Logging;

var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<UserModel, UserDto>();
    }, 
    new LoggerFactory());

var mapper = config.CreateMapper();

var user = new UserModel { Name = "John Doe", Age = 30 };
var userDto = mapper.Map<UserDto>(user);

Console.WriteLine(userDto.Name);
Описание моделек
public class UserModel
{
    public string Name { get; set; }
    public int Age { get; set; }
}
public class UserDto
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Как видим, свойство userDto.Name не устанавливается напрямую. Это делает AutoMapper.

Давайте выполним команду Find Usages на этом свойстве. Если найдено только одно упоминание, то мы сразу переходим к свойству-источнику:

Переход сразу к свойству-источнику данных.
Переход сразу к свойству-источнику данных.

Если упоминаний несколько, то видим стандартное окно. Обратите внимание, что искали мы UserDto.Name, а нашли в том числе UserModel.Name:

Окно с результатами поиска.
Окно с результатами поиска.

Возможности

На данный момент плагин поддерживает поиск по прямому и обратному маппингу через ReverseMap(), поддерживает исключение свойство через Ignore(). Это закрывает почти все мои потребности, но если у вас есть предложения, то пишите тут или приносите isuue. В моих ближайших планах:

  • Поддержка метода MapFrom()

  • Поддержка NamingConvention: property_name -> PropertyName

Заключение

Можно сказать, AutoMapper — необходимое зло, которое укоренилось в наших проектах и останется ещё с нами надолго. Поэтому стоит попытаться облегчить себе жизнь, ведь явное всегда лучше неявного. Сейчас уже появились мапперы на основе Source Generator, которые работают без рефлексии, а все преобразования выполняют в явном виде.

Если ты на новом проекте — смотри в сторону Mapperly, например. Если застрял с AutoMapper — ставь плагин.

Ссылки

Комментарии (10)


  1. Heggi
    02.07.2026 11:06

    Избавляюсь во всех проектах от автомаппера.
    Перехожу на Riok.Mapperly. Функционал послабее, но для простых перекладываний значений из одной DTO в другую, более чем достаточно. А благодаря кодогенерации сразу видно что где и как используется.


    1. MonkAlex
      02.07.2026 11:06

      Последние годы руками маппинг пишу, помнится что автомаппер умел ругаться, если в одном из участников есть поле, а в другом - нет (т.е. либо в дто либо в сущности завели поле и забыли продублировать).

      Есть что-то похожее у Mapperly?


      1. Heggi
        02.07.2026 11:06

        Да, ругается варнингами при компиляции, если с одной стороны есть лишние поля. Можно поднять до еррора. Можно настроить не ругаться, если с одной стороны лишние поля. Достаточно гибко сделано.


    1. pashagoroshko
      02.07.2026 11:06

      У меня какие то проблемы с ним были, то тип коллекции ему не нравился, то после добавления нового поля слетали старые маппинги, не знаю как это можно использовать всерьёз где то


    1. srogatnev Автор
      02.07.2026 11:06

      Полностью согласен, что source‑generator мапперы (Mapperly и т.п.) куда приятнее AutoMapper — явный код, нормальная навигация, никакой рефлексии.

      Этот плагин как раз для "тёмной реальности", где AutoMapper уже живёт в большом легаси и выкинуть его за неделю невозможно. Пока проект постепенно мигрирует (на Mapperly или что угодно ещё), разработчикам всё равно приходится отвечать на вопрос "откуда это значение взялось?" - и тут FindUsage по маппингам спасает нервы.


  1. hmspns
    02.07.2026 11:06

    Я бы сказал что automapper позволяет условно экономить время для простых моделек. Как только модельки становятся сложнее, с условными маппингами и конвертациями, кода приходится писать больше.

    Мы в итоге где это возможно избавлялись от него полностью, благо есть source generators.


  1. ProgerMan
    02.07.2026 11:06

    Рад тому, что в рабочем проекте удалили это почти везде.

    Сам пишу методы расширения для мапинга вместо автоматической ерунды. Максимальная скорость и прозрачность использования. Ещё и без лишней зависимости. Результат выглядит так: var result = repository.Create(dto.ToEntity()).


  1. pashagoroshko
    02.07.2026 11:06

    Реклама своей тулзы а не статья.

    Automapper это швейцарский нож в мире мапперов, что он только не умеет но платим производительностью

    Удобнее и проще чем Mapster не встречал, хотя не без нюансов


    1. srogatnev Автор
      02.07.2026 11:06

      Да, это осознанно "опыт + инструмент": я описываю конкретную проблему, в которую сам много раз упирался (да и мои коллеги тоже), и показываю путь, который прошёл до решения.

      Понятно, что ради этого плагина нет смыслы заносить в проект AutoMapper. Но раз он уже есть, то можно попытаться облегчить себе жизнь.

      Статья о том, когда этот швейцарский нож уже воткнули вам в проект и теперь как-то надо с ним жить. Mapster/Mapperly/ещё кто‑то действительно комфортнее по DX и производительности. Но там, где AutoMapper уже накопил сотни CreateMap, может быть проще сначала научиться хотя бы нормально по ним ходить, а потом уже планировать миграцию на что‑то более современное.


  1. for7raid
    02.07.2026 11:06

    Приходишь на собес, там как защита докторской: солид, ооп, все паттерны банды четырех нужно рассказать наизусть. А потом реальность - в проекте MediatR и AutoMapper (это все один автор), половина от cqrs и одна база на три микросервиса. Приехали. Тьфу, плять.