Skip to content

fix(strawberryshake): preserve entity fields across differing selection sets (#8063)#11

Open
cliedeman wants to merge 1 commit into
fork-ci-basefrom
fix/ss-inline-fragment-order-missing-data
Open

fix(strawberryshake): preserve entity fields across differing selection sets (#8063)#11
cliedeman wants to merge 1 commit into
fork-ci-basefrom
fix/ss-inline-fragment-order-missing-data

Conversation

@cliedeman

@cliedeman cliedeman commented Jun 27, 2026

Copy link
Copy Markdown

Summary

Fixes a Strawberry Shake runtime bug where data goes missing after deserialization when an entity is referenced more than once in a query with different selection sets. This is the root cause behind the inline-fragment-ordering report in ChilliCream#8063.

Root cause

When an interface/object field carries an id, Strawberry Shake treats it as an entity and fills the entity store recursively. The generated JSON result builder constructed an entity by inlining the constructor arguments directly, and the new-entity branch used default! for every field not present in the current selection set.

If the same entity id was written first by a nested/sibling reference (with one selection set) and then again by an outer reference (with a different, partial selection set), the second SetEntity call overwrote the first write, replacing the not-selected fields with default!. The result factory then read those defaulted fields, surfacing the data as missing / null, or throwing ArgumentNullException / Value cannot be null. for non-nullable fields.

The minimal reproduction (the self-referential selfishGuy.bestFriend == selfishGuy case): the outer selfishGuy selection (id/firstName/lastName) overwrote the age/phone that the inner bestFriend selection had just written, so bestFriend.Age came back 0.

Fix

JsonResultBuilderGenerator_UpdateEntity now, when constructing an entity:

  1. Evaluates the response-derived constructor arguments first into locals (this is what triggers any recursive write of the same entity into the store).
  2. In the new-entity branch, re-checks the store with TryGetEntity. If the entity now exists, the already-deserialized fields are preserved (merged) instead of being overwritten with defaults.

MethodCallBuilder.AddOutArgument accepts a nullable type reference so it can emit out entity (reusing the existing out variable) for the re-check.

Generated else branch, before vs after:

- session.SetEntity(entityId, new PersonEntity(arg0, arg1, arg2, arg3, default!, default!));
+ var arg0 = Deserialize_...("id");
+ var arg1 = Deserialize_...("firstName");
+ var arg2 = Deserialize_...("lastName");
+ var arg3 = Update_...BestFriend(...);   // may recursively write this same entity
+ if (session.CurrentSnapshot.TryGetEntity(entityId, out entity))
+ {
+     session.SetEntity(entityId, new PersonEntity(arg0, arg1, arg2, arg3, entity.Age, entity.Phone));
+ }
+ else
+ {
+     session.SetEntity(entityId, new PersonEntity(arg0, arg1, arg2, arg3, default!, default!));
+ }

This mirrors the approach in upstream draft PR ChilliCream#8595, scoped down to the single generator change plus the regenerated outputs.

Tests (red → green)

Two runtime tests driven through the in-memory transport (real client, real result builder, real entity store):

  • RecursiveEntitySelfReferenceTest reproduces the loss. Before the fix it failed with Expected: 42 / Actual: 0 for bestFriend.Age; it now passes.
  • InlineFragmentOrderTest covers the exact Order of Inline Fragments Causes Missing Data ChilliCream/graphql-platform#8063 shape: an interface field (pet: IPet) selected under two top-level fields where the first omits one implementing type's inline fragment and the second includes it. It asserts the second field's Bird.WingSpan survives.

Files changed

  • Generators/JsonResultBuilderGenerator_UpdateEntity.cs – the fix (evaluate args, re-check store, merge).
  • Builders/MethodCallBuilder.cs – nullable out type reference.
  • 13 integration *.Client.cs + 47 generator *.snap snapshots – regenerated entity-builder output.
  • New: InlineFragmentOrderTest, RecursiveEntitySelfReferenceTest (+ generated clients) and two facts in TestGeneration.cs.

Verification

  • CodeGeneration.CSharp.Tests: 193 passed, 1 skipped (incl. all integration clients + generator snapshots).
  • CodeGeneration.Razor.Tests: 1 passed. CodeGeneration.Tests: 29 passed, 1 skipped.
  • StrawberryShake.Core.Tests: 210 passed.
  • New tests verified green on net8.0 and net9.0.

Fixes ChilliCream#8063

— Claude
🤖 Generated with Claude Code

…on sets

Strawberry Shake lost data after deserialization when an entity was
referenced more than once in a query with different selection sets,
including the inline-fragment ordering case in ChilliCream#8063. The JSON result
builder filled the entity store recursively: a nested reference wrote
the entity first, then the outer reference overwrote it, replacing the
fields it had not selected with defaults. The result factory then read
those defaulted fields (or hit a non-null constructor) and surfaced the
data as missing or as Value cannot be null.

The fix changes the generated entity deserializer so that, when an
entity is about to be created, the constructor arguments are evaluated
first (which is what triggers any recursive write of the same entity),
and the store is then re-checked. If the entity now exists, the
already-deserialized fields are preserved instead of being overwritten
with defaults.

JsonResultBuilderGenerator_UpdateEntity now hoists each response-derived
argument into a local and, in the new-entity branch, re-runs TryGetEntity
and merges. MethodCallBuilder.AddOutArgument gains a null type reference
to emit `out entity` when reusing the existing out variable.

Adds two runtime tests driven through the in-memory transport:
- RecursiveEntitySelfReference reproduces the loss (red before this
  change: bestFriend.Age came back 0 instead of 42) and now passes.
- InlineFragmentOrder covers the exact ChilliCream#8063 shape (an interface field
  selected twice with different inline-fragment coverage).

All affected generator snapshots and integration client outputs were
regenerated.

Fixes ChilliCream#8063
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.

Order of Inline Fragments Causes Missing Data

2 participants