ContextR.Hosting.AspNetCore¶
ASP.NET Core middleware for ContextR. This package extracts context from incoming HTTP request headers and writes it into the ambient AsyncLocal storage, making it available to all downstream code via IContextAccessor, IContextSnapshot, and IContextWriter.
When to use this package¶
Use ContextR.Hosting.AspNetCore when your ASP.NET Core application receives context values (correlation IDs, tenant identifiers, feature flags) as HTTP headers and you want them available as typed context objects throughout the request pipeline.
Install¶
Dependencies: ContextR (core), ContextR.Propagation (for IContextPropagator<T>), ContextR.Transport.Http, Microsoft.AspNetCore.App (framework reference).
Quick start¶
builder.Services.AddContextR(ctx =>
{
ctx.Add<CorrelationContext>(reg => reg
.MapProperty(c => c.TraceId, "X-Trace-Id")
.MapProperty(c => c.SpanId, "X-Span-Id")
.UseAspNetCore());
});
public class CorrelationContext
{
public string? TraceId { get; set; }
public string? SpanId { get; set; }
}
With this configuration, every incoming HTTP request with X-Trace-Id or X-Span-Id headers will have a CorrelationContext automatically available in the ambient context.
How it works¶
Registration via IStartupFilter¶
UseAspNetCore() registers an IStartupFilter that adds the context extraction middleware at the beginning of the request pipeline. This means context is available before any other middleware, controllers, or filters execute.
UseAspNetCore() called
→ registers IStartupFilter = ContextStartupFilter<TContext>(domain)
→ at app startup, IStartupFilter.Configure() inserts:
app.UseMiddleware<ContextMiddleware<TContext>>()
before all other middleware
Using IStartupFilter instead of requiring app.UseMiddleware<T>() in Program.cs has two advantages:
- Automatic -- no explicit middleware registration needed in the application startup
- Early -- the middleware is inserted before user-configured middleware, ensuring context is available everywhere
ContextMiddleware<TContext>¶
The middleware runs on every request:
InvokeAsync(HttpContext, IContextPropagator<TContext>, IContextWriter, IServiceProvider, ...)
→ context = propagator.Extract(httpContext.Request.Headers, headerGetter)
→ if context is not null:
writer.SetContext(context) // writes to AsyncLocal
→ await _next(httpContext) // continue pipeline
The middleware resolves IContextPropagator<TContext> and IContextWriter from DI per request (method injection via ASP.NET Core's middleware convention). The propagator's Extract method reads header values from IHeaderDictionary using a static getter delegate:
What happens when headers are missing?¶
If the propagator's Extract method returns null (no matching headers found), the middleware does nothing -- no context is written, and the request continues normally. Downstream code calling GetContext<T>() will receive null.
If only some headers are present (partial extraction), the behavior depends on the propagator implementation. MappingContextPropagator returns a context object with only the found properties set -- null properties for missing headers.
Ingress enforcement (optional)¶
UseAspNetCore(...) supports ingress enforcement with fluent options:
ctx.Add<UserContext>(reg => reg
.Map(m => m
.ByConvention()
.Property(c => c.TenantId, "X-Tenant-Id")
.Property(c => c.TraceId, "X-Trace-Id"))
.UseAspNetCore(o => o.Enforcement(e =>
{
e.Mode = ContextIngressEnforcementMode.FailRequest;
e.OnFailure = failure => ContextIngressFailureDecision.Fail(
statusCode: 400,
message: "Required context is missing.");
})));
Available modes:
Disabled: extraction-only behavior (default)ObserveOnly: invoke failure callback/logging but continue requestFailRequest: short-circuit request unless callback overrides decision
You can also provide fallback context creation:
.UseAspNetCore(o => o.Enforcement(e =>
{
e.Mode = ContextIngressEnforcementMode.FailRequest;
e.FallbackContextFactory = http => new UserContext
{
TenantId = "default-tenant",
TraceId = http.TraceIdentifier
};
}))
DI-aware fluent configuration¶
When configuration needs services (for example a logger), use the DI-aware overload:
.UseAspNetCore((sp, o) =>
{
var logger = sp.GetRequiredService<ILogger<Startup>>();
o.Enforcement(e =>
{
e.Mode = ContextIngressEnforcementMode.ObserveOnly;
e.OnFailure = failure =>
{
logger.LogWarning("Context enforcement failure: {Reason}", failure.Reason);
return ContextIngressFailureDecision.Continue();
};
});
})
Domain-aware extraction¶
When context is registered within a domain, UseAspNetCore() captures the domain and passes it to the middleware. The middleware then writes context to the specific domain slot:
builder.Services.AddContextR(ctx =>
{
ctx.AddDomain("web-api", d => d.Add<CorrelationContext>(reg => reg
.MapProperty(c => c.TraceId, "X-Trace-Id")
.UseAspNetCore()));
});
In this configuration, the middleware calls writer.SetContext("web-api", context) instead of writer.SetContext(context). Downstream code must use GetContext<CorrelationContext>("web-api") to read the value.
Domain capture¶
The domain is captured at configuration time via closure:
UseAspNetCore() called
→ var domain = builder.Domain // null for default, "web-api" for domain registration
→ registers ContextStartupFilter<T>(domain)
→ middleware receives domain via constructor
When domain is null (default registration), the middleware calls app.UseMiddleware<ContextMiddleware<T>>() without the domain argument, and SetContext(context) writes to the default slot.
When domain is not null, the middleware calls app.UseMiddleware<ContextMiddleware<T>>(domain), and SetContext(domain, context) writes to the domain-specific slot.
Multiple context types¶
Register multiple context types, each with its own middleware:
builder.Services.AddContextR(ctx =>
{
ctx.Add<CorrelationContext>(reg => reg
.MapProperty(c => c.TraceId, "X-Trace-Id")
.UseAspNetCore());
ctx.Add<TenantContext>(reg => reg
.MapProperty(c => c.TenantId, "X-Tenant-Id")
.UseAspNetCore());
});
Each UseAspNetCore() call registers a separate IStartupFilter, and each filter adds its own ContextMiddleware<T>. The middleware instances run independently -- each extracts its own context type from headers.
Combined with HTTP propagation¶
The most common pattern combines incoming extraction with outgoing propagation:
builder.Services.AddContextR(ctx =>
{
ctx.Add<CorrelationContext>(reg => reg
.MapProperty(c => c.TraceId, "X-Trace-Id")
.MapProperty(c => c.SpanId, "X-Span-Id")
.UseAspNetCore() // extract from incoming requests
.UseGlobalHttpPropagation()); // inject into outgoing HttpClient calls
});
The flow for a request:
Incoming HTTP request with X-Trace-Id: abc123
→ ContextMiddleware extracts CorrelationContext { TraceId = "abc123" }
→ Writes to AsyncLocal via IContextWriter
→ Controller/service makes HttpClient call
→ ContextPropagationHandler reads from AsyncLocal via IContextAccessor
→ Injects X-Trace-Id: abc123 into outgoing request headers
Interaction with IContextSnapshot¶
The middleware writes to AsyncLocal via IContextWriter.SetContext(). The scoped IContextSnapshot is created when the DI scope is resolved (typically at the start of the request). Because middleware runs within the same scope, context set by middleware is captured in the snapshot.
However, the timing matters:
- If
IContextSnapshotis resolved after middleware runs (normal case), it captures the context - If
IContextSnapshotis resolved before middleware (unusual, e.g., in anotherIStartupFilter), the snapshot may be empty
In practice, the standard ASP.NET Core DI scope creation happens before middleware, so IContextSnapshot is resolved lazily and captures the correct values.
File map¶
| File | Role |
|---|---|
Internal/ContextMiddleware.cs |
Middleware that extracts context from HttpContext.Request.Headers |
Internal/ContextStartupFilter.cs |
IStartupFilter that registers middleware at pipeline start |
Extensions/ContextRAspNetCoreRegistrationExtensions.cs |
UseAspNetCore() extension on IContextRegistrationBuilder<T> |