Entity Framework Core: Changing Database Schema at Runtime
Entity Framework Core: Changing DB Migration Schema at Runtime

Entity Framework Core: Use TransactionScope with Caution!

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<ProductGroup> ProductGroups { get; set; }

public DemoDbContext(DbContextOptions<DemoDbContext> 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.

Comments

Feed You can follow this conversation by subscribing to the comment feed for this post.

Saeid

You don't need `ConfigureAwait(false)` in .NET Core apps.
https://blog.stephencleary.com/2017/03/aspnetcore-synchronization-context.html

Pawel

@Saeid: What we have here is just sample code. Usually the database accesses are implemented in a class library targeting .NET Standard (2.0). If that's the case and the library is being used in WPF (.NET 4.x) then the application is going to end in a deadlock.
I am putting ConfigureAwait(false) into demos as well because my Rider and ReSharper is configured that way :)

Verify your Comment

Previewing your Comment

This is only a preview. Your comment has not yet been posted.

Working...
Your comment could not be posted. Error type:
Your comment has been posted. Post another comment

The letters and numbers you entered did not match the image. Please try again.

As a final step before posting your comment, enter the letters and numbers you see in the image below. This prevents automated programs from posting comments.

Having trouble reading this image? View an alternate.

Working...

Post a comment

Your Information

(Name and email address are required. Email address will not be displayed with the comment.)