AddLifecycleHook
Registers a lifecycle hook that fires on train state transitions. Use lifecycle hooks to trigger side effects when trains start, complete, fail, or are cancelled, without coupling your train logic to the side effect.
Signatures
Register a hook directly (recommended):
public static TraxEffectBuilder AddLifecycleHook<T>(
this TraxEffectBuilder builder,
bool toggleable = true
) where T : class // ITrainLifecycleHook or ITrainLifecycleHookFactory
When T implements ITrainLifecycleHook, a factory is created internally and there is no need to write a factory class. When T implements ITrainLifecycleHookFactory, it is registered directly (advanced).
Register with an existing factory instance:
public static TraxEffectBuilder AddLifecycleHook<TFactory>(
this TraxEffectBuilder builder,
TFactory factory,
bool toggleable = true
) where TFactory : class, ITrainLifecycleHookFactory
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
builder | TraxEffectBuilder | Yes | N/A | The effect configuration builder |
factory | TFactory | No | N/A | An existing factory instance (when not using DI to create it) |
toggleable | bool | No | true | Whether the hook can be enabled/disabled at runtime via IEffectRegistry |
ITrainLifecycleHook
public interface ITrainLifecycleHook
{
Task OnStarted(Metadata metadata, CancellationToken ct) => Task.CompletedTask;
Task OnCompleted(Metadata metadata, CancellationToken ct) => Task.CompletedTask;
Task OnFailed(Metadata metadata, Exception exception, CancellationToken ct) => Task.CompletedTask;
Task OnCancelled(Metadata metadata, CancellationToken ct) => Task.CompletedTask;
}
All methods have default implementations that return Task.CompletedTask. Override only the events you care about.
| Method | When it fires |
|---|---|
OnStarted | After the train’s metadata is persisted and before the train’s junctions execute |
OnCompleted | After a successful run, after output is persisted |
OnFailed | After a failed run (exception that is not OperationCanceledException), after failure is persisted |
OnCancelled | After cancellation (OperationCanceledException), after cancellation is persisted |
Accessing Train Input and Output
Availability by hook
The input is set before junctions execute; the output is set only after a successful run. This determines which hooks can access each value:
| Hook | TrainInput / GetInput<T>() | TrainOutput / GetOutput<T>() |
|---|---|---|
OnStarted | Yes | No (train hasn’t run yet) |
OnCompleted | Yes | Yes |
OnFailed | Yes | No (train failed before producing output) |
OnCancelled | Yes | No (train was cancelled) |
When unavailable, both return default (null for reference types, zero for value types).
Typed access via Metadata
Metadata provides generic methods to retrieve the input and output as their original types:
T? GetInput<T>()
T? GetOutput<T>()
These return default if the object is null or not of the requested type. Useful in global lifecycle hooks (ITrainLifecycleHook) where you know the train’s types:
public class SlackNotificationHook(ISlackClient slack) : ITrainLifecycleHook
{
public async Task OnFailed(Metadata metadata, Exception exception, CancellationToken ct)
{
var input = metadata.GetInput<BanPlayerInput>();
await slack.PostAsync($"Ban failed for player {input?.PlayerId}: {exception.Message}", ct);
}
}
Typed access via TrainInput / TrainOutput
Per-train lifecycle hooks have an even simpler option. ServiceTrain<TIn, TOut> exposes typed properties that are automatically resolved from the metadata:
protected TIn TrainInput // available in all hooks
protected TOut TrainOutput // available in OnCompleted (default in OnFailed/OnCancelled)
No casting, no type parameters. The types come from the train’s generic arguments:
public class BanPlayerTrain(ILogger<BanPlayerTrain> logger)
: ServiceTrain<BanPlayerInput, Unit>, IBanPlayerTrain
{
protected override Task OnCompleted(Metadata metadata, CancellationToken ct)
{
logger.LogInformation("Banned player {PlayerId} for: {Reason}",
TrainInput?.PlayerId, TrainInput?.Reason);
return Task.CompletedTask;
}
}
Serialized JSON output
metadata.Output is always available as serialized JSON in OnCompleted hooks, regardless of whether SaveTrainParameters() is configured. When SaveTrainParameters() is not configured, the output is serialized in-memory before hooks fire but is not persisted to the database. This means GraphQL subscriptions and custom hooks can always read metadata.Output without requiring SaveTrainParameters().
ITrainLifecycleHookFactory (Advanced)
public interface ITrainLifecycleHookFactory
{
ITrainLifecycleHook Create();
}
Most users do not need to implement this interface. AddLifecycleHook<THook>() generates a factory automatically. Use a custom factory only if you need non-standard creation logic. The factory is a singleton; it creates a new hook instance per train execution.
Error Handling
Lifecycle hook exceptions are caught and logged, never propagated. A failing hook will never cause a train to fail. This is intentional: side effects like posting to Grafana or sending Slack notifications should not affect train reliability.
Example: Custom Slack Notification Hook
public class SlackNotificationHook(ISlackClient slack) : ITrainLifecycleHook
{
public async Task OnFailed(Metadata metadata, Exception exception, CancellationToken ct) =>
await slack.PostAsync($"Train {metadata.Name} failed: {exception.Message}", ct);
}
Registration:
builder.Services.AddTrax(trax => trax
.AddEffects(effects => effects
.UsePostgres(connectionString)
.AddLifecycleHook<SlackNotificationHook>()
)
.AddMediator(ServiceLifetime.Scoped, typeof(Program).Assembly)
);
No factory class needed. Trax creates one internally and resolves your hook’s constructor dependencies from DI.
Built-in Hooks
| Hook | Package | Description |
|---|---|---|
GraphQLSubscriptionHook | Trax.Api.GraphQL | Publishes lifecycle events to GraphQL subscriptions. Automatically registered by AddTraxGraphQL(). |
BroadcastLifecycleHook | Trax.Effect | Publishes lifecycle events to a message bus for cross-process delivery. Automatically registered by UseBroadcaster(). |
Per-Train Lifecycle Hooks
In addition to global hooks registered via AddLifecycleHook<T>(), individual trains can override lifecycle methods directly on ServiceTrain. This is useful when a hook only makes sense for one specific train and you want to use the train’s own injected dependencies.
public class BanPlayerTrain(ILogger<BanPlayerTrain> logger)
: ServiceTrain<BanPlayerInput, Unit>, IBanPlayerTrain
{
protected override Unit Junctions() => Chain<ApplyBanJunction>();
protected override Task OnCompleted(Metadata metadata, CancellationToken ct)
{
logger.LogInformation("Banned player {PlayerId} for: {Reason}",
TrainInput?.PlayerId, TrainInput?.Reason);
return Task.CompletedTask;
}
protected override Task OnFailed(Metadata metadata, Exception exception, CancellationToken ct)
{
logger.LogWarning("Failed to ban player {PlayerId}: {Message}",
TrainInput?.PlayerId, exception.Message);
return Task.CompletedTask;
}
}
No registration needed, just override the method. The available methods match the global hook interface:
| Method | Signature |
|---|---|
OnStarted | protected virtual Task OnStarted(Metadata metadata, CancellationToken ct) |
OnCompleted | protected virtual Task OnCompleted(Metadata metadata, CancellationToken ct) |
OnFailed | protected virtual Task OnFailed(Metadata metadata, Exception exception, CancellationToken ct) |
OnCancelled | protected virtual Task OnCancelled(Metadata metadata, CancellationToken ct) |
All default to no-op (Task.CompletedTask). Override only the ones you need.
In addition to the metadata parameter, per-train hooks can use the TrainInput and TrainOutput properties for typed access to the train’s input and output without casting. See Accessing Train Input and Output above.
Global vs Per-Train Hooks
Global (AddLifecycleHook<T>) | Per-Train (override) | |
|---|---|---|
| Scope | Fires for every train | Fires only for the overriding train |
| Registration | AddLifecycleHook<T>() in builder | No registration, just override |
| DI access | Constructor injection on the hook class | Constructor injection on the train itself |
| Execution order | Fires first | Fires after global hooks |
| Error handling | Caught and logged | Caught and logged |
| Use case | Cross-cutting concerns (metrics, audit logs) | Train-specific business logic (alerts, cleanup) |
Package
dotnet add package Trax.Effect