我正在尝试使用(POST/PUT)一个DTO对象与从JavaScript到ASP的子对象集合。. NET Core (Web API)与EF Core上下文作为我的数据源。
主DTO类是这样的(当然是简化的):public class CustomerDto {
public int Id { get;set }
...
public IList<PersonDto> SomePersons { get; set; }
...
}
我真的不知道的是如何以一种不包含大量代码的方式将其映射到Customer实体类,以找出哪些person已被添加/更新/删除等。
我用AutoMapper玩了一下,但在这种情况下(复杂的对象结构)和集合中,它似乎不能很好地与EF Core一起玩。
在谷歌搜索了一些建议之后,我没有找到任何关于什么是好的方法的好资源。我的问题基本上是:我应该重新设计js客户端,不使用"复杂"的dto,或者这是"应该"由我的dto和实体模型之间的映射层处理的东西,或者有任何其他好的解决方案,我不知道?
我已经能够通过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()扩展。
所以我创建了一个像这样的扩展方法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);
解析器负责映射
AutoMapper
是最佳解决方案。
你可以很容易地这样做:
Mapper.CreateMap<Customer, CustomerDto>();
Mapper.CreateMap<CustomerDto, Customer>();
Mapper.CreateMap<Person, PersonDto>();
Mapper.CreateMap<PersonDto, Person>();
注意:因为AutoMapper
会自动将List<Person>
映射到List<PersonDto>
,因为他们有same name
,并且已经有从Person
到PersonDto
的映射。
如果您需要知道如何将它注入ASP.net核心,您必须查看这篇文章:将AutoMapper与ASP集成。. NET Core DI
dto和实体之间的自动映射
使用属性和扩展方法的映射
首先,我建议使用JsonPatchDocument进行更新:
[HttpPatch("{id}")]
public IActionResult Patch(int id, [FromBody] JsonPatchDocument<CustomerDTO> patchDocument)
{
var customer = context.EntityWithRelationships.SingleOrDefault(e => e.Id == id);
var dto = mapper.Map<CustomerDTO>(customer);
patchDocument.ApplyTo(dto);
var updated = mapper.Map(dto, customer);
context.Entry(entity).CurrentValues.SetValues(updated);
context.SaveChanges();
return NoContent();
}
其次,你应该利用AutoMapper.Collections.EFCore。这就是我在Startup.cs
中使用扩展方法配置AutoMapper的方式,这样我就可以调用services.AddAutoMapper()
而不需要整个配置代码:
public static IServiceCollection AddAutoMapper(this IServiceCollection services)
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddCollectionMappers();
cfg.UseEntityFrameworkCoreModel<MyContext>(services);
cfg.AddProfile(new YourProfile()); // <- you can do this however you like
});
IMapper mapper = config.CreateMapper();
return services.AddSingleton(mapper);
}
YourProfile
应该是这样的:
public YourProfile()
{
CreateMap<Person, PersonDTO>(MemberList.Destination)
.EqualityComparison((p, dto) => p.Id == dto.Id)
.ReverseMap();
CreateMap<Customer, CustomerDTO>(MemberList.Destination)
.ReverseMap();
}
我有一个类似的对象图,这对我来说很好。
编辑我使用LazyLoading,如果你不需要,你必须显式加载navigationProperties/Collections
我在相当长的一段时间里都在为同样的问题而挣扎。在深入研究了许多文章之后,我想出了自己的实现方法,并与您分享。
首先,我创建了一个自定义的IMemberValueResolver
。
using System;
using System.Collections.Generic;
using System.Linq;
namespace AutoMapper
{
public class CollectionValueResolver<TDto, TItemDto, TModel, TItemModel> : IMemberValueResolver<TDto, TModel, IEnumerable<TItemDto>, IEnumerable<TItemModel>>
where TDto : class
where TModel : class
{
private readonly Func<TItemDto, TItemModel, bool> _keyMatch;
private readonly Func<TItemDto, bool> _saveOnlyIf;
public CollectionValueResolver(Func<TItemDto, TItemModel, bool> keyMatch, Func<TItemDto, bool> saveOnlyIf = null)
{
_keyMatch = keyMatch;
_saveOnlyIf = saveOnlyIf;
}
public IEnumerable<TItemModel> Resolve(TDto sourceDto, TModel destinationModel, IEnumerable<TItemDto> sourceDtos, IEnumerable<TItemModel> destinationModels, ResolutionContext context)
{
var mapper = context.Mapper;
var models = new List<TItemModel>();
foreach (var dto in sourceDtos)
{
if (_saveOnlyIf == null || _saveOnlyIf(dto))
{
var existingModel = destinationModels.SingleOrDefault(model => _keyMatch(dto, model));
if (EqualityComparer<TItemModel>.Default.Equals(existingModel, default(TItemModel)))
{
models.Add(mapper.Map<TItemModel>(dto));
}
else
{
mapper.Map(dto, existingModel);
models.Add(existingModel);
}
}
}
return models;
}
}
}
然后我配置AutoMapper并添加我的特定映射:
cfg.CreateMap<TDto, TModel>()
.ForMember(dst => dst.DestinationCollection, opts =>
opts.ResolveUsing(new CollectionValueResolver<TDto, TItemDto, TModel, TItemModel>((src, dst) => src.Id == dst.SomeOtherId, src => !string.IsNullOrEmpty(src.ThisValueShouldntBeEmpty)), src => src.SourceCollection));
这个实现允许我完全自定义我的对象匹配逻辑,因为keyMatch
函数在构造函数中传递。如果你出于某种原因需要验证传递的对象是否适合映射,你也可以传递一个额外的saveOnlyIf
函数(在我的例子中,有一些对象如果没有通过额外的验证,就不应该被映射和添加到集合中)。
然后,例如在你的控制器中,如果你想更新你的断开连接的图形,你应该做以下操作:
var model = await Service.GetAsync(dto.Id); // obtain existing object from db
Mapper.Map(dto, model);
await Service.UpdateAsync(model);
这对我有用。如果这个实现比这个问题的作者在他编辑的帖子中提出的更适合你,这取决于你:)