How to Add/Update Child Entities When Updating a Parent Entity in Ef

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

Because the model that gets posted to the WebApi controller is detached from any entity-framework (EF) context, the only option is to load the object graph (parent including its children) from the database and compare which children have been added, deleted or updated. (Unless you would track the changes with your own tracking mechanism during the detached state (in the browser or wherever) which in my opinion is more complex than the following.) It could 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 && 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 maps property values to the attached entity based on the property name. If the property names in your model are different from the names in the entity you can't use this method and must assign the values one by one.

Add or Update child entities when updating a parent entity in Entity Framework Core

Updated the following NuGets from 2.2.6 to 3.1.0 and then everything started working normally:

Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools

Updating parent entity without changing the children

is it possible?

Yes, you are doing right.

According to your sample

dbContext.Parent.Attach(parent);
dbContext.Entry(parent).State = EntityState.Modified;
dbContext.SaveChanges();

It just effects on parent table only.

update child entities when updating a parent entity in EF

How can I prevent the CarritoId from becoming null?

Ultimately by not passing entities between controller, view, and back. It may look like you are sending an entity to the view and the entity is being sent back in the Put, but what you are sending is a serialized JSON block and casting it to look like an entity. Doing this leads to issues like this where your Model becomes a serialization of an entity graph that may have missing pieces and gets mangled when you start attaching it to a DbContext to track as an entity. This is likely the case you're seeing, the data that was sent to the view was either incomplete, or by attaching the top level you're expecting all the related entities to be attached & tracked which isn't often the case. Altering FK references also can lead to unexpected results rather than updating available navigation properties. It also makes your application vulnerable to tampering as a malicious client or browser add-in can modify the data being sent to your server to adjust values your UI doesn't even present. Modern browser debug tools make this a pretty simple task.

Ideally the controller and view should communicate with view models based on loaded entities. This can also streamline the amount of data being shuttled around to reduce the payload size.

To help mitigate this, approach it as if the method did not receive an entity back. You are loading the entity again anyways but in your case you are doing nothing with it except detaching it again to try and attach your passed in JSON object. For example this code:

var cotizacionoriginal = context.Cotizaciones.Where(x => x.CotizacionId == cotizacion.CotizacionId).FirstOrDefault();

if (cotizacionoriginal != null)
{
context.Entry(cotizacionoriginal).State = EntityState.Detached;
}

... does absolutely nothing for you. It says "Attempt to find an object with this ID in the local cache or database, and if you find it, stop tracking it."

You're effectively going to the database for no reason. This doesn't even assert that the row exists because you're using an "OrDefault" variation.

For a start, this should read more like:

var cotizacionoriginal = context.Cotizaciones
.Where(x => x.CotizacionId == cotizacion.CotizacionId)
.Single();

This says "Load the one row from the local cache or database that has this CotizacionId". This asserts that there is actually a matching row in the database. If the record passed into the Put has an invalid ID this will throw an exception. We don't want to detach it.

One further detail. Since we are going to want to manipulate child collections and references in this object, we should eager-load them:

var cotizacionoriginal = context.Cotizaciones
.Include(x => x.Carriyo)
.ThenInclude(c => c.Articulo)
.ThenInclude(a => a.Producto)
.Where(x => x.CotizacionId == cotizacion.CotizacionId).Single();

With larger object graphs this can get rather wordy as you have to drill down each chain of related entities. A better approach is rather than updating a "whole" object graph at once, break it up into smaller legal operations where one entity relationship can be dealt with at a time.

The next step would be to validate the passed in values in your Put object. Does it appear to be complete or is anything out of place? At a minimum we should check the current session user ID to verify that they have access to this loaded Cortizacion row and have permissions to edit it. If not, throw an exception. The web site's exception handling should ensure that any serious exception, like attempting to access rows that don't exist or don't have permissions to, should be logged for admin review, and the current session should be ended. Someone may be tampering with the system or you have a bug which is resulting in possible data corruption. Either way it should be detected, reported, and fixed with the current session terminated as a precaution.

