Skip to content

ANcpLua/ErrorOrX

Repository files navigation

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 - Emits fluent calls for [Authorize], [EnableRateLimiting], [OutputCache]

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}")]
    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();
}

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

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

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

Middleware attributes are automatically translated to fluent calls:

[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. For custom JSON configuration:

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.AddErrorOrEndpoints(options => options
    .UseJsonContext<AppJsonSerializerContext>()  // Register JSON context
    .WithCamelCase()                              // Use camelCase (default: true)
    .WithIgnoreNulls());                          // Ignore nulls (default: true)

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

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;

Disable generator JSON context if providing your own:

<PropertyGroup>
    <ErrorOrGenerateJsonContext>false</ErrorOrGenerateJsonContext>
</PropertyGroup>

Packages

Package Target Description
ErrorOrX.Generators netstandard2.0 Source generator (includes ErrorOrX)
ErrorOrX net10.0 Runtime library (auto-referenced)

Documentation

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages