Exception Handling
Axiom distinguishes between expected and unexpected exceptions. An AxiomException is an expected exception one you intentionally throw to signal a known failure condition such as a missing resource, a failed authorization check, or a violated business rule. Unlike unhandled exceptions, they carry a code, an optional message, and arbitrary data entries that the framework uses to produce a structured response.
The base class for all Axiom exceptions is AxiomException. Several subtypes are provided out of the box:
| Exception | Intended use |
|---|---|
BusinessException | A violated business rule or precondition |
NotFoundException | A requested resource does not exist |
AuthorizationException | The caller lacks permission |
Throw them from anywhere in your application logic:
throw new NotFoundException(
code: "ORDER:404", message: "Order {id} not found"
).AddData("id", orderId);ASP.NET Core Integration
In ASP.NET Core applications, Axiom maps AxiomException instances to structured HTTP problem details responses. This is implemented as an IExceptionHandler that plugs into the standard ASP.NET Core exception handling middleware.
dotnet add package Allegory.Axiom.AspNetCore.ExceptionHandlingSetup
Add the ASP.NET Core exception handling middleware to your pipeline:
// In your middleware pipeline
app.UseExceptionHandler();How It Works
When an exception propagates out of your middleware pipeline, ASP.NET Core's exception handler middleware invokes registered IExceptionHandler implementations in order. AxiomExceptionHandler checks whether the exception is an AxiomException. If it is not, it returns false and the exception is passed to the next handler. If it is, it:
- Logs if the configured log level exists for that exception type
- Resolves a HTTP status code from the exception type using the configured mappings
- Sets
HttpContext.Response.StatusCodeto that code - Builds a
ProblemDetailsresponse withtitleset to the exception code anddetailset to the message - Optionally localizes the detail if no message is set and the exception code prefix has a registered resource mapping
- Interpolates any
Exception.Dataentries into the message using{key}placeholders and adds them as problem details extensions
Default Mappings
These are registered as defaults:
| Exception | Status Code | Log Level |
|---|---|---|
AuthorizationException | 403 Forbidden | Warning |
NotFoundException | 404 Not Found | - |
BusinessException | 409 Conflict | - |
If an exception type is not directly mapped, Axiom walks up the class hierarchy until a mapped base type is found. If no mapping exists at any level, the response status code is left unchanged and nothing is logged.
// Inherits NotFoundException mapping → 404 Not Found
public class OrderNotFoundException(string? code = null, string? message = null)
: NotFoundException(code, message);Exception Data
Entries in Exception.Data are interpolated into the message using {key} placeholders and also added as extensions on the problem details response. The placeholder name must match the data key exactly.
var ex = new BusinessException(
code: "BIZ:001", message: "Entity {id} not found"
).AddData("id", 42);
// ProblemDetails:
// title: "BIZ:001"
// detail: "Entity 42 not found"
// id: 42Localization
If an exception has no message but has a code, the handler can look up the detail from a string localizer. The code must contain a : separator and the prefix before it must be mapped to a resource name via MapExceptionCode in LocalizationOptions.
internal sealed class MyAppPackage : IConfigureApplication
{
public static Task ConfigureAsync(IHostApplicationBuilder builder)
{
builder.Services.Configure<LocalizationOptions>(options =>
{
// Map prefix "BIZ" to the MyApp.Resources.Messages resource
options.MapExceptionCode("BIZ", "MyApp.Resources.Messages");
// Or map by resource marker type
options.MapExceptionCode<MyAppResource>("BIZ");
});
return Task.CompletedTask;
}
}With this configuration, a BusinessException with code BIZ:001 and no message resolves its detail by calling IStringLocalizer["BIZ:001"] on the MyApp.Resources.Messages resource.
Localization is skipped when:
- The exception already has a non-empty message
- The code contains no
:separator - The code prefix has no entry in
ExceptionCodeMappings
// Localized no message, prefix "BIZ" is mapped
throw new BusinessException(code: "BIZ:001");
// Not localized message present, localizer never called
throw new BusinessException(code: "BIZ:001", message: "Order already placed");
// Not localized no colon separator in code
throw new BusinessException(code: "NoPrefixCode");See Localization for configuring translation files and file providers.
Problem Details Shape
Every handled exception produces a response conforming to RFC 9457. The title is the exception code, detail is the resolved message, and exception data entries appear as top-level extensions:
{
"title": "BIZ:001",
"detail": "Entity 42 not found",
"status": 409,
"id": 42
}Custom Exception Types
Define your own exception types by subclassing AxiomException or any of its subtypes:
public class OrderCalculationException() : AxiomException("ORD:CalculationFailed", "Failed to calculate order total");To map it to a status code and log level, configure AspNetCoreExceptionHandlerOptions in your application package:
internal sealed class MyAppPackage : IConfigureApplication
{
public static Task ConfigureAsync(IHostApplicationBuilder builder)
{
builder.Services.Configure<AspNetCoreExceptionHandlerOptions>(options =>
{
options.AddStatusCode<OrderCalculationException>(HttpStatusCode.BadRequest);
options.AddLogLevel<OrderCalculationException>(LogLevel.Error);
});
return Task.CompletedTask;
}
}