![](/img/trans.png)
[英]ASP.net MVC - Should I use AutoMapper from ViewModel to Entity Framework entities?
[英]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);
... 做的遠不止眼前一亮。
updatedSupplier
延遲加載其集合( Addresses
等),因為 AutoMapper (AM) 訪問它們。 您可以通過監視 SQL 語句來驗證這一點。UseDestinationValue
但UseDestinationValue
發生這種情況。 (個人覺得這個設定看不懂。)這種替換有一些意想不到的后果:
Local
集合中(如context.Addresses.Local
)但現在被剝奪了它們的父項,因為EF 已執行關系修正。 他們的狀態是Modified
。Added
狀態的上下文。 畢竟,他們對上下文不熟悉。 如果此時您希望context.Addresses.Local
1 個Address
,您會看到 2 個。但您只能在調試器中看到添加的項目。正是這些沒有父項的“修改”項導致了異常。 如果沒有,下一個驚喜就是您將新項目添加到數據庫中,而您只希望更新。
那么你如何解決這個問題?
答:我試圖盡可能地重播你的場景。 對我來說,一個可能的修復包括兩個修改:
禁用延遲加載。 我不知道你會如何用你的存儲庫安排它,但在某處應該有一條線
context.Configuration.LazyLoadingEnabled = false;
這樣做,您將只有Added
項目,而不是隱藏的Modified
項目。
將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.