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.