Я пытаюсь использовать (POST/PUT) объект DTO с набором дочерних объектов из JavaScript в ASP.NET Core (Web API) с основным EF-контекстом в качестве источника данных.
Основной класс DTO - это что-то вроде этого (упрощенно, конечно):
public class CustomerDto {
public int Id { get;set }
...
public IList<PersonDto> SomePersons { get; set; }
...
}
Я действительно не знаю, как сопоставить это с классом сущности клиента таким образом, чтобы он не включал много кода, чтобы узнать, какие люди были добавлены/обновлены/удалены и т.д.
Я немного поработал с AutoMapper, но на самом деле это не похоже на игру с EF Core в этом сценарии (сложная структура объекта) и коллекции.
После поиска по некоторым советам, я не нашел хороших ресурсов вокруг того, что будет хорошим подходом. Мои вопросы в основном: должен ли я перепроектировать JS-клиент, чтобы не использовать "сложные" DTO, или это то, что "должно" обрабатываться слоем отображения между моими DTO и моделью Entity или есть ли другое хорошее решение, которое я не понимаете?
Я смог решить это как с помощью AutoMapper, так и путем сопоставления вручную между объектами, но ни одно из решений не кажется правильным и быстро становится довольно сложным с большим количеством шаблонов.
РЕДАКТИРОВАТЬ:
В следующей статье описывается, что я имею в виду в отношении AutoMapper и EF Core. Это не сложный код, но я просто хочу знать, является ли это "лучшим" способом управления этим.
(Код из статьи отредактирован, чтобы соответствовать приведенному выше примеру кода)
http://cpratt.co/using-automapper-mapping-instances/
var updatedPersons = new List<Person>();
foreach (var personDto in customerDto.SomePersons)
{
var existingPerson = customer.SomePersons.SingleOrDefault(m => m.Id == pet.Id);
// No existing person with this id, so add a new one
if (existingPerson == null)
{
updatedPersons.Add(AutoMapper.Mapper.Map<Person>(personDto));
}
// Existing person found, so map to existing instance
else
{
AutoMapper.Mapper.Map(personDto, existingPerson);
updatedPersons.Add(existingPerson);
}
}
// Set SomePersons to updated list (any removed items drop out naturally)
customer.SomePersons = updatedPersons;
Код выше, написанный как общий метод расширения.
public static void MapCollection<TSourceType, TTargetType>(this IMapper mapper, Func<ICollection<TSourceType>> getSourceCollection, Func<TSourceType, TTargetType> getFromTargetCollection, Action<List<TTargetType>> setTargetCollection)
{
var updatedTargetObjects = new List<TTargetType>();
foreach (var sourceObject in getSourceCollection())
{
TTargetType existingTargetObject = getFromTargetCollection(sourceObject);
updatedTargetObjects.Add(existingTargetObject == null
? mapper.Map<TTargetType>(sourceObject)
: mapper.Map(sourceObject, existingTargetObject));
}
setTargetCollection(updatedTargetObjects);
}
.....
_mapper.MapCollection(
() => customerDto.SomePersons,
dto => customer.SomePersons.SingleOrDefault(e => e.Id == dto.Id),
targetCollection => customer.SomePersons = targetCollection as IList<Person>);
Редактировать:
Единственное, что я действительно хочу, - это настроить конфигурацию AutoMapper в одном месте (профиль), не нужно использовать расширение MapCollection() каждый раз, когда я использую mapper (или любое другое решение, которое требует усложнения кода отображения).
Поэтому я создал метод расширения, подобный этому
public static class AutoMapperExtensions
{
public static ICollection<TTargetType> ResolveCollection<TSourceType, TTargetType>(this IMapper mapper,
ICollection<TSourceType> sourceCollection,
ICollection<TTargetType> targetCollection,
Func<ICollection<TTargetType>, TSourceType, TTargetType> getMappingTargetFromTargetCollectionOrNull)
{
var existing = targetCollection.ToList();
targetCollection.Clear();
return ResolveCollection(mapper, sourceCollection, s => getMappingTargetFromTargetCollectionOrNull(existing, s), t => t);
}
private static ICollection<TTargetType> ResolveCollection<TSourceType, TTargetType>(
IMapper mapper,
ICollection<TSourceType> sourceCollection,
Func<TSourceType, TTargetType> getMappingTargetFromTargetCollectionOrNull,
Func<IList<TTargetType>, ICollection<TTargetType>> updateTargetCollection)
{
var updatedTargetObjects = new List<TTargetType>();
foreach (var sourceObject in sourceCollection ?? Enumerable.Empty<TSourceType>())
{
TTargetType existingTargetObject = getMappingTargetFromTargetCollectionOrNull(sourceObject);
updatedTargetObjects.Add(existingTargetObject == null
? mapper.Map<TTargetType>(sourceObject)
: mapper.Map(sourceObject, existingTargetObject));
}
return updateTargetCollection(updatedTargetObjects);
}
}
Тогда, когда я создаю сопоставления, мне это нравится:
CreateMap<CustomerDto, Customer>()
.ForMember(m => m.SomePersons, o =>
{
o.ResolveUsing((source, target, member, ctx) =>
{
return ctx.Mapper.ResolveCollection(
source.SomePersons,
target.SomePersons,
(targetCollection, sourceObject) => targetCollection.SingleOrDefault(t => t.Id == sourceObject.Id));
});
});
Это позволяет мне использовать его таким образом при сопоставлении:
_mapper.Map(customerDto, customer);
И резольвер заботится о картировании.