Why is AsyncLocal value lost between IAsyncEnumerable yield points
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
AsyncLocal<T> values are lost between yield return points in IAsyncEnumerable methods because each yield captures and restores the ExecutionContext from the point where the iterator was created, not from where it was last resumed. When the consumer calls MoveNextAsync(), the iterator resumes with the original execution context, which does not include AsyncLocal values set after iteration began. The fix is to pass values explicitly through parameters or closures instead of relying on AsyncLocal across yield boundaries.
The Problem
Output:
Why This Happens
AsyncLocal<T> stores values in the ExecutionContext, which flows through async calls. The key behavior:
- When
GetNumbers()is first called, it captures the currentExecutionContext(with_context.Value = "hello") - At
yield return, the iterator suspends and returns control to the caller - The caller modifies
_context.Value = "updated"— this creates a newExecutionContextcopy (copy-on-write) - When
MoveNextAsync()resumes the iterator, it restores the original captured context, not the modified one - The
AsyncLocalvalue in the restored context is whatever it was at capture time
This is by design — ExecutionContext flows downward (caller to callee) but changes in the callee do not propagate back to the caller, and the iterator's context is snapshot at creation.
Simplified Explanation
Fix 1: Pass Values as Parameters
Passing the value explicitly avoids AsyncLocal entirely and makes the data flow clear.
Fix 2: Capture in a Closure
Copying the AsyncLocal value to a local variable at the start of the method preserves it across yield points because local variables are stored in the state machine's fields.
Fix 3: Re-set AsyncLocal Before Each Yield
The Same Issue with Regular IEnumerable
This is not unique to IAsyncEnumerable. Regular IEnumerable<T> with yield has the same behavior because the iterator state machine captures ExecutionContext at creation:
When AsyncLocal Does Work
AsyncLocal flows correctly through regular async/await because the entire method runs as one logical operation:
The difference: await suspends and resumes within the same logical async operation. yield creates a producer-consumer boundary where the consumer controls when the producer resumes, potentially with a different context.
Practical Impact: Logging and Tracing
Common Pitfalls
- Assuming AsyncLocal flows through yield:
AsyncLocalvalues are lost at yield points becauseExecutionContextis restored from the initial capture, not the last resume point. - Middleware setting AsyncLocal before IAsyncEnumerable: ASP.NET middleware that sets
AsyncLocal(like correlation IDs) works for regular async methods but breaks when the response is produced by anIAsyncEnumerableendpoint. - Not capturing to a local variable: The simplest fix is
var local = asyncLocal.Value;at the start of the method. This copies the value to the state machine's fields where it survives yield points. - Thinking this is a bug: This is intentional behavior.
ExecutionContextcopy-on-write semantics prevent downstream changes from affecting upstream contexts, which is correct for most async patterns. - Using AsyncLocal for DI-like patterns in iterators: If you use
AsyncLocalas a service locator or ambient context, refactor to pass dependencies explicitly when usingIAsyncEnumerablemethods.
Summary
AsyncLocal<T>values are lost betweenyield returnpoints becauseExecutionContextis restored from the initial snapshot- Copy
AsyncLocalvalues to local variables at the start of iterator methods to preserve them - Pass values explicitly through parameters instead of relying on
AsyncLocalinIAsyncEnumerablemethods - This behavior affects both
IAsyncEnumerableandIEnumerablewithyield - Regular
async/await(without yield) preservesAsyncLocalvalues correctly

