【问题标题】:Update Entity from ViewModel in MVC using AutoMapper使用 AutoMapper 从 MVC 中的 ViewModel 更新实体
【发布时间】:2017-01-02 17:05:26
【问题描述】:

我有一个 Supplier.cs 实体及其 ViewModel SupplierVm.cs。我正在尝试更新现有供应商,但我收到了带有错误消息的黄屏死机 (YSOD):

操作失败:无法更改关系,因为一个或多个外键属性不可为空。当对关系进行更改时,相关的外键属性将设置为空值。如果外键不支持空值,则必须定义新关系,必须为外键属性分配另一个非空值,或者必须删除不相关的对象。

认为我知道为什么会这样,但我不知道如何解决它。这是正在发生的事情的screencast。我认为我收到错误的原因是因为当 AutoMapper 执行它的事情时这种关系丢失了。

代码

以下是我认为相关的实体

public abstract class Business : IEntity
{
  public int Id { get; set; }
  public string Name { get; set; }
  public string TaxNumber { get; set; }
  public string Description { get; set; }
  public string Phone { get; set; }
  public string Website { get; set; }
  public string Email { get; set; }
  public bool IsDeleted { get; set; }
  public DateTime CreatedOn { get; set; }
  public DateTime? ModifiedOn { get; set; }
  public virtual ICollection<Address> Addresses { get; set; } = new List<Address>();
  public virtual ICollection<Contact> Contacts { get; set; } = new List<Contact>();
}

public class Supplier : Business
{
  public virtual ICollection<PurchaseOrder> PurchaseOrders { get; set; }
}

public class Address : IEntity
{
  public Address()
  {
    CreatedOn = DateTime.UtcNow;
  }

  public int Id { get; set; }
  public string AddressLine1 { get; set; }
  public string AddressLine2 { get; set; }
  public string Area { get; set; }
  public string City { get; set; }
  public string County { get; set; }
  public string PostCode { get; set; }
  public string Country { get; set; }
  public bool IsDeleted { get; set; }
  public DateTime CreatedOn { get; set; }
  public DateTime? ModifiedOn { get; set; }
  public int BusinessId { get; set; }
  public virtual Business Business { get; set; }
}

public class Contact : IEntity
{
  public Contact()
  {
    CreatedOn = DateTime.UtcNow;
  }

  public int Id { get; set; }
  public string Title { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string Phone { get; set; }
  public string Email { get; set; }
  public string Department { get; set; }
  public bool IsDeleted { get; set; }
  public DateTime CreatedOn { get; set; }
  public DateTime? ModifiedOn { get; set; }

  public int BusinessId { get; set; }
  public virtual Business Business { get; set; }
}

这是我的ViewModel

public class SupplierVm
{
  public SupplierVm()
  {
    Addresses = new List<AddressVm>();
    Contacts = new List<ContactVm>();
    PurchaseOrders = new List<PurchaseOrderVm>();
  }

  public int Id { get; set; }
  [Required]
  [Display(Name = "Company Name")]
  public string Name { get; set; }
  [Display(Name = "Tax Number")]
  public string TaxNumber { get; set; }
  public string Description { get; set; }
  public string Phone { get; set; }
  public string Website { get; set; }
  public string Email { get; set; }
  [Display(Name = "Status")]
  public bool IsDeleted { get; set; }

  public IList<AddressVm> Addresses { get; set; }
  public IList<ContactVm> Contacts { get; set; }
  public IList<PurchaseOrderVm> PurchaseOrders { get; set; }

