Избегайте ломающих изменений с помощью простых и удивительных Custom Value Resolvers от AutoMapper

Вы используете AutoMapper для отображения свойств модели между вашими API? Вам необходимо корректировать свои модели в соответствии с меняющейся логикой и структурой домена? И вы хотите избежать поломки приложения при этом (без создания нескольких патчей отображения, как на картинке выше)? Тогда Custom Value Resolvers от AutoMapper — это то, что вам нужно! Продолжайте читать!

Предыстория — описание нашего приложения

Архитектура нашего приложения состоит из нескольких сервисов.

В каждом бэкенд-сервисе есть три основных слоя:

  • Слой Api — отвечает за связь с другими API, проверяет входящие запросы и модели, сопоставляет модели Api с моделями Core.
  • Слой Core — содержит модели, используемые в остальных частях решения.
  • Слой инфраструктуры — хранит данные в базе данных, содержит отображение Core и внешних моделей.

AutoMapper используется для отображения моделей между слоями, некоторые свойства могут быть проигнорированы или изменены в соответствии с логикой домена. Например, если вы добавите новое свойство, вы можете оставить отображение как есть, наши модели Api, Core и Storage User могут быть отображены в пару строк кода:

public class User {
    public Guid Id {get; set;}
    public string LastName {get; set;}
    public string Nationality {get; set;}
}
Вход в полноэкранный режим Выход из полноэкранного режима
public ApiModelMappingProfile()
    {
        CreateMap<ApiModel.User, CoreModel.User>().IncludeAllDerived();
    }
Вход в полноэкранный режим Выход из полноэкранного режима

Изменение модели Api в то время, когда модели других API еще не обновлены

В какой-то момент возникла необходимость не только хранить национальность пользователя, но и использовать код страны этой национальности где-то еще в приложении.
Вместо примитивного типа string нам нужно было создать модель с названием национальности и кодом страны:

public class Country
{
    public string Name {get; set;}
    public string Code {get; set;}
}
Вход в полноэкранный режим Выход из полноэкранного режима
public class User {
    public Guid Id {get; set;}
    public string LastName {get; set;}
    public string Nationality {get; set;}
    //new property added
    public Country SecondNationality {get; set;}
    }
Войти в полноэкранный режим Выход из полноэкранного режима

Модель Core должна была содержать только свойство SecondNationality. Другие API, которые все еще использовали национальность, могли получить ее из SecondNationality.Name:

 CreateMap<CoreModel.User, ApiModel.User>()
        .IncludeAllDerived()
        .ForMember(dest => dest.Nationality,
                    opt =>
                    {
                        opt.MapFrom((src, dest) => dest.Nationality =
                        {
                            src.SecondNationality.Name
                        });
                    });
Войти в полноэкранный режим Выйти из полноэкранного режима

В конечном счете, все использования Nationality должны были быть заменены новым SecondNationality, и в этот момент оно должно было быть переименовано обратно в «Nationality», что позволило бы нам вносить изменения без блокировки развертывания наших внутренних служб. Однако в процессе изменений выяснилось, что пользователь должен иметь возможность иметь несколько национальностей. Было добавлено еще одно свойство: Nationalities — Enumerable of Country.
И, к сожалению, первое изменение так и не было доработано до конца. В итоге мы получили модели с тремя свойствами разного типа, обозначающими одно и то же значение для конечного пользователя.
В результате в определенный момент различные API использовали в своих моделях пользователя Nationality, SecondNationality, Nationalities или их смесь:

public class User {
    public Guid Id {get; set;}
    public string LastName {get; set;}
    public string Nationality {get; set;}
    public Country SecondNationality {get; set;}
    public List<Country> Nationalities {get; set;}
}
Вход в полноэкранный режим Выход из полноэкранного режима

Мы решили очистить модели, удалить старые свойства и использовать только свойство Nationalities.
Но как это сделать, не сломав все приложение? Некоторые API будут посылать запросы с Nationality, другие — с SecondNationality, третьи — с Nationalities или их смесью.
Все эти свойства должны быть сохранены как одно свойство — Nationalities.

Вот где пригодятся пользовательские резольверы значений AutoMapper!

Пользовательские решатели значений

