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 workWhere it goes
Needs the input as part of the runA junction at the head of the chain, which receives the input as its argument
Must happen the moment a queued mutation is acceptedOnQueue, 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 bodyWhy it is refusedInstead
Awaits something before returning the chainIt does work instead of declaring a chainMove the awaited work into a junction
Returns a value directly, such as Task.FromResult(value)There is no chain to verifyChain the junction that produces the value, end in Resolve()
Ends in Resolve(value)It states the result instead of naming what produces itEnd in Resolve(); a train with no junctions uses Task.FromResult(Resolve())
Chain<T> or ShortCircuit<T> of a type that is not a junctionNothing can run itName a junction type
IChain<T> or AddServices<T> of a classBoth resolve by interface; the run refuses a class every timeName the interface, or use Chain<T> for a concrete junction
A ShortCircuit whose output cannot be the train's return typeThe value is returned as the result by a cast that would always failShort-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.