Skip to content

Exception Handling

Granit.ExceptionHandling provides a centralized exception-to-HTTP-response pipeline implementing RFC 7807 Problem Details. All exceptions are caught, mapped to status codes via a chain of responsibility, logged at the appropriate level, and serialized as application/problem+json.

[DependsOn(typeof(GranitExceptionHandlingModule))]
public class AppModule : GranitModule { }

In Program.cs (must be the first middleware):

app.UseGranitExceptionHandling(); // Before routing, authentication, authorization
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
flowchart TD
    A[Exception thrown] --> B{OperationCanceledException?}
    B -->|yes| C[Log Info, return true]
    B -->|no| D[IExceptionStatusCodeMapper chain]
    D --> E{5xx?}
    E -->|yes| F[Log Error]
    E -->|no| G{499?}
    G -->|yes| H[Log Info]
    G -->|no| I[Log Warning]
    F --> J[Build ProblemDetails]
    H --> J
    I --> J
    J --> K{IUserFriendlyException?}
    K -->|yes| L[Title = exception.Message]
    K -->|no| M{ExposeInternalErrorDetails?}
    M -->|yes| N[Title = message, Detail = stack trace]
    M -->|no| O["Title = 'An unexpected error occurred.'"]
    L --> P[Write RFC 7807 response]
    N --> P
    O --> P

Chain of responsibility pattern. Mappers are evaluated in registration order; the first non-null result wins. The DefaultExceptionStatusCodeMapper is registered last as a catch-all.

Default mappings:

ExceptionStatus code
EntityNotFoundException404
NotFoundException404
ForbiddenException403
UnauthorizedAccessException403
ValidationException422
IHasValidationErrors422
ConflictException409
BusinessRuleViolationException422
BusinessException400
IHasErrorCode400
NotImplementedException501
OperationCanceledException499
TimeoutException408
(any other)500

Other Granit modules register their own mappers to extend the chain:

internal sealed class InvoiceConcurrencyMapper : IExceptionStatusCodeMapper
{
public int? TryGetStatusCode(Exception exception) => exception switch
{
InvoiceLockedException => StatusCodes.Status423Locked,
_ => null
};
}
// In module ConfigureServices:
services.AddSingleton<IExceptionStatusCodeMapper, InvoiceConcurrencyMapper>();

The response body includes standard RFC 7807 fields plus Granit-specific extensions:

{
"status": 422,
"title": "Invoice amount must be positive.",
"detail": null,
"traceId": "abcd1234ef567890",
"errorCode": "Invoices:InvalidAmount",
"errors": {
"Amount": ["Granit:Validation:GreaterThanValidator"]
}
}
ExtensionSourcePresent when
traceIdActivity.Current?.TraceId or HttpContext.TraceIdentifierAlways
errorCodeIHasErrorCode.ErrorCodeException implements IHasErrorCode
errorsIHasValidationErrors.ValidationErrorsException implements IHasValidationErrors
CategoryKey typesPackage
ModuleGranitExceptionHandlingModule
PipelineIExceptionStatusCodeMapper, GranitExceptionHandlerGranit.ExceptionHandling
OptionsExceptionHandlingOptionsGranit.ExceptionHandling
ExtensionsAddGranitExceptionHandling(), UseGranitExceptionHandling()Granit.ExceptionHandling