Skip to main content

ErrorOrX by Alexander Nachtmanns

NuGet / site data​

Nuget GitHub last commit GitHub Repo stars

Details​

Info​

info

Name: ErrorOrX

A discriminated union type for .NET with source-generated ASP.NET Core Minimal API integration. Zero boilerplate, full AOT support.

Author: Alexander Nachtmanns

NuGet: https://www.nuget.org/packages/ErrorOrX/

You can find more details at https://github.com/ANcpLua/ErrorOrX

Source: https://github.com/ANcpLua/ErrorOrX

Author​

note

Alexander Nachtmanns Alt text

Original Readme​

note

ErrorOrX​

NuGet NuGet Downloads License: MIT

Railway-Oriented Programming for .NET with source-generated ASP.NET Core Minimal API integration. Zero boilerplate, full Native AOT support.

Features​
  • Discriminated Unions - ErrorOr<T> represents success or a list of typed errors
  • Fluent API - Chain operations with Then, Else, Match, Switch, and FailIf
  • Nullable Extensions - Convert nullable values with OrNotFound(), OrValidation(), and more
  • Source Generator - Auto-generates MapErrorOrEndpoints() from attributed static methods
  • Smart Binding - Automatic parameter inference based on HTTP method and type
  • OpenAPI Ready - Typed Results<...> unions for complete API documentation
  • Native AOT - Reflection-free code generation with JSON serialization contexts
  • Middleware Support - Translates ASP.NET Core attributes to Minimal API fluent calls (authorization, rate limiting, caching)
  • API Versioning - Integrates with Asp.Versioning.Http for versioned endpoint groups
  • 41 Analyzers - Real-time IDE feedback for route conflicts, binding errors, AOT compatibility
What the Generator Produces​

The source generator transforms your handler methods into complete ASP.NET Core Minimal API endpoints. You write the business logic, the generator handles everything else.

######### Endpoint Wiring

For each [Get], [Post], [Put], [Delete], [Patch] method:

  • Registers endpoint with app.MapGet(), app.MapPost(), etc.
  • Applies route constraints ({id:guid}, {count:int})
  • Sets operation name (.WithName()) and tags (.WithTags())

See EndpointMetadataEmitter.cs

######### Parameter Binding

Automatic inference based on type and HTTP method:

SourceInference Rule
RouteParameter name matches {param} in route
QueryPrimitive type not in route
BodyPOST/PUT/PATCH with complex type
ServiceInterface, abstract, or DI naming pattern (*Service, *Repository)
SpecialHttpContext, CancellationToken, IFormFile

See BindingCodeEmitter.cs

######### Error-to-HTTP Mapping

Converts ErrorOr errors to proper HTTP responses with RFC 7807 ProblemDetails:

ErrorTypeHTTP StatusResponse
Validation400ValidationProblem with field errors
Unauthorized401Unauthorized()
Forbidden403Forbid()
NotFound404NotFound<ProblemDetails>
Conflict409Conflict<ProblemDetails>
Failure500InternalServerError<ProblemDetails>
Unexpected500InternalServerError<ProblemDetails>
Custom(422)422UnprocessableEntity<ProblemDetails>

See ErrorMapping.cs

######### Request Validation

Generated code validates before calling your handler:

  • Required parameters (returns 400 if missing)
  • Type parsing (Guid, int, etc. with format errors)
  • JSON deserialization (catches JsonException)
  • Content-Type checking (returns 415 for wrong type)

######### OpenAPI Metadata

Full OpenAPI documentation without manual attributes:

  • Response types via ProducesResponseTypeMetadata
  • Accept types via AcceptsMetadata
  • Tags from class name
  • Operation IDs from method name
  • XML doc comments extracted to summaries

See OpenApiTransformerGenerator.cs

######### Builder API

Fluent configuration following ASP.NET Core patterns:

builder.Services.AddErrorOrEndpoints()
.UseJsonContext<AppJsonSerializerContext>() // AOT JSON
.WithCamelCase() // Property naming
.WithIgnoreNulls(); // Skip null values

app.MapErrorOrEndpoints()
.RequireAuthorization() // Global auth
.RequireRateLimiting("api"); // Global rate limit

######### Analyzers (38 Diagnostics)

Real-time IDE feedback covering:

CategoryDiagnosticsExamples
CoreEOE001-007Invalid return type, non-static handler, unbound route param
BindingEOE008-021Multiple body sources, invalid [FromRoute] type, ambiguous binding
ResultsEOE022-024Too many result types, unknown error factory, undocumented interface
AOT/JSONEOE025-026Missing camelCase, missing JsonSerializerContext
VersioningEOE027-031Version-neutral conflict, undeclared version, invalid format
NamingEOE032-033Duplicate route binding, non-PascalCase handler
AOT SafetyEOE034-038Activator.CreateInstance, dynamic, Expression.Compile()

