Skip to content

feat(strawberry-shake): persist IOperationResult via [PersistentState] (.NET 10)#2

Open
cliedeman wants to merge 2 commits into
fork-ci-basefrom
feat/razor-persistent-state-attribute
Open

feat(strawberry-shake): persist IOperationResult via [PersistentState] (.NET 10)#2
cliedeman wants to merge 2 commits into
fork-ci-basefrom
feat/razor-persistent-state-attribute

Conversation

@cliedeman

@cliedeman cliedeman commented Jun 26, 2026

Copy link
Copy Markdown

Summary

A declarative way to persist StrawberryShake results across the Blazor prerender → interactive boundary (related to ChilliCream#7209), using the .NET 10 [PersistentState] attribute directly on an IOperationResult<T> property. No generated UseQuery-style component (cf. the companion PR), and — as of the latest commit — no per-type registration:

// Program.cs — the generated client registers the serializers for you
builder.Services.AddFooClient(...);

// Component
[PersistentState]
public IOperationResult<IGetMeResult>? Result { get; set; }

protected override async Task OnInitializedAsync()
    => Result ??= await HubClient.GetMe.ExecuteAsync(CancellationToken.None);

On prerender Result is persisted; on the interactive client it's restored without re-executing the operation; the ??= skips the call. Opt in with "razorPersistedState": true in .graphqlrc.json (requires a store and a reference to StrawberryShake.Razor).

How it works

.NET 10 allows a custom serializer to be plugged into [PersistentState] via PersistentComponentStateSerializer<T> (a DI singleton). This sidesteps the interface-serialization wall: the serializer persists the raw transport data payload and rebuilds via the operation's result builder.

  • OperationResultBuilder captures the raw data element into ContextData (gated by the CapturePersistedData virtual) and rebuilds a result from a persisted payload via BuildFromPersistedData. IOperationResultBuilder exposes BuildFromPersistedData as a throwing default-interface-method (non-JSON transports unaffected).
  • OperationResultPersistentStateSerializer<TData> (StrawberryShake.Razor, #if NET10_0_OR_GREATER): Persist writes the captured payload; Restore rebuilds it through the result builder.
  • Generated wiring (opt-in razorPersistedState): the result builders emit CapturePersistedData => true, and the generated AddFooClient() registers, per result type and behind #if NET10_0_OR_GREATER, AddSingleton<PersistentComponentStateSerializer<IOperationResult<T>>, OperationResultPersistentStateSerializer<T>>(). All registrations reuse the single generic serializer class as singletons. AddPersistentOperationResult<TData>() remains available for manual/edge cases.

Trade-offs vs the UsePersistentQuery approach

  • More idiomatic and lighter: no generated component, standard .NET 10 declarative pattern, the consumer owns the component.
  • .NET 10+ only (the serializer and the generated registration are #if-guarded; net8/9 consumers compile with the registration excluded).
  • Snapshot, not reactive: Result is a one-time value (no Watch/store-update reactivity), which is the typical need for prerender hydration.

Tests

  • StrawberryShake.Razor.Tests (net10/net11): serializer Persist/Restore round-trip (no re-execution) and the empty-data fallback.
  • Core.Tests: capture into ContextData and BuildFromPersistedData round-trip.
  • Razor codegen snapshot: the emitted #if-guarded serializer registration and the CapturePersistedData override.
  • Client and CodeGeneration solutions build across net8.0/net9.0/net10.0/net11.0.

🤖 Generated with Claude Code

cliedeman and others added 2 commits June 26, 2026 12:51
…] (.NET 10)

Adds a PersistentComponentStateSerializer<IOperationResult<TData>> so a Blazor
component or service property of type IOperationResult<TData> annotated with the
.NET 10 [PersistentState] attribute survives the prerender to interactive
boundary without re-executing the operation. This is a more declarative
alternative to the generated UsePersistentQuery component.

- OperationResultBuilder captures the raw transport "data" payload into
  ContextData (gated by the CapturePersistedData virtual) and rebuilds a result
  from a persisted payload via BuildFromPersistedData. IOperationResultBuilder
  exposes BuildFromPersistedData as a throwing default-interface-method.
- OperationResultPersistentStateSerializer<TData> (StrawberryShake.Razor, guarded
  by #if NET10_0_OR_GREATER) writes the captured payload and restores it through
  the result builder. Register it per result type with
  services.AddPersistentOperationResult<TData>().
- Tests: Core.Tests cover capture + BuildFromPersistedData round-trip; a new
  StrawberryShake.Razor.Tests project (net10/net11) covers the serializer
  Persist/Restore round-trip and the empty-data fallback.

Relates to ChilliCream#7209.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ia codegen

The generated client now registers a PersistentComponentStateSerializer per
operation result type, so consumers no longer call AddPersistentOperationResult<T>()
per type - AddXxxClient() wires them up. The registration is emitted behind
#if NET10_0_OR_GREATER (the serializer type is .NET 10+), and the same opt-in turns
on raw-payload capture in the generated result builders.

- New RazorPersistedState generator setting, plumbed from .graphqlrc.json
  (StrawberryShakeSettings) through CSharpGeneratorSettings to
  CSharpSyntaxGeneratorSettings and CSharpGenerator.
- JsonResultBuilderGenerator emits "protected override bool CapturePersistedData
  => true;" when the opt-in is set and a store is enabled.
- DependencyInjectionGenerator emits, per operation result type, a guarded
  AddSingleton<PersistentComponentStateSerializer<IOperationResult<T>>,
  OperationResultPersistentStateSerializer<T>>(). All registrations reuse the
  single generic serializer class as singletons.
- Razor codegen snapshot covers the emitted registration and capture override.

Relates to ChilliCream#7209.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant