IPersistedOperationStore
Programmatic CRUD for trax.persisted_operation. The HTTP request path does NOT use this interface; HotChocolate calls IOperationDocumentStorage.TryReadAsync instead. Use this interface from CI manifest uploaders, admin tooling, custom dashboards, and tests.
Registered as a singleton when UsePersistedOperations is called.
Interface
public interface IPersistedOperationStore
{
Task<PersistedOperation?> GetAsync(string id, string? tenantKey, CancellationToken ct);
Task<IReadOnlyList<PersistedOperation>> ListAsync(string? tenantKey, CancellationToken ct);
Task<PersistedOperation> UpsertAsync(
string id,
string document,
UpsertOptions? options,
CancellationToken ct);
Task DeactivateAsync(string id, string? tenantKey, string reason, CancellationToken ct);
Task RestoreAsync(string id, string? tenantKey, CancellationToken ct);
}UpsertOptions
| Property | Type | Purpose |
|---|---|---|
TenantKey | string? | Tenant scope for multi-tenant deployments. Null targets the single-tenant row set. |
Description | string? | Operator-facing note recorded on the row. |
BypassShapeDiff | bool | When true, skips the shape-diff guardrail on an edit. Use only when the operator has verified the change is shape-safe. |
Version | int | Operator-controlled metadata stored on the row. Not used for request routing. Defaults to 0. |
Behavior
GetAsyncreturns null for missing or deactivated rows.ListAsyncreturns active and deactivated rows for the tenant.UpsertAsyncruns the document through IPersistedOperationValidator (HotChocolate-backed whenUsePersistedOperationsis wired in), extracts the GraphQL operation name from the document's operation definition, computes the response-shape fingerprint, writes both the live row and a history row in a single transaction, invalidates the local cache, and publishes a broadcast event when the broadcaster is configured.- Validation throws one of the structured PersistedOperationException subclasses. No row is written and no broadcast fires when validation rejects the document.
DeactivateAsyncandRestoreAsyncthrowInvalidOperationExceptionwhen the id does not exist.- All mutations append a row to
trax.persisted_operation_history.
Example
Manifest uploader:
var store = serviceProvider.GetRequiredService<IPersistedOperationStore>();
foreach (var (id, document) in manifest)
{
await store.UpsertAsync(id, document, options: null, ct);
}Soft-delete with reason:
await store.DeactivateAsync(
"userProfile_v1",
tenantKey: null,
reason: "broken filter in 2026-05-08 release",
ct
);