DeclaredChain
Reads a train's declared chain without resolving or running any junction. This is what the host's startup chain check calls for every registered train; you can call it yourself, for example in a test that asserts a train's route.
Signatures
// Train<TInput, TReturn>
public ChainRecorder DeclaredChain()
public bool IsDeclaringChain { get; }DeclaredChain() runs Junctions() against a monad that records each chain call's type arguments instead of executing it, and returns the recording. Nothing is resolved from the container and no junction runs, including on a monad created through NewMonad() while declaring. NewMonad() is the seam ServiceTrain uses to supply its container, not an extension point: it is protected because the published Trax.Effect overrides it, and hidden from completion.
IsDeclaringChain is true while the chain is being read. Per-execution accessors check it: ServiceTrain.TrainInput and TrainOutput throw ChainDeclarationException while it is true.
Throws: ChainDeclarationException when the train reads per-execution state while declaring, and whatever else Junctions() itself throws while it is read (a train that never overrides it throws NotImplementedException, for instance). A declaration that completes but is wrong, such as a body that awaits, returns a result, or starts a second chain in a separate statement, is recorded as a refusal instead.
ChainRecorder
namespace Trax.Core.Monad;
public sealed class ChainRecorder
{
public IReadOnlyList<ChainStep> Steps { get; }
public IReadOnlyList<string> Refusals { get; }
}| Member | Description |
|---|---|
Steps | The declared steps, in order. A step naming a type that is not a junction is kept with no In or Out, so a step's index is always its written position |
Refusals | Things the declaration did that no step can express, each phrased for whoever has to fix it: awaiting before returning, returning a result instead of ending in Resolve(), ending in Resolve(value), IChain or AddServices of a class, AddServices of null, Chain or ShortCircuit of a type that is not a junction, Chain<T>() or ShortCircuit<T>() of a junction Trax cannot build (not exactly one public constructor, abstract, or an interface) |
ChainStep and ChainStepKind
public readonly record struct ChainStep(ChainStepKind Kind, Type? Junction, Type? In, Type? Out);
public enum ChainStepKind { Chain, IChain, ShortCircuit, Extract, Resolve, Seed }| Field | Description |
|---|---|
Kind | Which chain primitive declared the step |
Junction | The junction type, or null for a step that names none |
In | The type the step consumes from Memory, or null |
Out | The type the step contributes to Memory, or null |
ChainStepKind | Declared by |
|---|---|
Chain | Chain<TJunction>() and its overloads |
IChain | IChain<TJunction>() |
ShortCircuit | ShortCircuit<TJunction>() |
Extract | Extract<TIn, TOut>(), projecting a value already in Memory |
Resolve | Resolve() |
Seed | A value handed to the chain directly, by AddServices(value) or Extract<TIn, TOut>(value), which puts its type into Memory without a junction producing it |
ChainVerification.Verify
namespace Trax.Core.Monad;
public static IReadOnlyList<ChainFault> Verify(
ChainRecorder chain,
Type input,
Type output,
Func<Type, bool>? availableElsewhere = null
)
public static IReadOnlyList<ChainFault> Verify(
ChainRecorder chain,
Type input,
Type output,
IServiceProviderIsService container
)
public readonly record struct ChainFault(int StepIndex, ChainStepKind Kind, Type? Junction, string Reason)
{
public bool IsRefusal { get; init; }
}| Parameter | Description |
|---|---|
chain | The recording from DeclaredChain() |
input | The train's input type, which seeds Memory |
output | The train's return type, which the chain must end holding |
availableElsewhere | Whether a type the chain never produces can still be supplied by the container. Without it, every junction taking an injected service reads as a fault |
container | The container the train runs with. Answers what availableElsewhere would, and also turns on the constructor check below |
Replays the recording over the types Memory would hold and returns every step that does not line up, plus one fault per refusal. The rules it applies are in Trains & Junctions.
Given the container, it also checks every junction Trax builds from its constructor (Chain<T>() and ShortCircuit<T>()): each constructor argument has to be in Memory by that step, registered in the container, the container itself, or an ILogger<T> an ILoggerFactory in Memory can make. A tuple argument is assembled from Memory only. A junction passed as an instance, or resolved by IChain, is already built and is not checked. The overload without a container leaves constructors unchecked. The host's startup check passes the container, from Trax.Mediator 1.24.0, so a junction whose constructor argument nothing supplies stops the host rather than failing its first run.
ChainFault member | Description |
|---|---|
StepIndex | The step's position, counting from zero. A refusal about the whole chain rather than one step sits one past the last step |
Kind | The step's kind. Resolve for a refusal about the whole chain |
Junction | The junction the step names, or null |
Reason | What is wrong, phrased for whoever has to fix it |
IsRefusal | True for one of Refusals, false for a fault the replay found. A refused step records no output, so faults after it may be its consequences |
Exceptions
| Type | Namespace | When |
|---|---|---|
ChainDeclarationException | Trax.Core.Exceptions | A train read TrainInput or TrainOutput while its chain was being declared. The message names the train and the member |
ChainRecordedException | Trax.Core.Exceptions | The sentinel Resolve() returns as Left while a chain is being read. The reader discards it; it should never be observed outside chain reading. It is public only because it shipped public, and is hidden from completion: do not catch, match on or throw it |
Example
[Test]
public void CreateUserTrain_DeclaresAChainThatLinesUp()
{
var train = new CreateUserTrain();
var chain = train.DeclaredChain();
chain.Refusals.Should().BeEmpty();
ChainVerification.Verify(chain, typeof(CreateUserRequest), typeof(User)).Should().BeEmpty();
}Package
dotnet add package Trax.Core