Skip to content

Let scalar to CLR type mapping be overridden from a settings file - #98

Merged
joadan merged 5 commits into
mainfrom
scalar-type-mapping-config
Sep 8, 2026
Merged

joadan merged 5 commits into
mainfrom
scalar-type-mapping-config

Conversation

@joadan

@joadan joadan commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

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 DateTime you want as DateTime rather than DateTimeOffset, or whose BigInt you want as long rather than a generated CustomScalar wrapper, had no recourse.

This adds a json settings file that carries every existing option plus a scalarMappings table:

{
  "endpoint": "https://api.example.com/graphql",
  "client": "MyClient",
  "namespace": "My.Ns",
  "scalarMappings": {
    "DateTime": "System.DateTime",
    "BigInt":   "long",
    "Json":     null
  }
}
Linq2GraphQL                                                 # picks up ./linq2graphql.json
Linq2GraphQL --config settings.json
Linq2GraphQL --config settings.json -o="SomewhereElse"       # flag wins
Linq2GraphQL https://api/graphql -c=Client                   # unchanged

Three mapping behaviours:

  • Mapping a scalar to a simple type emits that type and suppresses its generated CustomScalar class.
  • Mapping an already-mapped scalar changes the emitted type.
  • Mapping to null opts a built-in scalar out, generating a CustomScalar for it instead.

How

The mapping table moves off 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" — which is why one setting drives both halves.

Targets validate 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.

Unknown top-level settings are also an error: System.Text.Json drops 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: scalarMappings has no command line equivalent, so it is the one setting a discovered linq2graphql.json supplies 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-config flag.

Also here

regenerate-test-clients.ps1 -Check 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 while .gitattributes text=auto gives a CRLF working tree, so every regenerated file came back modified. It now uses git diff, which applies the same normalization git applies on checkin, paired with git ls-files --others to 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, --deprecated shipped at some point and was never added to Pages/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 Linq2GraphQL in a directory with the file generated the right namespace and client, applied "MacAddress": "string", and left only Longitude.cs in Scalars; an explicit --config in the same directory correctly did not inherit the discovered file's mappings.

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
joadan force-pushed the scalar-type-mapping-config branch from 40362e8 to 4a8a66f Compare September 8, 2026 05:48
@joadan
joadan merged commit 6c7d7f3 into main Sep 8, 2026
2 checks passed
@joadan
joadan deleted the scalar-type-mapping-config branch September 8, 2026 06:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant