带有 EF Core 的 ASP.NET Core - DTO 集合映射
Posted
技术标签:
【中文标题】带有 EF Core 的 ASP.NET Core - DTO 集合映射【英文标题】:ASP.NET Core with EF Core - DTO Collection mapping 【发布时间】:2017-01-19 23:51:23 【问题描述】:我正在尝试使用 (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 实体类,只是为了找出哪些 Persons 已被添加/更新/删除等。
我已经使用 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
有什么问题?
如果你有一个复杂的数据结构,将它映射到完全不同的东西通常会很复杂。但是,您应该只需要配置一次 AutoMapper 映射。
使用 AutoMapper 从实体到 DTO 的映射非常简单,但是当有集合等时,从 DTO 到 EF Core 托管实体变得非常复杂和乏味。@Sampath 这不是 AutoMapper 的直接问题,它做它应该做的很好,但我不确定在这种情况下它是否是适合这项工作的工具。这不是我需要配置 AutoMapper 的次数,而是需要配置的数量,但正如 Mike Brind 所说,这可能是一个比我“感觉”应该的更复杂的问题。无论如何,找不到任何“最佳实践”。
我们如何通过上面的简单示例查看您的复杂映射?您必须在此处添加更多代码。
@Sampath 你可能误解了我的意思,我不是在谈论特定的复杂映射,我只是指一般的复杂对象图,例如具有 OrderLines 等订单的客户。因此,它不是需要映射的超级复杂或复杂的图形,而只是 AutoMapper 是否是一个好的解决方案或是否有其他更好的选择的一些一般指导。也许 AutoMapper 是完成这项工作的最佳工具?
【参考方案1】:
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 core,你必须看到这篇文章:Integrating AutoMapper with ASP.NET Core DI
DTO 和实体之间的自动映射
Mapping using attributes and extension methods
【讨论】:
问题不在于映射代码,如上所示,它非常简单。但这就是它与 EF Core(或其他 ORM)结合的工作方式和行为方式。有关我的意思的更多详细信息,请参阅我上面的编辑。 我看不出将AutoMapper
与任何ORM
工具一起使用有任何问题。实际上,这些天这是非常标准的事情。否则您必须手动一一映射属性。这是一项非常繁琐的任务。我使用aspnetzero
作为我的应用程序开发框架。在该框架上AutoMapper
是默认映射Api。该框架还提供了许多带有AutoMapper'.They have
opensource`产品的内置扩展。如果你想看到我可以与你分享那个url。如果你需要它请告诉我。这是商业的:aspnetzero.com跨度>
所以我上面添加的代码对你来说不是必需的,还是我必须做的(或类似的)才能让它工作?
你也可以使用它。我们可以基于Automapper
编写任何扩展方法,在我的应用程序中我们使用attribute based
Automapper
extensions.usage是这样的:var property = input.Property.MapTo<Property>();
。有很多变体。但都是基于AutoMapper
。
说 AutoMapper 是最好的解决方案,就像说 是最好的黄油。使用 AutoMapper 可能是一个很好的解决方案。但你永远无法证明它是最好的。除此之外,AutoMapper 很棒,但这只是我的拙见。【参考方案2】:
我在同一个问题上苦苦挣扎了很长一段时间。在浏览了许多文章之后,我提出了自己的实现,我将与您分享。
首先我创建了一个自定义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);
这对我有用。如果这个实现比这个问题的作者在他编辑的帖子中提出的更适合你,这取决于你:)
【讨论】:
【参考方案3】:首先我建议您使用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。
【讨论】:
github.com/AutoMapper/… @LucianBargaoanu 你能告诉我如何使用方法,在你的链接中显示我的配置吗?我没有让这个与收集包一起按预期工作.. 谢谢:) 有一个AddAutoMapper
重载允许您自定义配置。不知道有没有更流畅的方法。以上是关于带有 EF Core 的 ASP.NET Core - DTO 集合映射的主要内容,如果未能解决你的问题,请参考以下文章
带有 EF Core 的 ASP.NET Core - DTO 集合映射
带有 EF Core 更新实体的 ASP.Net 核心 Web Api 如何
.NET Core 1.0ASP.NET Core 1.0和EF Core 1.0简介
如何在现有数据库 ASP.NET Core MVC 和 EF Core(DB-First)中使用 Identity