try-catch-FAIL

Failure is inevitable

NAVIGATION - SEARCH

Making TransactionScope Work with async/await in .NET 4.5

I ran into a frustrating little problem today.  I'm getting started with Entity Framework 6 and its async features on a new project, and my SpecsFor integration tests were bombing out unexpectedly.  In turns out the problem was caused by TransactionScope and async work together out of the box.  Or rather, how they don't work together.

Just for background: my integration tests are set up using SpecsFor, with its convention system automatically wrapping all specs in a TransactionScope that isolates the changes each spec makes.  Here's the custom behavior's registration and its definition:

[SetUpFixture]
public class SpecConfiguration : SpecsForConfiguration
{
    public SpecConfiguration()
    {
        //...snip
        WhenTesting<IDatabaseSpecs>().EnrichWith<DatabaseFactory>();
        WhenTesting<IDatabaseSpecs>().EnrichWith<TransactionScopeWrapper>();
        WhenTesting<IDatabaseSpecs>().EnrichWith<TestSeedData>();
        //...snip
    }
}

public class TransactionScopeWrapper : Behavior<IDatabaseSpecs>
{
    private TransactionScope _scope;

    public override void SpecInit(IDatabaseSpecs instance)
    {
        _scope = new TransactionScope();
    }

    public override void AfterSpec(IDatabaseSpecs instance)
    {
        _scope.Dispose();
    }
}

My tests worked fine prior to refactoring things to use async/await for calls to the Entity Framework context.  After switching to the async versions of things though, it all fell apart.  After stepping through it with the debugger, I noticed that my context had data at one "awaited" statement, then seemed to be completely empty after execution resumed:

//_context.Widgets.Count() is 50... we have data!
var widget = await _context.Widgets.SingleAsync(w => w.Id == id);
//_context.Widgets.Count() is now 0!  Where'd my data go?!?

"What the heck?!?" I thought... I even fired up SQL Management Studio, connected to my database, changed my transaction isolation level to "read uncommitted," and checked my database.  It had data in it!  But my context could no longer see it! 

After some digging around, I discovered the problem: TransactionScope does *not* work with async/await by default!  This was a major *eye roll* moment.  I mean, seriously?!?  This simple scenario not only doesn't work out of the box, but it silently fails?  Talk about frustrating!

In any case, there was a fix.  First, I had to retarget my spec project to .NET 4.5.1.  After that, I had to modify my specs' usage of TransactionScope to specify the correct flow option:

public class TransactionScopeWrapper : Behavior<IDatabaseSpecs>
{
    private TransactionScope _scope;

    public override void SpecInit(IDatabaseSpecs instance)
    {
        _scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
    }

    public override void AfterSpec(IDatabaseSpecs instance)
    {
        _scope.Dispose();
    }
}

Now my specs work as expected!

Credit where credit is due: this comment by ZunTzu on Stack Overflow helped me figure out what was going on. Thanks, ZunTzu!

About Matt Honeycutt...

Matt Honeycutt is a software architect specializing in ASP.NET web applications, particularly ASP.NET MVC. He has over a decade of experience in building (and testing!) web applications. He’s an avid practitioner of Test-Driven Development, creating both the SpecsFor and SpecsFor.Mvc frameworks.

He's also an author for Pluralsight, where he publishes courses on everything from web applications to testing!

blog comments powered by Disqus