The Context System provides structured, type-safe and async-flow-aware contextual data for your application. This document explains the internal architecture, design goals, and execution model.
A Context represents the execution environment for a unit of work. It carries metadata such as:
- correlation identifiers
- tenant and user information
- request metadata
- dependency flow state
Contexts are not global and do not use AsyncLocal directly. Instead, each context type is managed independently through a dedicated store and accessor.
The design is guided by four core objectives:
-
Isolation per context type Each TContext has independent lifecycle and storage.
-
Async-safe execution Context flows across await boundaries without leaking across unrelated operations.
-
Strong typing No casting, no dynamic bags, no string keys.
-
Clear ownership and deterministic cleanup Every context must be explicitly opened and explicitly closed.
ContextStore<TContext> is the low-level container that stores the currently active context.
Key characteristics:
- Per-type instance: each TContext has its own store
- Singleton in DI (intentionally)
- Uses an internal mechanism to scope state per logical async flow
- Does not create or manage contexts — only holds them
Typical operations:
- GetCurrent()
- SetCurrent(TContext?)
- Clear()
Your code should never use this directly; instead, use IContextAccessor and IContextManager.
IContextAccessor provides safe access to the active context.
| Method | Behavior |
|---|---|
TryGetCurrent() |
Returns the context or null. |
GetCurrent() |
Returns context or null; no exception. |
RequireCurrent() |
Throws if no context is active. |
HasContext() |
Boolean check. |
This ensures that service code can reliably depend on context availability without coupling to its lifecycle management.
The manager controls the context lifecycle and ensures proper cleanup:
await manager.ExecuteInContext(context, async () =>
{
// context is active here
});Responsibilities:
- Set the context before execution
- Ensure context flows correctly across async calls
- Restore previous context
- Prevent context leaks on exceptions
A single context is active only within the scope of the delegate passed to ExecuteInContext.
Some contexts must be exposed to untrusted or restricted components (e.g., plugins, sandboxed logic, external modules).
ReadOnlyContextAccessor<TContext>:
- Wraps a typed accessor
- Converts full context into an IReadOnlyContext
- Removes any access to application-specific fields
- Ensures immutability and isolation
Registration:
services.AddReadOnlyContextAccessor<MyContext>();This enables code to read basic context metadata without risk of modification or leakage.
A context has a strict lifecycle:
[CREATE] → [ACTIVATE] → [IN USE] → [CLEANUP]- Create Application code constructs the context instance.
- Activate
IContextManager<TContext>.ExecuteInContext()sets this context as current. - In Use All services resolved within the async flow may access the context.
- Cleanup Manager restores previous context (if stacked) or clears state.
Contexts cannot be nested unless your manager implementation explicitly supports stack semantics. The provided implementation allows proper restoration of previous contexts.
If code inside the context block throws:
- The exception is rethrown
- The context is still cleaned up deterministically
- There is no state corruption or leakage into other requests
This makes it structurally safe for:
- background jobs
- message handlers
- request pipelines
- domain workflows
Key characteristics:
- Context binds to the logical async flow, not physical thread.
- Context persists across await.
- Parallel tasks cannot see each other's context.
- Cleanup ensures no bleeding across execution boundaries.
This design is more predictable than raw AsyncLocal<T> while avoiding its common pitfalls.
The Context System provides:
- Type-safe context models
- Deterministic lifecycle
- Async-safe propagation
- Clear read/write separation
- A minimal, reliable API
It is well suited for:
- request correlation
- multi-tenant services
- event/command handlers
- hosted background workers
- plugin-based architectures
Built with ❤️ for .NET developers