UnionRailway 1.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package UnionRailway --version 1.0.0
                    
NuGet\Install-Package UnionRailway -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="UnionRailway" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="UnionRailway" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="UnionRailway" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add UnionRailway --version 1.0.0
                    
#r "nuget: UnionRailway, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package UnionRailway@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=UnionRailway&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=UnionRailway&version=1.0.0
                    
Install as a Cake Tool

UnionRailway

Build NuGet License: MIT Tests .NET

Native-union-ready railway programming for C#.

UnionRailway is a result-flow library for C# applications that want typed, composable failures without relying on exceptions for normal control flow.

It is intentionally shaped around the upcoming C# union model:

  • Rail<T> is the core result union: success value or UnionError
  • UnionError is a closed semantic error union
  • adapters for ASP.NET Core, HttpClient, and Entity Framework Core preserve the same error vocabulary end-to-end

Why UnionRailway instead of other C# railway/result libraries?

Most existing libraries were created to work around the absence of native unions in C#. UnionRailway takes a different path.

1. Native-union-first direction

UnionRailway is designed so its core abstractions can move naturally toward native C# unions as the language matures.

Today it uses custom union-compatible shapes:

  • Rail<T>
  • UnionError
  • preview language support for consuming those custom unions cleanly

On .NET 11, the library now uses native union declarations for those same shapes while keeping the same public API names.

This makes the library structurally aligned with the direction of the language instead of building an alternative ecosystem that ignores it.

2. Semantic errors, not generic string bags

Many result libraries expose generic failures and leave meaning to strings or open-ended metadata. UnionRailway ships with a shared semantic error model:

  • NotFound
  • Conflict
  • Unauthorized
  • Forbidden
  • Validation
  • SystemFailure

That gives application, infrastructure, and HTTP layers a common language.

Why Rail<T> instead of Result<T>?

The library intentionally avoids Result<T> as the primary type name.

  • Result is already overloaded in the .NET ecosystem
  • web projects already have Results / IResult concepts nearby
  • many teams already have their own Result<T> type

Rail<T> is short, specific to the library's purpose, and avoids those naming collisions while still reading naturally in service signatures.

3. Ecosystem adapters, not only a core type

UnionRailway is not just a Result<T>-like wrapper. It includes dedicated integrations for:

  • UnionRailway.AspNetCore
  • UnionRailway.HttpClient
  • UnionRailway.EntityFrameworkCore

This lets the same typed error flow from database access to outbound HTTP to incoming API responses.

4. RFC 7807 support out of the box

ASP.NET Core integration maps UnionError directly to ProblemDetails / ValidationProblemDetails.

return result.ToHttpResult();

5. Better migration story for existing code

Legacy exception-based code can be wrapped immediately:

var result = await UnionWrapper.RunAsync(() => service.LoadAsync());
var maybeUser = await UnionWrapper.RunNullableAsync(() => repository.FindAsync(id));

Core concepts

Rail<T>

Rail<T> represents exactly one of these outcomes:

  • a success value of type T
  • a UnionError
public async ValueTask<Rail<User>> GetUserAsync(int id)
{
    var user = await db.Users.FirstOrDefaultAsUnionAsync("User", x => x.Id == id);
    return user;
}

You can still construct results explicitly:

return Union.Ok(user);
return Union.Fail<User>(new UnionError.NotFound("User"));

UnionError

UnionError is the shared error contract across the whole library.

UnionError notFound = new UnionError.NotFound("User");
UnionError validation = UnionError.CreateValidation([
    ("Email", ["Invalid format"]),
    ("Password", ["Required"])
]);

Consume it with pattern matching on the wrapped value:

var message = error.Value switch
{
    UnionError.NotFound nf      => $"Missing: {nf.Resource}",
    UnionError.Conflict c       => $"Conflict: {c.Reason}",
    UnionError.Unauthorized     => "Authentication required",
    UnionError.Forbidden f      => $"Forbidden: {f.Reason}",
    UnionError.Validation v     => $"Validation: {v.Fields.Count} fields",
    UnionError.SystemFailure sf => sf.Ex.Message,
    _                           => "Unknown error"
};

