We've found a strange behavior while unit testing some code which saves related data as shown here: Saving Related Data. The principal entity holds a list of dependents, just like in the docs example.
``` C#
// Models
public class WorkItem
{
public Guid Id { get; set; }
public string Title { get; set; }
public List
}
public class WorkItemComment
{
public Guid Id { get; set; }
public string Comment { get; set; }
public WorkItem Parent { get; set; }
public Guid ParentId { get; set; }
}
I have this sample code for inserting one work item along with one comment:
```C#
var workItem = new WorkItem
{
Id = Guid.NewGuid(),
Title = "Some work item"
};
// Save the workItem first
wiContext.WorkItems.Add(workItem);
await wiContext.SaveChangesAsync();
var comment = new WorkItemComment
{
Id = Guid.NewGuid(),
Comment = "Some comment"
};
// Add the comment to the saved comment instance
workItem.Comments.Add(comment);
// State is unchanged
var workItemState = wiContext.Entry(workItem).State;
// PROBLEM: The comment has a state of Modified.
var state2 = wiContext.Entry(comment).State;
// Call fails with UPDATE comment instead of insert
await wiContext.SaveChangesAsync();
Odd, but okay. I then tried doing this: (simulated disconnected scenario)
```C#
var workItem = new WorkItem
{
Id = Guid.NewGuid(),
Title = "Some work item"
};
// Save the workItem first with the passed in DbContext
// Like a request to an endpoint "CreateWorkItem"
wiContext.WorkItems.Add(workItem);
await wiContext.SaveChangesAsync();
// simulates a "disconnected scenario" where I only want to add a comment
// Like a request to an endpoint "AddComment"
using (var newContext = new WorkItemDbContext())
{
var existingWorkItem = await newContext.WorkItems.Include(wi => wi.Comments)
.FirstAsync(wi => wi.Id == workItem.Id);
var comment = new WorkItemComment
{
Id = Guid.NewGuid(),
Comment = "SOme comment",
};
existingWorkItem.Comments.Add(comment);
// State is "Unchanged"
var workItemState = newContext.Entry(existingWorkItem).State;
// State is "Detached"
var state2 = newContext.Entry(comment).State;
// Fails as well
await newContext.SaveChangesAsync();
}
In both cases I get:
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. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.'
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ThrowAggregateUpdateConcurrencyException(Int32 commandIndex, Int32 expectedRowsAffected, Int32 rowsAffected)
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter1.GetResult()
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<SaveChangesAsync>d__97.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<SaveChangesAsync>d__101.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter1.GetResult()
at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.2.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter1.GetResult()
at Microsoft.EntityFrameworkCore.DbContext.
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at EFCore31.Program.
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at EFCore31.Program.
The only way it works (generating the new insert) is if I do this:
```C#
var workItem = new WorkItem
{
Id = Guid.NewGuid(),
Title = "Some work item"
};
var comment = new WorkItemComment
{
Id = Guid.NewGuid(),
Comment = "Some comment",
};
workItem.Comments.Add(comment);
// Add workitem to DbSet with comments
wiContext.WorkItems.Add(workItem);
// WORKS: Generates two inserts
await wiContext.SaveChangesAsync();
But of course, this is not great. The real scenario is more like having case1 or case2. Case 2 is even more real because it's the same as if I had two actions in a controller which handle both cases separately. Same as explained in the docs: Adding a related entity
The same code works on EF Core 2.2.6.
I have created a repro here: https://github.com/joaopgrassi/efcore-childupdate-bugrepro
It contains two console apps one using EF Core 2.2.6 and the other with EF Core 3.1.1. The code is the same. The 2.2.6 version works with 3 approaches above. The 3.1.1 just works with approach case 2
EF Core version: 3.1.1
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: NET Core 3.1
Operating system: Windows 10 1903
IDE: Visual Studio 2019 16.4.2
I think it has to do with the type of the Id property. For instance, I just tried this sample from the docs (Blog/Post example) https://github.com/aspnet/EntityFramework.Docs/tree/master/samples/core/GetStarted
and out of the box the code works.
But then I changed the PostId to be of type Guid and when I do:
```C#
var post = new Post
{
PostId = Guid.NewGuid(),
Title = "Hello World",
Content = "I wrote an app using EF Core!"
};
blog.Posts.Add(post);
// * State here is Modified instead of Added
var postState = db.Entry(post);
```
Causing the same error again.
@joaopgrassi That's because you are assigning a value to a store-generated key, see https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes#dc
Oh I overlooked that. Guess it makes sense. Thanks @AndriySvyryd for pointing it out.
Most helpful comment
@joaopgrassi That's because you are assigning a value to a store-generated key, see https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes#dc