0

Here's an interesting behavior I ran into the other day. I'm using EF Core's UseTransaction method to share a transaction with another DbContext. The problem I ran into was that the other context would keep the transaction even after the original transaction was disposed and that caused some unexpected bugs. What's more, the issue only seems to occur when using SQLite, SQL Server works fine.

Here's a shorter version of what the app is doing:

var connectionString = $"Data Source={Guid.NewGuid()};Mode=Memory;Cache=Shared";
var keepAliveConnection = new SqliteConnection(connectionString);
keepAliveConnection.Open();

var dbContext1 = new DbContext1(
    new DbContextOptionsBuilder<DbContext1>()
        .UseSqlite(connectionString)
        .Options);

var dbContext2 = new DbContext2(
    new DbContextOptionsBuilder<DbContext2>()
        .UseSqlite(dbContext1.Database.GetDbConnection())
        .Options);

using (var transaction = dbContext1.Database.BeginTransaction())
{
    dbContext2.Database.UseTransaction(transaction.GetDbTransaction());

    // ... do some work

    await transaction.CommitAsync();
}

Debug.Assert(dbContext2.Database.CurrentTransaction is null); // this fails

And here's the exception I get:

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

at Microsoft.Data.Sqlite.SqliteCommand.ExecuteReader(CommandBehavior behavior)
at Microsoft.Data.Sqlite.SqliteCommand.ExecuteDbDataReader(CommandBehavior behavior)
at System.Data.Common.DbCommand.ExecuteReader()
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.<MoveNext>b__19_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 System.Linq.Enumerable.TryGetSingle[TSource](IEnumerable`1 source, Boolean& found)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
at System.Linq.Queryable.Single[TSource](IQueryable`1 source, Expression`1 predicate)
at Company.DomainEvents.Services.GenericDomainEventService`4.UpdateEventStatus(Guid eventId, PublisherEventState status) in C:\Repos\domainevents\src\Company.DomainEvents\Services\GenericDomainEventService.cs:line 198
at Company.DomainEvents.Services.GenericDomainEventService`4.MarkEventAsInProgress(Guid eventId) in C:\Repos\domainevents\src\Company.DomainEvents\Services\GenericDomainEventService.cs:line 192
at Company.DomainEvents.Services.GenericDomainEventService`4.<PublishDomainEvent>d__8.MoveNext() in C:\Repos\domainevents\src\Company.DomainEvents\Services\GenericDomainEventService.cs:line 112

Fix / workaround

Disposing the transaction returned by dbContext2.Database.UseTransaction seems to do the trick. Though I'm not very confident it's the correct solution... To fix the problem, I only modified one line from:

dbContext2.Database.UseTransaction(transaction.GetDbTransaction());

to this:

using var _ = dbContext2.Database.UseTransaction(transaction.GetDbTransaction());

Now the other context's transaction will be disposed as well as the original one.

Michal Diviš
  • 2,008
  • 12
  • 19

0 Answers0