Основы AutoMapper
В ходе создания компонента мы инициируем загрузку AutoMapper, настроим его на работу с нашим преобразованием и применим правила форматирования. Важно также, чтобы у разработчиков была возможность проверить, что конфигурация является действительной. Мы рассмотрим эти и другие аспекты в данном разделе.
Инициализация AutoMapper
Прежде чем использовать AutoMapper, его нужно инициализировать при запуске приложения. Для
приложений ASP.NET MVC мы делаем это в Global.asax.cs
.
Вот пример класса, который инициализирует AutoMapper.
public class AutoMapperConfiguration
{
public static void Configure()
{
Mapper.Initialize(x => x.AddProfile<ExampleProfile>());
}
}
В этом примере класс AutoMapperConfiguration
объявляет статический метод Configure
, который может использоваться для инициализации AutoMapper путем добавления профиля в конфигурацию AutoMapper. Профили являются главным средством для настройки AutoMapper, и мы рассмотрим их далее - без них настроить AutoMapper невозможно.
Профили AutoMapper
Профиль представляет собой набор определений типов преобразования, в том числе правила, которые применяются ко всем сущностям, определенным в профиле. Профили AutoMapper – это классы, которые происходят из класса Profile
.
Профили эффективны для группировки наборов сущностей по контексту. Приложение может иметь один профиль для преобразования доменной модели в презентационную модель, и другой профиль для другой цели. Следующий листинг показывает богатый профиль с несколькими указаниями по конфигурации.
Листинг 11-5: Создание примера профиля
public class ExampleProfile : Profile
{
protected override void Configure()
{
ForSourceType<Name>().AddFormatter<NameFormatter>();
ForSourceType<decimal>().AddFormatExpression(context =>
((decimal)context.SourceValue).ToString("c"));
CreateMap<Customer, CustomerInfo>()
.ForMember(x => x.ShippingAddress, opt =>
{
opt.AddFormatter<AddressFormatter>();
});
}
}
Строка 1: Наследование от
Profile
Строка 6: Применение
AddFormatter
к исходному типу
Строка 8: Применение встроенного форматирования к исходному типу
Давайте исследуем этот профиль по частям. Во-первых, каждый профиль должен происходить от Profile
.
Метод Configure
содержит указания по конфигурации. Первое указание по форматированию дает AutoMapper инструкцию использовать NameFormatter
всякий раз, когда он преобразует объект Name
(мы исследуем NameFormatter
подробнее далее в этой главе).
Также есть указание, предоставляющее специальное выражение форматирования, которое AutoMapper
должен использовать, когда он пытается преобразовать объекты decimal
. Это выражение использует стандартную строку форматирования, когда отображает decimal
как валюту.
Наконец, инструкция CreateMap
указывает AutoMapper, чтобы он планировал преобразовать Customer
в CustomerInfo
. Вызов метода ForMember
сообщает AutoMapper, чтобы он применил AddressFormatter
при преобразовании в свойство назначения ShippingAddress
.
Остальные свойства CustomerInfo
не указаны, так как они преобразовываются по соглашению.
Проверка работоспособности
Опора на соглашение - палка о двух концах. С одной стороны, оно очень кстати устраняет обязательства разработчика указывать преобразование для каждого члена. Но есть опасность того, что свойство будет переименовано. Если элемент-источник переименован, он больше не будет соответствовать определенному элементу назначения, и соглашение будет нарушено. Разработчикам нужно быстрое уведомление, когда происходят подобные изменения. Ошибка выполнения не приемлема.
AutoMapper предоставляет метод, который гарантирует работоспособность конфигурации, проверяя, преобразуется ли каждый элемент назначения в элемент-источник на основе соглашения или конфигурации. Следующий листинг показывает профиль, который не будет работать из-за чьей-то опечатки.
Листинг 11-6: Потенциально опасная опечатка
public class Destination
{
public string Name { get; set; }
public string Typo { get; set; }
}
public class Source
{
public string Name { get; set; }
public int Number { get; set; }
}
public class BrokenProfile : Profile
{
protected override void Configure()
{
CreateMap<Source, Destination>();
}
}
Строка 4: Строка должна называться "Number"
Чтобы избежать подобных опечаток, мы можем запустить специальный вспомогательный тест как часть автоматизированного комплексного теста. Этот вспомогательный тест, AutoMapperConfigurationTester
, показан в следующем листинге.
Листинг 11-7: Подтверждение правильной конфигурации AutoMapper
[TestFixture]
public class AutoMapperConfigurationTester
{
[Test]
public void Should_map_everything()
{
AutoMapperConfiguration.Configure();
Mapper.AssertConfigurationIsValid();
}
}
Строка 8: Проверяет конфигурацию маппинга
Когда этот тест запускается для неработающего профиля из листинга 11-6, мы получим сообщение о том, что свойство Typo
не преобразовано.
Сокращение повторяющегося кода форматирования
Ранее в этой главе мы говорили о применении специальных средств форматирования к преобразованиям элементов. Эти средства форматирования являются реализациями IValueFormatter
, интерфейса AutoMapper, который определяет контракт между AutoMapper и нашим пользовательским кодом форматирования:
public interface IValueFormatter
{
string FormatValue(ResolutionContext context);
}
Наша пользовательская реализация форматирования примет ResolutionContext
, который предоставляет значение свойства модели представления и другие метаданные. Мы можем предоставить любые
трансформации или преобразования, которые считаем необходимыми, и просто вернуть строку результата.
Чтобы облегчить разработку клиентской части, можно реализовать простой базовый класс.
В следующем листинге показан ValueFormatter
, включенный в AutoMapper, который извлекает
исходное значение из контекста и проверяет его на значение null
.
Листинг 11-8: Реализация интерфейсаIValueFormatter
в классеValueFormatter
public abstract class ValueFormatter<T> : IValueFormatter
{
public string FormatValue(ResolutionContext context)
{
if (context.SourceValue == null)
return null;
if (!(context.SourceValue is T))
{
object value = context.SourceValue;
return value == null ? string.Empty : value.ToString();
}
return FormatValueCore((T)context.SourceValue);
}
protected abstract string FormatValueCore(T value);
}
Строка 7: Пробует
ToString
при неправильном типе
Строка 13: Возвращает результат абстрактного метода
Строка 15: Требует наследников для замещения метода
Пользовательское средство форматирования писать просто, так как оно происходит от ValueFormatter
.
Все, что нам нужно сделать, это реализовать его абстрактный метод FormatValueCore
, который получает
строго типизированное исходное значения. AutoMapper перехватит все исключения при обращении к null
при использовании средств форматирования или при обычном сопоставлении и вместо них вернет пустую строку или значение по умолчанию.
В следующем листинге показан NameFormatter
, который мы настроили в листинге 11-5.
Листинг 11-9: Получение NameFormatter
для обработки комбинирования свойств
public class NameFormatter : ValueFormatter<Name>
{
protected override string FormatValueCore(Name value)
{
var sb = new StringBuilder();
if (!string.IsNullOrEmpty(value.First))
{
sb.Append(value.First);
}
if (!string.IsNullOrEmpty(value.Middle))
{
sb.Append(" " + value.Middle);
}
if (!string.IsNullOrEmpty(value.Last))
{
sb.Append(" " + value.Last);
}
if (value.Suffix != null)
{
sb.Append(", " + value.Suffix.DisplayName);
}
return sb.ToString();
}
}
Строка 5: Использует StringBuilder для создания результата
Строки 6-9: Применяет базовую логику форматирования
Использование AutoMapper позволяет разработчикам написать этот код один раз и применять его во многих местах, всего лишь объявив его. Когда это средство форматирования настроено так же, как профиль из листинга 11-5, оно будет применяться ко всем элементам-источникам типа Name
.
Возвращаемся к представлению
Когда конфигурация завершена, наша разметка сосредоточена только на расположении элементов. Мы заменили рутинную логику из листинга 11-1. Вот окончательное представление.
Листинг 11-10: Окончательная разметка представления
<h2>Customer: @Model.Name</h2>
<div class="customerdetails">
<p>Status: @Model.Status</p>
<p>Total Amount Paid: @Model.TotalAmountPaid</p>
<p>Address: @Model.ShippingAddress</p>
</div>