Global Exception Handling
Excalibur.Hosting.Web provides a production-ready global exception handler that converts unhandled exceptions into standardized RFC 7807 Problem Details JSON responses. It integrates with ASP.NET Core's IExceptionHandler, supports FluentValidation errors, provides environment-aware stack traces, and includes an OpenAPI schema for Swagger documentation.
Before You Start
- .NET 10.0
- An ASP.NET Core web application
- Familiarity with ASP.NET Core deployment and error handling patterns
Installation
dotnet add package Excalibur.Hosting.Web
Quick Start
var builder = WebApplication.CreateBuilder(args);
// Register the global exception handler
builder.Services.AddGlobalExceptionHandler();
var app = builder.Build();
// Enable the exception handler middleware
app.UseExcaliburWebHost();
// Or, if you only want exception handling without the full middleware stack:
// app.UseExceptionHandler();
app.Run();
AddGlobalExceptionHandler() registers GlobalExceptionHandler as an IExceptionHandler, configures ASP.NET Core's ProblemDetailsService, and binds ProblemDetailsOptions from configuration.
UseExcaliburWebHost() adds the exception handler middleware along with tenant ID, correlation ID, ETag, and client address middleware.
How It Works
When an unhandled exception reaches the middleware pipeline:
- Status code extraction — The handler reads the status code from the exception (see Custom Exception Status Codes), or falls back to
500 Internal Server Error. - Exception ID — If the exception is an
ApiException, itsIdis used. Otherwise, a new UUID v7 is generated for correlation. - Trace ID — Pulled from
Activity.Current?.Id(OpenTelemetry) orHttpContext.TraceIdentifier. - Problem Details construction — A
ProblemDetailsobject is built with localized MDN type URI, HTTP reason phrase, instance URN, and trace/exception IDs. - Environment-aware response — In production (non-Development), 5xx errors are sanitized: generic title, no stack trace, no error codes. In Development, full details are included.
- Response writing — Attempts
IProblemDetailsServicefirst (for customization hooks), falls back toWriteAsJsonAsyncwithapplication/problem+jsoncontent type.
Response Format
Development Environment (full details)
{
"type": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404",
"title": "Not Found",
"status": 404,
"detail": "Order with ID 'abc-123' was not found.",
"instance": "urn:my-api:error:019478a2-3b4c-7d8e-9f01-234567890abc",
"traceId": "00-abcdef1234567890-abcdef12-01",
"errorCode": 1042,
"stack": "OrderNotFoundException at OrderService.GetByIdAsync(...) ..."
}
Production Environment (sanitized)
{
"type": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500",
"title": "An unhandled exception has occurred.",
"status": 500,
"detail": "Oops, something went wrong",
"instance": "urn:my-api:error:019478a2-3b4c-7d8e-9f01-234567890abc",
"traceId": "00-abcdef1234567890-abcdef12-01"
}
For non-5xx errors (e.g., 400, 404, 409), the full detail message is shown in all environments.
Validation Errors (FluentValidation)
When a FluentValidation.ValidationException is thrown, the response includes a validationErrors array:
{
"type": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400",
"title": "Bad Request",
"status": 400,
"detail": "Validation failed: ...",
"instance": "urn:my-api:error:...",
"traceId": "...",
"validationErrors": [
{ "propertyName": "Email", "errorMessage": "'Email' must not be empty." },
{ "propertyName": "Name", "errorMessage": "'Name' must be between 2 and 100 characters." }
]
}
Custom Exception Status Codes
The handler uses ExceptionExtensions.GetStatusCode(), which resolves the status code in this order:
DispatchException.DispatchStatusCode, when it is setApiException.StatusCode—DispatchExceptionand the data-access exceptions derive fromApiException, soResourceNotFoundExceptionanswers 404 andConflictExceptionandConcurrencyExceptionanswer 409- An
intstored under the"StatusCode"key of the exception'sDatadictionary - The inner exception, recursively
It does not look for a property named StatusCode on other exception types. An exception you
define yourself answers 500 unless it derives from ApiException or carries a Data entry:
using Excalibur.Dispatch;
// Option 1: derive from ApiException (recommended)
public sealed class OrderNotFoundException : ApiException
{
public OrderNotFoundException(string orderId)
: base(404, $"Order '{orderId}' was not found.", null) { }
}
// Option 2: Data dictionary, for an exception type you cannot change
var ex = new InvalidOperationException("Not allowed");
ex.Data["StatusCode"] = 403;
throw ex;
Similarly, GetErrorCode() returns the error code as text. A DispatchException answers with its own ErrorCode; any other exception answers with an "ErrorCode" entry in its Data dictionary, then its inner exceptions (every inner exception of an AggregateException). A numeric code is rendered as text.
Configuration
ProblemDetailsOptions
builder.Services.AddGlobalExceptionHandler(options =>
{
// Customize the base URL for RFC 7807 "type" URIs
// Default: "https://developer.mozilla.org"
options.StatusTypeBaseUrl = "https://developer.mozilla.org";
});
The type field in problem details responses links to localized MDN HTTP status documentation. The locale is automatically selected based on CultureInfo.CurrentUICulture, with 30+ supported locales including en-US, fr, de, ja, zh-CN, pt-BR, and more. If the current culture isn't supported, it falls back to en-US.
Example type URIs:
- English:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404 - French:
https://developer.mozilla.org/fr/docs/Web/HTTP/Status/404 - Japanese:
https://developer.mozilla.org/ja/docs/Web/HTTP/Status/404
OpenAPI / Swagger Integration
The package includes an OpenAPI schema and a Swagger extension for documenting problem details responses.
Swagger Schema Registration
builder.Services.AddSwaggerGen(options =>
{
options.AddProblemDetailsSchema();
});
This registers a standardized ProblemDetails schema in your OpenAPI specification with all extension fields (traceId, exceptionId, errorCode, stack, validationErrors).
OpenAPI YAML Schema
A reusable OpenAPI 3.1 schema is embedded in the Excalibur.Hosting.Web assembly. Access it programmatically:
// Get the YAML content as a string
string yaml = ProblemDetailsOpenApi.GetYaml();
// Or get a stream for efficient processing
using var stream = ProblemDetailsOpenApi.GetYamlStream();
Structured Logging
The handler logs every exception using source-generated [LoggerMessage] with structured fields:
[APPL]==> /api/orders/abc-123
[APPL]<== ERROR 404: Order 'abc-123' was not found.
Log entries include TraceId and ExceptionId in the logging scope, enabling correlation across distributed traces and support tickets. The event ID is 162510.
Full Web Host Middleware Stack
UseExcaliburWebHost() registers the complete Excalibur middleware pipeline:
| Middleware | Purpose |
|---|---|
UseExceptionHandler() | Global exception → Problem Details |
UseTenantIdMiddleware() | Extracts tenant ID from request headers |
UseCorrelationIdMiddleware() | Extracts/generates correlation ID |
UseETagMiddleware() | Manages incoming/outgoing ETags for concurrency |
UseClientAddressMiddleware() | Captures client remote IP address |
You can use individual middleware methods if you don't need the full stack.
Sample Usage
A complete example:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddExcalibur(excalibur => excalibur
.ScanAssemblies(typeof(PingCommand).Assembly));
builder.Services.AddGlobalExceptionHandler();
var app = builder.Build();
app.UseExcaliburWebHost();
app.MapPost("/ping", async (PingCommand command, IDispatcher dispatcher, CancellationToken ct) =>
{
var result = await dispatcher.DispatchAsync<PingCommand, string>(command, ct);
return Results.Ok(result.ReturnValue);
});
await app.RunAsync();
Package Reference
| Type | Namespace | Purpose |
|---|---|---|
GlobalExceptionHandler | Excalibur.Hosting.Web.Diagnostics | IExceptionHandler implementation |
ProblemDetailsOptions | Excalibur.Hosting.Web.Diagnostics | Configuration for type URIs and locales |
SwaggerGenOptionsExtensions | Excalibur.Hosting.Web.Diagnostics | .AddProblemDetailsSchema() extension |
ProblemDetailsOpenApi | Excalibur.Hosting.Web.Diagnostics | OpenAPI YAML schema path helper |
AddGlobalExceptionHandler() | Microsoft.Extensions.DependencyInjection | DI registration extension |
UseExcaliburWebHost() | Microsoft.AspNetCore.Builder | Middleware pipeline extension |
ExceptionExtensions | Excalibur.Dispatch.Extensions | .GetStatusCode(), .GetErrorCode() helpers |
See Also
- Error Handling Patterns — Error handling strategies and best practices
- Dead Letter Queues — Handle failed messages with dead letter patterns
- ASP.NET Core Deployment — Host Excalibur applications in ASP.NET Core