See Descriptors.cs

Installation​
dotnet add package ErrorOrX.Generators

This package includes both the source generator and the ErrorOrX runtime library.

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:guid}")]
public static ErrorOr<Todo> GetById(Guid 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:guid}")]
public static ErrorOr<Deleted> Delete(Guid id, ITodoService svc)
=> svc.Delete(id) ? Result.Deleted : Error.NotFound();
}
Error Types​

Create structured errors mapped to HTTP status codes:

Error.Validation("User.InvalidEmail", "Email format is invalid")   // 400
Error.Unauthorized("Auth.InvalidToken", "Token has expired") // 401
Error.Forbidden("Auth.InsufficientRole", "Admin role required") // 403
Error.NotFound("User.NotFound", "User does not exist") // 404
Error.Conflict("User.Duplicate", "Email already registered") // 409
Error.Failure("Db.ConnectionFailed", "Database unavailable") // 500
Error.Unexpected("Unknown", "An unexpected error occurred") // 500
Error.Custom(422, "Validation.Complex", "Complex validation failed")
Nullable-to-ErrorOr Extensions​

Convert nullable values to ErrorOr<T> with auto-generated error codes:

// Error code auto-generated 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()); // Lazy evaluation
ExtensionError TypeHTTPDescription
.OrNotFound()NotFound404Resource not found
.OrValidation()Validation400Input validation failed
.OrUnauthorized()Unauthorized401Authentication required
.OrForbidden()Forbidden403Insufficient permissions
.OrConflict()Conflict409State conflict
.OrFailure()Failure500Operational failure
.OrUnexpected()Unexpected500Unexpected error
.OrError(Error)AnyAnyCustom error
.OrError(Func)AnyAnyLazy custom error
Fluent API​

Chain operations using railway-oriented programming patterns:

// Chain operations - errors short-circuit the pipeline
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​

Use semantic markers for endpoints without response bodies:

Result.Success   // 200 OK (no body)
Result.Created // 201 Created (no body)
Result.Updated // 204 No Content
Result.Deleted // 204 No Content
Interface Types with [ReturnsError]​

Document possible errors on interface methods for OpenAPI generation:

public interface ITodoService
{
[ReturnsError(ErrorType.NotFound, "Todo.NotFound")]
[ReturnsError(ErrorType.Validation, "Todo.Invalid")]
ErrorOr<Todo> GetById(Guid id);
}

[Get("/todos/{id:guid}")]
public static ErrorOr<Todo> GetById(Guid id, ITodoService svc) =>
svc.GetById(id);
// Generates: Results<Ok<Todo>, NotFound<ProblemDetails>, ValidationProblem>

The generator reads [ReturnsError] attributes from interface/abstract methods to build the complete Results<...> union for OpenAPI documentation.

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:guid}")]
public static ErrorOr<Todo> GetById(
Guid id, // -> Route (matches {id})
ITodoService svc) // -> Service
=> svc.GetById(id).OrNotFound();
Middleware Attributes​

Standard ASP.NET Core attributes on your handler methods are translated to Minimal API fluent calls:

[Post("/admin")]
[Authorize("Admin")] // ASP.NET Core authorization
[EnableRateLimiting("fixed")] // Microsoft.AspNetCore.RateLimiting
[OutputCache(Duration = 60)] // ASP.NET Core output caching
public static ErrorOr<User> CreateAdmin(CreateUserRequest req) \{ }

// Generator emits:
// app.MapPost("/admin", handler)
// .RequireAuthorization("Admin")
// .RequireRateLimiting("fixed")
// .CacheOutput(policy => policy.Expire(TimeSpan.FromSeconds(60)));
Native AOT​

Fully compatible with PublishAot=true. Create a JsonSerializerContext with your endpoint types:

