Let scalar to CLR type mapping be overridden from a settings file - #98
Merged
Merged
Conversation
The generator mapped GraphQL scalars to CLR types through a hardcoded table with no way for a user to influence it: a schema whose DateTime you want as DateTime rather than DateTimeOffset, or whose BigInt you want as long rather than a generated CustomScalar wrapper, had no recourse. Add a json settings file (--config) that carries every existing option plus a scalarMappings table. An explicitly passed command line option always wins over the file, so flag-only invocations - including scripts/regenerate-test-clients.ps1 - behave exactly as before. The mapping table moves from a static Helpers.TypeMapping onto GeneratorSettings.Current, copied fresh per run so overrides cannot leak between runs. Its two consumers read it through Helpers.EffectiveTypeMapping: BaseType.GetCoreType for the emitted type name, and Schema.GetCustomScalars, which treats absence from the table as "generate a CustomScalar for this scalar". So mapping a scalar to a simple type also suppresses its generated class, and mapping it to null opts a built-in scalar out into a CustomScalar. Targets are validated against an allow-list of simple BCL types rather than accepting any string. CoreType.CSharpType is not decorative - it drives UseSharpNoneNull and InputFactoryClassTemplate - and a target that does not resolve to a real Type leaves it null, which silently emits the wrong nullability and the wrong input factory overload instead of failing. Bad targets now fail the run with the supported list in the message.
The -Check run reported drift on Windows for clients that had not drifted. It used git status, which compares the bytes on disk: the generator always writes LF (Program.cs), while .gitattributes "text=auto" gives a CRLF working tree, so every regenerated file came back modified. git diff was already being run for the message and correctly reported nothing, which is what made the failure look contradictory. Drive the check off git diff instead, which applies the same normalization git applies on checkin. git diff only compares tracked files, so pair it with git ls-files --others to keep catching a newly generated file.
The settings file was only read when --config named it, so the common case - a repository with its generator settings checked in next to the generated output - still needed the flag on every invocation. Look for linq2graphql.json in the current directory when --config is absent, so a bare Linq2GraphQL works there. An explicit --config still wins, and still fails when it names a file that does not exist; only the discovered file is allowed to be missing. GeneratorConfig.Load already announces the path it read, so a picked-up file is visible in the output rather than silent. Note that scalarMappings is the one setting with no command line equivalent, so it cannot be overridden back off once a discovered file supplies it.
README.md covered the config file and scalarMappings, but the published docs at linq2graphql.com are generated from Pages/Index.razor and still described only the command line. Mirror the README section there. The option list on that page had also fallen behind: -d/--deprecated was never added when it shipped.
The settings file was documented by example only. Three settings - token, enumStrategy and deprecated - appeared in no example anywhere, and the mapping from a flag like --enum-strategy onto the json key enumStrategy was never stated, so the only way to find them was to guess. Add a table of every setting with its type, default and command line equivalent to both the README and the docs site, and put the two boolean ones in the example. Unknown settings are now an error. System.Text.Json drops unmapped members silently, so the singular "scalarMapping" - the typo most worth catching - would produce a client with none of the mappings applied and no indication why. The known set is derived from the properties by reflection so it cannot fall out of sync, and only top level keys are checked: the keys inside scalarMappings are scalar names we cannot know up front.
joadan
force-pushed
the
scalar-type-mapping-config
branch
from
September 8, 2026 05:48
40362e8 to
4a8a66f
Compare
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.
What
The generator mapped GraphQL scalars to CLR types through a hardcoded table with no way for a user to influence it. A schema whose
DateTimeyou want asDateTimerather thanDateTimeOffset, or whoseBigIntyou want aslongrather than a generatedCustomScalarwrapper, had no recourse.This adds a json settings file that carries every existing option plus a
scalarMappingstable:{ "endpoint": "https://api.example.com/graphql", "client": "MyClient", "namespace": "My.Ns", "scalarMappings": { "DateTime": "System.DateTime", "BigInt": "long", "Json": null } }Three mapping behaviours:
CustomScalarclass.nullopts a built-in scalar out, generating aCustomScalarfor it instead.How
The mapping table moves off a static
Helpers.TypeMappingontoGeneratorSettings.Current, copied fresh per run so overrides cannot leak between runs. Its two consumers read it throughHelpers.EffectiveTypeMapping:BaseType.GetCoreTypefor the emitted type name, andSchema.GetCustomScalars, which treats absence from the table as "generate aCustomScalarfor this scalar" — which is why one setting drives both halves.Targets validate against an allow-list of simple BCL types rather than accepting any string.
CoreType.CSharpTypeis not decorative — it drivesUseSharpNoneNullandInputFactoryClassTemplate— and a target that does not resolve to a realTypeleaves it null, which silently emits the wrong nullability and the wrong input factory overload instead of failing. Bad targets now fail the run with the supported list in the message.Unknown top-level settings are also an error:
System.Text.Jsondrops unmapped members silently, so the singular"scalarMapping"would otherwise produce a client with none of the mappings applied and no indication why.Compatibility
Flag-only invocations behave exactly as before, including
scripts/regenerate-test-clients.ps1. An explicitly passed option always wins over the file. The checked-in test clients are unchanged.One sharp edge worth knowing:
scalarMappingshas no command line equivalent, so it is the one setting a discoveredlinq2graphql.jsonsupplies that cannot be overridden back off from the command line. The generator prints the path of the file it read, so a picked-up file is visible rather than silent. If that turns out to matter, the answer is a--no-configflag.Also here
regenerate-test-clients.ps1 -Checkreported drift on Windows for clients that had not drifted. It usedgit status, which compares the bytes on disk: the generator always writes LF while.gitattributestext=autogives a CRLF working tree, so every regenerated file came back modified. It now usesgit diff, which applies the same normalization git applies on checkin, paired withgit ls-files --othersto keep catching a newly generated file. Pre-existing, unrelated to the feature, but it would have made CI on Windows unusable.The docs site option list had also fallen behind independently —
-d, --deprecatedshipped at some point and was never added toPages/Index.razor. Fixed in passing.Tests
160 pass, 23 of them new, covering the three mapping behaviours, target-type spellings (
long/Int64/System.Int64), case-insensitive scalar names, overrides not leaking between runs, config resolution precedence, and unknown-setting rejection.Verified end to end against the nullable test server: a bare
Linq2GraphQLin a directory with the file generated the right namespace and client, applied"MacAddress": "string", and left onlyLongitude.csinScalars; an explicit--configin the same directory correctly did not inherit the discovered file's mappings.