自动映射器创建新实例而不是映射属性

Automapper creating new instance rather than map properties

这篇很长。

所以,我有一个模型和一个视图模型,我正在根据 AJAX 请求进行更新。 Web API 控制器接收视图模型,然后我使用 AutoMapper 更新现有模型,如下所示:

private User updateUser(UserViewModel entityVm)
{
    User existingEntity = db.Users.Find(entityVm.Id);
    db.Entry(existingEntity).Collection(x => x.UserPreferences).Load();

    Mapper.Map<UserViewModel, User>(entityVm, existingEntity);
    db.Entry(existingEntity).State = EntityState.Modified;

    try
    {
        db.SaveChanges();
    }
    catch
    { 
        throw new DbUpdateException(); 
    }

    return existingEntity;
}

我为 User -> UserViewModel(和返回)映射配置了 automapper。

Mapper.CreateMap<User, UserViewModel>().ReverseMap();

(请注意,明确设置相反的地图并省略 ReverseMap 表现出相同的行为)

我遇到 Model/ViewModel 的一个成员的问题,它是另一个对象的 ICollection:

[DataContract]
public class UserViewModel
{
    ...
    [DataMember]
    public virtual ICollection<UserPreferenceViewModel> UserPreferences { get; set; }
}

对应的模型是这样的:

public class User
{
    ...
    public virtual ICollection<UserPreference> UserPreferences { get; set; }
}

问题:

User 和 UserViewModel 类 的每个 属性 都正确映射,除了上面显示的 UserPreferences/UserPreferenceViewModels 的 ICollections。当这些集合从 ViewModel 映射到模型而不是映射属性时,会从 ViewModel 创建一个 UserPreference 对象的新实例,而不是使用 ViewModel 属性更新现有对象。

型号:

public class UserPreference
{
    [Key]
    public int Id { get; set; }

    public DateTime DateCreated { get; set; }

    [ForeignKey("CreatedBy")]
    public int? CreatedBy_Id { get; set; }

    public User CreatedBy { get; set; }

    [ForeignKey("User")]
    public int User_Id { get; set; }

    public User User { get; set; }

    [MaxLength(50)]
    public string Key { get; set; }

    public string Value { get; set; }
}

以及对应的ViewModel

public class UserPreferenceViewModel
{
    [DataMember]
    public int Id { get; set; }

    [DataMember]
    [MaxLength(50)]
    public string Key { get; set; }

    [DataMember]
    public string Value { get; set; }
}

和自动映射器配置:

Mapper.CreateMap<UserPreference, UserPreferenceViewModel>().ReverseMap();

//also tried explicitly stating map with ignore attributes like so(to no avail):

Mapper.CreateMap<UserPreferenceViewModel, UserPreference>().ForMember(dest => dest.DateCreated, opts => opts.Ignore());

将 UserViewModel 实体映射到用户时,UserPreferenceViewModels 的 ICollection 也映射到用户的 UserPreferences 的 ICollection,这是应该的。

但是,当发生这种情况时,单个 UserPreference 对象的属性(例如 "DateCreated"、"CreatedBy_Id" 和 "User_Id" 会被清空,就像创建了一个新对象而不是单个属性一样正在复制。

当映射集合中只有 1 个 UserPreference 对象的 UserViewModel 时,当检查 DbContext 时,映射语句后有两个本地 UserPreference 对象,这进一步显示为证据。一个似乎是从 ViewModel 创建的新对象,另一个是现有模型的原始对象。

如何使自动映射器更新现有模型集合的成员,而不是从 ViewModel 集合中实例化新成员?我在这里做错了什么?

截图演示 before/after Mapper.Map()

根据 AutoMapper source file that handles all ICollection (among other things) and the ICollection Mapper:

通过调用 Clear() 清除集合,然后再次添加,据我所知,这次 AutoMapper 无法自动进行映射。

我会实现一些逻辑来遍历集合,AutoMapper.Map那些相同的

据我所知,这是 AutoMapper 的一个限制。请记住,虽然该库广泛用于映射 to/from 视图模型和实体,但它是一个用于将任何 class 映射到任何其他 class 的通用库,因此,没有考虑像 Entity Framework.

这样的 ORM 的所有怪癖

所以,这是对正在发生的事情的解释。当您使用 AutoMapper 将一个集合映射到另一个集合时,您实际上是在映射 集合 ,而不是将该集合中的项的值映射到类似集合中的项。回想起来,这是有道理的,因为 AutoMapper 没有可靠和独立的方法来确定它应该如何将集合中的一个单独项目排列到另一个:通过 id?哪个 属性 是 id?也许名字应该匹配?

因此,您实体上的原始集合完全被由全新项目实例组成的全新集合所取代。在许多情况下,这不是问题,但是当您将其与 Entity Framework 中的更改跟踪结合使用时,您现在已经发出信号,应该删除整个原始集合并用一组全新的实体替换.显然,这不是你想要的。

那么,如何解决这个问题呢?好吧,不幸的是,这有点痛苦。第一步是告诉AutoMapper在映射时完全忽略集合:

Mapper.CreateMap<User, UserViewModel>();
Mapper.CreateMap<UserViewModel, User>()
    .ForMember(dest => dest.UserPreferences, opts => opts.Ignore());

请注意,我将其分为两张地图。将 映射到您的视图模型 时,您不需要忽略该集合。这不会导致任何问题,因为 EF 不会跟踪它。仅当您映射回您的实体时才重要 class.

但是,现在您根本没有映射该集合,那么如何将值返回到项目上呢?不幸的是,这是一个手动过程:

foreach (var pref in model.UserPreferences)
{
    var existingPref = user.UserPreferences.SingleOrDefault(m => m.Id == pref.Id);
    if (existingPref == null) // new item
    {
        user.UserPreferences.Add(Mapper.Map<UserPreference>(pref));
    }
    else // existing item
    {
        Mapper.Map(pref, existingPref);
    }
}

同时存在针对该特定问题的 AutoMapper 扩展:

cfg.AddCollectionMappers();
cfg.CreateMap<S, D>().EqualityComparison((s, d) => s.ID == d.ID);

使用 AutoMapper。EF6/EFCore 您还可以自动生成所有相等比较。请参阅 AutoMapper.Collection AutoMapper.EF6 or AutoMapper.Collection.EFCore