ServiceTrain

The train base class applications derive from. It extends Train with a metadata row per run, effect providers, junction effects, lifecycle hooks, and a container its junctions are built from. The mediator, the scheduler and the GraphQL API run ServiceTrains.

A service train implements its own interface, which derives from IServiceTrain<TIn, TOut>. That interface is how the train is registered, resolved, named and dispatched.

Signature

namespace Trax.Effect.Services.ServiceTrain;
 
public interface IServiceTrain<in TIn, TOut> : IRoute<TIn, TOut>, IDisposable
{
    Metadata? Metadata { get; }
    new Task<TOut> Run(TIn input, CancellationToken cancellationToken = default);
}
 
public abstract class ServiceTrain<TIn, TOut> : Train<TIn, TOut>, IServiceTrain<TIn, TOut>
{
    protected ServiceTrain();
 
    public Metadata? Metadata { get; }
    public string TrainName { get; }
    public string? CanonicalName { get; set; }
    public long? ParentId { get; }
 
    protected TIn TrainInput { get; }
    protected TOut TrainOutput { get; }
 
    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);
    public void Dispose();
 
    protected virtual Task OnStarted(Metadata metadata, CancellationToken ct);
    protected virtual Task OnCompleted(Metadata metadata, CancellationToken ct);
    protected virtual Task OnFailed(Metadata metadata, Exception exception, CancellationToken ct);
    protected virtual Task OnCancelled(Metadata metadata, CancellationToken ct);
    protected virtual Task OnQueue(Metadata metadata, CancellationToken ct);
    protected virtual string? QueueSubjectKey(Metadata metadata);
    protected virtual bool DeferQueuePromotion { get; }
 
    // Filled by property injection; see "Registration" below.
    [Inject] public IEffectRunner? EffectRunner { get; set; }
    [Inject] public IJunctionEffectRunner? JunctionEffectRunner { get; set; }
    [Inject] public ILifecycleHookRunner? LifecycleHookRunner { get; set; }
    [Inject] public ILogger<ServiceTrain<TIn, TOut>>? Logger { get; set; }
    [Inject] public IServiceProvider? ServiceProvider { get; set; }
}

Everything on Train (Junctions(), the chain methods, ExternalId, CancellationToken, RunEither, DeclaredChain()) is inherited unchanged. NewMonad() is sealed. EnterQueueHooks(Metadata) is public for the mediator's enqueue path and hidden from completion; consumers do not call it.

Properties

PropertyTypeDescription
MetadataMetadata?The row for the current or most recent run: state, input, output, timing, failure. null until a run starts. Not serialized.
TrainNamestringThe canonical name: CanonicalName when set, otherwise the concrete type's FullName. Metadata, work queue entries and subscriptions use it.
CanonicalNamestring?The train interface's FullName, set by the registration helpers when the train is resolved through its interface. Not serialized.
ParentIdlong?Copied onto each run's metadata row. No Trax package sets it, so it is null: a train dispatched from another train is recorded as a run of its own, not linked to the parent.
TrainInputTInThe run's typed input, available in OnStarted, OnCompleted, OnFailed and OnCancelled, and in QueueSubjectKey and OnQueue when the enqueue hands it over. Throws ChainDeclarationException while the chain is being read.
TrainOutputTOutThe run's typed output. Meaningful only in OnCompleted; default elsewhere. Throws ChainDeclarationException while the chain is being read.
EffectRunner, JunctionEffectRunner, LifecycleHookRunner, ServiceProviderFramework services filled by property injection. Run throws when any of them is null.
LoggerILogger<ServiceTrain<TIn, TOut>>?Filled by property injection; optional.

Run

OverloadDescription
Run(input, cancellationToken)Creates a metadata row, runs the chain, records the outcome and fires the hooks. Sealed. Each call records its own row: running the same instance again starts a new row, with a new ExternalId unless you set one first.
Run(input, metadata)Runs as a Pending row the caller already created, instead of creating one. Used by the mediator and the scheduler. Throws TrainException when the row is not Pending. Runs with the train's current CancellationToken.
Run(input, metadata, cancellationToken)As above, with a token.

Run throws the failure, as on Train. See Run / RunEither.

Lifecycle hooks

HookWhen it runsAn exception thrown in it
OnStartedAfter the row is persisted as in progress, before the chain runsIs logged; the train still runs
OnCompletedAfter a successful run, once the output is persisted and the registered lifecycle hooks have firedIs logged; the run stays successful
OnFailedAfter a failed run, once the failure is persisted and the registered hooks have fired. An OperationCanceledException nothing asked for, such as an HttpClient timeout, is a failure and lands hereIs logged; the original failure still propagates
OnCancelledAfter a cancellation the run was asked for: its token was cancelled, or its persisted cancel flag was setIs logged
OnQueueAt enqueue time, inside the mediator's queue path. Not on the run path, and not again when the queued run executesPropagates and aborts the enqueue
QueueSubjectKeyAt enqueue time. Return a key to stop two entries for the same subject being dispatched at once; null (the default) serializes nothingPropagates and aborts the enqueue
DeferQueuePromotionRead at enqueue time. true commits the entry unconfirmed and confirms it after OnQueue returns; no effect unless OnQueue is overridden

The enqueue-time hooks are covered in depth in OnQueue and QueueSubjectKey. For hooks that apply to every train, register an ITrainLifecycleHook with AddLifecycleHook.

Registration

A service train is resolved through its interface, which is what fills the [Inject] properties and sets CanonicalName.

A service train cannot be a singleton: AddSingletonTraxRoute refuses one with an InvalidOperationException, and AddMediator refuses a singleton train lifetime with an ArgumentException. An instance carries the state of the run in progress, so it runs one execution at a time and is not shared between concurrent callers.

Resolving the concrete class instead of the interface skips the property injection, and Run then throws because the framework services are null.

Example

using LanguageExt;
using Trax.Effect.Models.Metadata;
using Trax.Effect.Services.ServiceTrain;
 
public interface ICreateOrderTrain : IServiceTrain<CreateOrderInput, OrderResult>;
 
public class CreateOrderTrain(IOrderAlerts alerts)
    : ServiceTrain<CreateOrderInput, OrderResult>, ICreateOrderTrain
{
    protected override Task<Either<Exception, OrderResult>> Junctions() =>
        Chain<ValidateOrder>()
            .Chain<ChargePayment>()
            .Chain<PersistOrder>()
            .Resolve();
 
    protected override Task OnFailed(Metadata metadata, Exception exception, CancellationToken ct)
        // TrainInput is the typed input of the run that failed.
        => alerts.OrderFailed(TrainInput.OrderId, exception.Message, ct);
}

The train's own dependencies, IOrderAlerts here, come through its constructor like any other service. The junctions take theirs through their own constructors.

Dispose

Dispose() clears the in-memory input and output, disposes the effect, junction effect and lifecycle hook runners (and through them their providers) and the metadata, and drops the logger and service provider. The instance cannot run again afterwards. The container disposes a transient or scoped train for you.

See Trains and Junctions and Effect for the concepts.

Package

dotnet add package Trax.Effect