Effect system for Trax.Core — upgrade a bare locomotive into a full commercial train service with journey logging, station services, and dependency injection.
Trax.Core gives you Train<TIn, TOut>: a locomotive that carries cargo through a sequence of stops. That's enough for pure logic, but production services need to know what ran, when it departed, whether it arrived, what it was carrying, and what went wrong if it derailed.
Trax.Effect adds the ServiceTrain<TIn, TOut> base class — a full commercial train service that wraps every journey with:
- Journey logging — a persistent metadata record for each run (state, timing, cargo in, cargo out, derailment details)
- Station services — pluggable effect providers that fire during execution (data persistence, logging, parameter serialization, progress tracking)
- DI integration — stops are resolved from
IServiceProvider, so you get constructor injection out of the box
dotnet add package Trax.EffectFor data persistence, pick a storage depot:
# PostgreSQL (production)
dotnet add package Trax.Effect.Data.Postgres
# In-memory (testing / prototyping)
dotnet add package Trax.Effect.Data.InMemoryOptional station services:
dotnet add package Trax.Effect.Provider.Json # Debug logging of train state
dotnet add package Trax.Effect.Provider.Parameter # Serialize cargo to the journey logRegister station services in your IServiceCollection:
builder.Services.AddTrax(trax =>
trax.AddEffects(effects =>
effects.UsePostgres(connectionString).SaveTrainParameters().AddJunctionLogger(serializeJunctionData: true).AddJunctionProgress()
)
);For development or tests, swap Postgres for in-memory:
builder.Services.AddTrax(trax =>
trax.AddEffects(effects =>
effects.UseInMemory().AddJson()
)
);Inherit from ServiceTrain instead of Train:
public interface ICreateUserTrain : IServiceTrain<CreateUserRequest, User> { }
public class CreateUserTrain : ServiceTrain<CreateUserRequest, User>, ICreateUserTrain
{
protected override async Task<Either<Exception, User>> RunInternal(CreateUserRequest input)
=> Activate(input)
.Chain<ValidateEmailJunction>()
.Chain<CreateUserInDatabaseJunction>()
.Chain<SendWelcomeEmailJunction>()
.Resolve();
}The route syntax is identical to Train. The difference is what happens around it — ServiceTrain automatically opens a journey log when the train departs, updates it when it arrives, persists effect data at each station, and records the derailment details if any stop fails.
Junctions work the same way, with full DI:
public class CreateUserInDatabaseJunction(AppDbContext db) : Junction<CreateUserRequest, User>
{
public override async Task<User> Run(CreateUserRequest input)
{
var user = new User { Email = input.Email, Name = input.Name };
db.Users.Add(user);
await db.SaveChangesAsync();
return user;
}
}Every ServiceTrain journey transitions through:
Pending → InProgress → Completed
→ Failed
→ Cancelled
Think of it as: the train is boarding (Pending), in transit (InProgress), and then either arrives (Completed), derails (Failed), or is pulled from service (Cancelled). These states are persisted in the journey log and queryable through the data layer.
| Service | Package | What it does |
|---|---|---|
| Postgres | Trax.Effect.Data.Postgres |
Persists journey logs and execution data to PostgreSQL |
| InMemory | Trax.Effect.Data.InMemory |
In-memory store for tests and local dev |
| Json | Trax.Effect.Provider.Json |
Logs state transitions as JSON for debugging |
| Parameter | Trax.Effect.Provider.Parameter |
Serializes train cargo (inputs/outputs) into the journey log |
| JunctionLogger | Built-in | Logs each junction's execution with optional cargo serialization |
| JunctionProgress | Built-in | Tracks per-junction progress and checks for cancellation signals |
Station services compose — enable as many as you need:
effects
.UsePostgres(connectionString)
.AddJson()
.SaveTrainParameters()
.AddJunctionLogger(serializeJunctionData: true)
.AddJunctionProgress();Register your trains as scoped services with proper interface mapping:
builder.Services.AddScopedTraxRoute<ICreateUserTrain, CreateUserTrain>();
builder.Services.AddTransientTraxRoute<IProcessOrderTrain, ProcessOrderTrain>();Or use AddMediator (from Trax.Mediator) to auto-register all trains in an assembly.
Trax is a layered framework — each package builds on the one below it. Stop at whatever layer solves your problem.
Trax.Core pipelines, junctions, railway error propagation
└→ Trax.Effect ← you are here
└→ 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
Next layer: When you need decoupled dispatch (callers don't know which train handles a request), add Trax.Mediator.
Full documentation: traxsharp.net/docs
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.