The last step would be to go through the passed in object graph and alter your "original" data to match. The important thing here is again, you cannot trust/treat the passed in parameters as "entities", only deserialized data that looks like an entity. So, if the Product changed in one of the referenced items, we will fetch a reference and update it.

foreach (ArticuloCarrito articulo in cotizacion.Carrito.Articulos)
{
if (articulo.ArticuloId == 0)
{ // TODO: Handle adding a new article if that is supported.
}
else
{
var existingArticulo = existingCarrito.Articulos.Single(x => x.ArticuloId == articulo.ArticuloId);
if (existingArticulo.ProductoId != articulo.Producto.ProductoId)
{
var producto = context.Productos.Single(x => x.ProductoId == articulo.Producto.ProductoId);
existingArticulo.Producto = producto;
}
}
}
await context.SaveChangesAsync();

Optionally above we might check the Articulo to see if a new row has been added. (No ID yet) If we do have an ID, then we check the existing Carrito Articles for a matching item. If one is not found then this would result in an exception. Once we have one, we check if the Product ID has changed. If changed, we don't use the "Producto" passed in, as that is a deserialized JSON object, so we go to the Context to load a reference and set it on our existing row.

context.SaveChanges should only be called once per operation rather than inside the loop.

When copying across values from a detached, deserialized entity to a tracked entity, you can use:

context.Entry(existingArticulo).CurrentValues.SetValues(articulo); 

However, this should only be done if the values in the passed in object are validated. As far as I know this only updates value fields, not FKs or object references.

Hopefully this gives you some ideas on things to try to streamline the update process.

EF Core - adding/updating entity and adding/updating/removing child entities in one request

As usual, posting this question to StackOverflow helped me resolve the problem. The code originally didn't look like in the question above, but I was rather fixing the code while writing the question.

Before writing the question, I spent almost a day trying to figure out what the problem was and so I tried different things, such as recreating entity instances and attaching them manually, marking some entities as Unchanged/Modified, using AsNoTracking or even completely disabling automatic change tracking for all entities and marking all of them Added or Modified manually.

Turned out the code which caused this issue was in a private method of that child repository which I omitted as I didn't think it was relevant. It truly wouldn't be relevant though, if I haven't forgot to remove some manual change tracking code from it, which basically fiddled with EF's automatic change tracker and caused it to misbehave.

But, thanks to StackOverflow, the problem was solved. When you talk to someone about the problem, you need to re-analyze it yourself, to be able to explain all the little bits of it in order for that someone who you talk to (in this case, SO community) to understand it. While you re-analyze it, you notice all the little problem-causing bits and then it's easier to diagnose the issue.

So anyway, if someone gets attracted to this question because of the title, via google search or w/e, here are some key points:

  • If you are updating entities on multiple levels, always call .Include to include all related navigation properties when getting the existing entity. This will make all of them loaded into the change tracker and you won't need to attach/mark manually. After you're done updating, a call to SaveChanges will save all your changes properly.

  • Don't use AutoMapper for the top-level entity when you need to update child entities, especially if you have to implement some additional logic when updating children.

  • Never ever update primary keys like I tried to when setting the Id to -1, or like I tried to on this line right here in the controller Update method:

    // The mapper will also update child.RelatedEntity.Id
    Mapper.Map(child, oldChild);

  • If you need to handle deleted items, better detect them and store in a separate list, and then manually call the repository delete method for each of them, where the repository delete method would contain some eventual additional logic regarding the related entities.

  • If you need to change the primary key of a related entity, you need to first remove that related entity from the relation and just add a new one with an updated key.

So here is the updated controller action with null and safety checks omitted:

