AddStateMachines
Registers state-machine persistence as a step in the AddTrax builder chain. One call discovers every
Machine<TState, TTrigger> in the given assemblies and wires the whole subsystem: the snapshot store, the
effect-claim ledger, the exactly-once runner, the machine registry, and the four generic stateMachine
mutation trains. The stores reach their tables through the IDataContext the database provider you
configured in AddEffects registers (IDataContext.SnapshotDrafts and IDataContext.EffectClaims), so there
is no context of the subsystem's own to register, and it contributes the mutation trains to the mediator
scan so Trax routes them by input type. You name neither a context nor the mutations' assembly.
Call it after AddEffects(...) (it needs a data provider) and before AddMediator(...) (the mediator
builds its route registry when it runs, so the mutations must be contributed first). Called after AddMediator(...), it
does not compile: the error is Call AddStateMachines(...) before AddMediator(...).
Signature
public static TraxBuilderWithEffects AddStateMachines(
this TraxBuilderWithEffects builder,
params Assembly[] assemblies
)
public static TraxBuilderWithEffects AddStateMachines(
this TraxBuilderWithEffects builder,
Action<StateMachineOptions>? configure,
params Assembly[] assemblies
)Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
assemblies | Assembly[] | Yes | The assemblies to scan for Machine<TState, TTrigger> subclasses. Throws InvalidOperationException if none are found. |
configure | Action<StateMachineOptions>? | No | Sets host-level options (see below). The assemblies-only overload passes null. |
Throws InvalidOperationException if no data provider was configured in AddEffects (the store needs a
database), or if AddMediator has already run (the mutations would arrive too late to be dispatchable).
Options
StateMachineOptions carries host-level settings read when the registry builds a machine's draft service.
| Property | Type | Default | Description |
|---|---|---|---|
DraftTtl | TimeSpan? | null | How long a draft survives without activity before the next load discards it and the user starts fresh (a sliding window on the row's last update). The stale row is deleted, so an abandoned or completed draft can't linger or block a new one. null never expires a draft. Recommended: 7 to 30 days for a form-style flow. |
trax.AddStateMachines(
o => o.DraftTtl = TimeSpan.FromDays(30),
typeof(CheckoutMachine).Assembly);Returns
TraxBuilderWithEffects, so you can continue the chain into AddMediator(...).
What the host still supplies
Only the two things a machine genuinely can't know:
| Registration | Why |
|---|---|
ISnapshotPrincipal | Maps the current caller to the user key that scopes drafts. Bind it over your auth (for example Trax's TraxCaller). |
| Each effect implementation | Every effect a machine references with RunsOnce<TEffect> is resolved from the container when its transition is sent. |
The database wiring and the mutation-routing assembly are not host concerns: the stores use the
IDataContext and ISqlDialect your UsePostgres/UseSqlite/UseInMemory registers, and AddStateMachines
contributes the mutations to the mediator scan. UseInMemory registers no dialect, so there a concurrent create
of one draft throws instead of losing the race, and the in-memory provider cannot run the stores' atomic
updates at all: use it for wiring tests, not for the draft operations.
Example
builder.Services.AddTrax(trax =>
trax.AddEffects(effects => effects.UsePostgres(cs).AddJson())
.AddStateMachines(typeof(CheckoutMachine).Assembly)
.AddMediator(typeof(CheckoutMachine).Assembly));
builder.Services.AddScoped<ISnapshotPrincipal, TraxCallerSnapshotPrincipal>();
builder.Services.AddScoped<ICharge, StripeCharge>();The tables
snapshot_draft and effect_claim (both in the trax schema) ship as migrations in the core data
providers and apply automatically when you register one. UsePostgres(...) runs
040_state_machine_snapshots.sql, 048_snapshot_draft_request_scope.sql,
051_snapshot_draft_machine_key.sql and 053_effect_claim_content_fingerprint.sql; UseSqlite(...) runs 006,
013, 015 and 018 of the same names. A host
that calls AddStateMachines gets the tables for free; one that does not just carries two empty tables. You do
not create or migrate them yourself, and there is no EnsureCreated step. Their models are
Trax.Effect.Models.SnapshotDraft.SnapshotDraft and Trax.Effect.Models.EffectClaim.EffectClaim, mapped on the
core data context like every other Trax table, so on SQLite the data context strips the trax schema, maps the
jsonb context column to TEXT, and stores each timestamp as fixed-width UTC text that sorts in time order.
A draft is keyed by its user, its machine and its id, so two machines can each give one user a draft under the same well-known id without touching each other's.