AutoMapper 不更新集合项

AutoMapper does not update Collection items

我尝试使用 AutoMapper 将模型映射到 dtos。第一次尝试使用 EF-Core,但我能够消除 EF-Core 并在没有它的情况下重现它。

我重现了这个 DEMO 中的行为。 (使用 EF-Core 的旧 DEMO 是 here。)

TL;DR

看来这行不通:

var container = new Container("Container-Id 000", new List<Item> { new Item("Item-Id 000") { Name = "Item-Name" } });
var containerModel = mapper.Map<ContainerModel>(container);

// apply changes
container.Items[0].Name += " -- changed";

// update model
mapper.Map(container, containerModel);

// at this point the item does not contain the correct name:
container.Items[0].Name != containerModel.Items[0].Name   !!!!!

详细解释:

Dto 和模型具有以下结构:

Container
    + Id: string { get; }
    + Items: IReadOnlyList<Item> { get; }

Item
    + Id: string { get; }
    + Name: string { get; set; }

ContainerModel
    + Id: string { get; set; }
    + Items: List<ItemModel> { get; set; }

ItemModel
    + Id: string { get; set; }
    + Name: string { get; set; }

AutoMapper-Configuration 是(也许这就是我遗漏的地方):

var config = new MapperConfiguration(
    cfg =>
        {
            cfg.CreateMap<Container, ContainerModel>(MemberList.None)
                .EqualityComparison((src, dst) => src.Id == dst.Id)
                .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id))
                .ForMember(dst => dst.Items, opt => opt.MapFrom(src => src.Items));
            cfg.CreateMap<ContainerModel, Container>(MemberList.None)
                .EqualityComparison((src, dst) => src.Id == dst.Id)
                .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id))
                .ForMember(dst => dst.Items, opt => opt.MapFrom(src => src.Items));

            cfg.CreateMap<IReadOnlyList<Item>, List<ItemModel>>(MemberList.None)
                .ConstructUsing((src, ctx) => src.Select(ctx.Mapper.Map<ItemModel>).ToList());
            cfg.CreateMap<List<ItemModel>, IReadOnlyList<Item>>(MemberList.None)
                .ConstructUsing((src, ctx) => src.Select(ctx.Mapper.Map<Item>).ToList().AsReadOnly());

            cfg.CreateMap<Item, ItemModel>(MemberList.None)
                .EqualityComparison((src, dst) => src.Id == dst.Id)
                .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id))
                .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name));
            cfg.CreateMap<ItemModel, Item>(MemberList.None)
                .EqualityComparison((src, dst) => src.Id == dst.Id)
                .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id))
                .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name));
        });
var result = config.CreateMapper();
result.ConfigurationProvider.AssertConfigurationIsValid();
return result;

我创建了一个 dto 实例并将它们成功映射到模型。 (我还测试了从 model 到 dto 的返回方式,它也有效,但不需要重现问题。)

var mapper = CreateMapper();

var container = new Container("Container-Id 000", new List<Item> { new Item("Item-Id 000") { Name = "Item-Name" } });
var containerModel = mapper.Map<ContainerModel>(container);

// apply changes
container.Items[0].Name += " -- changed";

// update model
mapper.Map(container, containerModel);

Console.WriteLine($"Src.Name: {container.Items[0].Name}");
Console.WriteLine($"Dst.Name: {containerModel.Items[0].Name}");
if (container.Items[0].Name != containerModel.Items[0].Name)
{
    throw new InvalidOperationException("The names of dto and model doesn't match!");
}

抛出异常之前打印的输出显示了问题:

Src.Name: Item-Name -- changed
Dst.Name: Item-Name

指定的异常被抛出 - 但不应该(在我看来)。

我认为问题是mapper.Map(readContainer, readContainerModel);。 我指定了一个相等比较来帮助 AutoMapper 找到正确的实例,但没有运气。

我在这里错过了什么?我该怎么做才能解决这个问题?

所有持久性代码都封装在一个小框架中,对我的同事来说应该是透明的。他们所要做的就是指定 dtos、模型和映射配置文件。该框架不知道“导航”。是的,我能够创建代码来分析模型类型的所有导航,并尝试找到等效的 dto 并 foreach 所有属性并手动更新所有实例。但这接缝可以治疗很多痛苦和错误,这就是我尝试自动映射的原因。

为什么我需要mapper.Map(src, dst)

所有这些都与 EF-Core 一起工作,并为我的同事提供了一个小型持久性框架。我尝试使用 Persist()InsertOrUpdate(首选方法),但我发现 issue report for AutoMapper.CollectionInsertOrUpdate-方法被破坏了。指定的 workarround 是我在问题解决之前尝试使用的 - 但它没有解决问题。

我还发现那篇文章 WHY MAPPING DTOS TO ENTITIES USING AUTOMAPPER AND ENTITYFRAMEWORK IS HORRIBLE 包含相同的技巧。我不关心 AutoMapper 将为每个集合项生成的已创建模型实例。我也不容易将 DbContext 转发给映射函数。

我发现了问题。我添加了这个以将集合映射到 AutoMapper 配置:

cfg.CreateMap<IReadOnlyList<Item>, List<ItemModel>>(MemberList.None)
    .ConstructUsing((src, ctx) => src.Select(ctx.Mapper.Map<ItemModel>).ToList());
cfg.CreateMap<List<ItemModel>, IReadOnlyList<Item>>(MemberList.None)
    .ConstructUsing((src, ctx) => src.Select(ctx.Mapper.Map<Item>).ToList().AsReadOnly());

这似乎会阻止 AutoMapper 正常工作。

解决方案是删除两个打印行并添加以下内容:

cfg.AddCollectionMappers();

我添加了显式集合映射是因为我低估了 AddCollectionMappers 的力量,因为我使用了 IReadOnlyList<>IReadOnlyDictionary<,> 的不可变对象和接口,而我的观点是错误的AutoMapper 无法处理。我的错。

查看工作 DEMO