Многие знают про отличную библиотеку Automapper. С преобразованием Entity -> Dto проблем обычно не возникает. Но как обрабатывать обратный маппинг в случае, когда в API приходит корень агрегации? Хорошо, если read и write — контексты разделены и писать можно из Dto. Чаще, однако, нужно выбрать соответствующие сущности из ORM по Id и сохранить агрегат целиком. Занятие это муторное, однако зачастую поддающееся автоматизации.
Объявляем такой TypeConverter:
И создаем маппинг:
Объявляем такой TypeConverter:
public class EntityTypeConverter<TDto, TEntity> : ITypeConverter<TDto, TEntity>
where TEntity: PersistentObject, new()
{
public TEntity Convert(ResolutionContext context)
{
// Забираем из контейнера DbContext
// Да, ServiceLocator это плохо, но о том как прикрутить IOC вы сможете и сами прочесть по ссылке
// http://stackoverflow.com/questions/4204664/automapper-together-with-dependency-injection
var dc = ApplicationContext.Current.Container.Resolve<IDbContext>();
var sourceId = (context.SourceValue as IEntity)?.Id;
var dest = context.DestinationValue as TEntity
?? (sourceId.HasValue && sourceId.Value != 0 ? dc.Get<TEntity>(sourceId.Value) : new TEntity());
// Да, reflection, да медленно и может привести к ошибкам в рантайме.
// Можете написать Expression Trees, скомпилировать и закешировать для производительности
// И анализатор для проверки корректности Dto на этапе компиляции
var sp = typeof(TDto)
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.CanRead && x.CanWrite)
.ToDictionary(x => x.Name.ToUpper(), x => x);
var dp = typeof(TEntity)
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.CanRead && x.CanWrite)
.ToArray();
// проходимся по всем свойствам целевого объекта
foreach (var propertyInfo in dp)
{
var key = propertyInfo.PropertyType.InheritsOrImplements(typeof(PersistentObject))
? propertyInfo.Name.ToUpper() + "ID"
: propertyInfo.Name.ToUpper();
if (sp.ContainsKey(key))
{
// маппим один к одному примитивы, связанные сущности тащим из контекста
propertyInfo.SetValue(dest, key.EndsWith("ID")
&& propertyInfo.PropertyType.InheritsOrImplements(typeof(PersistentObject))
? dc.Get(propertyInfo.PropertyType, sp[key].GetValue(context.SourceValue))
: sp[key].GetValue(context.SourceValue));
}
}
return dest;
}
}
И создаем маппинг:
AutoMapper.Mapper
.CreateMap<TDto, TEntity>()
.ConvertUsing<EntityTypeConverter<TDto, TEntity>>();
Поделиться с друзьями
Комментарии (9)
js605451
09.08.2016 06:29Это никуда не годится! Давайте пойдём дальше и вынесем это поведение в (де)сериалайзер, чтобы вместо бестолковых DTO к нам в action methods сразу приходили готовые сущности (сэкономили кучу времени — DTO писать не нужно). А чтобы совсем симметрично было, давайте сделаем кастомный parameter binding: зачем читать "/{id}" как int/long/string, если в итоге нужна сущность? Давайте сразу в готовые сущность такие параметры резолвить! :-)
marshinov
09.08.2016 09:04Покажите ваш вариант, который «годится» и мы сможем обсудить все недостатки данной реализации. Пока в вашем комментарии много критики и не единого указания на конкретные проблемы.
kemsky
Уж очень коротко написано. С таким подходом автоматически лишаемся валидации маппинга, снижаются возможности кастомизации, не совсем понятна ситуация с обработкой ошибок и теперь код который должен маппить заодно лезет в базу и тащит связанные сущности по одной.
marshinov
TypeConverter by design лишает вас валидации маппинга, хотя можно написать вручную. В данном случае я предполагаю, что Dto может использоваться и для частичного update существующей сущности, соответственно необходимо обновить только часть полей. Ситуация с обработкой ошибок не совсем понятна, потому что зависит от того как вы обрабатываете ошибки в своем приложении: это не тема данного материала. По поводу «лезет в базу и вытаскивает по одной»: вы все-равно будете лезть в базу и вытаскивать по одной для корней агрегации (на то они и корни). Исключением может быть наличие нескольких связей с другой сущностью, но это очень редкий случай.
areht
> вы все-равно будете лезть в базу и вытаскивать по одной для корней агрегации
с чего бы?
marshinov
Покажите пожалуйста, как вы для сохранения Ar в БД получите инстансы Foo и Bar одним запросом? В первом абзаце я явно указал, что CQRS не используется, писать из Dto сразу в БД нельзя, нужно создавать «правильную» entity и сохранять ее с помощью ORM. У Ar нет FooId и BarId: принят такой стандарт.
areht
Мне показалось речь о том, что при маппинге массива из n «корней агрегации»(мн.ч.) мы получим n*m запросов для одноуровневого агрегата.
marshinov
Это не так.