[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
[JsonSerializable(typeof(Todo))]
[JsonSerializable(typeof(CreateTodoRequest))]
[JsonSerializable(typeof(ProblemDetails))]
internal partial class AppJsonSerializerContext : JsonSerializerContext;

Register it with ErrorOrEndpoints:

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.AddErrorOrEndpoints()
.UseJsonContext<AppJsonSerializerContext>(); // Uses options from [JsonSourceGenerationOptions]

var app = builder.Build();
app.MapErrorOrEndpoints();
app.Run();

The [JsonSourceGenerationOptions] on your context controls serialization behavior (camelCase, null handling). The builder methods WithCamelCase() and WithIgnoreNulls() are only needed if you want to override at runtime.

Packages​
PackageTargetDescription
ErrorOrX.Generatorsnetstandard2.0Source generator (includes ErrorOrX)
ErrorOrXnet10.0Runtime library (auto-referenced)
Changelog​

See CHANGELOG.md for version history.

About​

note

API results from Functional returns of ErroOrX

How to use​

Example (source csproj, source files)​

This is the CSharp Project that references ErrorOrX

<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ErrorOrX" Version="3.5.0" />
<PackageReference Include="ErrorOrX.Generators" Version="3.5.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" />
<PackageReference Include="OpenAPISwaggerUI" Version="9.2024.1215.2209" />
</ItemGroup>

<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
</Project>

Generated Files​

Those are taken from $(BaseIntermediateOutputPath)\GX

// <auto-generated/>
#nullable enable

namespace ErrorOr
{
/// <summary>
/// Marks a static method as an ErrorOr endpoint with explicit HTTP method and route.
/// Prefer using [Get], [Post], [Put], [Delete], or [Patch] for standard HTTP methods.
/// </summary>
[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false)]
public sealed class ErrorOrEndpointAttribute : global::System.Attribute
{
public ErrorOrEndpointAttribute(string httpMethod, string route)
{
HttpMethod = httpMethod;
Route = route;
}
public string HttpMethod \{ get; }
public string Route \{ get; }
}

[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false)]
public sealed class GetAttribute : global::System.Attribute
{
public GetAttribute(string route) => Route = route;
public string Route \{ get; }
}

[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false)]
public sealed class PostAttribute : global::System.Attribute
{
public PostAttribute(string route) => Route = route;
public string Route \{ get; }
}

[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false)]
public sealed class PutAttribute : global::System.Attribute
{
public PutAttribute(string route) => Route = route;
public string Route \{ get; }
}

[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false)]
public sealed class DeleteAttribute : global::System.Attribute
{
public DeleteAttribute(string route) => Route = route;
public string Route \{ get; }
}

[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false)]
public sealed class PatchAttribute : global::System.Attribute
{
public PatchAttribute(string route) => Route = route;
public string Route \{ get; }
}

[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
public sealed class ProducesErrorAttribute : global::System.Attribute
{
public ProducesErrorAttribute(int statusCode, string errorType)
{
StatusCode = statusCode;
ErrorType = errorType;
}
public int StatusCode \{ get; }
public string ErrorType \{ get; }
}

[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false)]
public sealed class AcceptedResponseAttribute : global::System.Attribute \{ }

[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
public sealed class ReturnsErrorAttribute : global::System.Attribute
{
public ReturnsErrorAttribute(global::ErrorOr.ErrorType errorType, string errorCode)
{
ErrorType = errorType;
ErrorCode = errorCode;
}
public ReturnsErrorAttribute(int statusCode, string errorCode)
{
StatusCode = statusCode;
ErrorCode = errorCode;
ErrorType = null;
}
public global::ErrorOr.ErrorType? ErrorType \{ get; }
public int? StatusCode \{ get; }
public string ErrorCode \{ get; }
}

/// <summary>
/// Marks a class as a route group for versioned API endpoints.
/// All endpoints in the class will be mapped under the specified path prefix
/// using the eShop-style NewVersionedApi() pattern when combined with [ApiVersion].
/// </summary>
[global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false)]
public sealed class RouteGroupAttribute : global::System.Attribute
{
public RouteGroupAttribute(string path) => Path = path;
public string Path \{ get; }
public string? ApiName \{ get; set; }
}
}

Useful​

Download Example (.NET C#)​

Share ErrorOrX​

https://ignatandrei.github.io/RSCG_Examples/v2/docs/ErrorOrX

Category "API" has the following generators:​

1 ErrorOrX Nuget GitHub Repo stars 2026-02-02

2 immediate.apis Nuget GitHub Repo stars 2025-03-27

3 Microsoft.Extensions.Configuration.Binder Nuget GitHub Repo stars 2023-11-18

4 MinimalApiBuilder Nuget GitHub Repo stars 2023-10-26

5 MinimalApis.Discovery Nuget GitHub Repo stars 2024-04-16

6 MinimalHelpers.Routing.Analyzers Nuget GitHub Repo stars 2024-10-21

7 RDG Nuget GitHub Repo stars 2023-11-19

8 Refit Nuget GitHub Repo stars 2023-07-31

9 RSCG_WebAPIExports Nuget GitHub Repo stars 2023-08-23

10 SafeRouting Nuget GitHub Repo stars 2023-09-23

11 SkinnyControllersCommon Nuget GitHub Repo stars 2023-04-16

12 XmlCommentGenerator Nuget GitHub Repo stars 2025-11-09

See category​

API