带有 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&lt;Person&gt; 映射到List&lt;PersonDto&gt;。因为他们有same name,并且已经存在从PersonPersonDto 的映射。

如果你想知道如何将它注入到 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 basedAutomapperextensions.usage是这样的:var property = input.Property.MapTo&lt;Property&gt;();。有很多变体。但都是基于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

带有标识 2 和 EntityFramework 6(Oracle)的 ASP.NET Core MVC

ASP.NET Core 使用 EF Core