ErrorApi.Abstractions
1.0.1
dotnet add package ErrorApi.Abstractions --version 1.0.1
NuGet\Install-Package ErrorApi.Abstractions -Version 1.0.1
<PackageReference Include="ErrorApi.Abstractions" Version="1.0.1" />
<PackageVersion Include="ErrorApi.Abstractions" Version="1.0.1" />
<PackageReference Include="ErrorApi.Abstractions" />
paket add ErrorApi.Abstractions --version 1.0.1
#r "nuget: ErrorApi.Abstractions, 1.0.1"
#:package ErrorApi.Abstractions@1.0.1
#addin nuget:?package=ErrorApi.Abstractions&version=1.0.1
#tool nuget:?package=ErrorApi.Abstractions&version=1.0.1

A Roslyn source generator that turns a Result<T> error catalog into a Minimal API error mapper and an OpenAPI document that actually lists the failures — plus a TypeScript union for the client.
Result libraries fixed the return type. They did not fix the contract: ErrorOr, FluentResults and friends map a failure to ProblemDetails at runtime, so the OpenAPI document still says 200 OK and nothing else. The frontend has no idea a 409 Orders.AlreadyPaid exists until it happens in production.
ErrorApi resolves that at compile time — and it does so whether you use its own Result<T>, ErrorOr, OneOf, language-ext, a hand-rolled discriminated union, or no result type at all: plain exceptions work too.
What it looks like
Swagger UI, POST /orders/{id}/pay — five responses, each carrying its own codes, titles and example bodies. Note the 422: two different failures share one status, and each keeps its own code.


dotnet run --project samples/Sample.Api
http://localhost:5080/swagger · /scalar · /openapi/v1.json · /openapi/errors.ts — and the same
API is built once per declaration style under samples/, each with its own README, all
producing the identical contract.
First steps
One package is the whole install — it brings the generator, the primitives and the ASP.NET Core integration (and the Swashbuckle filter on .NET 8/9):
dotnet add package ErrorApi
1. Declare the catalog — the class declares membership and the default status, the members declare the names; nothing else needs typing:
[ErrorCatalog("Orders", StatusCodes.Status404NotFound)]
public static partial class OrderErrors
{
public static partial Error NotFound { get; } // Orders.NotFound, 404
[Error(StatusCodes.Status409Conflict, Detail = "Order {0} was already paid.")]
public static partial Error AlreadyPaid(Guid orderId); // Orders.AlreadyPaid, 409
}
2. Return the entries — Error converts implicitly into Result<T>:
public Result<Order> GetById(Guid id) =>
_orders.TryGetValue(id, out var order) ? order : OrderErrors.NotFound;
3. Wire it up once — AddErrorApi() is the whole minimal setup, and every knob is a lambda on it:
builder.Services.AddOpenApi();
builder.Services.AddErrorApi(); // or AddErrorApi(x => x.AddExceptionHandler().Include(...))
app.MapGet("/orders/{id:guid}", (Guid id, IOrderService s) => s.GetById(id).ToHttpResult());
On .NET 8/9 the document comes through Swagger — the ErrorApi package already carries the filter,
so services.AddSwaggerGen(c => c.AddErrorApiResponses()); replaces AddOpenApi() and that is all.
A .NET 10 project that stays on Swagger adds ErrorApi.Swashbuckle for the identical responses,
built by the same shared code.
Already on ErrorOr, OneOf, language-ext, FluentResults, Ardalis.Result or CSharpFunctionalExtensions?
Take the matching adapter package and keep your types — often a bare [Error] on what you already
wrote is the entire onboarding. See docs/adapters.md and
docs/getting-started.md.
How it works
- The generator reads your
[Error]/[ErrorCatalog]declarations into a catalog — codes, statuses and titles inferred from what is already written (names, bodies, base constructors). - It finds every endpoint — Minimal API
Map*call sites and attribute-routed controllers — and walks each handler through the call graph: into interfaces and their implementations, past mediators via the message type, into pipeline behaviours, and across assembly boundaries through baked-in exports. - What it cannot see, it says out loud: thirteen
EAPIdiagnostics report stopped walks, drifting codes and unreachable entries at build time, instead of letting the contract lie. - It emits a reflection-free model — switch statements, no runtime scan — that one OpenAPI transformer (or the Swashbuckle filter) and a TypeScript writer render from.
- At runtime the same model maps every failure to
application/problem+jsoncarrying a stablecodemember, so the response always matches the document.
The full mechanics: docs/discovery.md · docs/catalog.md · docs/typescript.md.
Why it is worth it
- The contract stops lying. Every reachable failure is documented per endpoint, and the response body always matches — same model on both sides.
- Nothing is written twice. Codes come from names or bodies, statuses from catalogs or base constructors; duplication is what the diagnostics hunt down, not what the library asks for.
- Keep your result library. Seven adapters produce byte-identical contracts; exceptions work too.
- The client gets a compiler.
errors.tsturnscatchfolklore into an exhaustive union — add a failure server-side and the frontend build breaks instead of production. - Free at runtime. No reflection on the request path, native-AOT clean, and measured: success
costs the same as hand-written
TypedResults.Ok.
ErrorOr solves the return type, NSwag solves the client shape — neither answers "which errors does this endpoint return?". ErrorApi answers exactly that question and leans on the others for the rest.
Benchmarks
BenchmarkDotNet, raw TypedResults as the floor. One machine (x64, .NET 10):
| Mean | Allocated | |
|---|---|---|
Success path (any adapter) vs TypedResults.Ok |
4.2–5.9 ns vs 4.5 ns | 24 B vs 24 B |
Generated lookups (FindError, type switch, route switch) |
2.5–5.8 ns | 0 B |
Failure → application/problem+json |
60–81 ns | 304–328 B |
And per framework, measured together on one CI runner (Ubuntu 24.04, shared hardware — read as relative, not absolute):
| .NET | Success (any adapter) | vs raw Ok |
Lookups | Failure → problem |
|---|---|---|---|---|
| 10 | 5.5–7.3 ns / 24 B | 8.4 ns / 24 B | 2.6–4.4 ns / 0 B | 61–76 ns |
| 9 | 8.6–11.3 ns / 24 B | 7.9 ns / 24 B | 1.6–4.1 ns / 0 B | 92–130 ns |
| 8 | 9.1–12.9 ns / 48 B | 7.9 ns / 48 B | 1.8–4.7 ns / 0 B | 65–99 ns (OneOf 202 ns) |
The story holds on every framework the packages ship for: success at the floor's cost (the 48 B on
net8 is the framework's own Ok<int> box, identical for hand-written code), lookups allocation-free.
CI re-runs all three on every push and keeps the results as per-commit artifacts.
Full tables, methodology and the optimizations the first run bought: docs/performance.md · benchmarks/.
Documentation
| docs/getting-started.md | the quickstart in detail, exceptions, package-owned failure types |
| docs/catalog.md | declaring entries, inference rules, the "which attribute, when" table |
| docs/adapters.md | ErrorOr, OneOf, language-ext, FluentResults, Ardalis, CFE — and version compatibility |
| docs/discovery.md | the call-graph walk, boundaries, versioned routes, diagnostics, known limits |
| docs/typescript.md | the generated client contract |
| docs/performance.md | benchmarks and native AOT |
| docs/repository.md | layout, build & test, how this sits next to the alternatives |
| specs/ | the feature specifications: requirements and their acceptance gates |
| AGENTS.md | the map for coding agents and contributors: invariants and the checks that must pass |
MIT licensed.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. 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 is compatible. 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. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on ErrorApi.Abstractions:
| Package | Downloads |
|---|---|
|
ErrorApi.AspNetCore
Minimal API integration for ErrorApi: Result to IResult mapping, OpenAPI error responses, and a TypeScript error contract. Ships the source generator. |
GitHub repositories
This package is not used by any popular GitHub repositories.