c# net Aktualisieren Sie Entität von ViewModel in MVC mithilfe von AutoMapper




automapper entity framework lazy loading (3)

Ich habe eine Supplier.cs Entität und ihr ViewModel SupplierVm.cs . Ich versuche, einen vorhandenen Lieferanten zu aktualisieren, aber ich erhalte den Gelben Bildschirm des Todes (YSOD) mit der Fehlermeldung:

Die Operation ist fehlgeschlagen: Die Beziehung konnte nicht geändert werden, da eine oder mehrere Eigenschaften des Fremdschlüssels nicht nullfähig sind. Wenn eine Änderung an einer Beziehung vorgenommen wird, wird die zugehörige Fremdschlüsseleigenschaft auf einen Nullwert festgelegt. Wenn der Fremdschlüssel keine Nullwerte unterstützt, muss eine neue Beziehung definiert werden, der Fremdschlüsseleigenschaft muss ein anderer Wert ungleich Null zugewiesen werden, oder das nicht verwandte Objekt muss gelöscht werden.

Ich denke, ich weiß, warum es passiert, aber ich bin mir nicht sicher, wie ich es beheben soll . Hier ist ein Screencast von dem, was passiert. Ich denke, der Grund, warum ich den Fehler bekomme, ist, dass diese Beziehung verloren geht, wenn AutoMapper seine Sache macht .

CODE

Hier sind die Entitäten , die ich für relevant halte:

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

Und hier ist mein 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";
}

Meine AutoMapper-Mapping-Konfiguration sieht folgendermaßen aus :

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

Schließlich, hier ist meine SupplierController Edit-Methode:

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

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

Und hier ist die UpdateSupplier Methode auf SupplierService.cs :

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

Ich habe eine Menge gelesen und laut diesem Blog-Beitrag , was ich habe, sollte funktionieren! Ich habe auch solche Sachen gelesen, aber ich dachte, ich würde es mit den Lesern absprechen, bevor ich AutoMapper für die Aktualisierung von Entitäten ablege.


Die Ursache

Die Linie ...

Mapper.Map(supplier, updatedSupplier);

... ist viel mehr als nur das Auge.

  1. Während der Mapping-Operation lädt " updatedSupplier seine Sammlungen ( Addresses usw.) träge, da AutoMapper (AM) auf sie zugreift. Sie können dies überprüfen, indem Sie SQL-Anweisungen überwachen.
  2. AM ersetzt diese geladenen Sammlungen durch die Sammlungen, die es vom Ansichtsmodell abbildet. Dies geschieht trotz der Einstellung UseDestinationValue . (Ich persönlich finde diese Einstellung unverständlich.)

Dieser Austausch hat einige unerwartete Konsequenzen:

  1. Es belässt die ursprünglichen Elemente in den Sammlungen, die an den Kontext angehängt sind, aber nicht mehr im Bereich der Methode, in der Sie sich befinden. Die Elemente befinden sich immer noch in den Local Sammlungen (wie context.Addresses.Local ), aber jetzt ihrer Eltern beraubt EF hat die Beziehung repariert . Ihr Status ist Modified .
  2. Sie verknüpft die Elemente aus dem Ansichtsmodell mit dem Kontext in einem zusätzlichen Status. Schließlich sind sie für den Kontext neu. Wenn Sie zu diesem Zeitpunkt 1 Address in context.Addresses.Local erwarten, würden Sie 2 sehen. Aber Sie sehen nur die hinzugefügten Elemente im Debugger.

Es sind diese parent-less 'Modified' Elemente, die die Ausnahme verursachen. Und wenn nicht, wäre die nächste Überraschung, dass Sie der Datenbank neue Elemente hinzufügen, während Sie nur Updates erwarten.

OK, was nun?

Wie reparierst du das?

A. Ich habe versucht, dein Szenario so genau wie möglich zu wiederholen. Für mich bestand eine mögliche Lösung aus zwei Modifikationen:

  1. Deaktivieren Sie das verzögerte Laden. Ich weiß nicht, wie du das mit deinen Repositorien regeln würdest, aber irgendwo sollte es eine Zeile geben

    context.Configuration.LazyLoadingEnabled = false;

    Dadurch haben Sie nur die Added Elemente, nicht die ausgeblendeten Modified Elemente.

  2. Markieren Sie die Added Elemente als Modified . Wieder, "irgendwo", setzen Sie Zeilen wie

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

    ... und so weiter.