public async Task<OutputDto> Update(InputDto input)
{
// First get a real entity by Id from the repository
// This repository method returns:
// Context.Masters
// .Include(x => x.SuperMaster)
// .Include(x => x.Children)
// .ThenInclude(x => x.RelatedEntity)
// .FirstOrDefault(x => x.Id == id)
Master entity = await _masterRepository.Get(input.Id);

// Update the master entity properties manually
entity.SomeProperty = "Updated value";

// Prepare a list for any children with modified RelatedEntity
var changedChildren = new List<Child>();

foreach (var child in input.Children)
{
// Check to see if this is a new child item
if (entity.Children.All(x => x.Id != child.Id))
{
// Map the DTO to child entity and add it to the collection
entity.Children.Add(Mapper.Map<Child>(child));
continue;
}

// Check to see if this is an existing child item
var existingChild = entity.Children.FirstOrDefault(x => x.Id == child.Id);
if (existingChild == null)
{
continue;
}

// Check to see if the related entity was changed
if (existingChild.RelatedEntity.Id != child.RelatedEntity.Id)
{
// It was changed, add it to changedChildren list
changedChildren.Add(existingChild);
continue;
}

// It's safe to use AutoMapper to map the child entity and avoid updating properties manually,
// provided that it doesn't have child-items of their own
Mapper.Map(child, existingChild);
}

// Find which of the child entities should be deleted
// entity.IsTransient() is an extension method which returns true if the entity has just been added
foreach (var child in entity.Children.Where(x => !x.IsTransient()).ToList())
{
if (input.Children.Any(x => x.Id == child.Id))
{
continue;
}

// We don't have this entity in the list sent by the client.
// That means we should delete it
await _childRepository.DeleteAsync(child);
entity.Children.Remove(child);
}

// Parse children entities with modified related entities
foreach (var child in changedChildren)
{
var newChild = input.Children.FirstOrDefault(x => x.Id == child.Id);

// Delete the existing one
await _childRepository.DeleteAsync(child);
entity.Children.Remove(child);

// Add the new one
// It's OK to change the primary key here, as this one is a DTO, not a tracked entity,
// and besides, if the keys are autogenerated by the database, we can't have anything but 0 for a new entity
newChild.Id = 0;
entity.Djelovi.Add(Mapper.Map<Child>(newChild));
}

// And finally, call the repository update and return the result mapped to DTO
entity = await _repository.UpdateAsync(entity);
return MapToEntityDto(entity);
}

Updating child objects in Entity Framework 6

You need to add Cities to that particular Country object which is being updated.

public Country Update(Country country)
{
using (var dbContext =new DbContext())
{
var countryToUpdate = dbContext.Countries.SingleOrDefault(c => c.Id == country.Id);
countryToUpdate.Cities.Clear();
foreach (var city in country.Cities)
{
var existingCity =
dbContext.Cities.SingleOrDefault(
t => t.Id.Equals(city.cityId)) ??
dbContext.Cities.Add(new City
{
Id = city.Id,
Name=city.Name
});

countryToUpdate.Cities.Add(existingCity);
}
dbContext.SaveChanges();
return countryToUpdate;
}
}

Update :

  public class City
{
public int CityId { get; set; }
public string Name { get; set; }

[ForeignKey("Country")]
public int CountryId {get;set;}
public virtual Country Country {get; set;}
}

Hope it helps.

Update child entities from parent entity

I think you're looking for the following:

    public int SaveBasket(Addition addition)
{
var entity = ApplicationDbContext.Additions.Find(addition.AdditionId); // Find by primary key

//Remove Basket
if (entity.Basket.Count > 0)
{
entity.Basket.Clear(); // Empty out the basket

ApplicationDbContext.SaveChanges();
}

//Add new basket entities from posting json data
addition.Basket.ForEach(b => entity.Basket.Add(b)); // Add the items

return ApplicationDbContext.SaveChanges();
}

Hard to tell what your data structure is, but this should help.

Update

From your comment, it sounds like you want to remove the entire basket record.

    public int SaveBasket(Addition addition)
{
var baskets = ApplicationDbContext.Basket.Where(b => b.AdditionId == addition.AdditionId);

//Remove Basket
ApplicationDbContext.Basket.RemoveRange(baskets);
ApplicationDbContext.SaveChanges();
ApplicationDbContext.Basket.AddRange(addition.Basket);
return ApplicationDbContext.SaveChanges();
}


Related Topics



Leave a reply



Submit