Полиморфное отображение коллекций с помощью AutoMapper
TL;DR: у меня проблемы с полиморфным отображением. Я сделал репозиторий github с набором тестов, который иллюстрирует мою проблему. Пожалуйста, найдите здесь: ССЫЛКА НА РЕПО
Я работаю над реализацией функции сохранения / загрузки. Для этого мне нужно убедиться, что модель домена, которую я сериализую, представлена в удобной для сериализации форме. Для этого я создал набор DTO, которые содержат минимальный набор информации, необходимой для значимого сохранения или загрузки.
Как то так для домена:
public interface IDomainType
{
int Prop0 { get; set; }
}
public class DomainType1 : IDomainType
{
public int Prop1 { get; set; }
public int Prop0 { get; set; }
}
public class DomainType2 : IDomainType
{
public int Prop2 { get; set; }
public int Prop0 { get; set; }
}
public class DomainCollection
{
public IEnumerable<IDomainType> Entries { get; set; }
}
... и для DTO
public interface IDto
{
int P0 { get; set; }
}
public class Dto1 : IDto
{
public int P1 { get; set; }
public int P0 { get; set; }
}
public class Dto2 : IDto
{
public int P2 { get; set; }
public int P0 { get; set; }
}
public class DtoCollection
{
private readonly IList<IDto> entries = new List<IDto>();
public IEnumerable<IDto> Entries => this.entries;
public void Add(IDto entry) { this.entries.Add(entry); }
}
Идея состоит в том, что DomainCollection представляет текущее состояние приложения. Цель состоит в том, чтобы сопоставление DomainCollection с DtoCollection приводило к экземпляру DtoCollection, который содержит соответствующие реализации IDto при их сопоставлении с доменом. И наоборот.
Небольшая дополнительная хитрость здесь заключается в том, что разные конкретные типы доменов происходят из разных сборок плагинов, поэтому мне нужно найти элегантный способ, чтобы AutoMapper (или аналогичный, если вы знаете более качественную инфраструктуру сопоставления) делал тяжелую работу для меня.
Используя Structuremap, я уже могу найти и загрузить все профили из плагинов и настроить приложения IMapper с ними.
Я пытался создать такие профили...
public class CollectionMappingProfile : Profile
{
public CollectionMappingProfile()
{
this.CreateMap<IDomainType, IDto>().ForMember(m => m.P0, a => a.MapFrom(x => x.Prop0)).ReverseMap();
this.CreateMap<DtoCollection, DomainCollection>().
ForMember(fc => fc.Entries, opt => opt.Ignore()).
AfterMap((tc, fc, ctx) => fc.Entries = tc.Entries.Select(e => ctx.Mapper.Map<IDomainType>(e)).ToArray());
this.CreateMap<DomainCollection, DtoCollection>().
AfterMap((fc, tc, ctx) =>
{
foreach (var t in fc.Entries.Select(e => ctx.Mapper.Map<IDto>(e))) tc.Add(t);
});
}
public class DomainProfile1 : Profile
{
public DomainProfile1()
{
this.CreateMap<DomainType1, Dto1>().ForMember(m => m.P1, a => a.MapFrom(x => x.Prop1))
.IncludeBase<IDomainType, IDto>().ReverseMap();
}
}
public class DomainProfile2 : Profile
{
public DomainProfile2()
{
this.CreateMap<DomainType2, IDto>().ConstructUsing(f => new Dto2()).As<Dto2>();
this.CreateMap<DomainType2, Dto2>().ForMember(m => m.P2, a => a.MapFrom(x => x.Prop2))
.IncludeBase<IDomainType, IDto>().ReverseMap();
}
}
Затем я написал набор тестов, чтобы убедиться, что сопоставление будет работать так, как ожидалось, когда пришло время интегрировать эту функцию с приложением. Я обнаружил, что всякий раз, когда DTO сопоставлялись с доменом (подумайте о загрузке), AutoMapper будет создавать прокси-серверы IDomainType вместо того, чтобы преобразовывать их в домен.
Я подозреваю, что проблема связана с моими профилями картирования, но у меня закончились таланты. Спасибо заранее за ваш вклад.
1 ответ
Я потратил немного времени на реорганизацию репо. Я зашел так далеко, что имитировал основной проект и два плагина. Это позволило мне не получить ложноположительного результата, когда тесты наконец-то начали проходить.
Я обнаружил, что решение состоит из двух частей.
1) Я злоупотреблял методом конфигурации AutoMapper.ReverseMap(). Я предполагал, что он будет выполнять взаимную обратную связь с любым пользовательским отображением, которое я делал. Не так! Это делает только простые развороты. Справедливо. Некоторые SO вопросы / ответы по этому поводу: 1, 2
2) Я не полностью определил наследование отображения должным образом. Я сломаю это.
2.1) Мои DomainProfiles следовали этому шаблону:
public class DomainProfile1 : Profile
{
public DomainProfile1()
{
this.CreateMap<DomainType1, IDto>().ConstructUsing(f => new Dto1()).As<Dto1>();
this.CreateMap<DomainType1, Dto1>().ForMember(m => m.P1, a => a.MapFrom(x => x.Prop1))
.IncludeBase<IDomainType, IDto>().ReverseMap();
this.CreateMap<Dto1, IDomainType>().ConstructUsing(dto => new DomainType1()).As<DomainType1>();
}
}
Так что теперь, зная, что.ReverseMap() здесь не подходит, становится очевидным, что карта между Dto1 и DomainType1 была плохо определена. Кроме того, отображение между DomainType1 и IDto не связывалось обратно с базовым IDomainType для сопоставления IDto. Также проблема. Конечный результат:
public class DomainProfile1 : Profile
{
public DomainProfile1()
{
this.CreateMap<DomainType1, IDto>().IncludeBase<IDomainType, IDto>().ConstructUsing(f => new Dto1()).As<Dto1>();
this.CreateMap<DomainType1, Dto1>().IncludeBase<DomainType1, IDto>().ForMember(m => m.P1, a => a.MapFrom(x => x.Prop1));
this.CreateMap<Dto1, IDomainType>().IncludeBase<IDto, IDomainType>().ConstructUsing(dto => new DomainType1()).As<DomainType1>();
this.CreateMap<Dto1, DomainType1>().IncludeBase<Dto1, IDomainType>().ForMember(m => m.Prop1, a => a.MapFrom(x => x.P1));
}
}
Теперь каждое направление отображения определено явно, и наследование уважается.
2.2) Самое основное отображение для IDomainType и IDto было внутри профиля, который также определял отображения для типов "коллекции". Это означало, что после того, как я разделил проект, имитируя архитектуру плагинов, тесты, которые проверяли только самые простые наследования, потерпели неудачу по-новому. Базовое отображение не было найдено. Все, что мне нужно было сделать, это поместить эти отображения в их собственный профиль и использовать этот профиль также в тестах. Это просто хороший SRP.
Я применю то, что я узнал, к моему реальному проекту, прежде чем я отмечу свой собственный ответ как принятый ответ. Надеюсь, я понял и надеюсь, что это будет полезно для других.
Полезные ссылки:
это было хорошее упражнение по рефакторингу. По общему признанию я использовал это как отправную точку, чтобы создать мой пример. Итак, спасибо @Olivier.
Я наткнулся на этот вопрос, когда сам изучал проблему с полиморфным отображением. Ответ хороший, но это еще один вариант, если вы хотите подойти к нему с точки зрения базового сопоставления и иметь много производных классов, вы можете попробовать следующее:
CreateMap<VehicleEntity, VehicleDto>()
.IncludeAllDerived();
CreateMap<CarEntity, CarDto>();
CreateMap<TrainEntity, TrainDto>();
CreateMap<BusEntity, BusDto>();
Дополнительную информацию см. В документации по automapper.