Railway Oriented Programming for .NET. Build trains that carry data through a sequence of stops, with automatic derailment handling when something goes wrong.
Trax is a layered framework split across several repos. You can stop at whatever layer solves your problem. You are here: Trax.Core.
| Repo | Adds |
|---|---|
| Trax.Core | Pipelines, junctions, railway error propagation |
| Trax.Effect | Execution logging, DI, pluggable storage |
| Trax.Mediator | Decoupled dispatch via TrainBus |
| Trax.Scheduler | Cron schedules, retries, dead-letter queues |
| Trax.Api | GraphQL API for remote access |
| Trax.Dashboard | Blazor monitoring UI |
| Trax.Cli | trax-cli project scaffolding tool |
| Trax.Samples | Sample apps and a dotnet new template |
Full documentation: traxsharp.net/docs.
Error handling tends to bury the actual logic:
public async Task<OrderReceipt> ProcessOrder(OrderRequest request)
{
var inventory = await _inventory.CheckAsync(request.Items);
if (!inventory.Available)
return Error("Items out of stock");
var payment = await _payments.ChargeAsync(request.PaymentMethod, request.Total);
if (!payment.Success)
return Error("Payment failed");
var shipment = await _shipping.CreateAsync(request.Address, request.Items);
if (shipment == null)
return Error("Shipping setup failed");
return new OrderReceipt(payment, shipment);
}Every junction needs its own null check, error branch, and early return. The business logic (check inventory, charge payment, create shipment) gets lost in the noise.
public class ProcessOrderTrain : Train<OrderRequest, OrderReceipt>
{
protected override Task<Either<Exception, OrderReceipt>> Junctions() =>
Chain<CheckInventoryJunction>()
.Chain<ChargePaymentJunction>()
.Chain<CreateShipmentJunction>()
.Resolve();
}A train picks up its cargo, visits each stop along the route (.Chain<T>), and arrives at its destination (Resolve). If CheckInventoryJunction throws, the train derails and ChargePaymentJunction and CreateShipmentJunction are never reached. The exception propagates through the chain automatically.
Main Track: Input → [Stop 1] → [Stop 2] → [Stop 3] → Output
↓
Derailed: Exception → [Skip] → [Skip] → Exception
Each junction is its own class with its own dependencies, testable in isolation.
Requires net10.0.
dotnet add package Trax.Core1. Define a junction. Each junction takes one type of cargo in and produces one type of cargo out:
public class ValidateEmailJunction(IUserRepository repo) : Junction<CreateUserRequest, Unit>
{
public override async Task<Unit> Run(CreateUserRequest input)
{
var existing = await repo.GetByEmailAsync(input.Email);
if (existing is not null)
throw new ValidationException($"Email {input.Email} is already taken");
return Unit.Default;
}
}2. Build a route by chaining junctions into a train:
public class CreateUserTrain : Train<CreateUserRequest, User>
{
protected override Task<Either<Exception, User>> Junctions() =>
Chain<ValidateEmailJunction>()
.Chain<CreateUserInDatabaseJunction>()
.Chain<SendWelcomeEmailJunction>()
.Resolve();
}When the train is run with an input, the cargo is loaded automatically. At each stop, .Chain<T> picks up the cargo T needs from what the train is carrying, runs the junction, and loads the output back on. Resolve unloads the final delivery at the destination.
The train carries all of this in Memory, a type-keyed store that accumulates as the train moves through its route. Each stop can use anything a previous stop produced.
3. Run it:
var train = new CreateUserTrain();
Either<Exception, User> result = await train.RunEither(request);
// Or throw on failure:
User user = await train.Run(request);Trax.Core ships with a Roslyn analyzer that validates your route at build time. If a stop expects cargo that no previous stop has loaded, you get a compiler error, not a runtime derailment.
| Diagnostic | Meaning |
|---|---|
| CHAIN001 | A junction expects cargo that isn't on the train at that point in the route |
| CHAIN002 | The train's final delivery type isn't on board when Resolve() is called |
Inlay hint extensions show TIn → TOut types inline for each .Chain<TJunction>() call, so you can see what cargo flows through each stop at a glance.
- VSCode: Trax.Core Chain Hints on the Marketplace
- Rider / ReSharper: Search for Trax.Core Chain Hints in JetBrains Marketplace
When you need execution logging, DI, or persistent metadata, move up to Trax.Effect.
MIT
Trax is an open-source .NET framework provided by TraxSharp. This project is an independent community effort and is not affiliated with, sponsored by, or endorsed by the Utah Transit Authority, Trax Retail, or any other entity using the "Trax" name in other industries.