Compare types instead of their names to recognize options and lists - #628
Open
xperiandri wants to merge 2 commits into
Open
xperiandri wants to merge 2 commits into
xperiandri wants to merge 2 commits into
Conversation
Contributor
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
No blocking issues remain; regression tests cover the corrected array handling and documented validation change.
Review effort: Balanced
Findings: None
What changed in this PR
Replaces name-prefix checks with generic type identity comparisons, improving performance and correctly distinguishing options from arrays of options.
Changes:
- Centralizes type checks across shared helpers, server coercion, and middleware.
- Adds regression tests for option arrays and nullable-field validation.
- Documents the breaking validation change and helper fixes.
| File | Description |
|---|---|
| tests/FSharp.Data.GraphQL.Tests/Helpers and Extensions/ReflectionHelperTests.fs | Tests type recognition, helpers, and input validation. |
| tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj | Includes the new tests. |
| src/FSharp.Data.GraphQL.Shared/Helpers/Reflection.fs | Adds identity checks and updates option helpers. |
| src/FSharp.Data.GraphQL.Server/Values.fs | Uses identity checks during coercion. |
| src/FSharp.Data.GraphQL.Server/ReflectionHelper.fs | Updates optionality and collection checks. |
| src/FSharp.Data.GraphQL.Server.Middleware/TypeCoercion.fs | Updates middleware wrapper recognition. |
| RELEASE_NOTES.md | Documents fixes and the compatibility change. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Test Results 9 files 9 suites 14m 28s ⏱️ Results for commit cd7e32d. ♻️ This comment has been updated with latest results. |
The checks for options, value options, skippable values and F# lists compared the full name of a type with the name of the generic type definition through the culture-sensitive `String.StartsWith`, which takes hundreds of nanoseconds per check, on every coerced input value and every resolved nullable field. The full name of an array of options starts with the same name, so these checks also took `int option[]` for an option: `Helpers.unwrap` and `Helpers.objectOptionCast` threw a `NullReferenceException` for it, and an `int option[]` constructor parameter was taken for an optional one. `ReflectionHelper.isConstructedFrom` compares the generic type definition with `Type.(=)`, which takes about a nanosecond, while the F# equality of types takes about ten. `isOptionType`, `isValueOptionType`, `isSkippableType` and `isListType` replace every comparison of names, and the type name literals are removed. The ``System.Array`1`` check of `isAssignableWithUnwrap` never matched, as no array type has that name, and is dropped: input coercion builds an array only for a GraphQL list whose type is an array, which is assignable without it. Breaking change: a nullable GraphQL field bound to an array of options must be declared as an option of the array, as a list of options always had to be. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The F# equality of types goes through generic equality and is several times slower than `Type.op_Equality`, and comparing the names of types is slow and takes an array of a generic type for the generic type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
reflection-type-comparison
branch
from
October 4, 2026 20:19
faa08eb to
cd7e32d
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The checks for options, value options, skippable values and F# lists compared the full name of a type with the name of the generic type definition through the culture-sensitive
String.StartsWith. Input coercion runs them on every coerced value, and execution on every resolved nullable field throughHelpers.objectOptionCast. They also took an array of options for an option, since its full name starts with the same name.This PR compares the types themselves with
Type.(=). #625 builds on it.Benchmark
Each way to tell whether a type is an F# option, measured with BenchmarkDotNet 0.15 on .NET 10, x64 (the source is below):
int optionint option[]int voptionList<int>stringFullName.StartsWith name(before)FullName.StartsWith (name, Ordinal)GetGenericTypeDefinition().FullName = nameGetGenericTypeDefinition () = definition(F# equality)Object.ReferenceEquals (GetGenericTypeDefinition (), definition)A second run compared the type comparisons with
Type.(=), theType.op_Equalityoperator. Other processes loaded the machine during it, so its absolute numbers are higher than above, but the three methods ran under the same load:int optionint option[]int voptionList<int>stringGetGenericTypeDefinition () = definition(F# equality)Type.(=) (GetGenericTypeDefinition (), definition)(after)Object.ReferenceEquals (GetGenericTypeDefinition (), definition)None of them allocates.
Type.(=)is as fast as comparing references and stays correct for anyTypeimplementation, while the F# equality of types is several times slower, even slower than comparing the names ordinally. The Copilot instructions now requireType.(=)for comparing types. An end-to-end run ofSimpleExecutionBenchmarkwas inconclusive: other processes kept the machine 30–70% busy, and the standard deviation reached 20–60% of the mean.Benchmark source
Changes
ReflectionHelper.isConstructedFromcompares the generic type definition of a type withType.(=), andisOptionType,isValueOptionType,isSkippableTypeandisListTypebuild on it. They replace every comparison of names inHelpers, the serverReflectionHelper,Valuesand theObjectListFiltermiddleware'sTypeCoercion, and the type name literals are removed.Helpers.unwrapandHelpers.objectOptionCastno longer take an array of options for an option, for which they threw aNullReferenceException.System.Array`1check ofisAssignableWithUnwrapis dropped: no array type has that name, so it never matched. Input coercion builds an array only for a GraphQL list whose type is an array, which is assignable without it, so matching arrays there would only pass a list to an array parameter.Valueskeeps its two comparisons of the short names,inputType.Name <> outputType.Name. They compare whole strings ordinally, which is fast, and comparing the types instead would change which values get wrapped.Type.(=)for comparing types, in a commit of its own.Breaking changes
The old checks took an
int option[]constructor parameter for an optional one, so a nullable GraphQL field bound to it passed the schema validation, while the same field bound to anint option listalways failed it withInvalidInputTypeException. Both now fail. Such a parameter must be declared as an option of the array, such asint option[] option. The release notes list it.Verification
FSharp.Data.GraphQL.Testsin Release: 769 passed, 5 skipped, 0 failed. The newReflectionHelperTestscover the type checks, the helpers on an array of options, an input object with anint option[]field, and the rejected nullable field. Ondev, the helper test fails withNullReferenceException, and anint option listparameter fails with the sameInvalidInputTypeExceptiontheint option[]one now fails with.FSharp.Data.GraphQL.slnxwith SDK10.0.401: 0 warnings, 0 errors🤖 Generated with Claude Code