In this Article

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.

Related Articles

entity framework core
Do Not Waste Performance by Not Using Temp Tables With Entity Framework Core
It has been a while since I released my article about the usage of temp tables in Entity Framework (v6). Meanwhile, Microsoft has released a completely rewritten version of its O/R mapper so my old approach is no longer applicable. But before we learn about a new one, let us…
Pawel Gerr
entity framework core
Better Entity Framework Core Performance by Reading Execution Plans
Both a LINQ query and an SQL statement are descriptions that state which data should be fetched, but not how.. Sure, when reading LINQ or SQL, we can make assumptions about the performance but not in every case. Some queries are either too fancy or too big to grasp, so our…
Pawel Gerr
entity framework core
Unnecessary Fuzzy Searches may hurt your Entity Framework Core Performance
After talking about performance issues like N+1 Queries and the Cartesian Explosion that made its comeback in Entity Framework Core 3, we will today look at a performance issue that is not tied to any Entity Framework version but is rather a general one. What do I mean by…
Pawel Gerr
entity framework core
The performance issue "Cartesian Explosion" made its comeback in Entity Framework Core 3
In Entity Framework Core 3.0/3.1 the SQL statement generation underwent significant changes. As we have seen in the previous post these changes removed both the implicit client-side evaluation and the N+1 Query Problem (which is good!). Unfortunately, these changes (re)introduced…
Pawel Gerr