NormaTest.AspNetCore 1.0.0

There is a newer version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NormaTest.AspNetCore" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="NormaTest.AspNetCore" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="NormaTest.AspNetCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add NormaTest.AspNetCore --version 1.0.0
                    
#r "nuget: NormaTest.AspNetCore, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package NormaTest.AspNetCore@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=NormaTest.AspNetCore&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=NormaTest.AspNetCore&version=1.0.0
                    
Install as a Cake Tool

Norma.AspNetCore

Consistent API responses and structured error handling for ASP.NET Core — in two lines.

NuGet Build License: MIT


Overview

Norma.AspNetCore solves two problems that every ASP.NET Core API team solves from scratch:

  1. Consistent success responses — wraps every controller response in a typed ApiEnvelope<T> with metadata (request ID, timestamp, version, pagination) — without touching a single controller.
  2. Structured error responses — maps all unhandled exceptions to RFC 7807 Problem Details JSON automatically. Throw a KeyNotFoundException, get a clean 404. Throw an ArgumentException, 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

// 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

MIT

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.12 144 3/11/2026
1.0.8 119 3/8/2026
1.0.7 110 3/8/2026
1.0.6 116 3/8/2026
1.0.0 111 3/7/2026