【问题标题】:Automapper creating new instance rather than map propertiesAutomapper 创建新实例而不是映射属性
【发布时间】:2019-11-13 00:23:31
【问题描述】:

这是一篇很长的文章。

所以,我有一个模型和一个视图模型,我正在从 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(和返回)映射配置了这样的自动映射器。

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

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

我对作为不同对象的 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 映射到 Model 而不是映射属性时,会从 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 时,在 map 语句之后有两个本地 UserPreference 对象。一个似乎是从 ViewModel 创建的新对象,另一个是现有模型的原始对象。

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

在 Mapper.Map() 之前/之后演示的屏幕截图

【问题讨论】:

  • 您可能需要覆盖UserPreferenceUserPreferenceViewModel 中的GetHashCode,以便集合可以将它们识别为相同。也许 automapper 有一些魔力,但我很怀疑
  • 是的,一定是为 CreateMap 行复制/粘贴了错误的行。我目前正在研究 GetHashCode 覆盖,如果发现任何内容,我会报告。感谢您的帮助。
  • 您需要退后一步,停止使用 AutoMapper 从 ViewModel 映射到您的实体或领域模型。 AutoMapper 不是为此而设计的,不会按预期工作。使用 AutoMapper ONLY 从实体/域模型映射到 ViewModel/DTO/BindingModel,NEVER 其他方式。 AutoMapper 作者强烈反对这样做
  • @Tseng,我在哪里可以阅读更多关于此的信息?我以前没有遇到过这种区别。
  • 特别是看看这篇帖子 uglybugger.org/software/post/… 和 AutoMapper 作者 Jimmy 的评论:“3) AutoMapper 从来没有打算将 back 映射到行为模型中。 AutoMapper 旨在构建 DTO,而不是映射回"

标签: c# mapping automapper


【解决方案1】:

据我所知,这是 AutoMapper 的一个限制。请记住,虽然该库通常用于映射到视图模型和实体,但它是一个通用库,用于将任何类映射到任何其他类,因此没有考虑到所有的怪癖类似于实体框架的 ORM。

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

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

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

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

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

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

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);
    }
}

【讨论】:

  • 你很快就会成为我最喜欢的贡献者之一 :)
  • 不要忘记同步删除的项目:user.UserPreferences.RemoveAll(x =&gt; model.UserPreferences.All(z =&gt; z.Id != x.Id));
【解决方案2】:

与此同时,针对该特定问题存在 AutoMapper 扩展:

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

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

【讨论】:

  • 完美!它很简单,就像一个魅力。这应该被接受为答案。谢谢。
【解决方案3】:

根据处理所有 ICollection(除其他外)的 AutoMapper source fileICollection Mapper

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-23
    • 1970-01-01
    • 2020-01-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多