  public string ButtonText => Id != 0 ? "Update Supplier" : "Add Supplier";
}

我的AutoMapper映射配置是这样的:

cfg.CreateMap<Supplier, SupplierVm>();
cfg.CreateMap<SupplierVm, Supplier>()
  .ForMember(d => d.Addresses, o => o.UseDestinationValue())
  .ForMember(d => d.Contacts, o => o.UseDestinationValue());
cfg.CreateMap<Contact, ContactVm>();
cfg.CreateMap<ContactVm, Contact>()
  .Ignore(c => c.Business)
  .Ignore(c => c.CreatedOn);
cfg.CreateMap<Address, AddressVm>();
cfg.CreateMap<AddressVm, Address>()
  .Ignore(a => a.Business)
  .Ignore(a => a.CreatedOn);

最后,这是我的SupplierController编辑方法:

[HttpPost]
public ActionResult Edit(SupplierVm supplier)
{
  if (!ModelState.IsValid) return View(supplier);

  _supplierService.UpdateSupplier(supplier);
  return RedirectToAction("Index");
}

这是SupplierService.cs 上的UpdateSupplier 方法:

public void UpdateSupplier(SupplierVm supplier)
{
  var updatedSupplier = _supplierRepository.Find(supplier.Id);
  Mapper.Map(supplier, updatedSupplier); // I lose navigational property here
  _supplierRepository.Update(updatedSupplier);
  _supplierRepository.Save();
}

我已经阅读了大量内容,根据this blog post,我所拥有的应该可以工作!我也读过stuff like this,但我想在放弃 AutoMapper 以更新实体之前,我会先与读者核实一下。

【问题讨论】:

  • 你应该检查 updatedSupplied 里面有什么,它有没有子属性。错误使您的子项变为空,因此外键错误。
  • 在什么阶段?你的意思是在 AutoMapper 完成它的事情之后还是在它之前?如果您的意思是在此之前,一切看起来都很好,据我所知,一切都应该如此。
  • 好的,这表明映射没有设置导航属性。本质上 .ForMember(d => d.Addresses, o => o.UseDestinationValue()) 不起作用。您可能需要显式创建映射配置。
  • 看看this post上的第二个答案。像您一样处理映射和保留嵌套类
  • 谢谢@big_water,.ForMember(dest =&gt; dest.ID, opt =&gt; { opt.UseDestinationValue(); opt.Ignore(); }); 是做什么的?这就像告诉它使用 DestinationValue 但同时忽略它,这对我来说没有意义!

标签: c# entity-framework asp.net-mvc-5 automapper


【解决方案1】:

原因

线...

Mapper.Map(supplier, updatedSupplier);

... 做的远比表面上看到的要多。

  1. 在映射操作期间,updatedSupplier 会延迟加载其集合(Addresses 等),因为 AutoMapper (AM) 会访问它们。您可以通过监控 SQL 语句来验证这一点。
  2. AM 用它从视图模型映射的集合替换这些加载的集合。尽管有UseDestinationValue 设置,但仍会发生这种情况。 (我个人认为这个设置是不可理解的。)

这种替换有一些意想不到的后果:

  1. 它将集合中的原始项目保留在附加到上下文中,但不再在您所在方法的范围内。这些项目仍在Local 集合中(如context.Addresses.Local)但现在被剥夺了它们的父级,因为 EF 已执行关系修复。他们的状态是Modified
  2. 它将视图模型中的项目以Added 状态附加到上下文。毕竟,它们是上下文的新手。如果此时您在 context.Addresses.Local 中期望 1 Address,您会看到 2。但您只能在调试器中看到添加的项目。

正是这些没有父级的“修改”项导致了异常。如果没有,下一个惊喜就是您向数据库中添加了新项目,而您只期望更新。

好的,现在怎么办?

那么你如何解决这个问题?

答。我试图尽可能地重播你的场景。对我来说,一个可能的修复包括两个修改:

  1. 禁用延迟加载。我不知道你会如何安排你的存储库,但某处应该有一行像

    context.Configuration.LazyLoadingEnabled = false;
    

    这样做,您将只有 Added 项目,而不是隐藏的 Modified 项目。

  2. Added 项目标记为Modified。再一次,“某处”,把行像

    foreach (var addr in updatedSupplier.Addresses)
    {
        context.Entry(addr).State = System.Data.Entity.EntityState.Modified;
    }
    

    ...等等。

B.另一个选择是将视图模型映射到新的实体对象...

