Trax.Effect

Trax.Effect gives every train run a record: a row with its state, start and end time, the junction that failed and the exception it threw, stored in PostgreSQL, SQLite or memory. It also resolves junctions from dependency injection and runs pluggable effect providers around each run.

dotnet add package Trax.Effect
dotnet add package Trax.Effect.Data.Postgres  # or Trax.Effect.Data.InMemory
dotnet add package Trax.Effect.Provider.Parameter  # SaveTrainParameters, in the setup below
dotnet add package Trax.Effect.JunctionProvider.Logging  # AddJunctionLogger

What It Adds

Everything in Core, plus:

  • ServiceTrain<TIn, TOut> - extends Train with automatic metadata tracking
  • Execution metadata - every train run produces a queryable record (state, timing, input/output, errors)
  • Dependency injection - junctions resolved from your DI container
  • Effect providers - pluggable providers for persistence, logging, and serialization
  • Lifecycle hooks - fire on train state transitions (started, completed, failed, cancelled) for notifications, metrics, or real-time updates
  • Independent side effects - each provider saves in turn, and one that throws does not stop the others; metadata (state, timing, errors) is persisted regardless of outcome

Train vs ServiceTrain

Train<TIn, TOut> (Core) - The core pipeline engine. Chains junctions, propagates errors, manages Memory. No logging, no DI, no side effects.

ServiceTrain<TIn, TOut> (Effect) - A Train with the services around it. Wraps every run with:

  • A run record (start time, end time, success or failure, and the input and output when parameter saving is on)
  • Effect providers (database persistence, JSON logging, parameter serialization)
  • Integration with ITrainBus for dispatch discovery
  • IServiceProvider access for junction instantiation
using LanguageExt;
using Trax.Effect.Services.ServiceTrain;
 
public class CreateUserTrain : ServiceTrain<CreateUserRequest, User>, ICreateUserTrain
{
    protected override Task<Either<Exception, User>> Junctions() =>
        Chain<ValidateUserJunction>()
            .Chain<CreateUserJunction>().Resolve();
}

The code inside Junctions() is identical. ServiceTrain adds the infrastructure around it.

The Effect Pattern

Effects are operations that happen as the train passes through its route, provided by pluggable effect providers. Junctions don't write directly to a database or logger. Instead, the train tracks models during the journey, and effect providers handle the actual work at the end:

  • On both tracks, effect providers run SaveChanges, and metadata (state, timing, errors) is always persisted
  • If the train reaches the right track (success), output is recorded alongside the metadata
  • If any junction takes the left track (failure), the exception and failure details are recorded, and the same SaveChanges runs on every provider. Nothing rolls back: whatever the train tracked before it failed, including entities added to the train's DataContext and models a custom provider holds, is committed with the failure record.

This gives you full audit trails on every outcome and modularity (add/remove providers without changing train code).

Setup

builder.Services.AddTrax(trax => trax
    .AddEffects(effects => effects
        .UsePostgres(connectionString)          // Persist metadata (returns TraxEffectBuilderWithData)
        .SaveTrainParameters()                  // Include input/output in metadata
        .AddJunctionLogger(serializeJunctionData: true) // Log individual junction executions
    )
);

The AddEffects callback is a Func<TraxEffectBuilder, TraxEffectBuilder>, so the lambda returns the builder from the last chained call. Data provider methods (UsePostgres, UseInMemory) return TraxEffectBuilderWithData, which unlocks data-dependent methods like AddDataContextLogging at compile time.

Remove any line and the train still runs; it just passes through fewer junctions.

When to Use Effect

  • Web APIs where you need to know what ran and why it failed
  • Services that need execution audit trails
  • Any application where you want observability without building custom logging

When you need decoupled dispatch (callers don't know which train handles a request), add Trax.Mediator.

SDK Reference

AddTrax / AddEffects | UsePostgres | SaveTrainParameters | AddJunctionLogger | AddJunctionProgress | AddMediator | AddTraxDashboard | UseTraxDashboard