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:
| Method | Returns | Description |
|---|---|---|
ScanAssemblies(params Assembly[]) | TraxMediatorBuilder | Adds assemblies to scan for IServiceTrain<,> implementations |
TrainLifetime(ServiceLifetime) | TraxMediatorBuilder | Sets 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() | TraxMediatorBuilder | Turns 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
- Scans the specified assemblies for all types implementing
IServiceTrain<TIn, TOut> - Registers each discovered train with the DI container at the specified lifetime
- Registers
ITrainBusfor dynamic train dispatch - Registers
ITrainRegistryfor train type lookup - 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 fromTrain<,>is logged as a warning and skipped rather than refused. The check runs inStartingAsync, so it refuses the host before any hosted service'sStartAsyncruns, including underHostOptions.ServicesStartConcurrently; when something starts hosted services itself and calls onlyStartAsync(a customIHost, a test harness), it runs fromStartAsyncinstead. A train it can build whoseJunctions()throws is refused, whatever the exception: aJunctions()that dereferencesMetadata, 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 withSkipChainVerification(), and not on the Lambda runner or any other bareServiceProvider, where such a train fails when it first runs instead. See Trains & Junctions
How Discovery Works
- Scans the specified assemblies for all concrete classes implementing
IServiceTrain<TIn, TOut>. - 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 closedIServiceTrain<TIn, TOut>. No other interface counts, so a marker interface orIDisposableon 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 aTrainException.RegisterServiceTrainsselects the same way. - Registers the train in the
ITrainRegistrykeyed by theTInof its service type. - 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
| Lifetime | When to Use |
|---|---|
Transient (default) | Most trains -- each execution gets a fresh instance |
Scoped | When the train needs to share state with other scoped services in the same request |
Singleton | Not 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 toScanAssemblies()or the shorthand overload. - Trains registered here are available both through
ITrainBus.RunAsyncand through the scheduler system. - See TrainBus for the runtime dispatch API.
Package
Part of Trax.Mediator.