  var updatedSupplier = Mapper.Map<Supplier>(supplier);

...并将其及其所有子代标记为Modified。不过,这在更新方面相当“昂贵”,请参阅下一点。

C. 在我看来,更好的解决方法是将 AM 完全排除在等式之外并手动绘制状态。我总是对将 AM 用于复杂的映射场景持谨慎态度。首先,因为映射本身的定义与使用它的代码相距甚远,这使得代码难以检查。但主要是因为它带来了自己的做事方式。并不总是很清楚它如何与其他精细操作(如更改跟踪)交互。

绘制状态是一个艰苦的过程。基础可以是这样的陈述......

context.Entry(updatedSupplier).CurrentValues.SetValues(supplier);

...如果它们的名称匹配,则将supplier 的标量属性复制到updatedSupplier。或者您可以使用 AM(毕竟)将单个视图模型映射到它们的实体对应物,但忽略导航属性。

选项 C 让您可以按照最初的意图对更新内容进行细粒度控制,而不是选项 B 的全面更新。如有疑问,this 可以帮助您决定使用哪个选项。

【讨论】:

  • 感谢 Gert,这是一个很好的答案!
  • 嗨@Gert,我在问题中看到,op 将 ViewModel 传递给服务层?这样的设计好吗?
  • @Willy 大多数时候,视图模型只是一个 DTO。对我来说,将 DTO 传入和传出服务层是可以的——实际上这就是它们的用途。如果视图模型真的是一个视图模型(包含 INotifyPropertyChanged 和/或指导视图的整个 shebang),不,我不会这样做,因为它可能会将 UI 库依赖项引入服务层。
【解决方案2】:

我搜索了所有 stackoverflow 答案和谷歌搜索。最后我刚刚添加了'db.Configuration.LazyLoadingEnabled = false;'线,它对我来说非常有效。

 var message = JsonConvert.DeserializeObject<UserMessage>(@"{.....}");
        using (var db = new OracleDbContex())
        {
            db.Configuration.LazyLoadingEnabled = false;
            var msguser = Mapper.Map<BAPUSER>(message);
            var dbuser = db.BAPUSER.FirstOrDefault(w => w.BAPUSERID == 1111);
            Mapper.Map(msguser, dbuser);
            //  db.Entry(userx).State = EntityState.Modified;

            db.SaveChanges();
        }

【讨论】:

    【解决方案3】:

    我已经多次遇到这个问题,通常是这样的:

    父引用上的 FK Id 与该 FK 实体上的 PK 不匹配。即,如果您有一个 Order 表和一个 OrderStatus 表。当您将两者都加载到实体中时,Order 的 OrderStatusId = 1 和 OrderStatus.Id = 1。如果您更改 OrderStatusId = 2 但不将 OrderStatus.Id 更新为 2,那么您将收到此错误。要修复它,您要么需要加载 2 的 Id 并更新引用实体,要么只需在保存之前将 Order 上的 OrderStatus 引用实体设置为 null。

    【讨论】:

      【解决方案4】:

      我不确定这是否符合您的要求,但我建议您遵循。

      从您的代码看来,您在某处映射期间肯定会失去关系。

      在我看来,作为 UpdateSupplier 操作的一部分,您实际上并没有更新供应商的任何子详细信息。

      如果是这种情况,我建议仅将更改的属性从 SupplierVm 更新到域供应商类。您可以编写一个单独的方法,将 SupplierVm 中的属性值分配给 Supplier 对象(这应该只更改非子属性,例如名称、描述、网站、电话等)。

      然后执行db Update。这将使您免受跟踪实体的可能混乱。

      如果您要更改供应商的子实体,我建议您独立于供应商更新它们,因为从数据库中检索整个对象图需要执行大量查询,并且更新它还会在数据库上执行不必要的更新查询。

      独立更新实体将节省大量数据库操作并提高应用程序的性能。

      如果您必须在一个屏幕上显示有关供应商的所有详细信息,您仍然可以使用整个对象图的检索。对于更新,我不建议更新整个对象图。

      我希望这将有助于解决您的问题。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-12-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-09-13
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多