B. Eine weitere Option besteht darin, das Ansichtsmodell neuen Entitätsobjekten zuzuordnen ...

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

... und markieren Sie es und alle seine Kinder als Modified . Dies ist jedoch ziemlich "teuer" in Bezug auf Updates, siehe den nächsten Punkt.

C. Eine bessere Lösung ist meiner Meinung nach, AM komplett aus der Gleichung zu entfernen und den Zustand manuell zu zeichnen . Ich bin immer vorsichtig bei der Verwendung von AM für komplexe Mapping-Szenarien. Erstens, weil das Mapping selbst weit entfernt von dem Code definiert ist, in dem es verwendet wird, was es schwierig macht, den Code zu inspizieren. Aber vor allem, weil es eigene Wege bringt, Dinge zu tun. Es ist nicht immer klar, wie es mit anderen heiklen Operationen interagiert - wie Änderungsverfolgung.

Den Staat zu malen ist eine mühsame Prozedur. Die Grundlage könnte eine Aussage sein wie ...

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

... die die skalaren Eigenschaften des supplier an updatedSupplier wenn ihre Namen übereinstimmen. Oder Sie könnten AM (immerhin) verwenden, um einzelne Ansichtsmodelle ihren Entitätsgegenstücken zuzuordnen, aber die Navigationseigenschaften zu ignorieren.

Option C bietet Ihnen eine fein abgestufte Kontrolle darüber, was aktualisiert wird, so wie Sie es ursprünglich geplant hatten, anstelle der umfassenden Aktualisierung von Option B. Im Zweifelsfall kann dies Ihnen bei der Entscheidung helfen, welche Option Sie verwenden möchten.


Ich habe dieses Problem viele Male bekommen und ist normalerweise das:

Die FK-ID der übergeordneten Referenz stimmt nicht mit der PK dieser FK-Entität überein. dh wenn Sie eine Auftragstabelle und eine OrderStatus-Tabelle haben. Wenn Sie beide in Entitäten laden, hat Order OrderStatusId = 1 und die OrderStatus.Id = 1. Wenn Sie OrderStatusId = 2 ändern, aber OrderStatus.Id nicht auf 2 aktualisieren, erhalten Sie diesen Fehler. Um es zu beheben, müssen Sie entweder die ID 2 laden und die Referenzeinheit aktualisieren oder die OrderStatus-Referenzeinheit vor dem Speichern auf null setzen.


Ich bin mir nicht sicher, ob dies Ihrer Anforderung entspricht, aber ich würde vorschlagen, dass Sie folgen.

Von Ihrem Code aus sieht es so aus, als ob Sie beim Mappen irgendwo eine Beziehung verlieren.

Für mich sieht es so aus, dass Sie als Teil der UpdateSupplier-Operation keine Kinddaten des Lieferanten aktualisieren.

Wenn dies der Fall ist, würde ich vorschlagen, nur geänderte Eigenschaften von der SupplierVm in die Domain-Supplier-Klasse zu aktualisieren. Sie können eine separate Methode schreiben, bei der Sie dem Supplier-Objekt Eigenschaftswerte von SupplierVm zuweisen (Dies sollte nur nicht untergeordnete Eigenschaften wie Name, Beschreibung, Website, Telefon usw. ändern).

Und dann db Update durchführen. Dadurch werden Sie vor möglichen Fehlern der verfolgten Entitäten geschützt.

Wenn Sie die untergeordneten Entitäten des Lieferanten ändern, würde ich vorschlagen, sie unabhängig von den Lieferanten zu aktualisieren, da das Abrufen eines gesamten Objektgraphen aus der Datenbank viele Abfragen erfordern würde und das Aktualisieren auch unnötige Aktualisierungsabfragen in der Datenbank ausführen würde.

Ein unabhängiges Aktualisieren von Entitäten würde viele db-Vorgänge speichern und die Leistung der Anwendung erhöhen.

Sie können weiterhin den gesamten Objektgraphen aufrufen, wenn Sie alle Details zum Lieferanten auf einem Bildschirm anzeigen müssen. Für Updates würde ich die Aktualisierung des gesamten Objektdiagramms nicht empfehlen.

Ich hoffe, dies würde Ihnen bei der Lösung Ihres Problems helfen.





automapper