UnionRailway 1.0.0
See the version list below for details.
dotnet add package UnionRailway --version 1.0.0
NuGet\Install-Package UnionRailway -Version 1.0.0
<PackageReference Include="UnionRailway" Version="1.0.0" />
<PackageVersion Include="UnionRailway" Version="1.0.0" />
<PackageReference Include="UnionRailway" />
paket add UnionRailway --version 1.0.0
#r "nuget: UnionRailway, 1.0.0"
#:package UnionRailway@1.0.0
#addin nuget:?package=UnionRailway&version=1.0.0
#tool nuget:?package=UnionRailway&version=1.0.0
UnionRailway
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 orUnionErrorUnionErroris 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:
NotFoundConflictUnauthorizedForbiddenValidationSystemFailure
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.
Resultis already overloaded in the .NET ecosystem- web projects already have
Results/IResultconcepts 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.AspNetCoreUnionRailway.HttpClientUnionRailway.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
mainrun the test matrix fornet8.0,net9.0, andnet11.0 - pushes to
mainrun validation only - tag pushes like
v1.2.3publish 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 | Versions 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. |
-
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.