In this lesson, you will learn how to reliably carry user identity information across asynchronous service boundaries to ensure consistent authorization throughout a call chain.
The Challenge of Asynchronous Context
In modern C# applications, a single user request often triggers a series of asynchronous calls across multiple microservices. Because async/await offloads work to different threads, the traditional ThreadStatic storage (which binds data to a single thread) breaks down. If you rely on thread-local storage, you will find your user identity disappearing or, worse, leaking into other requests as threads are reused from the pool.
The Solution: AsyncLocal<T>
To propagate user identity—such as a ClaimsPrincipal or a custom UserContext object—reliably across asynchronous continuations, we use the AsyncLocal<T> class. Unlike thread-local storage, AsyncLocal<T> is designed to flow with the execution context, meaning the value follows the code as it moves across await points, regardless of which physical thread executes the logic.
Here is how you can implement a simple context holder:
public static class UserContext
{
private static readonly AsyncLocal<ClaimsPrincipal> _currentPrincipal = new();
public static ClaimsPrincipal Current
{
get => _currentPrincipal.Value;
set => _currentPrincipal.Value = value;
}
}
Implementing in Service Chains
When a request enters your system (e.g., via a Middleware), you extract the identity and set UserContext.Current. Because AsyncLocal flows up and down the call stack, any service deep within your logic—even one running on a different thread after an await—can access UserContext.Current to perform authorization checks.
However, remember that AsyncLocal flows downstream. If you spawn a new task using Task.Run without capturing the execution context, the value may not be visible. Always ensure that your background work is orchestrated through patterns that respect the execution context, or explicitly pass the identity object as a parameter if the chain requires complete decoupling.
By relying on this mechanism, you prevent "Identity Loss," where downstream services fail to authorize requests because the user context was dropped during an asynchronous context switch.
Key Takeaway: Use AsyncLocal<T> to maintain user identity across asynchronous boundaries, ensuring your claims-based authorization remains intact regardless of which thread handles the execution.