Snapshot resolvers and restore policies
Use case
Use these contracts when project-owned extension data must be serialized, restored, migrated, or validated before it replaces live board state.
Contract and ownership
A restore policy decides whether an otherwise readable board or extension payload is acceptable for the current project.
An IGridExtensionSnapshotResolver owns one stable extension ID and snapshot-format ID. The typed resolver base validates casts and delegates conversion to two focused methods.
Snapshot data is independent of live state. Restoration creates a new owned extension; it must not retain references to the payload object or another board.
Restore policies have two ownership scopes:
- IGridBoardStateRestorePolicy inspects the staged board and complete snapshot;
- IGridExtensionRestorePolicy claims one matching top-level extension payload.
Both run inside restoration. Throwing rejects the restore and leaves the live board unchanged.
Implementation
Excerpt — serialize and restore one runtime extension. Tested using public APIs.
public sealed class TurnCounterSnapshotResolver :
GridExtensionSnapshotResolver<TurnCounterExtension, GridStringExtensionSnapshotData>
{
public TurnCounterSnapshotResolver()
: base(TurnCounterExtension.Id, "game.turn-counter.v1")
{
}
protected override GridStringExtensionSnapshotData CreateSnapshotData(
TurnCounterExtension extension,
GridExtensionSnapshotContext context) =>
new(extension.Turn.ToString(CultureInfo.InvariantCulture));
protected override TurnCounterExtension CreateExtension(
GridStringExtensionSnapshotData data,
GridExtensionSnapshotContext context) =>
new(int.Parse(data.Payload, CultureInfo.InvariantCulture));
}
Increment the snapshot type ID only for an incompatible payload format. Keep old resolvers or perform an explicit migration when old saves must remain supported.
Use policies for validation or for a project-owned payload that cannot follow the normal resolver path. Keep their ownership narrow:
- a board policy must not modify unrelated external services;
- an extension policy should return
trueonly for the exact extension and payload format it owns; - zero matching extension policies leaves the payload on the ordinary resolver/preservation path;
- more than one matching policy rejects the restore as ambiguous.
Excerpt — reject a snapshot missing required board data. Tested using public APIs.
public sealed class RequiredExtensionRestorePolicy : IGridBoardStateRestorePolicy
{
private readonly string requiredExtensionId;
public RequiredExtensionRestorePolicy(string requiredExtensionId)
{
this.requiredExtensionId = string.IsNullOrWhiteSpace(requiredExtensionId)
? throw new ArgumentException("A required extension ID is needed.", nameof(requiredExtensionId))
: requiredExtensionId.Trim();
}
public void Restore(GridBoardStateRestoreContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
bool present = context.Snapshot.ExtensionSnapshots.Any(snapshot =>
string.Equals(snapshot.ExtensionId, requiredExtensionId, StringComparison.Ordinal));
if (!present)
{
throw new InvalidOperationException(
$"Snapshot is missing required extension '{requiredExtensionId}'.");
}
}
}
Excerpt — claim and reject one legacy extension payload. Tested using public APIs.
public sealed class RejectedLegacyPayloadPolicy : IGridExtensionRestorePolicy
{
private readonly string extensionId;
public RejectedLegacyPayloadPolicy(string extensionId)
{
this.extensionId = string.IsNullOrWhiteSpace(extensionId)
? throw new ArgumentException("An extension ID is needed.", nameof(extensionId))
: extensionId.Trim();
}
public bool CanRestore(GridExtensionSnapshot snapshot) =>
string.Equals(snapshot.ExtensionId, extensionId, StringComparison.Ordinal);
public void Restore(
GridBoardStateRestoreContext context,
GridExtensionSnapshot snapshot)
{
throw new InvalidOperationException(
$"Legacy payload '{snapshot.SnapshotTypeId}' requires an explicit migration.");
}
}
Registration
Add resolvers to one GridExtensionSnapshotResolverSet and pass it through GridRuntimeConfiguration. Do not rely on Editor discovery at runtime.
Pass ordered board policies and extension policies through GridBoardStateRestoreOptions when building or applying the restore plan. Preview the restore plan before applying it when rejection must be shown to the user.
Verification
Missing resolvers, duplicate IDs, type mismatches, invalid payloads, and newer schema versions must be visible through the snapshot diagnostic sink.
Unknown payloads can remain opaque GridExtensionSnapshot values so a load/save cycle does not erase data from a temporarily missing module; choose preservation policy explicitly.
Excerpt — preserve an unknown payload without interpreting it. Tested using public APIs.
public static bool VerifyUnknownPayloadPreservation()
{
GridBoardDefinition definition =
SquareGridBoardFactory.CreateRectangle(2, 2);
using GridBoardState source =
new GridBoardStateBuilder(definition).Build();
GridBoardStateSnapshot baseline = source.CreateSnapshotOrThrow();
GridExtensionSnapshot unknown = new(
"game.temporarily-unavailable",
"game.temporarily-unavailable.v1",
new GridStringExtensionSnapshotData("preserve-me"));
GridBoardStateSnapshot persisted = new(
baseline.CellStates,
new[] { unknown },
baseline.Revision,
baseline.Occupancy);
using GridBoardState restored = GridBoardState.FromSnapshot(
definition,
persisted,
new GridBoardStateRestoreOptions(),
new GridRuntimeConfiguration());
GridExtensionSnapshot roundTrip = restored
.CreateSnapshotOrThrow()
.ExtensionSnapshots
.Single(snapshot =>
snapshot.ExtensionId == unknown.ExtensionId);
return roundTrip.SnapshotTypeId == unknown.SnapshotTypeId &&
roundTrip.Data is GridStringExtensionSnapshotData data &&
data.Payload == "preserve-me";
}
Tests
Round-trip values, verify independent ownership, load older payload versions, test unknown-payload preservation, reject a missing required payload, and test that two policies cannot claim the same payload. Every failure must preserve the live revision, cells, structures, and extensions.
Related
- Snapshots and restoration
- Save and restore state
- Configure policies and snapshot resolvers
- IGridExtensionSnapshotResolver
- GridExtensionSnapshotResolverSet
- GridExtensionSnapshot
- GridRuntimeConfiguration
- IGridBoardStateRestorePolicy
- IGridExtensionRestorePolicy
- GridBoardStateRestoreOptions
Done when
Known payloads round-trip deterministically, unknown payloads follow the selected policy, each claimed payload has exactly one owner, and a failed restore leaves the live board unchanged.