Entity Framework Core – Use TransactionScope With Caution!

In diesem Artikel:

pg
Pawel Gerr ist Architekt und Consultant bei Thinktecture. Er hat sich auf .NET Core Backends spezialisiert und kennt Entity Framework von vorne bis hinten.

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.

Kostenloser
Newsletter

Aktuelle Artikel, Screencasts, Webinare und Interviews unserer Experten für Sie

Verpassen Sie keine Inhalte zu Angular, .NET Core, Blazor, Azure und Kubernetes und melden Sie sich zu unserem kostenlosen monatlichen Dev-Newsletter an.

Newsletter Anmeldung
Diese Artikel könnten Sie interessieren
.NET
pg

Advanced Value Object Patterns in .NET

While basic value objects solve primitive obsession, complex domain requirements need sophisticated modeling techniques. This article explores advanced patterns using Thinktecture.Runtime.Extensions to tackle real-world scenarios: open-ended dates for employment contracts, composite file identifiers across storage systems, recurring anniversaries without year components, and geographical jurisdictions using discriminated unions.
19.10.2025
.NET
pg

Discriminated Unions in .NET: Modeling States and Variants

Domain models often involve concepts that exist in multiple distinct states or variations. Traditional approaches using enums and nullable properties can lead to invalid states and scattered logic. This article explores how discriminated unions provide a structured, type-safe way to model domain variants in .NET, aligning perfectly with Domain-Driven Design principles while enforcing invariants at the type level.
06.10.2025
.NET
pg

Smart Enums in .NET: Integration with Frameworks and Libraries

Learn how to seamlessly integrate Smart Enums with essential .NET frameworks and libraries. This article covers practical solutions for JSON serialization, ASP.NET Core model binding for both Minimal APIs and MVC controllers, and Entity Framework Core persistence using value converters. Discover how Thinktecture.Runtime.Extensions provides dedicated packages to eliminate integration friction and maintain type safety across your application stack.
21.09.2025
.NET
pg

Value Objects in .NET: Enhancing Business Semantics

Value objects are fundamental building blocks in Domain-Driven Design, serving far more than simple data wrappers. This article explores their strategic importance in bridging technical code and business concepts, enforcing domain rules, and fostering clearer communication with domain experts. Learn how to build robust aggregates, cultivate ubiquitous language, and encapsulate domain-specific behavior using Thinktecture.Runtime.Extensions in .NET applications.
16.09.2025
.NET
pg

Pattern Matching with Discriminated Unions in .NET

Traditional C# pattern matching with switch statements and if/else chains is error-prone and doesn't guarantee exhaustive handling of all cases. When you add new types or states, it's easy to miss updating conditional logic, leading to runtime bugs. The library Thinktecture.Runtime.Extensions solves this with built-in Switch and Map methods for discriminated unions that enforce compile-time exhaustiveness checking.
26.08.2025
.NET
pg

Value Objects in .NET: Integration with Frameworks and Libraries

Value Objects in .NET provide a structured way to improve consistency and maintainability in domain modeling. This article examines their integration with popular frameworks and libraries, highlighting best practices for seamless implementation. From working with Entity Framework to leveraging their advantages in ASP.NET, we explore how Value Objects can be effectively incorporated into various architectures. By understanding their role in framework integration, developers can optimize data handling and enhance code clarity without unnecessary complexity.
12.08.2025