One of the new features of Entity Framework Core 2.1 is the support of TransactionScopes. The usage of a TransactionScope
is very easy, just put a new instance in a using
, write the code inside the block and when you are finished then call Complete()
to commit the transaction:
using (var scope = new TransactionScope())
{
var groups = MyDbContext.ProductGroups.ToList();
scope.Complete();
}
But, before changing your code from using BeginTransaction()
to TransactionScope
you should know some issues caused by them.
The demos are on GitHub: github.com/PawelGerr/Presentation-EntityFrameworkCore
In all examples we will select ProductGroups
from a DemoDbContext
.
public class DemoDbContext : DbContext
{
public DbSet ProductGroups { get; set; }
public DemoDbContext(DbContextOptions options)
: base(options)
{
}
}
public class ProductGroup
{
public Guid Id { get; set; }
public string Name { get; set; }
}
Async methods
EF has for (almost?) every synchronous operation an asynchronous one. So, it is nothing special (even recommended) to use async-await for I/O operations.
In the first example we are using await
inside a TransactionScope
.
using (var scope = new TransactionScope())
{
var groups = await Context.ProductGroups.ToListAsync().ConfigureAwait(false);
}
Looks harmless but it throws a System.InvalidOperationException:
A TransactionScope must be disposed on the same thread that it was created.
The reason is that the TransactionScope
doesn’t flow from one thread to another by default. To fix that we have to use TransactionScopeAsyncFlowOption.Enabled
:
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
var groups = await Context.ProductGroups.ToListAsync().ConfigureAwait(false);
}
Does it work now? It depends.
If the calls with and without TransactionScopeAsyncFlowOption
are using the same database connection and the call without the option is executed first, then we get another exception: System.InvalidOperationException:
Connection currently has transaction enlisted. Finish current transaction and retry.
In other words, the first call is the culprit but the second one breaks:
try
{
using (var scope = new TransactionScope())
{
// We know this one - System.InvalidOperationException:
// A TransactionScope must be disposed on the same thread that it was created.
var groups = await Context.ProductGroups.ToListAsync().ConfigureAwait(false);
}
}
catch (Exception e)
{
// error handling
}
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
// Implemented correctly but throws anyways
// System.InvalidOperationException:
// Connection currently has transaction enlisted. Finish current transaction and retry.
var groups = await Context.ProductGroups.ToListAsync().ConfigureAwait(false);
}
Imagine the first call is done in a 3rd party lib or a framework you are using, i.e. you don’t know the code – you will be searching for the cause forever, if you haven’t seen this error before.
BeginTransaction within TransactionScope
The transaction scopes can be nested. For example, if the outer scope is rolled back then the changes made in the inner scope are reverted as well. The following example works without problems:
using (var scope = new TransactionScope())
{
// some code
Do();
}
public void Do()
{
using (var anotherScope = new TransactionScope())
{
var groups = Context.ProductGroups.ToList();
}
}
Let’s try to change the inner scope to BeginTransaction()
.
using (var scope = new TransactionScope())
{
// some code
Do();
}
public void Do()
{
using (var tx = Context.Database.BeginTransaction())
{
var groups = Context.ProductGroups.ToList();
}
}
The shown use case is not supported, and we get a System.InvalidOperationException:
An ambient transaction has been detected.
The ambient transaction needs to be completed before beginning a transaction on this connection.
Yet again, if Do()
is part of a 3rd party lib or a framework then this method has be moved out of outer TransactionScope
.
Multiple instances of DbContext (or rather DB connections)
Depending on the project we could end up having multiple instances of DbContext
. The instances could be of the same or different type and it may be that the other context doesn’t even belong to your application but is being used by a framework you are using.
The use case is the following, we are having a TransactionScope
with 2 database accesses using different database connections.
using (var scope = new TransactionScope())
{
var groups = Context.ProductGroups.ToList();
var others = AnotherCtx.SomeEntities.ToList();
}
This use case is not supported as well because a distributed transaction coordinator is required and there is none besides on Windows, so EF team has dropped the support altogether. The exception we get on Windows and Linux is System.PlatformNotSupportedException:
This platform does not support distributed transactions.
Conclusion
The issues mentioned in this blog post are neither new nor specific to Entity Framework Core. I recommend putting some research into this matter before deciding to use or not to use transaction scopes.