Junctions
Override Junctions() to define the train’s route, the sequence of junctions it passes through. This is the primary way to compose junctions in a train.
Signature
// Train<TInput, TReturn>
protected virtual TReturn Junctions()
// ServiceTrain<TIn, TOut>: same signature
protected virtual TOut Junctions()
Returns
TReturn, the train’s output type. The return value is resolved automatically from Memory via an implicit conversion on Monad. You do not need to call Resolve() or wrap the result in Either.
Examples
Basic Train
public class CreateUserTrain : ServiceTrain<CreateUserRequest, User>, ICreateUserTrain
{
protected override User Junctions() =>
Chain<ValidateEmailJunction>()
.Chain<CreateUserJunction>();
}
With ShortCircuit and Extract
All chain methods (Chain, ShortCircuit, Extract, AddServices) are available as protected methods on the train:
public class ProcessOrderTrain : ServiceTrain<OrderInput, OrderResult>
{
protected override OrderResult Junctions() =>
ShortCircuit<CheckCacheJunction>()
.Chain<ValidateOrderJunction>()
.Extract<OrderInput, OrderDetails>()
.Chain<ProcessPaymentJunction>();
}
With AddServices
public class NotifyTrain(ISlackClient slack) : ServiceTrain<NotifyInput, Unit>
{
protected override Unit Junctions() =>
AddServices<ISlackClient>(slack)
.Chain<SendNotificationJunction>();
}
Behavior
- The framework calls
Activate(input)automatically beforeJunctions()executes, seeding Memory with the train input andUnit. - Chain methods are called as protected methods on the train itself (not on a separate
Monadreturned byActivate). - The final chain call returns a
Monad<TInput, TReturn>, which is implicitly converted toTReturnby extracting the result from Memory. - If any junction threw an exception, the implicit conversion returns
default(TReturn)and the framework handles the exception via the railway error path. - The
Run()/RunEither()public API is unchanged for callers.
When to Use RunInternal Instead
Junctions() covers the common case. Override RunInternal when you need:
- Custom logic before or after the chain: try/catch around the chain, logging, or conditional branching
- Extra objects in Memory:
Activate(input, extraObject)passes additional objects into Memory - Manual Either construction: returning
Left(exception)orRight(value)directly - Async setup: awaiting something before building the chain
- Combining nested train results: calling
TrainBus.RunAsyncand merging the result with the chain viaResolve(explicitValue)
protected override async Task<Either<Exception, ParentResult>> RunInternal(ParentInput input)
{
var childResult = await TrainBus.RunAsync<ChildResult>(
new ChildRequest { Data = input.ChildData }, Metadata);
return Activate(input)
.Chain<ValidateJunction>()
.Resolve(new ParentResult
{
ParentData = input.ParentData,
ChildResult = childResult
});
}
Remarks
Junctions()andRunInternalare mutually exclusive. Override one or the other, not both. If both are overridden,RunInternaltakes precedence.- The implicit conversion from
Monad<TInput, TReturn>toTReturncallsResolve()internally, following the same resolution priority: exception > short-circuit value > Memory lookup. - Backwards compatible: existing trains using
RunInternalcontinue to work without changes.