Класс Resolver имеет единственный метод Resolve, который принимает в качестве аргументов исходную модель, модель назначения и свойство модели назначения.
Он проверяет исходное значение и разрешает его в соответствии с типом члена назначения.
Приведенный ниже метод Resolver может разрешить значение для сохранения в модели Core, независимо от того, содержит ли исходная модель SecondNationality или Nationalities:


public class NationalitiesResolverForUser : IValueResolver<ApiModel.User, CoreModel.User, IEnumerable<CoreModel.Country>>
{
    public IEnumerable<CoreModel.Country> Resolve(ApiModel.User source,
        CoreModel.User destination, IEnumerable<CoreModel.Country> destMember,
        ResolutionContext context)
    {
        CoreModel.Country[] resolved = { };

        if (source.Nationalities != null)
        {
            resolved = source.Nationalities
                .Select(Country => new CoreModel.Country(Country.Code, Country.Name)).ToArray();
        }
        else if (source.SecondNationality != null)
        {
            resolved = new []
            {
                new CoreModel.Country(source.SecondNationality.Code, source.SecondNationality.Name)
            };
        }

        return resolved;
    }
Войти в полноэкранный режим Выйти из полноэкранного режима

Резольвер может быть вызван в профиле Api Mapping следующим образом:

CreateMap<ApiModel.User, CoreModel.User>()
            .ForMember(dest => dest.Nationalities,
                opt =>
                {
                    opt.MapFrom<NationalitiesResolverForUser>();
                });
            .ReverseMap().IncludeAllDerived();
Войти в полноэкранный режим Выйти из полноэкранного режима

Поскольку некоторые API все еще используют только SecondNationality, а другие используют Nationalaities, нам нужно отобразить список Nationalities ядра на оба свойства.
Мы можем использовать другие резольверы значений:

SecondNationality resolver:

public class SecondNationalityResolver : IValueResolver<CoreModel.User,
    ApiModel.User, ApiModel.Country>
{
    public ApiModel.Country Resolve(CoreModel.User source,
        ApiModel.User destination, ApiModel.Country destMember,
        ResolutionContext context)
    {
        var firstNationality = source.Nationalities?.FirstOrDefault();

        return firstNationality == null
            ? null
            : new ApiModel .Country(firstNationality?.Code, firstNationality?.Naam,
                firstNationality?.WeergaveNaam);
    }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Разрешитель национальностей:

public class NationalitiesResolver : IValueResolver<CoreModel.User, ApiModel.User, IEnumerable<ApiModel.Country>>
{
    public IEnumerable<ApiModel.Country> Resolve(CoreModel.User source, ApiModel.User destination, IEnumerable<ApiModel.Country> destMember, ResolutionContext context)
    {
        return source.Nationalities
            ?.Select(country => new ApiModel.Country(country.Code, country.Naam, country.WeergaveNaam)).ToArray();
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Резольверы затем вызываются в маппере для соответствующих свойств:


CreateMap<CoreModel.User, ApiModel.User>()
            .IncludeAllDerived()
            .ForMember(dest => dest.SecondNationality,
                opt =>
                {
                    opt.MapFrom<SecondNationalityResolver>();
                })
            .ForMember(dest => dest.Nationalities, opt =>
            {
                opt.MapFrom<NationalitiesResolver>();
            })
            .ForMember(dest => dest.Nationality, opt => opt.Ignore());
    }
Войти в полноэкранный режим Выход из полноэкранного режима

Заключение

Наличие этих Custom Value Resolvers на уровне Core прямо перед уровнем инфраструктуры гарантирует, что какая бы пользовательская модель ни была отправлена из любой точки приложения, правильное значение будет сохранено в базе данных. Это также гарантирует, что при любом запросе из базы данных результирующая Api модель будет иметь все правильные и последовательные значения.

Благодаря этому мы можем легко начать удалять старые свойства из всех API и их моделей без опасности сломать приложение в любой момент.

После обновления всех APi можно удалить резольверы. Затем мы можем с радостью вернуться к стандартному отображению AutoMapper:

CreateMap<ApiModel.User, CoreModel.User>().IncludeAllDerived();
Вход в полноэкранный режим Выход из полноэкранного режима

Для получения более подробной информации о ресолверах ознакомьтесь с документацией.

Фото Тима Моссхолдера на Unsplash

Оцените статью
devanswers.ru
Добавить комментарий