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
| Property | Type | Description |
|---|---|---|
Metadata | Metadata? | The row for the current or most recent run: state, input, output, timing, failure. null until a run starts. Not serialized. |
TrainName | string | The canonical name: CanonicalName when set, otherwise the concrete type's FullName. Metadata, work queue entries and subscriptions use it. |
CanonicalName | string? | The train interface's FullName, set by the registration helpers when the train is resolved through its interface. Not serialized. |
ParentId | long? | 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. |
TrainInput | TIn | The 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. |
TrainOutput | TOut | The run's typed output. Meaningful only in OnCompleted; default elsewhere. Throws ChainDeclarationException while the chain is being read. |
EffectRunner, JunctionEffectRunner, LifecycleHookRunner, ServiceProvider | Framework services filled by property injection. Run throws when any of them is null. | |
Logger | ILogger<ServiceTrain<TIn, TOut>>? | Filled by property injection; optional. |
Run
| Overload | Description |
|---|---|
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
| Hook | When it runs | An exception thrown in it |
|---|---|---|
OnStarted | After the row is persisted as in progress, before the chain runs | Is logged; the train still runs |
OnCompleted | After a successful run, once the output is persisted and the registered lifecycle hooks have fired | Is logged; the run stays successful |
OnFailed | After 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 here | Is logged; the original failure still propagates |
OnCancelled | After a cancellation the run was asked for: its token was cancelled, or its persisted cancel flag was set | Is logged |
OnQueue | At enqueue time, inside the mediator's queue path. Not on the run path, and not again when the queued run executes | Propagates and aborts the enqueue |
QueueSubjectKey | At enqueue time. Return a key to stop two entries for the same subject being dispatched at once; null (the default) serializes nothing | Propagates and aborts the enqueue |
DeferQueuePromotion | Read 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.
- AddMediator scans the assemblies you give it and registers every non-abstract
IServiceTrain<,>implementation under its own interface, transient by default. - Outside the mediator, register it with AddTransientTraxRoute or AddScopedTraxRoute.
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