Skip to content

test(strawberry-shake): characterize shared-fragment model naming gap (#6873)#10

Open
cliedeman wants to merge 1 commit into
fork-ci-basefrom
fix/ss-shared-fragment-model-naming
Open

test(strawberry-shake): characterize shared-fragment model naming gap (#6873)#10
cliedeman wants to merge 1 commit into
fork-ci-basefrom
fix/ss-shared-fragment-model-naming

Conversation

@cliedeman

@cliedeman cliedeman commented Jun 27, 2026

Copy link
Copy Markdown

Relates to ChilliCream#6873

Problem

When the same named GraphQL fragment is spread across two operations, Strawberry Shake's code generator produces two separate, divergently named per-operation models instead of one shared model named from the fragment.

For the schema/operations in ChilliCream#6873 (a MyStudent fragment spread by both GetStudentById and GetStudents, where MyStudent itself spreads MySchool), the generator emits today:

Concern GetStudentById GetStudents
Student data class GetStudentById_Student_Student GetStudents_Students_Student
Student interface IGetStudentById_Student_Student IGetStudents_Students_Student
School data class GetStudentById_Student_School_School GetStudents_Students_School_School
School interface IGetStudentById_Student_School IGetStudents_Students_School

The shared fragment interfaces IMyStudent and IMySchool are generated (one each), but:

  • there is no single shared data class named from the fragment (no MyStudent / MySchool class), and
  • the student model's school property is typed by an operation-scoped interface (IGetStudentById_Student_School) rather than the nested fragment interface (IMySchool), which is the "randomly assigned type" symptom reported in the issue.

These names are derived from the operation name plus the field path, so they change when operations/fields are added or reordered, which is why several users in the issue report build pipelines that "randomly" fail across machines.

Root cause (analysis)

  • InterfaceTypeSelectionSetAnalyzer.AnalyzeWithDefaults / AnalyzeOperation build each per-operation interface and class via FragmentHelper.CreateInterface(...) / CreateClass(...), passing fieldSelection.Path (operation name + field path). (src/StrawberryShake/CodeGeneration/src/CodeGeneration/Analyzers/InterfaceTypeSelectionSetAnalyzer.cs)
  • FragmentHelper.CreateFragmentNode(...) calls CreateName(path, GetClassName), which joins the path segments with _, so the names are inherently operation-scoped. (.../Analyzers/FragmentHelper.cs)
  • The fragment interfaces are named from fragmentNode.Fragment.Name (the GraphQL fragment name) and cached by name, which is why only they are shared. (.../Analyzers/FragmentHelper.cs, CreateInterface)
  • Crucially, there is no cross-operation model registry: DocumentAnalyzer.Analyze() creates a fresh DocumentAnalyzerContext (and therefore a fresh model cache) per operation. The named-fragment identity is also lost during RewriteForConcreteType, so the data class has no fragment name to fall back to.

What this PR does

  • Adds SharedFragmentModelNamingTests, a dedicated characterizing test class (in src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/) that asserts on the generated source (CSharpGenerator.Generate(...).Documents[].SourceText) and locks in the current behaviour:
    • both divergent per-operation Student data classes exist and no shared MyStudent class exists;
    • both divergent nested School data classes exist and no shared MySchool class exists;
    • exactly one IMyStudent and one IMySchool interface are generated;
    • the nested school property is typed by the per-operation interface, not IMySchool.
  • Passes on all four target frameworks (net8.0/9.0/10.0/11.0). No production code is modified and no existing snapshots are touched.

These tests are written so that the Assert.False/Assert.Equal(0, ...) expectations flip when the shared-fragment behaviour is eventually fixed, giving a future change a clear target.

What this PR does NOT do (remaining scope)

It does not fix the divergence. A complete fix (one shared, fragment-named data class reused across operations, with nested properties typed by the fragment interface) is large and invasive:

  • there is no cross-operation model identity today (DocumentAnalyzerContext and its model cache are per-operation), so a shared registry plus a structural-merge/equivalence guarantee would have to be introduced;
  • the per-operation data-class name is the de-facto key threaded through the result builders, JSON deserializers, entity mappers, result-info and store wiring, all of which would need to change in lockstep;
  • the operation result-root must remain per-operation (collision guards in TypeDescriptorMapper depend on it);
  • roughly 86 of ~113 snapshot files plus the integration baselines would need regenerating.

Given the fork's "minimal, focused, safe PR" guidance and the risk of destabilizing the generator or mass-updating snapshots, the unification is intentionally deferred to a separate, explicitly budgeted effort. A lower-risk partial step identified during analysis (typing nested properties by the fragment interface in TypeDescriptorMapper.GetFieldTypeDescriptor) is also out of scope here because it would still churn many snapshots.

— Claude
🤖 Generated with Claude Code

Add a dedicated, cross-framework characterizing test for
ChilliCream#6873. When the same named fragment is
spread across two operations, the generator emits two divergently named
per-operation data classes (e.g. GetStudentById_Student_Student and
GetStudents_Students_Student) instead of one shared model named from the
fragment. The fragment INTERFACES (IMyStudent, IMySchool) are already
shared, but the data classes are not, and a nested property such as the
student's school is typed by an operation-scoped interface
(IGetStudentById_Student_School) rather than the fragment interface
(IMySchool).

The test asserts on the generated source from
CSharpGenerator.Generate(...).Documents[].SourceText and documents the
current behaviour so a future fix has a clear target. No production code
is changed: a complete fix is large and invasive (no cross-operation
model registry exists today and unifying the data classes would touch the
JSON deserializers, entity mappers, store wiring and ~86 snapshot files),
so it is intentionally out of scope for this PR.
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.

2 participants