簡體   English   中英

使用 AutoMapper 從 MVC 中的 ViewModel 更新實體

[英]Update Entity from ViewModel in MVC using AutoMapper

我有一個Supplier.cs實體及其 ViewModel SupplierVm.cs 我正在嘗試更新現有的供應商,但我收到帶有錯誤消息的黃屏死機 (YSOD):

操作失敗:無法更改關系,因為一個或多個外鍵屬性不可為空。 當對關系進行更改時,相關的外鍵屬性將設置為空值。 如果外鍵不支持空值,則必須定義新關系,必須為外鍵屬性分配另一個非空值,或者必須刪除不相關的對象。

我知道它為什么會發生,但我不知道如何解決它 這是正在發生的事情的截屏 我認為我收到錯誤的原因是因為 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();
}

我已經完成了大量閱讀,根據這篇博客文章,我所擁有的應該可以工作! 我也讀過這樣的東西,但我想我會在放棄 AutoMapper 更新實體之前與讀者核實。

原因

線...

Mapper.Map(supplier, updatedSupplier);

... 做的遠不止眼前一亮。

  1. 在映射操作期間, updatedSupplier延遲加載其集合( Addresses等),因為 AutoMapper (AM) 訪問它們。 您可以通過監視 SQL 語句來驗證這一點。
  2. AM它從視圖模型映射的集合替換這些加載的集合。 盡管設置了UseDestinationValueUseDestinationValue發生這種情況。 (個人覺得這個設定看不懂。)

這種替換有一些意想不到的后果:

  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 的全面更新。如有疑問,可能有助於您決定使用哪個選項。

我搜索了所有 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(); }

我已經多次遇到這個問題,通常是這樣的:

父引用上的 FK Id 與該 FK 實體上的 PK 不匹配。 即如果您有一個 Order 表和一個 OrderStatus 表。 當您將兩者加載到實體中時,Order 的 OrderStatusId = 1 且 OrderStatus.Id = 1。如果您更改 OrderStatusId = 2 但不將 OrderStatus.Id 更新為 2,那么您將收到此錯誤。 要修復它,您需要加載 2 的 Id 並更新參考實體,或者在保存之前將 Order 上的 OrderStatus 參考實體設置為 null。

我不確定這是否符合您的要求,但我建議您遵循。

從您的代碼來看,您在某處映射期間肯定會失去關系。

在我看來,作為 UpdateSupplier 操作的一部分,您實際上並未更新供應商的任何子詳細信息。

如果是這種情況,我建議僅將已更改的屬性從 SupplierVm 更新到域供應商類。 您可以編寫一個單獨的方法,在該方法中您將來自 SupplierVm 的屬性值分配給 Supplier 對象(這應該僅更改非子屬性,例如 Name、Description、Website、Phone 等)。

然后執行數據庫更新。 這將使您免於被跟蹤實體可能出現的混亂。

如果您要更改供應商的子實體,我建議獨立於供應商更新它們,因為從數據庫檢索整個對象圖將需要執行大量查詢,並且更新它也會對數據庫執行不必要的更新查詢。

獨立更新實體將節省大量的數據庫操作,並會增加應用程序的性能。

如果您必須在一個屏幕中顯示有關供應商的所有詳細信息,您仍然可以使用整個對象圖的檢索。 對於更新,我不建議更新整個對象圖。

我希望這將有助於解決您的問題。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM