ErrorOrX 2.6.2
See the version list below for details.
dotnet add package ErrorOrX --version 2.6.2
NuGet\Install-Package ErrorOrX -Version 2.6.2
<PackageReference Include="ErrorOrX" Version="2.6.2" />
<PackageVersion Include="ErrorOrX" Version="2.6.2" />
<PackageReference Include="ErrorOrX" />
paket add ErrorOrX --version 2.6.2
#r "nuget: ErrorOrX, 2.6.2"
#:package ErrorOrX@2.6.2
#addin nuget:?package=ErrorOrX&version=2.6.2
#tool nuget:?package=ErrorOrX&version=2.6.2
ErrorOrX
Discriminated unions for .NET with source-generated ASP.NET Core Minimal API integration. Zero boilerplate, full AOT support.
Installation
dotnet add package ErrorOrX.Generators
Quick Start
// Program.cs
var app = WebApplication.CreateSlimBuilder(args).Build();
app.MapErrorOrEndpoints();
app.Run();
// TodoApi.cs
using ErrorOr;
public static class TodoApi
{
[Get("/todos/{id}")]
public static ErrorOr<Todo> GetById(int id, ITodoService svc)
=> svc.GetById(id).OrNotFound($"Todo {id} not found");
[Post("/todos")]
public static ErrorOr<Todo> Create(CreateTodoRequest req, ITodoService svc)
=> svc.Create(req); // 201 Created
[Delete("/todos/{id}")]
public static ErrorOr<Deleted> Delete(int id, ITodoService svc)
=> svc.Delete(id) ? Result.Deleted : Error.NotFound();
}
Nullable-to-ErrorOr Extensions
Convert nullable values to ErrorOr<T> with fluent extensions:
// Auto-generates error code from type name (e.g., "Todo.NotFound")
return _todos.Find(t => t.Id == id).OrNotFound($"Todo {id} not found");
return user.OrUnauthorized("Invalid credentials");
return record.OrValidation("Record is invalid");
// Custom errors
return value.OrError(Error.Custom(422, "Custom.Code", "Custom message"));
return value.OrError(() => BuildExpensiveError());
| Extension | Error Type | HTTP | Description |
|---|---|---|---|
.OrNotFound() |
NotFound | 404 | Resource not found |
.OrValidation() |
Validation | 400 | Input validation failed |
.OrUnauthorized() |
Unauthorized | 401 | Authentication required |
.OrForbidden() |
Forbidden | 403 | Insufficient permissions |
.OrConflict() |
Conflict | 409 | State conflict |
.OrFailure() |
Failure | 500 | Operational failure |
.OrUnexpected() |
Unexpected | 500 | Unexpected error |
.OrError(Error) |
Any | Any | Custom error |
.OrError(Func) |
Any | Any | Lazy custom error |
Error Types
Error.Validation("User.InvalidEmail", "Email format is invalid")
Error.NotFound("User.NotFound", "User does not exist")
Error.Conflict("User.Duplicate", "Email already registered")
Error.Unauthorized("Auth.InvalidToken", "Token has expired")
Error.Forbidden("Auth.InsufficientRole", "Admin role required")
Error.Failure("Db.ConnectionFailed", "Database unavailable")
Error.Unexpected("Unknown", "An unexpected error occurred")
Error.Custom(422, "Validation.Complex", "Complex validation failed")
| Factory | HTTP | Use Case |
|---|---|---|
Error.Validation() |
400 | Input/request validation |
Error.Unauthorized() |
401 | Authentication required |
Error.Forbidden() |
403 | Insufficient permissions |
Error.NotFound() |
404 | Resource doesn't exist |
Error.Conflict() |
409 | State conflict (duplicate) |
Error.Failure() |
500 | Known operational failure |
Error.Unexpected() |
500 | Unhandled/unknown error |
Error.Custom() |
Any | Custom HTTP status code |
Fluent API
// Chain operations (railway-oriented programming)
var result = ValidateOrder(request)
.Then(order => ProcessPayment(order))
.Then(order => CreateShipment(order))
.FailIf(order => order.Total <= 0, Error.Validation("Order.InvalidTotal", "Total must be positive"));
// Handle both cases
return result.Match(
order => Ok(order),
errors => BadRequest(errors.First().Description));
// Provide fallback on error
var user = GetUser(id).Else(errors => DefaultUser);
// Side effects
GetUser(id).Switch(
user => Console.WriteLine($"Found: {user.Name}"),
errors => Logger.LogError(errors.First().Description));
Result Markers
Result.Success // 200 OK (no body)
Result.Created // 201 Created (no body)
Result.Updated // 204 No Content
Result.Deleted // 204 No Content
Smart Parameter Binding
The generator automatically infers parameter sources:
[Post("/todos")]
public static ErrorOr<Todo> Create(
CreateTodoRequest req, // -> Body (POST + complex type)
ITodoService svc) // -> Service (interface)
=> svc.Create(req);
[Get("/todos/{id}")]
public static ErrorOr<Todo> GetById(
int id, // -> Route (matches {id})
ITodoService svc) // -> Service
=> svc.GetById(id).OrNotFound();
Middleware Attributes
[Post("/admin")]
[Authorize("Admin")]
[EnableRateLimiting("fixed")]
[OutputCache(Duration = 60)]
public static ErrorOr<User> CreateAdmin(CreateUserRequest req) { }
// Generates: .RequireAuthorization("Admin").RequireRateLimiting("fixed").CacheOutput(...)
Native AOT
Fully compatible with PublishAot=true. The generator produces reflection-free code that works seamlessly with Native
AOT compilation.
Zero-Config (Recommended)
Add one line - the generator auto-discovers all types from your endpoints:
// Add this anywhere in your project
[assembly: ErrorOr.AotJsonAssembly]
Or for more control, decorate your own context:
[AotJson] // Auto-generates [JsonSerializable] for all endpoint types
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
internal partial class AppJsonSerializerContext : JsonSerializerContext;
Program.cs Setup
Use the fluent builder for configuration:
var builder = WebApplication.CreateSlimBuilder(args);
// Fluent configuration - CamelCase and IgnoreNulls enabled by default
builder.Services.AddErrorOrEndpoints(options => options
.UseJsonContext<AppJsonSerializerContext>());
var app = builder.Build();
app.MapErrorOrEndpoints();
app.Run();
Available Options
services.AddErrorOrEndpoints(options => options
.UseJsonContext<AppJsonSerializerContext>() // Register JSON context for AOT
.WithCamelCase() // Use camelCase naming (default: true)
.WithIgnoreNulls()); // Ignore null values (default: true)
AotJson Attribute Options
[AotJson(
ScanEndpoints = true, // Discover types from ErrorOr endpoints (default: true)
IncludeProblemDetails = true, // Include ProblemDetails types (default: true)
TraversePropertyTypes = true, // Discover nested types from properties (default: true)
GenerateCollections = CollectionKind.List | CollectionKind.Array, // Collection variants
IncludeTypes = new[] { typeof(CustomType) }, // Explicit includes
ExcludeTypes = new[] { typeof(InternalType) } // Explicit excludes
)]
internal partial class AppJsonSerializerContext : JsonSerializerContext;
How It Works
The generator emits AOT-compatible endpoint handlers using a wrapper pattern:
- Typed Map methods - Uses
MapGet,MapPost, etc. instead ofMapMethodswith delegate cast - ExecuteAsync pattern - Handlers return
Taskand explicitly write responses viaIResult.ExecuteAsync() - No reflection - All routing and serialization uses compile-time generated code
This design avoids the reflection-based RequestDelegateFactory path that requires JSON metadata for
Task<Results<...>> types, which cannot be satisfied for BCL generic types at compile time.
Documentation
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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. |
-
net10.0
- Microsoft.Sbom.Targets (>= 4.1.5)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on ErrorOrX:
| Package | Downloads |
|---|---|
|
ErrorOrX.Generators
Roslyn source generator for ASP.NET Core Minimal API integration with ErrorOrX. Auto-generates MapErrorOrEndpoints() with typed Results unions for OpenAPI, smart parameter binding (body/route/query/service inference), middleware attribute emission, and JSON serialization context. Full Native AOT support. |
GitHub repositories
This package is not used by any popular GitHub repositories.