dotnet / dotnet/efcore

Clear transaction before TransactionCommittedAsync call

Open
#37,642 3 comments 1 reaction 1 assignee Claimed by @AndriySvyryd View on GitHub
area-interception customer-reported
Dominant language
C#
Stars
14.8k
Forks
3.4k
PR merge metrics
PR metrics pending

Description

### Bug description

Executing code which triggers a database read/write inside a DbTransactionInterceptor.TransactionCommitted(Async)
causes a InvalidOperationException with the message "The transaction object is not associated with the same connection object as this command".

This seems to be due to the fact that inside [RelationalTransaction.Commit(Async)](https://github.com/dotnet/efcore/blob/fc65c2d452ff7812bc3845a7d0facfd7062553f1/src/EFCore.Relational/Storage/RelationalTransaction.cs#L180) the transaction is only cleared after the interceptors have been called.
When executing a dbcommand within this function efcore seems to still try and use the transaction for the command which in turn fails for the database because from its view the transaction is already committed.

I can see that this is probably intended behavior, because calling this interceptor before the transaction is disposed would be beneficial for usecases in need of inspecting it.
Is there any workaround or other option to execute my logic after a transaction, or am i "missusing a feature" that
was only intended for small logentries etc

### Your code

```csharp
using System.Data.Common;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;

namespace EfPlayground;

public class Parent
{
public int Id { get; set; }

public virtual ICollection Children { get; set; } = new List();
}

public class Child
{
public int Id { get; set; }
public int ParentId { get; set; }
}

public class TestContext : DbContext
{
public DbSet Parents => Set();
public DbSet Children => Set();

public TestContext(DbContextOptions options)
: base(options)
{
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity()
.HasMany(p => p.Children)
.WithOne()
.HasForeignKey(c => c.ParentId);
}
}

public sealed class QueryOnCommitInterceptor : DbTransactionInterceptor
{
public TestContext Context { get; set; } = null!;
private Parent? _trackedParent;

public void Capture(Parent parent)
=> _trackedParent = parent;

public override void TransactionCommitted(DbTransaction transaction, TransactionEndEventData eventData)
{
// This triggers a query via lazy loading
// -> throws InvalidOperationException (SqliteCommand.ExecuteReader)
var count = _trackedParent?.Children.Count;
Console.WriteLine($"Children count: {count}");
}
}

public class Bugreport
{
public static void main()
{
var connection = new SqliteConnection("Data Source=:memory:");
connection.Open();

var queryOnCommitInterceptor = new QueryOnCommitInterceptor();
var optionsBuilder = new DbContextOptionsBuilder()
.UseLazyLoadingProxies()
.AddInterceptors(queryOnCommitInterceptor)
.UseSqlite(connection);

using var context = new TestContext(optionsBuilder.Options);
context.Database.EnsureCreated();
queryOnCommitInterceptor.Context = context;

context.Parents.Add(new Parent { Id = 1 });
context.Children.Add(new Child { Id = 1, ParentId = 1 });
context.SaveChanges();
context.ChangeTracker.Clear();

using var tx = context.Database.BeginTransaction();
var parent = context.Parents.First();

queryOnCommitInterceptor.Capture(parent);

context.SaveChanges();

// Exception is thrown during Commit()
tx.Commit();
}
}
```

### Stack traces

```text
The transaction object is not associated with the same connection object as this command.

--- EXCEPTION #1/1 [InvalidOperationException]
Message = “The transaction object is not associated with the same connection object as this command.”
ExceptionPath = Root
ClassName = System.InvalidOperationException
HResult = 80131509
Source = Microsoft.Data.Sqlite
StackTraceString = “
at Microsoft.Data.Sqlite.SqliteCommand.ExecuteReader(CommandBehavior behavior)
at Microsoft.Data.Sqlite.SqliteCommand.ExecuteDbDataReader(CommandBehavior behavior)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReader(RelationalCommandParameterObject parameterObject)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.InitializeReader(Enumerator enumerator)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.<>c.b__21_0(DbContext _, Enumerator enumerator)
at Microsoft.EntityFrameworkCore.Storage.NonRetryingExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.Load[TSource](IQueryable`1 source)
at Microsoft.EntityFrameworkCore.Internal.EntityFinder`1.Load(INavigation navigation, InternalEntityEntry entry, LoadOptions options)
at Microsoft.EntityFrameworkCore.Internal.EntityFinderCollectionLoaderAdapter.Load(InternalEntityEntry entry, LoadOptions options)
at Microsoft.EntityFrameworkCore.ChangeTracking.CollectionEntry.Load(LoadOptions options)
at Microsoft.EntityFrameworkCore.Infrastructure.Internal.LazyLoader.Load(Object entity, String navigationName)
at Microsoft.EntityFrameworkCore.Proxies.Internal.LazyLoadingInterceptor.Intercept(IInvocation invocation)
at Castle.DynamicProxy.AbstractInvocation.Proceed()
at Castle.Proxies.ParentProxy.get_Children()
at EfPlayground.QueryOnCommitInterceptor.TransactionCommitted(DbTransaction transaction, TransactionEndEventData eventData) in C:\Users\samuel.reithmeir\RiderProjects\EfPlayground\EfPlayground\Bugreport.cs:line 52
at Microsoft.EntityFrameworkCore.Diagnostics.RelationalLoggerExtensions.TransactionCommitted(IDiagnosticsLogger`1 diagnostics, IRelationalConnection connection, DbTransaction transaction, Guid transactionId, DateTimeOffset startTime, TimeSpan duration)
at Microsoft.EntityFrameworkCore.Storage.RelationalTransaction.Commit()
at EfPlayground.Bugreport.main() in C:\Users\samuel.reithmeir\RiderProjects\EfPlayground\EfPlayground\Bugreport.cs:line 87
at InvokeStub_Bugreport.main(Object, Object, IntPtr*)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

Error: JetBrains Launcher could not run. The transaction object is not associated with the same connection object as this command.
```

### Verbose output

```text

```

### EF Core version

8.0.10

### Database provider

Microsoft.EntityFrameworkCore.Sqlite

### Target framework

.NET 8.0

### Operating system

Windows 11

### IDE

Rider

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.