Overview

Isolating Static Context in Parallel Tests Using AsyncLocal in .NET

Introduction

When building applications with .NET and the ASP.NET Boilerplate, it is common to rely on static providers and ambient context. Examples include Clock.Provider. These static entry points make the framework easy to use, but they introduce problems when running automated tests in parallel.

Parallel test execution is enabled by default in modern test runners such as xUnit. While this improves performance, it also means that any shared static state can be modified by multiple tests at the same time, leading to race conditions and flaky tests.

This article explains how AsyncLocal<T> can be used to safely override static context in tests, with a concrete example of implementing a fixed clock in ASP.NET Boilerplate.


The Problem with Static State in Parallel Tests

Consider a static value used to store contextual information:

public static class TestContext
{
public static string CurrentTenant;
}

If two tests run at the same time and both modify this value, they will overwrite each other’s state. This leads to unpredictable behavior:

[Fact]
public async Task TestA()
{
TestContext.CurrentTenant = "TenantA";
await SomeService.DoWork();
}
[Fact]
public async Task TestB()
{
TestContext.CurrentTenant = "TenantB";
await SomeService.DoWork();
}

Because CurrentTenant is static, both tests share the same memory. When the test runner executes them in parallel, whichever test writes last determines the value seen by both tests.


Why Disabling Parallel Tests Is Not Ideal

A common workaround is to disable parallelization:

[assembly: CollectionBehavior(DisableTestParallelization = true)]

Although this avoids race conditions, it also makes the test suite slower and reduces the benefits of modern multi-core machines. A better solution is to keep parallel execution and isolate the contextual state instead.


What Is AsyncLocal?

AsyncLocal<T> is a .NET feature that stores data within the logical asynchronous execution context. Unlike static fields, values stored in AsyncLocal are isolated per async flow and automatically propagated across await boundaries.

This means each parallel test can maintain its own copy of a value, even though the field itself is static.


Fixed Clock Implementation in ASP.NET Boilerplate

ASP.NET Boilerplate provides a static Clock.Provider that is used whenever code calls Clock.Now. This makes it easy to access the current time, but also means that overriding time in tests affects all tests globally.

To solve this, we keep the provider static but store the overridden value in AsyncLocal.

FixedClockProvider

public class FixedClockProvider : IClockProvider
{
public static readonly AsyncLocal<DateTime?> Override = new();
public DateTime Now => Override.Value ?? DateTime.UtcNow;
public DateTimeKind Kind => DateTimeKind.Utc;
public DateTime Normalize(DateTime dateTime)
{
if (dateTime.Kind == DateTimeKind.Unspecified)
return DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
return dateTime.Kind == DateTimeKind.Local
? dateTime.ToUniversalTime()
: dateTime;
}
public bool SupportsMultipleTimezone => true;
}

In this implementation, each asynchronous execution flow can override the current time independently.


Scoped Clock Override Using IDisposable

To ensure that overrides are always reset after use, a disposable scope can be used.

public static class FixedClock
{
public static IDisposable Use(DateTime time)
{
FixedClockProvider.Override.Value = time;
return new Reset();
}
private class Reset : IDisposable
{
public void Dispose()
{
FixedClockProvider.Override.Value = null;
}
}
}

Example Usage in Tests

var fixedDate = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
using (FixedClock.Use(fixedDate))
{
var now = Clock.Now;
// now is always 2026-01-01
}

This pattern ensures that the clock is automatically restored after the using block exits, preventing leaks between tests.


Registering the Provider in a Module

In ASP.NET Boilerplate, providers are usually configured during module initialization. The custom provider can be registered in PreInitialize:

public override void PreInitialize()
{
Clock.Provider = new FixedClockProvider();
}

This sets the provider globally once, while still allowing per-test overrides via AsyncLocal.


Behavior Across Async Calls

Because AsyncLocal flows with the execution context, the overridden time remains consistent across await boundaries.

using (FixedClock.Use(fixedDate))
{
await Task.Delay(50);
var now = Clock.Now; // still fixedDate
}

This is essential for testing asynchronous application services, domain events, and background jobs.


Conclusion

Static state and parallel test execution are fundamentally at odds with each other. While frameworks like ASP.NET Boilerplate rely on static providers for convenience, this design can cause serious issues in automated testing.

By storing override values in AsyncLocal<T>, it is possible to keep the existing static API while ensuring that each test runs in isolation. This approach enables fast parallel test execution without sacrificing determinism or reliability.

The fixed clock implementation presented in this article is a practical and reusable pattern for any ABP-based application that requires deterministic time during testing.