AddMediator

Registers the ITrainBus and ITrainRegistry services, and discovers all IServiceTrain<,> implementations via assembly scanning. This enables dynamic train dispatch by input type.

Signatures

There are two overloads: a builder overload for full control, and a shorthand overload for the common case.

Builder Overload

public static TraxBuilderWithMediator AddMediator(
    this TraxBuilderWithEffects builder,
    Func<TraxMediatorBuilder, TraxMediatorBuilder> configure
)

Accepts a lambda that receives a TraxMediatorBuilder for configuring assembly scanning and train lifetime.

Shorthand Overload

public static TraxBuilderWithMediator AddMediator(
    this TraxBuilderWithEffects builder,
    params Assembly[] assemblies
)

Scans the given assemblies with the default Transient lifetime. Equivalent to:

.AddMediator(mediator => mediator
    .ScanAssemblies(typeof(Program).Assembly)
)

Both overloads are called on TraxBuilderWithEffects (the return type of AddEffects()), which enforces at compile time that effects are configured before the mediator. Both return TraxBuilderWithMediator, which exposes AddScheduler() as the next valid step. Called before AddEffects(), AddMediator fails to compile with Call AddEffects(...) before AddMediator(...)., and called a second time with AddMediator(...) is already called. Call it once and configure everything in that call.

TraxMediatorBuilder

The builder overload passes a TraxMediatorBuilder with the following methods:

MethodReturnsDescription
ScanAssemblies(params Assembly[])TraxMediatorBuilderAdds assemblies to scan for IServiceTrain<,> implementations
TrainLifetime(ServiceLifetime)TraxMediatorBuilderSets the DI lifetime for discovered train registrations: Transient (the default) or Scoped. Singleton throws an ArgumentException naming TrainLifetime, because a train instance carries the state of the run in progress and one shared instance would mix concurrent runs together. RegisterServiceTrains and AddServiceTrainBus refuse it the same way
SkipChainVerification()TraxMediatorBuilderTurns off the startup chain check. Logs a warning at startup instead. Use it for the check's blind spot (a junction asking for an interface only a subtype of the declared input implements), or temporarily while migrating a codebase whose chains do not pass yet. See Trains & Junctions

Returns

TraxBuilderWithMediator -- enables AddScheduler() as the next step in the fluent chain.

Examples

Shorthand (Most Common)

services.AddTrax(trax => trax
    .AddEffects(effects => effects
        .UsePostgres(connectionString)
    )
    .AddMediator(typeof(Program).Assembly)
);

Builder with Custom Lifetime

services.AddTrax(trax => trax
    .AddEffects(effects => effects
        .UsePostgres(connectionString)
    )
    .AddMediator(mediator => mediator
        .ScanAssemblies(typeof(Program).Assembly)
        .TrainLifetime(ServiceLifetime.Scoped)
    )
);

Multiple Assemblies

services.AddTrax(trax => trax
    .AddEffects(effects => effects
        .UsePostgres(connectionString)
    )
    .AddMediator(mediator => mediator
        .ScanAssemblies(
            typeof(Program).Assembly,
            typeof(SharedTrains).Assembly
        )
    )
);

What It Registers

  1. Scans the specified assemblies for all types implementing IServiceTrain<TIn, TOut>
  2. Registers each discovered train with the DI container at the specified lifetime
  3. Registers ITrainBus for dynamic train dispatch
  4. Registers ITrainRegistry for train type lookup
  5. Registers the startup chain validator, a hosted service that reads every registered train's Junctions() declaration when the host starts and refuses to start if any chain cannot run, reporting every failing train at once. It also checks that every junction a chain builds (Chain<T>(), ShortCircuit<T>()) will be handed its constructor arguments, from Memory as the chain has filled it by that step or from the container, and refuses a junction that would not. A train that can never be built is refused, naming the train: its class has no public constructor, or its constructor needs a type the container does not register, directly or through a registered dependency whose own constructor needs one (the check follows registrations that name an implementation class, open generics included, but not factories or instances). A train it cannot build for another reason (a service registered through a factory that only works inside a request) or that does not derive from Train<,> is logged as a warning and skipped rather than refused. The check runs in StartingAsync, so it refuses the host before any hosted service's StartAsync runs, including under HostOptions.ServicesStartConcurrently; when something starts hosted services itself and calls only StartAsync (a custom IHost, a test harness), it runs from StartAsync instead. A train it can build whose Junctions() throws is refused, whatever the exception: a Junctions() that dereferences Metadata, which is null at startup, fails the start, reported as a train whose chain could not be read. Being a hosted service, the check runs only where hosted services start: not with SkipChainVerification(), and not on the Lambda runner or any other bare ServiceProvider, where such a train fails when it first runs instead. See Trains & Junctions

How Discovery Works

  1. Scans the specified assemblies for all concrete classes implementing IServiceTrain<TIn, TOut>.
  2. Selects each train's service type: its own interface, the non-generic one that derives from IServiceTrain<TIn, TOut>. When one train interface extends another, the most derived is selected. A train with no such interface is registered under the closed IServiceTrain<TIn, TOut>. No other interface counts, so a marker interface or IDisposable on a shared base class never becomes a train's service type. A train that implements two train interfaces, neither extending the other, has no single canonical name and is refused with a TrainException. RegisterServiceTrains selects the same way.
  3. Registers the train in the ITrainRegistry keyed by the TIn of its service type.
  4. Registers the train in the DI container under that service type, with the specified lifetime.

Input Type Uniqueness

TrainBus.RunAsync dispatches by input type, so it reaches one train per input type. If two trains accept the same TIn, the registry keeps the first one scanned (via TryAdd), and RunAsync with that input type runs it.

Both trains are still registered and discovered, and a call that names a train runs that train, whichever one the registry keeps: ITrainExecutionService.RunAsync, the GraphQL run operations and ITrainBus.RunByNameAsync all do.

// This is fine -- different input types
public class CreateOrderTrain : ServiceTrain<CreateOrderInput, OrderResult> { }
public class CancelOrderTrain : ServiceTrain<CancelOrderInput, OrderResult> { }
 
// trainBus.RunAsync(new OrderInput()) runs CreateOrderTrain; run UpdateOrderTrain by name
public class CreateOrderTrain : ServiceTrain<OrderInput, OrderResult> { }
public class UpdateOrderTrain : ServiceTrain<OrderInput, OrderResult> { }

To run a specific one of several trains that share an input type, call ITrainBus.RunByNameAsync with its interface's full name, or inject it by its interface.

Lifetime Considerations

LifetimeWhen to Use
Transient (default)Most trains -- each execution gets a fresh instance
ScopedWhen the train needs to share state with other scoped services in the same request
SingletonNot supported: a train carries per-run state, and AddSingletonTraxRoute refuses a service train at registration with an InvalidOperationException

Remarks

  • The assembly scanning uses reflection to find IServiceTrain<,> implementations. The assemblies containing your trains must be passed to ScanAssemblies() or the shorthand overload.
  • Trains registered here are available both through ITrainBus.RunAsync and through the scheduler system.
  • See TrainBus for the runtime dispatch API.

Package

Part of Trax.Mediator.