Migrating an existing codebase
Statesman is designed for staged adoption. The safe path is to establish observation and lineage first, then move ownership.
Phase 1: Inventory state
Identify state that is copied, cached, refreshed, invalidated, or independently mutated in several places. Give each concept one static StateKey<T>. Do not start by converting every DTO or local variable.
Phase 2: Mirror legacy updates
Declare state with no proactive loader and push values from existing change events, polling jobs, or setters.
[StateMutationBoundary("Legacy bridge until session ownership moves to Statesman.")]
public sealed class LegacySessionBridge
{
public ValueTask PublishAsync(LegacySession legacy) =>
_state.SetAsync(
new SessionState(legacy.UserId, legacy.IsAuthenticated),
new StateWriteOptions { Source = "legacy-session" });
}
Consumers can begin reading IState<T> while the legacy system remains the source. This immediately creates snapshots, observation, history, and diagnostics.
Phase 3: Replace duplicate caches
Move existing acquisition code into declared loaders. Keep the old API surface as an adapter over IState<T> so callers do not all change at once.
Phase 4: Move transitions
Replace ad hoc mutation with UpdateAsync or typed interactions. Prefer immutable records. Add invariants at the state boundary.
Phase 5: Turn on analyzers
Install Statesman.Analyzers as warnings first. Mark authoritative state types with [ManagedState]. Add explicit mutation boundaries only around intentional adapters. Ratchet STM001 and STM003 to errors in CI once the migration surface is understood.
[*.cs]
dotnet_diagnostic.STM001.severity = error
dotnet_diagnostic.STM002.severity = warning
dotnet_diagnostic.STM003.severity = error
Phase 6: Move durability
Start in memory. Select filesystem storage for local applications, Redis for a shared low-latency authority, EF Core for relational durability, or a tiered store when hot and cold responsibilities differ.
Upgrading across the serializer-envelope release
ROADMAP 0.3 Phase 22 adds StateRecord.Envelope and StateCommit.Envelope, stamped on every new write
with the content type and serializer id the runtime wrote with. Nothing in an existing deployment needs
to change to pick this up. Records written before this release read back with a null envelope, and no
stored byte moves to get there. A consumer on Entity Framework Core who uses one of the shipped
migration packages applies one generated SerializerEnvelope migration per engine
(docs/providers/entity-framework-core-migrations.md); a consumer who owns their own migrations must
add one nullable EnvelopeJson column to each of StatesmanLedgerHead and StatesmanLedgerRecord in a
migration of their own before upgrading — the column being nullable means there is no backfill, not that
there is nothing to apply. Without it, every read fails with a missing-column error. Nothing validates an envelope on
read this release, so an envelope that disagrees with the serializer actually in use changes no
behaviour today — that check is not built yet.
Avoid the dual-authority trap
A mirror is intentionally temporary. Document which side is authoritative, tag mirrored writes with a source, and give the bridge an exit condition. Two components that both write independently are not migration, they are competing state authorities.