How to add / update child entities when updating a parent in EF

These two objects are one-to-many relationships (created using the first api code).

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 my WebApi controller, I have actions to create a parent object (which works fine) and update the parent object (which has some problems). The update action looks like this:

 public void Update(UpdateParentModel model) { //what should be done here? } 

I currently have two ideas:

  • Get the monitored parent named existing on model.Id and assign the values ​​to the model one after another entity. That sounds stupid. And in model.Children I don’t know which child is new, which child is modified (or even deleted).

  • Create a new parent through model and bind it to the DbContext and save it. But how does DbContext know the state of children (new addition / deletion / change)?

What is the correct way to implement this function?

+115
c # asp.net-mvc asp.net-web-api entity-framework
Nov 27 '14 at 17:17
source share
7 answers

Since the model that is sent to the WebApi controller is separated from any entity-structure (EF) context, the only option is to load the object graph (the parent, including its children) from the database, and compare which children were added, deleted, or updated . (If you do not track changes using your own tracking mechanism during a detached state (in the browser or elsewhere), which, in my opinion, is more complicated than the following.) It may look like this:

 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 can accept any objects and display property values ​​for the attached object based on the name of the property. If the property names in your model are different from the names in essence, you cannot use this method and must assign values ​​one by one.

+163
Nov 27 '14 at 19:27
source share

I was messing with something like this ...

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

which you can call with:

 UpdateChildCollection(dbCopy, detached, p => p.MyCollectionProp, collectionItem => collectionItem.Id) 

Unfortunately, this view drops if there are collection properties for the child type that also need to be updated. Given an attempt to solve this problem, passing an IRepository (with basic CRUD methods), which will be responsible for calling UpdateChildCollection on its own. Repo call instead of direct calls in DbContext.Entry.

I don’t know how all this will be carried out on a scale, but I’m not sure what else to do with this problem.

+10
Nov 23 '15 at 18:54
source share

If you use EntityFrameworkCore, you can do the following in your controller action ( the Attach method recursively attaches navigation properties, including collections):

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

It is assumed that each updated object has all the properties set and provided in the post data from the client (for example, it will not work for partial updating of the object).

You must also ensure that you are using the new / highlighted entity platform database context for this operation.

+5
May 29 '18 at 11:37
source share

There are several projects that facilitate the interaction between the client and server, as it relates to saving the entire graph of objects.

Here are two that you would like to pay attention to:

Both of the above projects recognize disconnected objects when they return to the server, detect and save changes, and return data affected by the client.

+1
Nov 12 '17 at 1:12
source share

Ok guys. I got this answer once, but lost it along the way. absolute torture when you know the best way, but can't remember or find it! It is very simple. I just checked this in several ways.

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

You can replace the whole list with a new one! SQL code will remove and add objects as needed. No need to worry about it. Do not forget to include a children's collection or not cubes. Good luck

+1
Nov 14 '18 at 19:43
source share

Just a proof of concept of Controler.UpdateModel will not work correctly.

Full class here :

 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; } } 
0
Sep 12 '16 at 2:36
source share

Try using the ForEach loop supported by LINQ in this case:

 public void Update(ParentModel model, string newData) { var parent = _dbContext.Parents .Where(p => p.Id == model.Id) .Include(p => p.Children) .FirstOrDefault(); if(parent != null){ //update parent ... //update children if(parent.Children != null && parent.Children.Any()){ parent.Children.ForEach(x => x.Data = newData); } _dbContext.SaveChanges(); } 
0
Apr 18 '18 at 12:42
source share



All Articles