MIT licensed.

A framework for
readable .NET

Single responsibility code is hard to write. Business requirements lose their meaning somewhere between controllers, handlers, services, and routing. Side effects begin to bleed across these boundaries. Code becomes mismanaged, and difficult to understand. Without a strong code style, readability stops being a priority.

Trax consolidates all of that into one framework. Your configuration lives in Program.cs, your logic lives in pipelines, and everything reads like a language your team already understands.

$ dotnet add package Trax.Core

  Input
    │
    ▼
  ┌─ CheckInventory ─┐
  │                   │
  ├─ ChargePayment ──┤
  │                   │
  ├─ CreateShipment ─┤
  │                   │
                   
  Output             Exception

  success             failure

The problem

Error handling buries the thing you actually care about.

Three operations, three if-statements, three early returns. The “what we do” disappears behind “what might go wrong.”

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

With Trax

Same logic. Read it top to bottom.

Each junction's output feeds the next. Errors short-circuit automatically — no if-statements, no try-catch, no early returns. What's left is just the business logic, in order.

protected override OrderReceipt Junctions() =>
    Chain<CheckInventoryJunction>()
        .Chain<ChargePaymentJunction>()
        .Chain<CreateShipmentJunction>();

Each junction's output is stored in Memory by type. The next junction declares what it needs as its input, and Trax wires them together automatically. A compile-time analyzer catches broken chains before you ever run the code.

Pipelines are just the starting point.

Trax is a stack of packages, each adding one capability on top of the last. You install only what you need — most projects start with Core and add layers over time as requirements grow.

What you get.

Compile-time chain validation

A Roslyn analyzer checks your chains at build time. Mismatched types between junctions? You’ll know before you run anything.

Execution metadata

Every pipeline run is tracked — start time, end time, which junction failed, the exception, the input parameters. Queryable in Postgres or in-memory.

Job scheduling with dependencies

Cron and interval schedules. Dependent job chains (DAGs). Automatic retries with backoff. Dead-letter queues for jobs that give up.

CancellationToken everywhere

First-class cancellation through pipelines, junctions, the dashboard, and the mediator. Not bolted on — it’s threaded through the whole stack.

Auto-generated GraphQL API

Tag a pipeline with [TraxMutation] and it gets a typed GraphQL mutation. [TraxQuery] makes a query. Real-time lifecycle events via WebSocket subscriptions.

Monitoring dashboard

A Blazor Server UI that mounts into your existing app. View executions, inspect failures, requeue jobs, or run pipelines with entirely new inputs.

Deploy it however you want.

Same pipelines, different topologies. Move from a single process to distributed workers to serverless dispatch by changing a few lines of configuration — the pipeline code stays the same.

01

All-in-one

API, scheduler, and job execution in a single process. The simplest setup — good for small services, internal tools, or getting started. One deployable, one connection string, done.

Program.cs
builder.Services.AddTrax(trax =>
    trax.AddEffects(effects => effects.UsePostgres(conn))
        .AddMediator(assembly)
        .AddScheduler(scheduler => scheduler
            .Schedule<IProcessOrderTrain>(...)
        )
);

builder.AddTraxDashboard();
builder.Services.AddTraxGraphQL();
02

API + Scheduler

The API handles requests and queues work. A separate scheduler process picks up jobs and runs them locally. Good when you want to isolate heavy processing from your request path — the API stays fast, the scheduler does the lifting.

Api / Program.cs
builder.Services.AddTrax(trax =>
    trax.AddEffects(effects => effects.UsePostgres(conn))
        .AddMediator(assembly)
);
builder.Services.AddTraxGraphQL();
Scheduler / Program.cs
builder.Services.AddTrax(trax =>
    trax.AddEffects(effects => effects.UsePostgres(conn))
        .AddMediator(assembly)
        .AddScheduler(scheduler => scheduler
            .Schedule<IProcessOrderTrain>(...)
        )
);
builder.AddTraxDashboard();
03

Hub + Workers

A hub manages scheduling and the API. Stateless worker nodes poll the database for jobs and execute them independently. Scale workers horizontally — add more when load increases, remove them when it drops. Workers coordinate through PostgreSQL row locking, no message broker required.

Hub / Program.cs
builder.Services.AddTrax(trax =>
    trax.AddEffects(effects => effects.UsePostgres(conn))
        .AddMediator(assembly)
        .AddScheduler(scheduler => scheduler
            .Schedule<IProcessOrderTrain>(...)
        )
);
builder.Services.AddTraxGraphQL();
Worker / Program.cs
builder.Services.AddTrax(trax =>
    trax.AddEffects(effects => effects.UsePostgres(conn))
        .AddMediator(assembly)
);
builder.Services.AddTraxWorker(opts => {
    opts.WorkerCount = 4;
    opts.PollingInterval = TimeSpan.FromSeconds(1);
});
04

Ephemeral dispatch

The API dispatches work over HTTP to a stateless runner — think Lambda functions or short-lived containers. No persistent worker process, no polling. The runner boots, executes one pipeline, and exits. Good for bursty workloads where you don't want idle compute.

Api / Program.cs
builder.Services.AddTrax(trax =>
    trax.AddEffects(effects => effects.UsePostgres(conn))
        .AddMediator(assembly)
        .AddScheduler(scheduler => scheduler
            .UseRemoteWorkers(
                remote => remote.BaseUrl = "https://runner/trax/execute",
                routing => routing
                    .ForTrain<IProcessOrderTrain>()
            )
            .UseRemoteRun(
                remote => remote.BaseUrl = "https://runner/trax/run"
            )
        )
);
Runner / Function.cs
public class Function : TraxLambdaFunction
{
    protected override void ConfigureServices(
        IServiceCollection services,
        IConfiguration configuration)
    {
        services.AddTrax(trax =>
            trax.AddEffects(effects =>
                    effects.UsePostgres(conn))
                .AddMediator(assembly)
        );
    }
}
05

Just the scheduler

No API at all. A standalone process that runs pipelines on a schedule — data processing, ETL, report generation, background maintenance. If you don't need an external trigger, you don't need an API.

Program.cs
builder.Services.AddTrax(trax =>
    trax.AddEffects(effects => effects.UsePostgres(conn))
        .AddMediator(assembly)
        .AddScheduler(scheduler => scheduler
            .Schedule<IDataProcessingTrain>(
                "data-processing",
                new DataProcessingInput(),
                Every.Minutes(5)
            )
            .ThenInclude<IReportingTrain>(...)
        )
);
builder.AddTraxDashboard();

Import an existing API.

OpenAPI or GraphQL. The CLI reads the schema, generates typed trains and junctions, wires up the hub. You fill in the business logic.

From an OpenAPI spec:

$ dotnet tool install -g Trax.Cli
$ trax generate --schema ./payments-api.json --output ./Payments --name Payments

Or a GraphQL schema:

$ trax generate --schema ./schema.graphql --output ./Payments --name Payments

What comes out:

Payments/
├── Payments.Hub/              API + Scheduler + Dashboard
│   └── Program.cs
└── Payments.Trains/
    ├── Models/
    │   └── Transaction.cs
    └── Trains/
        └── Transactions/
            ├── ListTransactions/
            │   ├── IListTransactionsTrain.cs       [TraxQuery]
            │   ├── ListTransactionsTrain.cs
            │   └── Junctions/
            │       └── ListTransactionsJunction.cs  ← your code here
            └── ChargeCard/
                ├── IChargeCardTrain.cs              [TraxMutation]
                ├── ChargeCardTrain.cs
                └── Junctions/
                    └── ChargeCardJunction.cs        ← your code here

Fill in the junctions, run the hub.