Efcore: Saving related entities does not work

Created on 31 Jan 2020  路  3Comments  路  Source: dotnet/efcore

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.

Steps to reproduce

``` C#
// Models
public class WorkItem
{
public Guid Id { get; set; }
public string Title { get; set; }
public List Comments { get; set; } = new 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.d__6.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.d__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.TaskAwaiter.GetResult()
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.d__29.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.d__29.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.d__8.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.d__8.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.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.d__72.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.d__54.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.TaskAwaiter`1.GetResult()
at EFCore31.Program.d__3.MoveNext() in C:\github\efcore-childupdate-bugrepro\src\EFCore31\Program.cs:line 137
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.

d__0.MoveNext() in C:\github\efcore-childupdate-bugrepro\src\EFCore31\Program.cs:line 23


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

Further technical details

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

closed-question customer-reported

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

All 3 comments

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.

Was this page helpful?
0 / 5 - 0 ratings