Skip to content

Latest commit

 

History

History
100 lines (65 loc) · 5.34 KB

File metadata and controls

100 lines (65 loc) · 5.34 KB

CoreDesign.ExceptionHandling

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.

Installation

dotnet add package CoreDesign.ExceptionHandling

Quick start

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.

Mapping your own exceptions

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.

Mapping exceptions you don't own

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.

Matching subtypes

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.

Options

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.

Compile-time safety

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).

Performance

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.

Dependencies

  • ASP.NET Core shared framework (IExceptionHandler, ProblemDetails)

Feedback

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.