efcore: Exception when inserting multiple rows into the same table on SQL Server

Include your code

Beginning with EF Core 7.0 rc2 when a database entity is generated through a projection and then saved as a new entity, EF Core will throw exception due to “temporary value” of Id property.

Workaround: set Id to default before saving.

Pre-7.0 this was not necessary. If this is expected, it would be nice to add to https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/breaking-changes

var digests = await _context.Users
    .Select(u => new DailyDigest
    {
        User = u,
        TimeCreatedUtc = utcNow,
    })
    .ToListAsync();

foreach (var digest in digests)
{
    // next line is necessary in 7.0 rc2, was not necessary in 6.0 and below (down to 1.0 ?)
    // digest.Id = default;

    _context.DailyDigests.Add(digest);
}

await _context.SaveChangesAsync();

Include stack traces

System.InvalidOperationException: The property 'DailyDigest.Id' has a temporary value while attempting to change the entity's state to 'Unchanged'. Either set a permanent value explicitly, or ensure that the database is configured to generate values for this property.
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges, Boolean modifyProperties)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState entityState, Boolean acceptChanges, Boolean modifyProperties, Nullable`1 forceStateWhenUnknownKey, Nullable`1 fallbackState)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.AcceptChanges()
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.AcceptAllChanges(IReadOnlyList`1 changedEntries)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.<>c__DisplayClass30_0`2.<<ExecuteAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
   at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync[TState,TResult](Func`4 operation, Func`4 verifySucceeded, TState state, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)

Include provider and version information

EF Core version: 7.0 rc2 Database provider: Microsoft.EntityFrameworkCore.SqlServer Target framework: 7.0 rc2 Operating system: Win11 IDE: Visual Studio 2022 17.4

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 2
  • Comments: 34 (29 by maintainers)

Commits related to this issue

Most upvoted comments

Setting MaxBatchSize to 1 or use HasTriggers should both work I think.

@roji The User object above is quite big (28 columns) so my plan right now is to try bringing the data to a minimal db that I can provide for you (fingers crossed it still reproduces…) I’ll keep you posted.

@stevendarby this problem may occur when inserting multiple rows into the same table within a single SaveChanges, with 7.0. Doing so doesn’t guarantee this bug occurs (which is why it was difficult to repro).

However, using HasTrigger() on the target table guarantees that this bug does not occur; so rather than not upgrading, you can simply do that.

Yeah, @ErikEJ is right. I’d advise configuring the table with HasTriggers, which would revert to the slower legacy SQL, but would still batch (and therefore be more efficient than setting MaxBatchSize=1). See the docs for more info.

@joakimriedel thanks again - I’ve pinpointed the bug with the help of your repro and submitted a fix; we’ll very likely patch 7.0 for this.

Thanks for the dedicated work on this and for the repro - much appreciated. I’ll dive into this in the coming days to get to the root cause, will post back here when I figure out what’s going on.

@joakimriedel we’re going to need the data here as well, as well as preferably a fully runnable console program (see @ajcvickers’s repro above), in order to see the error happening on our side; once that’s done, we can investigate and get to the root cause.

If the data is private, you can also send the stuff to my email (on my github profile), or invite me to a private github repo with the code and data.

@ajcvickers as you see earlier in the log, 82485 has already been set from temporary to database value.

Microsoft.EntityFrameworkCore.ChangeTracking: Debug: The foreign key property 'Notification.Id' was detected as changed from '-2147482400' to '82485' for entity with key '{Id: 82485}'.

I’m currently working on minimizing the real project down until it doesn’t reproduce anymore as suggested by @roji will keep you posted soon. Notification is the abstract base class for DailyDigest.

@ajcvickers while on the topic of logs, I noted that the following log message seems to have reversed property and class name in output string?

Microsoft.EntityFrameworkCore.ChangeTracking: Debug: Context 'ApplicationDbContext' started tracking 'DailyDigest' entity with key '{Id: -2147482447}'.
Microsoft.EntityFrameworkCore.ChangeTracking: Debug: 'ApplicationDbContext' generated temporary value '-2147482446' for the property 'Id.DailyDigest'.
Microsoft.EntityFrameworkCore.ChangeTracking: Debug: 'ApplicationDbContext' generated value 'DailyDigest' for the property 'Kind.DailyDigest'.

Should be DailyDigest.Id not Id.DailyDigest right?