Problem
One-to-many relationship between the two entities (built by 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; }
}
I have actions in my WebApi controller to create a parent object (which works well) and update a parent entity (which also works good) (which has some problem). The following is the update action:
public void Update(UpdateParentModel model)
{
//what should be done here?
}
Currently, I’m considering two options:
What is the best method to go about implementing this feature?
Asked by Cheng Chen
Solution #1
The only alternative is to load the object graph (parent and its children) from the database and compare which children have been added, destroyed, or altered because the model that is pushed to the WebApi controller is unattached from any entity-framework (EF) context. (Unless you track the changes with your own tracking system when unattached (in the browser or elsewhere), which I believe is more complicated than the following.) This is what it might look like:
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 && c.Id != default(int))
.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 take any object and, based on the property name, map property values to the connected entity. If the names of the properties in your model differ from the names in the entity, you won’t be able to utilize this approach and will have to assign the values one by one.
Answered by Slauma
Solution #2
Guys, let’s get started. This was my answer once, but I misplaced it along the way. When you know there’s a better method but can’t remember or find it, it’s torture! It’s fairly straightforward. I just put it to the test in a variety of 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 make a new list to replace the old one! As needed, the SQL code will remove and add entities. There’s no reason to be concerned about that. If you don’t add a child collection, you won’t get any dice. Best of luck!
Answered by Charles McIntosh
Solution #3
I’ve been tinkering with a concept similar to 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 something along the lines of:
UpdateChildCollection(dbCopy, detached, p => p.MyCollectionProp, collectionItem => collectionItem.Id)
Unfortunately, this breaks down if the child type has collection characteristics that need to be updated as well. Consider supplying an IRepository (with basic CRUD functions) that would be responsible for calling UpdateChildCollection on its own to fix the problem. Instead of making direct calls to DbContext, I’d use the repo. Entry.
I’m not sure how this will scale, but I’m at a loss on what else to do with this problem.
Answered by brettman
Solution #4
If you’re using EntityFrameworkCore, you may use the Attach method to recursively attach navigation properties, including collections, in your controller post action:
_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 entity that was modified has all properties set and provided in the post data from the client (partially updating an object will not work).
For this procedure, you must additionally ensure that you are using a new/dedicated entity framework database context.
Answered by hallz
Solution #5
public async Task<IHttpActionResult> PutParent(int id, Parent parent)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != parent.Id)
{
return BadRequest();
}
db.Entry(parent).State = EntityState.Modified;
foreach (Child child in parent.Children)
{
db.Entry(child).State = child.Id == 0 ? EntityState.Added : EntityState.Modified;
}
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ParentExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return Ok(db.Parents.Find(id));
}
This is how I came up with a solution to the problem. EF will recognize which to add and which to update this manner.
Answered by Jokeur
Post is based on https://stackoverflow.com/questions/27176014/how-to-add-update-child-entities-when-updating-a-parent-entity-in-ef