Entity Framework Core – Use TransactionScope With Caution!

In this article:

pg
Pawel Gerr is architect consultant at Thinktecture. He focuses on backends with .NET Core and knows Entity Framework inside out.

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.

Free
Newsletter

Current articles, screencasts and interviews by our experts

Don’t miss any content on Angular, .NET Core, Blazor, Azure, and Kubernetes and sign up for our free monthly dev newsletter.

EN Newsletter Anmeldung (#7)
Related Articles
Angular
SL-rund
If you previously wanted to integrate view transitions into your Angular application, this was only possible in a very cumbersome way that needed a lot of detailed knowledge about Angular internals. Now, Angular 17 introduced a feature to integrate the View Transition API with the router. In this two-part series, we will look at how to leverage the feature for route transitions and how we could use it for single-page animations.
15.04.2024
.NET
KP-round
.NET 8 brings Native AOT to ASP.NET Core, but many frameworks and libraries rely on unbound reflection internally and thus cannot support this scenario yet. This is true for ORMs, too: EF Core and Dapper will only bring full support for Native AOT in later releases. In this post, we will implement a database access layer with Sessions using the Humble Object pattern to get a similar developer experience. We will use Npgsql as a plain ADO.NET provider targeting PostgreSQL.
15.11.2023
.NET
KP-round
Originally introduced in .NET 7, Native AOT can be used with ASP.NET Core in the upcoming .NET 8 release. In this post, we look at the benefits and drawbacks from a general perspective and perform measurements to quantify the improvements on different platforms.
02.11.2023