Request-scoped diagnostics
The scoped accumulator and record types that collect per-request warnings, errors, and info messages, plus the two dev-mode transports (X-Pennington-Diagnostic header, on-page overlay) that surface them to the author. The accumulator and record types live in Pennington.Diagnostics; the header emission and overlay processor live in Pennington.Infrastructure.
DiagnosticContext
Scoped accumulator registered in DI as Scoped — a fresh instance per HTTP request, backed by a private List<Diagnostic> with no thread-safety. Consumers resolve it via context.RequestServices.GetService<DiagnosticContext>() (or constructor injection) and call one of the Add* methods; the middleware and overlay read Diagnostics, HasAny, and HasErrors during response flush.
Members
Add(Diagnostic diagnostic)— appends a pre-constructedDiagnosticto the request's list. Used when the caller already has aDiagnosticinstance (for example, one forwarded from a helper service).AddError(string message, string? source = null)— appends a newDiagnosticwithSeverity = DiagnosticSeverity.Error. CausesHasErrorsto returntruefor the remainder of the request.AddWarning(string message, string? source = null)— appends a newDiagnosticwithSeverity = DiagnosticSeverity.Warning. Contributes to the overlay warning count but does not flipHasErrors.Diagnostics(property) — read-only view of the diagnostics accumulated so far, in insertion order. Read byResponseProcessingMiddleware.WriteDiagnosticHeadersandDiagnosticOverlayProcessor.ProcessAsync.HasAny(property) —truewhen at least one diagnostic has been appended. Gate before enumerating the list to emitX-Pennington-Diagnosticheaders.HasErrors(property) —truewhen at least one appended diagnostic hasSeverity = Error. A context with only warnings returnstruefromHasAnyandfalsefromHasErrors.
Diagnostic
Immutable record carrying one diagnostic event. Route-agnostic — the request that produced it supplies the route context via HttpContext; the record itself only carries severity, message, and an optional Source label used as the overlay pill subtitle and the third pipe-delimited segment of the X-Pennington-Diagnostic header.
Parameters
SeverityDiagnosticSeverityThe severity band controlling how the overlay colors the entry and whether
HasErrorsflips.MessagestringHuman-readable body rendered into the overlay panel and after the first pipe in the
X-Pennington-Diagnosticheader value.Sourcestring?Default:nullOptional label identifying the producer (for example,
"XrefResolver"); rendered as the small subtitle next to the severity pill in the overlay and appended after a second pipe in the header value when non-null.
DiagnosticSeverity
Two-value enum in ascending severity order. The overlay's aggregate badge color picks the highest present severity (Error > Warning); DiagnosticContext.HasErrors fires only on Error.
Values
| Name | Value | Description |
|---|---|---|
Warning |
0 |
Recoverable issue (for example, an unresolved xref); contributes to the overlay warning count and the amber badge color. |
Error |
1 |
Fatal issue for the request's output; flips HasErrors and renders with the red badge color. |
Dev-mode overlay and X-Pennington-Diagnostic header
Two transports surface the accumulated diagnostics, both wired inside UsePennington via the response-processor pipeline.
| Transport | Type | Availability | Shape |
|---|---|---|---|
| Response header | X-Pennington-Diagnostic |
Every request that has HasAny |
One header value per diagnostic, pipe-delimited: Severity|Message (or Severity|Message|Source when Source is non-null). Emitted by ResponseProcessingMiddleware.WriteDiagnosticHeaders immediately before the buffered body is flushed. |
| On-page overlay | DiagnosticOverlayProcessor (Order = 30, IResponseProcessor) |
Requests where DOTNET_WATCH is set, status is 2xx, and content type contains text/html |
Floating badge injected before </body> summarizing error/warning counts; clicking expands a panel listing every diagnostic. Re-renders on the spa:diagnostics DOM event for SPA navigations. |
Example
The canonical in-repo consumer is XrefResolvingService, which reports unresolved uids. Any service or response processor that resolves DiagnosticContext and calls AddWarning / AddError during request handling flows entries into the X-Pennington-Diagnostic response header and the dev overlay without further wiring. The examples/ExtensibilityLabExample lab registers a scoped IResponseProcessor that follows this shape:
public int Order => 50;
public bool ShouldProcess(HttpContext context)
{
if (context.Response.StatusCode is < 200 or >= 300) return false;
var contentType = context.Response.ContentType;
return contentType is not null
&& contentType.StartsWith("text/html", StringComparison.OrdinalIgnoreCase);
}
public Task<string> ProcessAsync(string responseBody, HttpContext context)
{
if (!responseBody.Contains("rel=\"canonical\"", StringComparison.OrdinalIgnoreCase))
{
diagnostics.AddWarning(
"Page is missing a <link rel=\"canonical\"> tag.",
source: context.Request.Path);
}
return Task.FromResult(responseBody);
}
See also
- Related reference: Build report fields
- Related reference: Response processing interfaces
- How-to: Write a response processor