Railway composition

UnionRailway supports both pragmatic early-return code and railway-style chaining.

Early return

var result = await service.GetUserAsync(id);

if (!result.IsSuccess(out var user, out var error))
    return error.GetValueOrDefault().ToHttpResult();

return Results.Ok(user);

Match

return result.Match(
    onOk: user => Results.Ok(user),
    onError: error => error.ToHttpResult());

Map and Bind

var result = Union.Ok(5)
    .Map(x => x * 2)
    .Bind(x => x > 5
        ? Union.Ok($"value={x}")
        : Union.Fail<string>(new UnionError.Conflict("Too small")));

Async Task<Rail<T>> and ValueTask<Rail<T>> composition

UnionRailway also provides first-class async extensions so callers do not need a separate TaskRail<T> wrapper type.

var result = await service.GetUserAsync(id)
    .BindAsync(user => service.GetOrdersAsync(user.Id))
    .MapAsync(orders => orders.Count);

var httpResult = await service.GetUserAsync(id).ToHttpResultAsync();

.NET 11 native union direction

When targeting .NET 11, the library uses native union declarations for its core types. Conceptually, the shapes are:

public union Rail<T>(T, UnionError);

public union UnionError(
    UnionError.NotFound,
    UnionError.Conflict,
    UnionError.Unauthorized,
    UnionError.Forbidden,
    UnionError.Validation,
    UnionError.SystemFailure);

That keeps UnionRailway aligned with the language instead of building a permanently separate abstraction model.


Adapters

ASP.NET Core

app.MapGet("/users/{id:int}", async (int id, UserService service) =>
{
    var result = await service.GetUserAsync(id);
    return result.ToHttpResult();
});

Entity Framework Core

public ValueTask<Rail<User>> GetUserAsync(int id, CancellationToken ct = default) =>
    db.Users.FirstOrDefaultAsUnionAsync("User", x => x.Id == id, ct);

HttpClient

var result = await http.GetFromJsonAsUnionAsync<UserDto>("/users/42", ct);

Release automation

The repository includes a GitHub Actions workflow at .github/workflows/ci.yml.

  • pull requests to main run the test matrix for net8.0, net9.0, and net11.0
  • pushes to main run validation only
  • tag pushes like v1.2.3 publish stable NuGet versions
  • manual runs require an explicit package version through workflow_dispatch

To publish to NuGet.org, configure this repository secret:

  • NUGET_API_KEY

Packages

UnionRailway

Core result type, error type, helpers, and legacy migration wrappers.

UnionRailway.AspNetCore

Rail<T> / UnionError to IResult conversion with RFC 7807 mapping.

UnionRailway.HttpClient

HTTP and problem-details responses to Rail<T> conversion.

UnionRailway.EntityFrameworkCore

EF Core queries and save operations to Rail<T> conversion.


Current implementation note

The project now multi-targets .NET 8, .NET 9, and .NET 11. Preview language support remains enabled so the custom union model can be consumed consistently across all targets. The current .NET 11 preview still requires the temporary union runtime polyfill declared in this repository; that polyfill can be removed once the runtime ships those types.

Product Compatible and additional computed target framework versions.
.NET 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 is compatible.  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 was computed.  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.  net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net11.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on UnionRailway:

Package Downloads
UnionRailway.AspNetCore

ASP.NET Core integration for UnionRailway with RFC 7807 ProblemDetails mapping for Rail<T> and UnionError.

UnionRailway.HttpClient

HttpClient integration for UnionRailway that maps HTTP status codes and RFC 7807 payloads into Rail<T> results.

UnionRailway.EntityFrameworkCore

Entity Framework Core integration for UnionRailway with typed query and persistence results built on Rail<T> and UnionError.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.2.1 158 5/5/2026
1.2.0 182 4/29/2026
1.1.0 161 4/25/2026
1.0.0 153 4/6/2026