Minimal API demonstrating DI lifetimes, concurrency, and state boundaries.
POST /api/orders/process— simulate order processing, returns CorrelationId and durationGET /api/orders/stats— returns total orders, average duration, last 5 durationsGET /health— liveness check
| Service | Lifetime | Reason |
|---|---|---|
RequestContext |
Scoped | Holds per-request data (CorrelationId, UserAgent). Must be isolated per request. |
OrderProcessor |
Scoped | Depends on RequestContext. Captive dependency if Singleton. |
StatisticsCollector |
Singleton | Accumulates application-wide metrics. Must survive across requests. |
OrderMetrics |
Singleton | Owns thread-safe, application-wide metric instruments created once. |
RandomGenerator |
Singleton | Stateless. Uses Random.Shared — thread-safe since .NET 6. |
flowchart TD
A["POST /api/orders/process"] --> B["OrderValidator"]
B --> C["OrderProcessor (Scoped)"]
C --> D["RequestContext (Scoped)\nCorrelationId · UserAgent"]
C --> E["RandomGenerator (Singleton)\nRandom delay"]
C --> F["StatisticsCollector (Singleton)\nThread-safe metrics"]
C --> G["OrderMetrics (Singleton)\nCount · Duration · Outcome"]
H["GET /api/orders/stats"] -->|reads| F
I["GET /health"] --> J["Liveness: Healthy"]
OrderProcessing.Api/
Contracts/ # Request/response records and configuration options
DependencyInjection/ # Service registration
Endpoints/ # Minimal API endpoint mappings
Services/ # Processing, request context, statistics, randomization, and metrics
Validation/ # OrderValidator
Program.cs # Application composition and middleware pipeline
OrderProcessing.Api.Tests/
OrderApiSpecs.cs # Integration tests — HTTP end-to-end
ServiceLifetimeSpecs.cs # DI lifetime verification
BugDemoSpecs.cs # Intentional bug demonstration and fix
StatsEdgeCaseSpecs.cs # Input validation and zero-stats baseline
RateLimitAndHealthSpecs.cs # Rate limiting, health check, concurrency
ErrorHandlingSpecs.cs # Generic 500 response and response headers
OrderMetricsSpecs.cs # Custom metric measurements and outcome tags
OrderProcessorSpecs.cs # Success, cancellation, and failure outcomes
GET /healthprovides a liveness signal.- Structured logs include
CorrelationIdandOrderIdthrough anILoggerscope. - Every response includes
X-Correlation-ID, allowing callers to match a response to server logs. - Security and correlation headers are added when the response starts, including for handled errors.
System.Diagnostics.Metricsinstruments processing count and duration, tagged by the low-cardinalityoutcomevaluessuccess,cancelled, andfailed.
No telemetry exporter is configured because the assignment has no monitoring backend. In production, OpenTelemetry could collect the custom meter and export it through OTLP or expose it to Prometheus.
dotnet test OrderProcessingApi.sln
dotnet run --project OrderProcessing.Api/OrderProcessing.Api.csproj
# Swagger UI: https://localhost:{port}/swaggerRequestContext holds data unique to a single HTTP request: CorrelationId, UserAgent, and StartTime. All properties are set once in the constructor and never change (immutable). If registered as Singleton, one instance would be shared across all concurrent requests — all requests would carry the CorrelationId and UserAgent captured when the Singleton was first resolved. Request A and Request B would be indistinguishable in logs. In a system with UserId, this would be a critical security vulnerability: User A could see User B's data.
It must be Scoped so each request gets its own isolated instance, bound to the HttpContext lifecycle.
Singleton is dangerous in two scenarios:
Shared mutable state without synchronization:
Multiple concurrent requests write to the same object simultaneously, causing race conditions.
Example: two threads both read total=5, both write total=6 — one increment is lost.
Solution: use lock to ensure atomic updates across related fields.
Captive Dependency:
A Singleton captures a Scoped dependency in its constructor. The Scoped object is never released —
it lives for the entire application lifetime instead of per request.
ASP.NET Core throws InvalidOperationException in Development (ValidateScopes=true),
but is silently broken in Production (ValidateScopes=false).
When an object is expensive to construct and resolved multiple times within the same request. Each resolution creates a new instance — unnecessary allocations and GC pressure. For stateless, lightweight services Transient is fine. For services resolved repeatedly in the same pipeline, Scoped is more efficient.
The RequestContext-as-Singleton bug. The most insidious aspect was that it bypassed ASP.NET Core's built-in scope validation entirely. Because a Singleton is technically resolvable from anywhere, the framework did not throw at startup. The application launched cleanly but silently shared the same CorrelationId and UserAgent across entirely different incoming requests.
This highlights why validating container registrations in automated tests is critical —
BugDemoSpecs proves the bug with Assert.Same and catches the fix with Assert.NotSame,
catching silent state bleeding before it reaches production.