Skip to content

When a transaction is rolled back, the ChangeTracker does not accurately reflect the state of the entities it is tracking #37408

@ddashwood

Description

@ddashwood

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions