UnionRailway 1.2.1

dotnet add package UnionRailway --version 1.2.1
                    
NuGet\Install-Package UnionRailway -Version 1.2.1
                    
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.2.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="UnionRailway" Version="1.2.1" />
                    
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.2.1
                    
#r "nuget: UnionRailway, 1.2.1"
                    
#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.2.1
                    
#: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.2.1
                    
Install as a Cake Addin
#tool nuget:?package=UnionRailway&version=1.2.1
                    
Install as a Cake Tool

UnionRailway

<div align="center">

Build Tests NuGet Downloads License: MIT .NET

Type-safe error handling for C# that actually feels native.

Quick Start ยท Why This? ยท Benchmarks ยท Full Docs

</div>


๐ŸŽฏ The Problem

Your error handling probably looks like this:

public async Task<IResult> GetUser(int id)
{
    try
    {
        var user = await db.Users.FindAsync(id);
        if (user == null)
            return Results.NotFound();

        return Results.Ok(user);
    }
    catch (UnauthorizedAccessException)
    {
        return Results.Unauthorized();
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "Failed to get user");
        return Results.Problem("Something went wrong");
    }
}

Problems:

  • โŒ Exceptions for control flow (slow, unclear)
  • โŒ No type safety (caller doesn't know what errors to expect)
  • โŒ Inconsistent error responses across APIs
  • โŒ Manual mapping everywhere

โœจ The Solution

With UnionRailway, it becomes:

public async ValueTask<Rail<User>> GetUserAsync(int id)
{
    return await db.Users.FirstOrDefaultAsUnionAsync("User", x => x.Id == id);
}

// In your endpoint:
app.MapGet("/users/{id:int}", async (int id, UserService service) =>
{
    var result = await service.GetUserAsync(id);
    return result.ToHttpResult(); // โœ… Automatic RFC 7807 Problem Details
});

Benefits:

  • โœ… Type-safe - Compiler knows all possible errors
  • โœ… Zero boilerplate - One line to return RFC 7807 responses
  • โœ… Fast - 0.5ns success path, zero allocations
  • โœ… Consistent - Same error vocabulary everywhere (DB โ†’ Service โ†’ HTTP)

๐Ÿš€ Quick Start (2 Minutes)

1. Install

dotnet add package UnionRailway
dotnet add package UnionRailway.AspNetCore
dotnet add package UnionRailway.EntityFrameworkCore  # Optional

2. Return Rail<T> from your services

public class UserService
{
    public async ValueTask<Rail<User>> GetUserAsync(int id)
    {
        var user = await db.Users.FindAsync(id);
        if (user == null)
            return new UnionError.NotFound("User");

        return user; // โœ… Implicit conversion
    }
}

3. Convert to HTTP responses

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

Or even simpler with the endpoint filter โ€” no .ToHttpResult() needed:

var api = app.MapGroup("/api").WithRailwayFilter();

api.MapGet("/users/{id}", async (int id, UserService svc) =>
    await svc.GetUserAsync(id));  // Returns Rail<User> directly!

That's it! You now have:

  • โœ… Automatic 404 for NotFound
  • โœ… RFC 7807 Problem Details
  • โœ… OpenAPI documentation
  • โœ… Type-safe error handling

๐ŸŽญ Why UnionRailway?

vs. Exceptions

// โŒ Exceptions: Slow, unclear, unsafe
try { var user = await repo.GetAsync(id); }
catch (NotFoundException) { return Results.NotFound(); }
catch (UnauthorizedException) { return Results.Unauthorized(); }
// What other exceptions can this throw? ๐Ÿคท

// โœ… UnionRailway: Fast, explicit, type-safe
var result = await repo.GetAsync(id);
return result.ToHttpResult();
// Compiler knows: Success | NotFound | Unauthorized โœ…

vs. ErrorOr / LanguageExt

// โŒ Other libraries: Manual mapping, no ecosystem
ErrorOr<User> result = await repo.GetAsync(id);
return result.Match(
    value => Results.Ok(value),
    errors => errors[0].Type switch {
        ErrorType.NotFound => Results.NotFound(),
        ErrorType.Unauthorized => Results.Unauthorized(),
        _ => Results.Problem() // ๐Ÿ˜ฐ Manual everywhere
    });

// โœ… UnionRailway: Automatic RFC 7807, built-in integrations
var result = await repo.GetAsync(id);
return result.ToHttpResult(); // โœจ Done!

๐Ÿ“Š Full comparison with LanguageExt, ErrorOr, OneOf, FluentResults โ†’


๐ŸŽ What You Get

1. Semantic Error Types (not strings)

UnionError error = result.Error.GetValueOrDefault();

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     => $"{v.Fields.Count} validation errors",
    UnionError.SystemFailure sf => sf.Ex.Message,
    UnionError.Custom c         => $"{c.Code}: {c.Message}",
    _                           => "Unknown"
};
Custom Domain Errors

Don't let the predefined categories limit you. UnionError.Custom lets you represent application-specific error conditions with a machine-readable code, custom HTTP status, and optional metadata:

// Return a domain-specific error
return new UnionError.Custom(
    Code: "RATE_LIMIT_EXCEEDED",
    Message: "Too many requests, please retry later.",
    StatusCode: 429,
    Extensions: new Dictionary<string, object>
    {
        ["retryAfter"] = 30,
        ["limit"] = 100
    });

// Maps to ProblemDetails with errorCode extension and your status code

2. Railway Composition

var result = await GetUserAsync(id)
    .BindAsync(user => GetOrdersAsync(user.Id))
    .MapAsync(orders => new OrderSummary(orders))
    .ToHttpResultAsync();
Working with Anonymous Types & Object Results

When returning object (e.g., anonymous types for DTOs), use BindObject to avoid generic type inference issues:

// โœ… GOOD: Use BindObject for object/anonymous type results
var result = await GetProductAsync(id)
    .BindObject(
        product => product.Stock > 0,
        product => new { product.Id, product.Name, Status = "In Stock" },
        product => new UnionError.Conflict("Out of stock"));

// โœ… GOOD: Classic style also works
var result = await GetProductAsync(id)
    .BindObject(product => product.Stock > 0
        ? Union.Ok(new { product.Id, product.Name })  // Union.Ok(object) overload
        : Union.Fail<object>(new UnionError.Conflict("Out of stock")));

// โŒ AVOID: Generic inference issues with Bind<T, object>
var result = await GetProductAsync(id)
    .Bind(product => Union.Ok(new { product.Id }));  // May cause type issues!

Why BindObject?

  • Prevents C# generic type inference issues with anonymous types
  • Predicate-style syntax is more readable for conditional logic
  • Explicitly signals that boxing to object will occur

When to use what:

  • Use Map when transforming the success value (no error path)
  • Use Bind when chaining operations that return Rail<T> (with explicit types)
  • Use BindObject when returning object or anonymous types with conditional errors
Recovery & Side Effects
// Recover from specific errors with a fallback
var user = await GetUserAsync(id)
    .RecoverAsync<User, UnionError.NotFound>(_ => guestUser);

// Execute side effects without changing the value
var result = await GetUserAsync(id)
    .TapAsync(user => LogAccessAsync(user.Id))
    .ToHttpResultAsync();

3. Ecosystem Integration

Package What It Does
UnionRailway Core Rail<T> and UnionError types
UnionRailway.AspNetCore RFC 7807 Problem Details mapping
UnionRailway.AspNetCore.OpenApi Automatic Swagger/OpenAPI docs
UnionRailway.EntityFrameworkCore FirstOrDefaultAsUnionAsync helpers
UnionRailway.HttpClient HTTP status โ†’ Rail<T> conversion

4. Migration-Friendly

// Wrap legacy exception-based code:
var result = await UnionWrapper.RunAsync(() => legacyService.LoadAsync());

// Wrap nullable returns:
var maybeUser = await UnionWrapper.RunNullableAsync(() => repo.FindAsync(id));

โšก Performance

Real benchmarks (BenchmarkDotNet, .NET 8):

Operation Time Allocated Notes
Create success 0.5 ns 0 B Stack-only, zero allocation
Create failure 2.8 ns 24 B Error record allocation
Create Custom error ~3.5 ns 32 B Record with optional extensions
Map operation 1.3 ns 0 B AggressiveInlining
Bind operation 36 ns 40 B Function call overhead
Recover (matching) ~2 ns 0 B Single is type check
Recover (non-matching) ~1 ns 0 B Short-circuit, no work
MapAsync 48 ns 0 B ValueTask overhead only
Service chain (3 ops) 20 ns 120 B Real-world scenario

Why so fast?

  • โœ… Struct-based Rail<T> lives on stack
  • โœ… Zero allocations on success path
  • โœ… ValueTask for async (vs Task)
  • โœ… AggressiveInlining on hot paths
  • โœ… configureProblem callback is null-guarded โ€” zero overhead when unused
  • โœ… Custom.Extensions is null by default โ€” pay only when you use it

Run yourself:

cd tests/UnionRailway.Benchmarks
dotnet run -c Release

๐Ÿ“– Complete Guide

Rail<T> - The Core Type

Represents exactly one of:

  • โœ… Success value of type T
  • โŒ UnionError
public ValueTask<Rail<User>> GetUserAsync(int id)
{
    // Option 1: Implicit conversion
    return user;

    // Option 2: Explicit creation
    return Union.Ok(user);
    return Union.Fail<User>(new UnionError.NotFound("User"));
}

Pattern Matching

// Style 1: IsSuccess pattern
if (!result.IsSuccess(out var user, out var error))
    return error.GetValueOrDefault().ToHttpResult();

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

// Style 3: Error property check
if (result.Error is not null)
    return result.Error.GetValueOrDefault().ToHttpResult();

Railway Composition

// Sync
var result = Union.Ok(5)
    .Map(x => x * 2)           // Transform success
    .Bind(x => ValidateAsync(x)); // Chain operations

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

Ensure โ€” Guard Values in the Chain

Ensure validates the success value against a predicate. If it fails, the rail short-circuits to an error โ€” preventing null or invalid values from reaching downstream Bind/Map calls:

var result = await GetTransactionAsync(id)
    .EnsureAsync(
        t => t is not null,
        _ => new UnionError.NotFound("Transaction"))
    .BindAsync(async t =>
    {
        t.Items.ForEach(i => i.Issued = true);
        return await SaveAsync(t);
    });
// If GetTransactionAsync returns Ok(null), Ensure converts it to NotFound
// and BindAsync is never called.

ToFail โ€” Shorthand Error Creation

Create failed rails directly from any UnionError:

// Before:
return Union.Fail<Order>(new UnionError.Conflict("duplicate"));

// After:
UnionError error = new UnionError.Conflict("duplicate");
return error.ToFail<Order>();

SystemFailure โ€” Message Constructor

Create system failures without wrapping in an exception:

// Message only (wraps in InvalidOperationException internally):
var sf = new UnionError.SystemFailure("Database connection lost");

// Message with inner exception:
var sf = new UnionError.SystemFailure("Operation failed", innerException);

Switch โ€” Void Match for Side Effects

Switch is the void counterpart of Match โ€” execute side effects on both success and error branches without needing a return value:

result.Switch(
    onOk: user => logger.LogInformation("Found user {Id}", user.Id),
    onError: err => logger.LogWarning("Failed: {Error}", err));

// Async version:
await resultTask.SwitchAsync(
    onOk: user => { logger.LogInformation("Found user {Id}", user.Id); return Task.CompletedTask; },
    onError: err => { logger.LogWarning("Failed: {Error}", err); return Task.CompletedTask; });

ASP.NET Core Integration

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

app.MapPost("/users", async (CreateUserRequest req, UserService service) =>
    (await service.CreateAsync(req)).ToHttpResult(createdUri: $"/users/{id}"))
    .WithCreatedRailOpenApi<RouteHandlerBuilder, UserDto>();

// Rail<Unit> automatically returns 204 No Content
app.MapDelete("/users/{id}", async (int id, UserService service) =>
    (await service.DeleteAsync(id)).ToHttpResult());
Customizing ProblemDetails

Post-process any ProblemDetails response to add trace IDs, strip details in production, or enrich extensions:

return result.ToHttpResult(configureProblem: pd =>
{
    pd.Extensions["traceId"] = Activity.Current?.Id;
    if (env.IsProduction())
        pd.Detail = "An error occurred.";
});
Custom Error Mapping (IUnionErrorMapper)

For full control over how errors translate to HTTP responses, implement IUnionErrorMapper and register it in DI. When TryMap returns non-null, that result is used directly; when it returns null, the default RFC 7807 mapping kicks in:

public class CustomErrorMapper : IUnionErrorMapper
{
    public IResult? TryMap(UnionError error) => error.Value switch
    {
        UnionError.NotFound nf => Results.Problem(
            detail: $"We could not locate '{nf.Resource}'.",
            statusCode: 404,
            title: "Resource Not Found"),
        UnionError.Custom { Code: "RATE_LIMIT" } c => Results.Problem(
            detail: c.Message,
            statusCode: 429,
            title: "Rate Limited"),
        _ => null // fall back to default mapping
    };
}

