Railway-Oriented Programming for .NET with source-generated ASP.NET Core Minimal API integration. Zero boilerplate, full Native AOT support.
- Discriminated Unions —
ErrorOr<T>represents success or a list of typed errors - Fluent API —
Then,Else,Match,Switch,FailIf - Nullable Extensions —
OrNotFound(),OrValidation(), … - 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 Pass-Through —
[Authorize],[EnableRateLimiting],[OutputCache],[EnableCors]→ fluent calls - API Versioning — Integrates with
Asp.Versioning.Httpfor versioned endpoint groups - 36 Analyzers — Real-time IDE feedback for route conflicts, binding errors, AOT compatibility
For the full mental model of what your code becomes after the generator runs, see CLAUDE.md.
dotnet add package ErrorOrX.GeneratorsIncludes the ErrorOrX runtime as a transitive dependency.
// Program.cs
var app = WebApplication.CreateSlimBuilder(args).Build();
app.MapErrorOrEndpoints();
app.Run();// TodoApi.cs
[Get("/api/todos/{id:guid}")]
public static Task<ErrorOr<Todo>> GetById(Guid id, ITodoService svc, CancellationToken ct)
=> svc.GetByIdAsync(id, ct);Full working example: samples/ErrorOrX.Samples.Api/Program.cs +
TodoApi.cs.
Errors map to RFC 9457 ProblemDetails:
| ErrorType | HTTP | TypedResult |
|---|---|---|
| Validation | 400 | ValidationProblem with field errors |
| Unauthorized | 401 | UnauthorizedHttpResult |
| Forbidden | 403 | ForbidHttpResult |
| NotFound | 404 | NotFound<ProblemDetails> |
| Conflict | 409 | Conflict<ProblemDetails> |
| Failure | 500 | InternalServerError<ProblemDetails> |
| Unexpected | 500 | InternalServerError<ProblemDetails> |
| Custom(422) | 422 | UnprocessableEntity<ProblemDetails> |
Source: ErrorMapping.cs.
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")Error code is auto-generated from the type name (e.g., Todo.NotFound).
| 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 |
Usage in context: Domain/TodoService.cs.
Chain operations railway-style — errors short-circuit the pipeline. Each operator has a worked endpoint in the sample:
| Operator | Sample |
|---|---|
Then/ThenAsync |
AdvancedErrorHandlingApi.cs:10-12 |
FailIf |
AdvancedErrorHandlingApi.cs:17-21 (single) · :41-45 (chained) |
Else |
AdvancedErrorHandlingApi.cs:25-26 |
Match |
AdvancedErrorHandlingApi.cs:31-34 |
Switch |
AdvancedErrorHandlingApi.cs:93-97 |
For endpoints without a response body:
Result.Success // 200 OK
Result.Created // 201 Created
Result.Updated // 204 No Content
Result.Deleted // 204 No ContentDocument possible errors on interface methods — the generator reads them when building the Results<…> union for
OpenAPI. See Domain/ITodoService.cs for five attribute usages
spanning Failure, NotFound, and Validation.
Standard ASP.NET Core attributes on handlers are translated to Minimal API fluent
calls. The generator also auto-adds 401/403 (for [Authorize]) and 429
(for [EnableRateLimiting]) ProducesResponseTypeMetadata so OpenAPI documents
the failure modes:
| Attribute | Emitted fluent call |
|---|---|
[Authorize(...)] |
.RequireAuthorization(...) |
[EnableRateLimiting] |
.RequireRateLimiting(...) |
[OutputCache] |
.CacheOutput(p => p.Expire(TimeSpan.From...)) |
[EnableCors] |
.RequireCors(...) |
Four worked endpoints (one per attribute, including stacked combinations):
samples/ErrorOrX.Samples.Api/AdminApi.cs.
Service wiring (AddAuthorizationBuilder, AddRateLimiter, AddOutputCache,
AddCors) lives in
Program.cs.
Fully compatible with PublishAot=true. Define a JsonSerializerContext covering your request/response/problem types:
[JsonSerializable(typeof(Todo))]
[JsonSerializable(typeof(CreateTodoRequest))]
[JsonSerializable(typeof(ProblemDetails))]
[JsonSerializable(typeof(HttpValidationProblemDetails))]
internal sealed partial class AppJsonSerializerContext : JsonSerializerContext;Wire it via the builder — WithCamelCase() / WithIgnoreNulls() override at runtime, or set them once on the context
via [JsonSourceGenerationOptions]:
builder.Services.AddErrorOrEndpoints()
.UseJsonContext<AppJsonSerializerContext>()
.WithCamelCase()
.WithIgnoreNulls();Working setup: AppJsonSerializerContext.cs +
Program.cs.
| Category | Diagnostics | Examples |
|---|---|---|
| Core | EOE001-006 | Invalid return type, non-static handler, unbound route param |
| Binding | EOE008-021 | Multiple body sources, invalid [FromRoute] type, ambiguous binding |
| Results | EOE022-024 | Too many result types, unknown error factory, undocumented interface |
| AOT/JSON | EOE007, EOE025-026, EOE034-036 | Not AOT-serializable, missing camelCase, missing context, validation reflection |
| Versioning | EOE027-031 | Version-neutral conflict, undeclared version, invalid format |
| Naming | EOE032-033 | Duplicate route binding, non-PascalCase handler |
Source: Descriptors.*.cs. Diagnostics-in-action walkthrough:
samples/ErrorOrX.Samples.Diagnostics/README.md.
| Package | Target | Description |
|---|---|---|
ErrorOrX.Generators |
netstandard2.0 |
Source generator (pulls in ErrorOrX) |
ErrorOrX |
net10.0 |
Runtime library (auto-referenced) |
See CHANGELOG.md.