Run / RunEither

Executes the train from the outside. Run throws on failure; RunEither returns an Either<Exception, TReturn> for Railway-oriented error handling.

These are called by consumers of the train, not inside Junctions().

Signatures

Run (throws on failure)

public virtual async Task<TReturn> Run(TInput input, CancellationToken cancellationToken = default)

On a ServiceTrain it is a sealed override, and the two overloads that take a pre-created Metadata are not virtual:

public sealed override Task<TOut> Run(TIn input, CancellationToken cancellationToken = default)
public Task<TOut> Run(TIn input, Metadata metadata)
public Task<TOut> Run(TIn input, Metadata metadata, CancellationToken cancellationToken)

A service train does its work in Junctions(), and Run owns the metadata row, the lifecycle hooks and the outcome write around it, so none of them can be overridden. A plain Train<TInput, TReturn> keeps a virtual Run.

RunEither (returns Either)

public Task<Either<Exception, TReturn>> RunEither(TInput input)

Parameters

ParameterTypeRequiredDescription
inputTInputYesThe input data for the train
cancellationTokenCancellationTokenNoToken to monitor for cancellation requests. When provided, it is stored on the Train.CancellationToken property and propagated to every junction before execution. Defaults to CancellationToken.None when omitted.

Note: RunEither does not accept a CancellationToken parameter, and a caller cannot assign Train.CancellationToken (its setter is not public). Pass the token to Run, or run the train through a host (a ServiceTrain resolved from the container, the mediator or the scheduler), which sets it.

Returns

  • Run: Task<TReturn>, the train result. Throws the captured exception if the train failed. Throws OperationCanceledException if the token is cancelled.
  • RunEither: Task<Either<Exception, TReturn>>, Left on failure, Right on success. Nothing escapes it, cancellation included: a cancelled run returns Left holding the OperationCanceledException. Tell it apart from a business failure with is OperationCanceledException before treating the Left as one.

Examples

Using Run (imperative style)

try
{
    var result = await train.Run(new OrderInput { OrderId = "123" });
    Console.WriteLine($"Order processed: {result.ConfirmationId}");
}
catch (Exception ex)
{
    Console.WriteLine($"Train failed: {ex.Message}");
}

Using RunEither (functional style)

var result = await train.RunEither(new OrderInput { OrderId = "123" });
 
result.Match(
    Right: success => Console.WriteLine($"Order processed: {success.ConfirmationId}"),
    Left: error => Console.WriteLine($"Train failed: {error.Message}")
);

With CancellationToken

// From an ASP.NET controller
public async Task<IActionResult> ProcessOrder(
    OrderInput input,
    CancellationToken cancellationToken)
{
    var result = await train.Run(input, cancellationToken);
    return Ok(result);
}

Behavior

  1. If a CancellationToken is provided, stores it on the Train.CancellationToken property.
  2. Seeds Memory with the input.
  3. Calls the train's Junctions() declaration.
  4. Run: Unwraps the Either result. If Left, rethrows the exception. If Right, returns the value.
  5. RunEither: Returns the Either directly without unwrapping.

During junction execution, the train's CancellationToken is automatically propagated to each junction before its Run method is called. Junctions access the token via this.CancellationToken. Before each junction executes, CancellationToken.ThrowIfCancellationRequested() is called. If the token is already cancelled, the junction is skipped entirely.

Remarks

  • RunEither is useful when you want functional-style error handling without try/catch. It pairs naturally with LanguageExt's Match, Map, Bind, etc.
  • In most applications, trains are executed through ITrainBus.RunAsync (which calls Run internally) rather than calling Run directly. See TrainBus.
  • The cancellationToken parameter stores the token before calling the train's route definition. All junctions in the chain then receive the token automatically. See Cancellation Tokens for details on how cancellation propagates through the pipeline.
  • Run is virtual on plain Train<TInput, TReturn>, for code that uses Trax.Core without Trax.Effect. An override that does not call base.Run never reaches Junctions(), so the chain that DeclaredChain() reads, and any check built on it, is not the chain that runs. Call base.Run from an override, or put the work in a junction. A ServiceTrain cannot override Run at all: its overloads are sealed. Trax.Core/docs/adr/0003 records why.