Removal of RunInternal and Activate
Junctions() is now the only way to declare a train's chain. Train.RunInternal is private,
Train.Activate is internal, and ServiceTrain.Activate is gone. A train that overrode
RunInternal or called Activate no longer compiles. The reason is in
Trax.Docs/adr/0016: a chain built
imperatively has no single shape, so the host could not read it at startup.
The same release changed how work is enqueued and how a run's outcome is recorded; see Enqueue and Outcome Changes.
Overriding RunInternal
Before:
protected override Task<Either<Exception, TOut>> RunInternal(TIn input) =>
Activate(input).Chain<A>().Resolve();After:
protected override Task<Either<Exception, TOut>> Junctions() =>
Chain<A>().Resolve();The framework seeds Memory with the input before Junctions() runs, so the chain starts at the
first junction. A chain that names no junctions, because the return type is already in Memory as
the input type or Unit, becomes Task.FromResult(Resolve()).
Code that read the input above the chain
Junctions() declares a chain; it does not process a value. TrainInput and TrainOutput throw
ChainDeclarationException while the chain is being read, and the startup check turns that into a
refused start. Code that used the input parameter of RunInternal moves to one of two places:
| The work | Where it goes |
|---|---|
| Needs the input as part of the run | A junction at the head of the chain, which receives the input as its argument |
| Must happen the moment a queued mutation is accepted | OnQueue, which is handed a Metadata carrying the input |
Values passed through Activate(input, otherInputs) need a declared source instead. Produce them
from an earlier junction, or seed them in the chain with AddServices<IService>(value) (under an
interface) or Extract<TIn, TOut>(value), both of which the startup check records as seeds.
Bodies that are not a pure declaration
A RunInternal body could do anything before returning. Junctions() cannot, and the startup
check refuses these shapes, which tend to come across from the old code:
| The body | Why it is refused | Instead |
|---|---|---|
| Awaits something before returning the chain | It does work instead of declaring a chain | Move the awaited work into a junction |
Returns a value directly, such as Task.FromResult(value) | There is no chain to verify | Chain the junction that produces the value, end in Resolve() |
Ends in Resolve(value) | It states the result instead of naming what produces it | End in Resolve(); a train with no junctions uses Task.FromResult(Resolve()) |
Chain<T> or ShortCircuit<T> of a type that is not a junction | Nothing can run it | Name a junction type |
IChain<T> or AddServices<T> of a class | Both resolve by interface; the run refuses a class every time | Name the interface, or use Chain<T> for a concrete junction |
A ShortCircuit whose output cannot be the train's return type | The value is returned as the result by a cast that would always fail | Short-circuit with a junction producing the return type |
Memory rules the check enforces
The check replays Memory the way the runtime fills it, which can surface a chain that only worked
by accident. The train's input is available under its declared type and every interface it
implements (a run also stores it under the runtime subtype); each element of a tuple is available
under its declared type and interfaces (again, a run also stores the runtime type); a junction's output, an Extract result and an AddServices value
are available only under their exact declared type. A junction taking a tuple has its elements
assembled from Memory only, never from the container. A ShortCircuit junction's output is not counted: at runtime it is stored only when the junction returns Right (the chain keeps running either way), so a later junction or Resolve() cannot rely on it. A junction that asks for an interface the previous
junction's output merely implements is refused, because the run would not find it either. Declare
the producer's output as that interface, or ask for the concrete type.
If the upgrade is blocked on it
The input-reading fault compiles cleanly and only surfaces at startup. While chains are being moved over, the check can be turned off:
.AddMediator(mediator => mediator.SkipChainVerification())It silences every other chain fault too, so it is a stopgap to remove once the trains are moved, not a setting to leave on. See Trains & Junctions for the full list of what the check refuses.