Die beiden Entitäten sind eine Eins-zu-Viele-Beziehung (erstellt durch Code First Fluent API).
public class Parent
{
public Parent()
{
this.Children = new List<Child>();
}
public int Id { get; set; }
public virtual ICollection<Child> Children { get; set; }
}
public class Child
{
public int Id { get; set; }
public int ParentId { get; set; }
public string Data { get; set; }
}
In meinem WebApi-Controller stehen Aktionen zum Erstellen einer übergeordneten Entität (die ordnungsgemäß funktioniert) und zum Aktualisieren einer übergeordneten Entität (die ein Problem aufweist) zur Verfügung. Die Update-Aktion sieht folgendermaßen aus:
public void Update(UpdateParentModel model)
{
//what should be done here?
}
Derzeit habe ich zwei Ideen:
Rufen Sie eine nachverfolgte übergeordnete Entität namens existing
mit model.Id
Ab und weisen Sie der Entität nacheinander Werte in model
zu. Das klingt dumm. Und in model.Children
Weiß ich nicht, welches Kind neu ist, welches Kind geändert (oder sogar gelöscht) wird.
Erstellen Sie über model
eine neue übergeordnete Entität, fügen Sie sie dem DbContext hinzu und speichern Sie sie. Aber wie kann der DbContext den Status von Kindern erkennen (neu hinzufügen/löschen/modifizieren)?
Wie kann diese Funktion korrekt implementiert werden?
Da das Modell, das an den WebApi-Controller gesendet wird, nicht mit einem Entity-Framework-Kontext (EF) verknüpft ist, besteht die einzige Option darin, das Objektdiagramm (übergeordnetes Element einschließlich seiner untergeordneten Elemente) aus der Datenbank zu laden und zu vergleichen, welche untergeordneten Elemente hinzugefügt, gelöscht oder gelöscht wurden Aktualisiert. (Es sei denn, Sie würden die Änderungen mit Ihrem eigenen Verfolgungsmechanismus im gelösten Zustand (im Browser oder wo auch immer) verfolgen, der meiner Meinung nach komplexer ist als der folgende.) Es könnte so aussehen:
public void Update(UpdateParentModel model)
{
var existingParent = _dbContext.Parents
.Where(p => p.Id == model.Id)
.Include(p => p.Children)
.SingleOrDefault();
if (existingParent != null)
{
// Update parent
_dbContext.Entry(existingParent).CurrentValues.SetValues(model);
// Delete children
foreach (var existingChild in existingParent.Children.ToList())
{
if (!model.Children.Any(c => c.Id == existingChild.Id))
_dbContext.Children.Remove(existingChild);
}
// Update and Insert children
foreach (var childModel in model.Children)
{
var existingChild = existingParent.Children
.Where(c => c.Id == childModel.Id)
.SingleOrDefault();
if (existingChild != null)
// Update child
_dbContext.Entry(existingChild).CurrentValues.SetValues(childModel);
else
{
// Insert child
var newChild = new Child
{
Data = childModel.Data,
//...
};
existingParent.Children.Add(newChild);
}
}
_dbContext.SaveChanges();
}
}
...CurrentValues.SetValues
kann ein beliebiges Objekt übernehmen und der angehängten Entität auf der Grundlage des Eigenschaftsnamens Eigenschaftswerte zuordnen. Wenn sich die Eigenschaftsnamen in Ihrem Modell von den Namen in der Entität unterscheiden, können Sie diese Methode nicht verwenden und müssen die Werte einzeln zuweisen.
Ich habe mit so etwas rumgespielt ...
protected void UpdateChildCollection<Tparent, Tid , Tchild>(Tparent dbItem, Tparent newItem, Func<Tparent, IEnumerable<Tchild>> selector, Func<Tchild, Tid> idSelector) where Tchild : class
{
var dbItems = selector(dbItem).ToList();
var newItems = selector(newItem).ToList();
if (dbItems == null && newItems == null)
return;
var original = dbItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();
var updated = newItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();
var toRemove = original.Where(i => !updated.ContainsKey(i.Key)).ToArray();
var removed = toRemove.Select(i => DbContext.Entry(i.Value).State = EntityState.Deleted).ToArray();
var toUpdate = original.Where(i => updated.ContainsKey(i.Key)).ToList();
toUpdate.ForEach(i => DbContext.Entry(i.Value).CurrentValues.SetValues(updated[i.Key]));
var toAdd = updated.Where(i => !original.ContainsKey(i.Key)).ToList();
toAdd.ForEach(i => DbContext.Set<Tchild>().Add(i.Value));
}
was du mit so etwas wie anrufen kannst:
UpdateChildCollection(dbCopy, detached, p => p.MyCollectionProp, collectionItem => collectionItem.Id)
Leider stürzt dies ein bisschen ab, wenn es Auflistungseigenschaften für den untergeordneten Typ gibt, die ebenfalls aktualisiert werden müssen. Überlegen Sie, ob Sie dieses Problem lösen möchten, indem Sie ein IRepository (mit grundlegenden CRUD-Methoden) übergeben, das für den eigenständigen Aufruf von UpdateChildCollection verantwortlich ist. Würde das Repo anstelle von direkten Aufrufen von DbContext.Entry aufrufen.
Habe keine Ahnung, wie sich das alles in großem Maßstab auswirkt, bin mir aber nicht sicher, was ich sonst mit diesem Problem anfangen soll.
Wenn Sie EntityFrameworkCore verwenden, können Sie in Ihrer Controller-Post-Aktion Folgendes ausführen (Die Attach-Methode fügt rekursiv Navigationseigenschaften einschließlich Sammlungen hinzu):
_context.Attach(modelPostedToController);
IEnumerable<EntityEntry> unchangedEntities = _context.ChangeTracker.Entries().Where(x => x.State == EntityState.Unchanged);
foreach(EntityEntry ee in unchangedEntities){
ee.State = EntityState.Modified;
}
await _context.SaveChangesAsync();
Es wird angenommen, dass für jede Entität, die aktualisiert wurde, alle Eigenschaften festgelegt und in den Post-Daten vom Client bereitgestellt wurden (z. B. funktioniert dies nicht für eine teilweise Aktualisierung einer Entität).
Sie müssen auch sicherstellen, dass Sie für diesen Vorgang einen neuen/dedizierten Entity Framework-Datenbankkontext verwenden.
Okay Leute. Ich hatte diese Antwort einmal, habe sie aber auf dem Weg verloren. Absolute Folter, wenn Sie wissen, dass es einen besseren Weg gibt, sich aber nicht daran erinnern oder ihn finden können! Es ist sehr einfach. Ich habe es nur auf verschiedene Arten getestet.
var parent = _dbContext.Parents
.Where(p => p.Id == model.Id)
.Include(p => p.Children)
.FirstOrDefault();
parent.Children = _dbContext.Children.Where(c => <Query for New List Here>);
_dbContext.Entry(parent).State = EntityState.Modified;
_dbContext.SaveChanges();
Sie können die gesamte Liste durch eine neue ersetzen! Der SQL-Code entfernt und fügt bei Bedarf Entitäten hinzu. Sie müssen sich nicht darum kümmern. Achten Sie darauf, dass Sie eine Kindersammlung oder keine Würfel hinzufügen. Viel Glück!
Es gibt einige Projekte, die die Interaktion zwischen Client und Server erleichtern, da sie das Speichern eines gesamten Objektgraphen betreffen.
Hier sind zwei, die Sie sich ansehen möchten:
Beide oben genannten Projekte erkennen die getrennten Entitäten, wenn sie an den Server zurückgesendet werden, erkennen und speichern die Änderungen und kehren zu den vom Client betroffenen Daten zurück.
Nur Proof of Concept Controler.UpdateModel
funktioniert nicht richtig.
Volle Klasse hier :
const string PK = "Id";
protected Models.Entities con;
protected System.Data.Entity.DbSet<T> model;
private void TestUpdate(object item)
{
var props = item.GetType().GetProperties();
foreach (var prop in props)
{
object value = prop.GetValue(item);
if (prop.PropertyType.IsInterface && value != null)
{
foreach (var iItem in (System.Collections.IEnumerable)value)
{
TestUpdate(iItem);
}
}
}
int id = (int)item.GetType().GetProperty(PK).GetValue(item);
if (id == 0)
{
con.Entry(item).State = System.Data.Entity.EntityState.Added;
}
else
{
con.Entry(item).State = System.Data.Entity.EntityState.Modified;
}
}