-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Description
Bug description
In Entity Framework Core 10, create a Transaction, and, within that Transaction, add an entity to the database, which has a foreign key linking it to an existing parent entity.
Roll back the transaction. This means that the entity has not actually been added to the database. However, the ChangeTracker believes that it has been added.
The knock-on effect from this is that if you attempt to delete the parent entity (assuming the relationship is configured with On Delete Cascade), the deletion will fail, because it will attempt to delete the new entity from the database, and throw an exception because the entity does not exist in the database.
The code below reproduces the problem. To run the code:
- Add a reference to Microsoft.EntityFrameworkCore.SqlServer
- Add a reference to Microsoft.EntityFrameworkCore.Tools
- In Package Manage Console, run
add-migration Initial - Run the code in debug mode in Visual Studio
Note that I have reproduced this issue in both Sql Server and PostgreSql - therefore I believe it to be an issue with core Entity Framework, not with an individual database provider. I have also reproduced it in both Entity Framework Core 9 and 10.
Your code
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using System.Diagnostics;
var connectionString = "Data Source=localhost;Initial Catalog=rollbackdemo;Integrated Security=True;TrustServerCertificate=True";
var context = new RollbackContext(connectionString);
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();
EntityEntry? changeTrackerEntry;
// Create a Parent object
var parent = new Parent();
context.Parents.Add(parent);
await context.SaveChangesAsync();
// Create a Child object, belonging to the Parent, inside a Transaction
var transaction = await context.Database.BeginTransactionAsync();
parent.Children.Add(new Child());
changeTrackerEntry = context.ChangeTracker.Entries().Single(e => e.Entity is Child);
Debug.Assert(changeTrackerEntry.State == EntityState.Added);
await context.SaveChangesAsync();
changeTrackerEntry = context.ChangeTracker.Entries().Single(e => e.Entity is Child);
Debug.Assert(changeTrackerEntry.State == EntityState.Unchanged); // Is this correct? The entity has not actually been saved because of the transaction.
await transaction.RollbackAsync();
transaction.Dispose();
changeTrackerEntry = context.ChangeTracker.Entries().Single(e => e.Entity is Child);
Debug.Assert(changeTrackerEntry.State == EntityState.Unchanged); // This is definitely not correct - the transaction has been rolled back, the entity does not exist in the database.
context.Parents.Remove(parent);
// The following line throws an exception, because it tries to remove the Child entity, which it mistakenly believes to be in the database.
await context.SaveChangesAsync();
public class Parent
{
public Guid ParentId { get; set; }
public ICollection<Child> Children { get; set; } = new List<Child>();
}
public class Child
{
public Guid ChildId { get; set; }
public Guid ParentId { get; set; }
public Parent Parent { get; set; } = null!;
}
public class RollbackContext : DbContext
{
public DbSet<Parent> Parents { get; set; }
public RollbackContext(string connectionString)
:base(SqlServerDbContextOptionsExtensions.UseSqlServer(new DbContextOptionsBuilder(), connectionString).Options)
{
}
public RollbackContext()
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Child>().HasOne(c => c.Parent).WithMany(c => c.Children).HasForeignKey(c => c.ParentId);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer();
}
}Stack traces
Unhandled exception. Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. See https://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ThrowAggregateUpdateConcurrencyExceptionAsync(RelationalDataReader reader, Int32 commandIndex, Int32 expectedRowsAffected, Int32 rowsAffected, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeResultSetWithRowsAffectedOnlyAsync(Int32 commandIndex, RelationalDataReader reader, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeAsync(RelationalDataReader reader, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.SqlServer.Update.Internal.SqlServerModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChangesAsync(IList`1 entries, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
at Program.<Main>$(String[] args) in C:\Users\DeanDashwood\source\repos\RollbackIssue\RollbackIssue\Program.cs:line 34
at Program.<Main>(String[] args)
Verbose output
EF Core version
10.0.1
Database provider
No response
Target framework
.Net 10
Operating system
No response
IDE
Visual Studio 2026 18.1.1