CoreDesign.ExceptionHandling provides a single global exception handler for ASP.NET Core, producing consistent RFC 7807 ProblemDetails responses for unhandled exceptions. A Roslyn source generator collects every exception type marked with [ProblemMapping] and produces a compile-time dispatch table at build time. No reflection, no hand-maintained switch statement to keep in sync with your exception types.
This is a different concern from CoreDesign.Logging: the logging decorator logs and rethrows exceptions raised by decorated service methods, while CoreDesign.ExceptionHandling is the last line of defense at the edge of the HTTP pipeline, catching anything that escapes and turning it into a response the caller can act on.
dotnet add package CoreDesign.ExceptionHandling
Register the handler at startup:
builder.Services.AddCoreDesignExceptionHandling();
builder.Services.AddGeneratedProblemMappings();
// ...
app.UseExceptionHandler();AddGeneratedProblemMappings() is generated by the source generator and always available once the package is referenced, even with zero [ProblemMapping] attributes anywhere in the project. Call order between the two Add... calls does not matter.
With nothing else configured, every unhandled exception produces a generic RFC 7807 500 response. In Development, the response detail includes the exception; in every other environment it is omitted.
Apply [ProblemMapping] directly to an exception type you own:
[ProblemMapping(404, Title = "Resource not found")]
public sealed class EntityNotFoundException(string entity, Guid id)
: Exception($"{entity} '{id}' was not found.");Throwing EntityNotFoundException anywhere in the request pipeline now produces a 404 response with detail set to the exception's Message.
For BCL or third-party exception types, apply [ProblemMapping] at the assembly level instead, with ExceptionType set:
[assembly: ProblemMapping(408, ExceptionType = typeof(TaskCanceledException), Title = "Request timed out")]
[assembly: ProblemMapping(503, ExceptionType = typeof(SqlException), Title = "Database unavailable")]One attribute, two places to put it, depending on whether you own the type.
By default a mapping also covers every subtype of the target exception, unless the subtype carries its own, more specific mapping:
[ProblemMapping(400, Title = "Domain rule violated")]
public class DomainException(string message) : Exception(message);
[ProblemMapping(404, Title = "Resource not found")]
public sealed class EntityNotFoundException(string entity, Guid id)
: DomainException($"{entity} '{id}' was not found.");Throwing an EntityNotFoundException produces a 404 (the more specific mapping wins). Throwing any other DomainException subtype produces a 400 (the base mapping applies). The generator orders the dispatch table by inheritance depth, most derived first, so this resolves correctly regardless of declaration order.
Set MatchDerived = false to restrict a mapping to the exact type only:
[ProblemMapping(422, Title = "Validation failed", MatchDerived = false)]
public class ValidationException(string message) : Exception(message);A subtype of ValidationException with no mapping of its own now falls through to the generic 500 fallback instead of inheriting the 422 mapping.
| Property | Default | Effect |
|---|---|---|
StatusCode |
required | The HTTP status code written to the response. |
Title |
null |
The RFC 7807 title. Falls back to the standard reason phrase for StatusCode when omitted. |
Type |
null |
The RFC 7807 type URI. Omitted from the response when not set. |
MatchDerived |
true |
Whether this mapping also covers subtypes that have no mapping of their own. |
IncludeMessage |
true |
Whether Exception.Message is written to the response detail. Set to false for exceptions whose message should never reach a client. |
ExceptionType |
null |
Required, and only meaningful, at the assembly level, to target a type the project does not own. |
Two [ProblemMapping] attributes targeting the exact same concrete exception type is a build error (CDEH001), not a silent pick of one over the other. An [assembly: ProblemMapping] usage without ExceptionType set is also a build error (CDEH002).
Exception handling runs on the exceptional path, not on every request, so the generated dispatch table favors simplicity over the reflection-avoidance techniques used in CoreDesign.Logging: it is a plain C# type-pattern switch, which the compiler already turns into a sequence of direct type checks with no allocation and no runtime type lookup.
- ASP.NET Core shared framework (
IExceptionHandler,ProblemDetails)
Feedback on this package is welcome. If you run into a missing feature, an unexpected behavior, or something that required more effort than it should have, open an issue at github.com/codyskidmore/CoreDesign/issues or tag @codyskidmore. Suggestions about missing features and priority input are especially appreciated.