IPersistedOperationsService

The persisted-operation management surface. The operations.persistedOperations GraphQL fields call it, and the dashboard calls it, so an upload, deactivation or restore is accepted or refused the same way, with the same codes, from either. Use it from admin tooling that should behave exactly as the API does.

Registered as a singleton by UsePersistedOperations and by AddPersistedOperationStore.

Interface

public interface IPersistedOperationsService
{
    Task<PersistedOperationsPage> ListAsync(PersistedOperationFilter? filter, int skip, int take, CancellationToken ct);
    Task<PersistedOperationDto?> GetAsync(string id, string? tenantKey, CancellationToken ct);
    Task<IReadOnlyList<PersistedOperationHistoryDto>> GetHistoryAsync(string id, string? tenantKey, int skip, int take, CancellationToken ct);
    Task<UploadPersistedOperationPayload> UploadAsync(UploadPersistedOperationInput input, CancellationToken ct);
    Task<DeactivatePersistedOperationPayload> DeactivateAsync(DeactivatePersistedOperationInput input, CancellationToken ct);
    Task<RestorePersistedOperationPayload> RestoreAsync(RestorePersistedOperationInput input, CancellationToken ct);
}

Behavior

  • The writes never throw for a refused change. The payload carries Success = false and Errors, each with a stable Code: INVALID_INPUT, NOT_FOUND, PARSE_FAILED, SCHEMA_VALIDATION_FAILED or SHAPE_DIFF_VIOLATION. The codes are the ones in the management mutations' error payload.
  • UploadAsync goes through IPersistedOperationStore.UpsertAsync: schema validation, exactly one operation per document, the shape-diff guardrail, history, cache invalidation and broadcast.
  • DeactivateAsync requires a reason and acts on an active operation; an unknown or already deactivated id is NOT_FOUND.
  • RestoreAsync acts on any existing operation, active or deactivated.
  • GetAsync returns active and deactivated operations, and null when there is none.
  • ListAsync returns the most recently updated first. GetHistoryAsync returns the most recent change first. For both, take outside 1 to 200 reads as 50 and a negative skip reads as 0.

Example

var service = serviceProvider.GetRequiredService<IPersistedOperationsService>();
 
var result = await service.DeactivateAsync(
    new DeactivatePersistedOperationInput("userProfile_v1", Reason: "replaced by userProfile_v2"),
    cancellationToken
);
 
if (!result.Success)
    throw new InvalidOperationException(string.Join("; ", result.Errors.Select(e => $"{e.Code}: {e.Message}")));