Middleware-based ASP.NET library that enriches HTTP requests with contextual metadata — request IDs, correlation IDs, client details, user information, and more.
$ dotnet add package Nedo.AspNet.Request.EnrichmentA middleware-based library for .NET 9 that enriches HTTP requests with contextual metadata — request IDs, correlation IDs, client details, user information, and more — making it available throughout the request lifecycle via dependency injection.
In distributed systems, having rich contextual data on every request is critical for:
Program.csappsettings.json support with validationX-Request-Id and X-Correlation-Id to response headersShouldEnrich(HttpContext) lets enrichers skip requests based on method, path, or conditionsSystem.Diagnostics.Metrics counters and histograms for observability via OpenTelemetryIEnrichmentContextAccessor provides AsyncLocal-based access from background servicesIRequestEnricher for domain-specific dataEnrichmentContext injectable into controllers, services, and minimal API endpointsdotnet add package Nedo.AspNet.Request.Enrichment
// Program.cs
using Nedo.AspNet.Request.Enrichment.DependencyInjection;
using Nedo.AspNet.Request.Enrichment.Middleware;
builder.Services.AddRequestEnrichment(enrichment =>
{
enrichment.AddRequestId();
enrichment.AddCorrelationId();
enrichment.AddClientIp();
enrichment.AddRequestTime();
});
app.UseRequestEnrichment();
Minimal API:
app.MapGet("/info", (EnrichmentContext ctx) => new
{
ctx.RequestId,
ctx.CorrelationId,
ctx.ClientIp,
ctx.RequestTime
});
Controller:
using Nedo.AspNet.Request.Enrichment.Middleware;
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly EnrichmentContext _enrichment;
private readonly ILogger<OrdersController> _logger;
public OrdersController(
EnrichmentContext enrichment,
ILogger<OrdersController> logger)
{
_enrichment = enrichment;
_logger = logger;
}
[HttpGet]
public IActionResult Get()
{
_logger.LogInformation(
"Request {RequestId} from {ClientIp}",
_enrichment.RequestId,
_enrichment.ClientIp);
return Ok(new
{
_enrichment.RequestId,
_enrichment.CorrelationId,
_enrichment.ClientIp
});
}
}
Background Service / Non-DI Access:
using Nedo.AspNet.Request.Enrichment.Abstractions;
public class AuditService
{
private readonly IEnrichmentContextAccessor _accessor;
public AuditService(IEnrichmentContextAccessor accessor)
=> _accessor = accessor;
public void LogAction(string action)
{
var ctx = _accessor.EnrichmentContext;
Console.WriteLine($"[{ctx?.RequestId}] {action} by {ctx?.ClientIp}");
}
}
Note: The middleware automatically writes
X-Request-IdandX-Correlation-Idto every HTTP response header — no extra code needed.
| # | Enricher | Name | Priority | Output Type |
|---|---|---|---|---|
| 1 | RequestTimeEnricher | RequestTime | 5 | DateTimeOffset |
| 2 | RequestIdEnricher | RequestId | 10 | string |
| 3 | CorrelationIdEnricher | CorrelationId | 11 | string |
| 4 | RequestUrlEnricher | RequestUrl | 20 | string |
| 5 | RequestBodyEnricher | RequestBody | 25 | long, string |
| 6 | ClientIpEnricher | ClientIp | 30 | string |
| 7 | ClientInfoEnricher | ClientInfo | 40 | ClientInfo |
| 8 | UserInformationEnricher | UserInformation | 50 | UserInfo |
| 9 | MachineInformationEnricher | MachineInformation | 60 | MachineInfo |
| 10 | HttpHeaderEnricher | HttpHeader | 70 | IReadOnlyDictionary |
| 11 | QueryParameterEnricher | QueryParameter | 80 | IReadOnlyDictionary |
Register all at once:
builder.Services.AddRequestEnrichment(enrichment =>
{
enrichment.AddAllDefaults();
});
builder.Services.AddRequestEnrichment(enrichment =>
{
enrichment.AddClientIp(options =>
{
options.TrustForwardedHeaders = true;
options.ForwardedHeaderName = "X-Forwarded-For";
});
enrichment.AddHttpHeaders(options =>
{
options.ExcludeHeaders = ["Authorization", "Cookie", "Set-Cookie"];
});
enrichment.AddUserInformation(options =>
{
options.IncludeClaims = ["sub", "email", "role"];
});
});
{
"Enrichment": {
"EnabledEnrichers": [
"RequestId", "CorrelationId", "ClientIp",
"RequestTime", "UserInformation"
],
"EnableDebugLogging": false,
"Resilience": {
"MaxRetries": 3,
"RetryBaseDelayMs": 100,
"TimeoutMs": 5000
}
}
}
// Sequential (default) — predictable priority ordering
enrichment.UseSequentialPipeline();
// Parallel — concurrent execution for high-throughput
enrichment.UseParallelPipeline(maxConcurrency: 5);
app.UseRequestEnrichment(options =>
{
options.ExcludePaths = ["/health", "/ready", "/metrics"];
options.ExcludeExtensions = [".css", ".js", ".png"];
});
Implement IRequestEnricher for domain-specific data:
public class TenantEnricher : IRequestEnricher
{
public string Name => "Tenant";
public int Priority => 15;
public bool IsCritical => true;
// Only enrich API routes, skip health checks
public bool ShouldEnrich(HttpContext context)
=> context.Request.Path.StartsWithSegments("/api");
public async Task<EnrichmentResult> EnrichAsync(
HttpContext context,
CancellationToken cancellationToken = default)
{
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
if (string.IsNullOrEmpty(tenantId))
return EnrichmentResult.Failed(Name, "X-Tenant-Id header is required");
return new EnrichmentResult
{
EnricherName = Name,
Success = true,
Data = new Dictionary<string, object> { ["TenantId"] = tenantId }
};
}
}
Register it:
builder.Services.AddRequestEnrichment(enrichment =>
{
enrichment.AddAllDefaults();
enrichment.AddCustom<TenantEnricher>();
});
├── doc/ # Documentation (00–08)
├── sample/ # Sample projects
│ └── Nedo.AspNet.Request.Enrichment.Sample/
│ └── Controllers/ # Sample controllers
├── src/ # Source code
│ └── Nedo.AspNet.Request.Enrichment/
│ ├── Abstractions/ # IRequestEnricher, IEnrichmentContextAccessor, Models
│ ├── Enrichers/ # 11 built-in enrichers
│ ├── Configuration/ # EnrichmentOptions, EnrichmentBuilder
│ ├── Infrastructure/ # EnrichmentContextAccessor, EnrichmentMetrics
│ ├── Middleware/ # EnrichmentMiddleware, EnrichmentContext
│ ├── Pipeline/ # Sequential + Parallel strategies
│ └── DependencyInjection/ # ServiceCollection + ApplicationBuilder extensions
└── test/ # Unit tests (25 tests)
dotnet build
dotnet run --project sample/Nedo.AspNet.Request.Enrichment.Sample
dotnet test
| Doc | Title | Description |
|---|---|---|
| 00 | Overview | Core concepts and quick start |
| 01 | Architecture & Design | Internal architecture, design patterns, and project structure |
| 02 | Enrichers Reference | Complete reference for all built-in enrichers |
| 03 | Configuration Guide | JSON and fluent configuration options |
| 04 | Middleware Pipeline | How the enrichment pipeline processes requests |
| 05 | Custom Enrichers | Creating and registering your own enrichers |
| 06 | Resilience & Error Handling | Retry policies, timeouts, and error strategies |
| 07 | Testing Guide | Unit and integration testing patterns |
| 08 | Migration Guide | Migrating from Sindika.AspNet.Enrichment |
This project is licensed under the MIT License.