// Registration:
builder.Services.AddSingleton<IUnionErrorMapper, CustomErrorMapper>();

// Usage (inject via DI or pass directly):
return result.ToHttpResult(errorMapper: mapper);
Zero-Boilerplate with RailEndpointFilter

Instead of calling .ToHttpResult() on every endpoint, add the filter once and return Rail<T> directly from your handlers:

// Option 1: Per-group (recommended)
var api = app.MapGroup("/api").WithRailwayFilter();

api.MapGet("/users/{id}", async (int id, UserService svc) =>
    await svc.GetUserAsync(id));  // Rail<User> โ†’ 200/404 automatically

api.MapDelete("/users/{id}", async (int id, UserService svc) =>
    await svc.DeleteAsync(id));   // Rail<Unit> โ†’ 204 automatically

// Option 2: Per-endpoint
app.MapGet("/users/{id}", handler).WithRailwayFilter();

The filter resolves IUnionErrorMapper and RailwayOptions from DI automatically.

Global Exception Handling

Catch unhandled exceptions across all endpoints and return consistent RFC 7807 responses โ€” matching UnionRailway's error format:

var app = builder.Build();
app.UseRailwayExceptionHandler(); // Add early in pipeline
app.UseAuthentication();
app.UseAuthorization();
AddRailway() โ€” One-line DI Setup

Register all UnionRailway services with a single call:

builder.Services.AddRailway(options =>
{
    // Global ProblemDetails enrichment (applied to ALL error responses)
    options.ConfigureProblem = pd =>
        pd.Extensions["traceId"] = Activity.Current?.Id;
});

// Or with a custom mapper:
builder.Services.AddRailway<CustomErrorMapper>(options => ...);

Entity Framework Core

// FirstOrDefault โ†’ Rail<T>
var user = await db.Users.FirstOrDefaultAsUnionAsync("User", x => x.Id == id);

// SaveChanges โ†’ Rail<int>
var result = await db.SaveChangesAsUnionAsync();

HttpClient

var result = await httpClient.GetFromJsonAsUnionAsync<UserDto>("/users/42");
// Automatic mapping: 2xx โ†’ Success, 4xx/5xx โ†’ Error

๐Ÿ”ฎ Future: Native C# Unions

UnionRailway is architected for seamless migration to native C# unions when .NET 11 stable is released:

Current Implementation (.NET 8 & 11 Preview):

[Union]  // High-performance struct-based implementation
public readonly struct Rail<T> { /* ... */ }
public readonly struct UnionError { /* ... */ }

Future (.NET 11 Stable Release):

public union Rail<T>(T, UnionError);      // Native union types
public union UnionError(...);             // Native discriminated union

Your Application Code: Zero changes required! ๐ŸŽ‰

The library is designed to automatically switch to native unions when:

  • .NET 11 stable is released with finalized union support
  • The union feature is production-ready (expected in .NET 11 RTM)

Until then, UnionRailway uses a highly optimized struct-based implementation that provides:

  • โœ… 0.5ns success path performance
  • โœ… Zero heap allocations
  • โœ… Full type safety
  • โœ… Compatible API surface with future native unions

Note: Union types are currently available in .NET 11 preview builds but are not yet enabled in this library until the feature stabilizes in the final release.

Target Frameworks: .NET 8.0, .NET 11.0 (and future versions)


๐Ÿ’ก Best Practices & Common Pitfalls

โœ… Do's

1. Use BindObject for Anonymous Types / object Results

// โœ… GOOD: Type-safe and clear
.BindObject(
    product => product.Stock > 0,
    product => new { product.Id, product.Name, Status = "Available" },
    product => new UnionError.Conflict("Out of stock"))

2. Use Union.Ok(object) When Boxing to Object

// โœ… GOOD: Explicit object overload prevents inference issues
Rail<object> result = Union.Ok(new { Id = 1, Name = "Test" });

3. Explicit Generic Parameters When Mixing Types

// โœ… GOOD: Clear and type-safe
.Bind<Product, OrderDto>(product => CreateOrderAsync(product))

4. Use ValueTask<Rail<T>> for Async Methods

// โœ… GOOD: Zero allocation on cached results
public async ValueTask<Rail<User>> GetUserAsync(int id)
{
    var user = await db.Users.FindAsync(id);
    return user ?? new UnionError.NotFound("User");
}

5. Chain Operations for Readability

// โœ… GOOD: Railway-style composition
var result = await GetProductAsync(id)
    .BindAsync(product => ValidateStockAsync(product))
    .MapAsync(product => new ProductDto(product))
    .ToHttpResultAsync();

โŒ Don'ts

1. Don't Use Generic Bind with Anonymous Types

// โŒ BAD: Generic type inference issues!
.Bind(p => Union.Ok(new { p.Id, p.Name }))
// May serialize as Rail wrapper instead of the object!

// โœ… GOOD: Use BindObject instead
.BindObject(p => new { p.Id, p.Name })

2. Don't Mix Error Handling Styles

// โŒ BAD: Mixing exceptions with Railway
public async ValueTask<Rail<User>> GetUserAsync(int id)
{
    try
    {
        var user = await db.Users.FindAsync(id);
        return user ?? throw new NotFoundException(); // โŒ Don't throw!
    }
    catch (Exception ex)
    {
        return new UnionError.SystemFailure(ex);
    }
}

// โœ… GOOD: Pure Railway style
public async ValueTask<Rail<User>> GetUserAsync(int id)
{
    var user = await db.Users.FindAsync(id);
    return user ?? new UnionError.NotFound("User");
}

3. Don't Ignore Error Cases

// โŒ BAD: Assuming success without checking
var user = result.Unwrap(); // May throw UnwrapException!

// โœ… GOOD: Check first or use Match
if (result.IsSuccess(out var user, out var error))
{
    // use user
}
else
{
    // handle error
}

// OR use Match
var message = result.Match(
    onSuccess: user => $"Hello {user.Name}",
    onError: error => $"Error: {error}");

4. Don't Forget to await Async Chains

// โŒ BAD: Returning Task<Rail<T>> instead of Rail<T>
public async Task<IResult> GetUser(int id, UserService svc)
{
    var result = svc.GetUserAsync(id); // Missing await!
    return result.ToHttpResult(); // Won't compile!
}

// โœ… GOOD
public async Task<IResult> GetUser(int id, UserService svc)
{
    var result = await svc.GetUserAsync(id);
    return result.ToHttpResult();
}

// โœ… EVEN BETTER: Use ToHttpResultAsync
public async Task<IResult> GetUser(int id, UserService svc)
{
    return await svc.GetUserAsync(id).ToHttpResultAsync();
}

5. Don't Over-Use object as Return Type

// โŒ BAD: Loses type safety
public ValueTask<Rail<object>> GetUserAsync(int id)
{
    return db.Users.FirstOrDefaultAsUnionAsync("User", x => x.Id == id)
        .MapAsync(user => (object)user); // Why box?
}

// โœ… GOOD: Keep strong typing as long as possible
public ValueTask<Rail<User>> GetUserAsync(int id)
{
    return db.Users.FirstOrDefaultAsUnionAsync("User", x => x.Id == id);
}

// Use object only at HTTP boundary for DTOs:
.MapAsync(user => new { user.Id, user.Name })
.ToHttpResultAsync();

๐ŸŽฏ Performance Tips

  • โœ… Use ValueTask<Rail<T>> instead of Task<Rail<T>> for hot paths
  • โœ… Prefer MapAsync / BindAsync over synchronous versions when awaiting
  • โœ… Use Rail<Unit> for void-like operations (0 allocation)
  • โœ… Avoid boxing to object unless necessary (DTOs at HTTP layer)
  • โœ… Use AggressiveInlining extension methods for custom operators

๐ŸŽ“ Learn More


๐Ÿ“ฆ Packages

Package Description Version Downloads
UnionRailway Core types and railway operators NuGet Downloads
UnionRailway.AspNetCore RFC 7807 Problem Details mapping NuGet Downloads
UnionRailway.AspNetCore.OpenApi Swagger/OpenAPI metadata NuGet Downloads
UnionRailway.EntityFrameworkCore EF Core extensions NuGet Downloads
UnionRailway.HttpClient HttpClient extensions NuGet Downloads

โค๏ธ Show Your Support

If UnionRailway helps your project, consider:

  • โญ Star this repo on GitHub
  • ๐Ÿ“ข Share with your team
  • ๐Ÿ› Report issues or suggest features
  • ๐Ÿค Contribute - PRs welcome!

๐Ÿ“„ License

MIT License - see LICENSE for details.


<div align="center">

Built with โค๏ธ for the C# community

Get Started ยท Documentation ยท GitHub

</div>

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 was computed.  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.

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