I am struggling with what seemed to be a couple of basic operations.
Let's say I've got a class named Master:
public class Master
{
public Master()
{
Children = new List<Child>();
}
public int Id { get; set; }
public string SomeProperty { get; set; }
[ForeignKey("SuperMasterId")]
public SuperMaster SuperMaster { get; set; }
public int SuperMasterId { get; set; }
public ICollection<Child> Children { get; set; }
}
public class Child
{
public int Id { get; set; }
public string SomeDescription { get; set; }
public decimal Count{ get; set; }
[ForeignKey("RelatedEntityId")]
public RelatedEntity RelatedEntity { get; set; }
public int RelatedEntityId { get; set; }
[ForeignKey("MasterId")]
public Master Master { get; set; }
public int MasterId { get; set; }
}
We have a controller action like this:
public async Task<OutputDto> Update(UpdateDto updateInput)
{
// 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 properties
entity.SomeProperty = "Updated value";
entity.SuperMaster.Id = updateInput.SuperMaster.Id;
foreach (var child in input.Children)
{
if (entity.Children.All(x => x.Id != child.Id))
{
// This input child doesn't exist in entity.Children -- add it
// Mapper.Map uses AutoMapper to map from the input DTO to entity
entity.Children.Add(Mapper.Map<Child>(child));
continue;
}
// The input child exists in entity.Children -- update it
var oldChild = entity.Children.FirstOrDefault(x => x.Id == child.Id);
if (oldChild == null)
{
continue;
}
// The mapper will also update child.RelatedEntity.Id
Mapper.Map(child, oldChild);
}
foreach (var child in entity.Children.Where(x => x.Id != 0).ToList())
{
if (input.Children.All(x => x.Id != child.Id))
{
// The child doesn't exist in input anymore, mark it for deletion
child.Id = -1;
}
}
entity = await _masterRepository.UpdateAsync(entity);
// Use AutoMapper to map from entity to DTO
return MapToEntityDto(entity);
}
Now the repository method (MasterRepository):
public async Task<Master> UpdateAsync(Master entity)
{
var superMasterId = entity.SuperMaster.Id;
// Make sure SuperMaster properties are updated in case the superMasterId is changed
entity.SuperMaster = await Context.SuperMasters
.FirstOrDefaultAsync(x => x.Id == superMasterId);
// New and updated children, skip deleted
foreach (var child in entity.Children.Where(x => x.Id != -1))
{
await _childRepo.InsertOrUpdateAsync(child);
}
// Handle deleted children
foreach (var child in entity.Children.Where(x => x.Id == -1))
{
await _childRepo.DeleteAsync(child);
entity.Children.Remove(child);
}
return entity;
}
And finally, the relevant code from ChildrenRepository:
public async Task<Child> InsertOrUpdateAsync(Child entity)
{
if (entity.Id == 0)
{
return await InsertAsync(entity, parent);
}
var relatedId = entity.RelatedEntity.Id;
entity.RelatedEntity = await Context.RelatedEntities
.FirstOrDefaultAsync(x => x.Id == relatedId);
// We have already updated child properties in the controller method
// and it's expected that changed entities are marked as changed in EF change tracker
return entity;
}
public async Task<Child> InsertAsync(Child entity)
{
var relatedId = entity.RelatedEntity.Id;
entity.RelatedEntity = await Context.RelatedEntities
.FirstOrDefaultAsync(x => x.Id == relatedId);
entity = Context.Set<Child>().Add(entity).Entity;
// We need the entity Id, hence the call to SaveChanges
await Context.SaveChangesAsync();
return entity;
}
The Context
property is actually DbContext
and the transaction is started in an action filter. If the action throws an exception, the action filter performs the rollback, and if not, it calls SaveChanges.
The input object being sent looks like this:
{
"someProperty": "Some property",
"superMaster": {
"name": "SuperMaster name",
"id": 1
},
"children": [
{
"relatedEntity": {
"name": "RelatedEntity name",
"someOtherProp": 20,
"id": 1
},
"count": 20,
"someDescription": "Something"
}],
"id": 10
}
The Masters
table currently has one record with Id 10. It has no children.
The exception being thrown is:
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.
What's going on here? I thought EF was supposed to track changes and that includes knowing that we called SaveChanges in that inner method.
EDIT Removing that call to SaveChanges changes nothing. Also, I couldn't find any INSERT or UPDATE SQL statement generated by EF when watching what happens in SQL Server Profiler.
EDIT2 The INSERT statement is there when SaveChanges is called, but still there is no UPDATE statement for Master entity.