Analyzer

Trax.Core includes a Roslyn analyzer that validates your train’s route at compile time, like a route planner that checks every junction has the cargo it needs before the train ever departs. When you chain junctions via .Chain<TJunction>(), the analyzer simulates the runtime Memory dictionary to verify that each junction’s input type is available before that junction executes.

The Problem

Consider this train:

Chain<LoadMetadataJunction>()                // TIn=RunJobRequest -> TOut=Metadata
    .Chain<ValidateMetadataStateJunction>()  // TIn=Metadata -> TOut=Unit
    .Chain<RunScheduledTrainJunction>()
    .Chain<UpdateManifestSuccessJunction>();

If someone removes LoadMetadataJunction, ValidateMetadataStateJunction expects Metadata in Memory but nothing produces it. Today this is a runtime error. The train fails when it tries to find Metadata in the dictionary. You won’t discover this until the code actually runs.

The analyzer makes it a compile-time error. You see the problem immediately in your IDE, before you even build.

What It Checks

The analyzer triggers on Junctions() overrides and .Resolve() calls in Train<,> or ServiceTrain<,> subclasses. It walks through the chain and simulates Memory forward:

Junctions()           -> Memory = { TInput, Unit }
Chain<JunctionA>()    -> Check: is JunctionA's TIn in Memory? Add JunctionA's TOut.
.Chain<JunctionB>()   -> Check: is JunctionB's TIn in Memory? Add JunctionB's TOut.
                      -> Check: is TReturn in Memory?
Method What the analyzer does
Junctions() / Activate(input) Seeds Memory with TInput and Unit
.Chain<TJunction>() Checks TIn in Memory, then adds TOut
.ShortCircuit<TJunction>() Same as Chain: checks TIn in Memory, adds TOut
.AddServices<T1, T2>() Adds each type argument to Memory
.Extract<TIn, TOut>() Adds TOut to Memory
.Resolve() / end of chain Checks TReturn in Memory

Diagnostics

CHAIN001: Junction input type not available (Error)

Fires when a junction needs a type that no previous junction has produced.

public class BrokenTrain : ServiceTrain<string, Unit>
{
    protected override Unit Junctions() =>
        Chain<LogGreetingJunction>();  // <- CHAIN001: LogGreetingJunction requires HelloWorldInput,
                                      //   but Memory only has [string, Unit]
}

The message tells you exactly what’s missing and what’s available:

error CHAIN001: Junction 'LogGreetingJunction' requires input type 'HelloWorldInput'
which has not been produced by a previous junction. Available: [string, Unit].

CHAIN002: Train return type not available (Error)

Fires when Resolve() needs a type that hasn’t been produced. The analyzer tracks all chain methods including ShortCircuit, so a missing return type is always an error.

public class MissingReturnTrain : ServiceTrain<OrderRequest, Receipt>
{
    protected override Receipt Junctions() =>
        Chain<ValidateOrderJunction>();  // Returns Unit
                                         // <- CHAIN002: Receipt not in Memory
}

Tuple and Interface Handling

The analyzer mirrors the runtime’s Memory behavior:

Tuple outputs are decomposed. When a junction produces (User, Order), the analyzer adds User and Order to Memory individually (not the tuple itself). This matches how the runtime stores tuple elements.

Tuple inputs are validated component-by-component. When a junction takes (User, Order), the analyzer checks that both User and Order are individually available in Memory.

Interface resolution works through concrete types. When a junction produces ConcreteUser (which implements IUser), the analyzer adds both ConcreteUser and IUser to Memory. A subsequent junction requiring IUser will pass validation.

Known Limitations

Sibling interface inputs. When the train’s TInput is an interface (e.g., Train<IFoo, Unit>) and a junction requires a different interface that the runtime concrete type also implements, the analyzer can’t verify this. Suppress with #pragma warning disable CHAIN001.

Cross-method chains. The analyzer only looks within a single method body. If you build a chain across helper methods, it won’t follow the calls.

Setup

The analyzer ships with the Trax.Core NuGet package. If you’re referencing Trax.Core, you already have it. No additional setup required.

For development within the Trax.Core solution itself, the analyzer is propagated to all projects via Directory.Build.props:

<ItemGroup Condition="'$(MSBuildProjectName)' != 'Trax.Core.Analyzers'">
    <ProjectReference Include="$(MSBuildThisFileDirectory)src/Trax.Core.Analyzers/Trax.Core.Analyzers.csproj"
                      ReferenceOutputAssembly="false"
                      OutputItemType="Analyzer" />
</ItemGroup>

Suppressing Diagnostics

If the analyzer fires on a chain that you know is correct (interface patterns, dynamic Memory seeding, etc.), suppress it with a pragma:

#pragma warning disable CHAIN001
    .Chain<MyDynamicJunction>()
#pragma warning restore CHAIN001

Or suppress at the project level in your .csproj:

<PropertyGroup>
    <NoWarn>$(NoWarn);CHAIN001</NoWarn>
</PropertyGroup>

SDK Reference


Back to top

Trax - A .NET framework for Railway Oriented Programming with Effects, Scheduling, and more