NormaTest.AspNetCore
1.0.0
See the version list below for details.
dotnet add package NormaTest.AspNetCore --version 1.0.0
NuGet\Install-Package NormaTest.AspNetCore -Version 1.0.0
<PackageReference Include="NormaTest.AspNetCore" Version="1.0.0" />
<PackageVersion Include="NormaTest.AspNetCore" Version="1.0.0" />
<PackageReference Include="NormaTest.AspNetCore" />
paket add NormaTest.AspNetCore --version 1.0.0
#r "nuget: NormaTest.AspNetCore, 1.0.0"
#:package NormaTest.AspNetCore@1.0.0
#addin nuget:?package=NormaTest.AspNetCore&version=1.0.0
#tool nuget:?package=NormaTest.AspNetCore&version=1.0.0
Norma.AspNetCore
Consistent API responses and structured error handling for ASP.NET Core — in two lines.
Overview
Norma.AspNetCore solves two problems that every ASP.NET Core API team solves from scratch:
- Consistent success responses — wraps every controller response in a typed
ApiEnvelope<T>with metadata (request ID, timestamp, version, pagination) — without touching a single controller. - Structured error responses — maps all unhandled exceptions to RFC 7807 Problem Details JSON automatically. Throw a
KeyNotFoundException, get a clean 404. Throw anArgumentException, get a 400 with the parameter name. No try/catch boilerplate required.
Key Features
| Feature | Detail |
|---|---|
| Zero controller changes | Install and register — existing controllers work immediately |
| Dual envelope mode | OptOut for greenfield (on everywhere); OptIn for brownfield (adopt endpoint-by-endpoint) |
| RFC 7807 errors always | Error responses are always structured, regardless of envelope mode |
| 7 built-in exception conventions | KeyNotFoundException, ArgumentException, ValidationException, UnauthorizedAccessException, NotImplementedException, OperationCanceledException, TimeoutException |
IApiProblem interface |
Domain exceptions in your domain layer can self-describe their HTTP representation |
| Pagination metadata | One HttpContext.SetPagination(...) call adds pagination to the meta block |
| Slim mode | Clients send X-Norma-Meta: false to strip the meta block from responses |
| AOT / trim safe | Source-generated JSON serialization; IsTrimmable and IsAotCompatible in the csproj |
| .NET 9+ | Built on native IExceptionHandler and IAsyncResultFilter |
Installation
dotnet add package Norma.AspNetCore
If your domain layer needs to throw self-describing exceptions (implementing IApiProblem) without depending on ASP.NET Core:
dotnet add package Norma.AspNetCore.Abstractions
Requirements: .NET 9.0 or later.
Quickstart
Greenfield Project (OptOut mode — recommended default)
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(); // ← your existing line
builder.Services.AddNorma(opt => // ← add this
{
opt.ProblemBaseUrl = "https://api.yourapp.com/problems";
});
var app = builder.Build();
app.UseNorma(); // ← add this (before UseRouting / MapControllers)
app.MapControllers();
app.Run();
That's it. Every controller response is now wrapped:
// GET /api/v1/users → 200 OK
{
"success": true,
"data": [ { "id": "...", "name": "Alice" } ],
"meta": {
"requestId": "0HN8P1234567:00000001",
"timestamp": "2026-03-03T08:30:00Z",
"version": "v1"
}
}
And every unhandled exception returns RFC 7807:
// KeyNotFoundException thrown in controller → 404
{
"success": false,
"type": "https://api.yourapp.com/problems/not-found",
"title": "Resource Not Found",
"status": 404,
"detail": "User with ID 'abc' was not found.",
"instance": "/api/v1/users/abc",
"requestId": "0HN8P1234567:00000001",
"timestamp": "2026-03-03T08:30:00Z"
}
Brownfield Project (OptIn mode — adopt gradually)
builder.Services.AddNorma(opt =>
{
opt.ProblemBaseUrl = "https://api.yourapp.com/problems";
opt.EnvelopeMode = EnvelopeMode.OptIn; // envelope off by default
});
Then decorate only the controllers/actions you're ready to migrate:
[UseEnvelope] // ← opt this controller in
[ApiController, Route("api/v2/orders")]
public class OrdersController : ControllerBase { ... }
Example Usage
Skipping the Envelope on a Specific Endpoint
[HttpGet("{id}/avatar")]
[SkipEnvelope] // ← return raw bytes, no JSON wrapper
public IActionResult GetAvatar(Guid id)
{
var bytes = _avatarService.GetPng(id);
return File(bytes, "image/png");
}
Adding Pagination Metadata
[HttpGet]
public IActionResult GetUsers([FromQuery] int page = 1, [FromQuery] int perPage = 20)
{
var users = _repo.GetPage(page, perPage, out int total);
HttpContext.SetPagination(page, perPage, total); // ← one call adds pagination to meta
return Ok(users);
}
Response meta block:
"meta": {
"requestId": "...",
"timestamp": "...",
"version": "v1",
"pagination": { "page": 1, "perPage": 20, "total": 85, "totalPages": 5 }
}
Custom Domain Exception with IApiProblem
// In your domain project — references only Norma.AspNetCore.Abstractions (no ASP.NET Core dep)
public sealed class InsufficientStockException : Exception, IApiProblem
{
public InsufficientStockException(string sku) : base($"SKU '{sku}' is out of stock.") { }
public int StatusCode => 409;
public string ProblemType => "out-of-stock"; // becomes the URI suffix
public string Title => "Insufficient Stock";
public string? Detail => Message;
}
Throw it anywhere in your stack — Norma resolves it to a 409 with the full Problem Details body automatically.
Requesting Slim Responses (No Meta Block)
Clients that don't need the meta overhead can suppress it per-request:
GET /api/v1/users
X-Norma-Meta: false
Configuration Options
All options are set via AddNorma(opt => { ... }):
| Property | Type | Default | Description |
|---|---|---|---|
ApiVersion |
string |
"v1" |
Version string emitted in every meta.version |
ProblemBaseUrl |
string? |
null |
Base URL for RFC 7807 type URIs. If null, uses "about:blank" |
EnvelopeMode |
EnvelopeMode |
OptOut |
OptOut — on everywhere, use [SkipEnvelope] to suppress. OptIn — off everywhere, use [UseEnvelope] to apply |
IncludeRequestId |
bool |
true |
Include requestId in envelope meta and problem responses |
IncludeTimestamp |
bool |
true |
Include timestamp in envelope meta and problem responses |
SuppressNullFields |
bool |
true |
Omit null fields from JSON output |
SlimModeHeader |
string? |
"X-Norma-Meta" |
Header name clients set to "false" to receive a meta-free response. Set to null to disable slim mode |
ExceptionHandling |
ExceptionHandlingMode |
RfcProblemDetails |
RfcProblemDetails — Norma handles all exceptions. Passthrough — lets your existing handler run |
ExcludedPaths |
List<string> |
["/health", "/metrics", "/swagger", "/favicon.ico"] |
Path prefixes that bypass envelope wrapping |
Response Shapes
Success — ApiEnvelope<T>
{
"success": true,
"data": { },
"meta": {
"requestId": "string",
"timestamp": "ISO-8601",
"version": "v1",
"pagination": {
"page": 1, "perPage": 20, "total": 100, "totalPages": 5
}
}
}
Error — ProblemEnvelope (RFC 7807)
{
"success": false,
"type": "https://api.yourapp.com/problems/validation-error",
"title": "Validation Failed",
"status": 422,
"detail": "One or more fields failed validation.",
"instance": "/api/v1/users",
"requestId": "string",
"timestamp": "ISO-8601",
"errors": [
{ "field": "email", "code": "VALIDATION_FAILED", "message": "Invalid email address." }
]
}
Built-in Exception Conventions
| Exception | HTTP Status | Problem Type |
|---|---|---|
KeyNotFoundException |
404 | not-found |
ArgumentException / ArgumentNullException / ArgumentOutOfRangeException |
400 | bad-request |
ValidationException (DataAnnotations) |
422 | validation-error |
UnauthorizedAccessException |
403 | forbidden |
NotImplementedException |
501 | not-implemented |
OperationCanceledException |
499 | request-cancelled |
TimeoutException |
504 | gateway-timeout |
Any exception implementing IApiProblem |
your value | your value |
| Everything else | 500 | internal-error (message suppressed) |
Customising the Request ID Source
Replace the built-in provider by registering your own IRequestIdProvider before calling AddNorma:
services.AddSingleton<IRequestIdProvider, MyCorrelationIdProvider>();
services.AddNorma(...);
Repository Layout
/
├── src/
│ ├── Norma.AspNetCore/ # Main package
│ └── Norma.AspNetCore.Abstractions/ # IApiProblem, FieldError — zero ASP.NET Core dep
├── tests/
│ ├── Norma.AspNetCore.UnitTests/
│ └── Norma.AspNetCore.IntegrationTests/
├── samples/
│ ├── SampleWebApi/ # Minimal integation demo
│ └── ShoppingWebApi/ # Realistic e-commerce walkthrough
├── docs/
│ └── api-flow.md # Full request lifecycle walkthrough
├── README.md
├── CHANGELOG.md
├── LICENSE
└── MAINTAINERS.md
Contributing
See CONTRIBUTING.md for guidelines.
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- NormaTest.AspNetCore.Abstractions (>= 1.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.