14

我正在尝试使用(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);

解析器负责映射。

4

3 回答 3

9

AutoMapper是最好的解决方案。

你可以像这样很容易地做到这一点:

    Mapper.CreateMap<Customer, CustomerDto>();
    Mapper.CreateMap<CustomerDto, Customer>();

    Mapper.CreateMap<Person, PersonDto>();
    Mapper.CreateMap<PersonDto, Person>();

注意:因为AutoMapper会自动映射List<Person>List<PersonDto>.since 他们有same name,而且已经有一个映射 from Personto PersonDto

如果你想知道如何将它注入到 ASP.NET Core,你必须看这篇文章:Integrating AutoMapper with ASP.NET Core DI

DTO 和实体之间的自动映射

使用属性和扩展方法进行映射

于 2016-09-12T18:09:13.133 回答
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);

这对我有用。如果这个实现比这个问题的作者在他编辑的帖子中提出的更适合你,这取决于你:)

于 2017-09-22T09:23:02.980 回答
2

首先,我建议您使用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,如果你不需要显式加载导航属性/集合。

于 2019-09-17T14:39:16.637 回答