* Ask for a type its model converts as one field, not a selection set
A type carrying [JsonConverter] was descended into like any other object, so a
member of one went into the document as a selection set over its properties:
[JsonConverter(typeof(StampConverter))]
class Stamp { public string Issuer { get; set; } public string Serial { get; set; } }
things { stamp { issuer serial } }
The server sends that field as one value — whatever the converter reads, here a
string — so the document is wrong before anything gets as far as parsing the
reply. What makes it more than a wrong guess is that the other half of the same
query already knew: ResponseStructWriter has always read a member with a
converter through that converter, "whatever its type is and whatever the query
asked for beneath it". One half of the query asked for `stamp { issuer serial }`
and the other read `stamp` whole. They were never going to agree, because the
rule lived in one of them.
So the rule moves to GraphQLTypeFacts, where the question of what a selection
set contains is already answered, and both halves read it there. A type or a
member with a converter is a leaf: the document asks for the field alone, a
path through it ends there the way a path ends at a string — `x.Stamp.Serial`
asks for `stamp` and reads `Serial` off what the converter handed back — and
FGQL014 stops asking for a projection of a value that has nothing to project.
Only a converter the reader could actually build counts, since that is the same
condition the reader applies and a second answer here would be the two halves
disagreeing again in the other direction.
What the converter writes — a string, a number, an object — cannot be seen from
an analyzer: nothing in the compilation can be run, and there is no schema to
ask. What can be seen is that the model took the value out of the serializer's
hands, and a value that is not shaped by its properties has no selection set to
write. A model that wants the properties selected individually is one that does
not put a converter over them.
Descend now returns Reach.None where it used to break, so a leaf ends the reach
and not merely the path. Handing back the value's type let an operator further
out take that type for rows of the graph and collect its properties onto the
field, which is the same bug one step along the chain.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdVrgkAM14vi9JfNBLCc5g
* Give the IDE test explorers a testhost to discover through
The test projects run on Microsoft.Testing.Platform — NUnit's own runner, an
entry point generated into each project, `dotnet test` driving the executable
directly. That is the point of it: what CI runs and what a developer runs are
the same program. What it also means is that nothing pulls in VSTest, and so
nothing puts a testhost in the output directory.
Rider's test explorer still discovers through VSTest, and with no testhost to
enumerate it finds nothing at all — not "this runner is not supported yet" but
an empty window, which reads like a broken checkout. JetBrains name the package
as the workaround on the tickets tracking their own support for this
(RIDER-131530, RSRP-498837), and there is no ETA on it.
So Microsoft.NET.Test.Sdk comes back, alongside the platform rather than instead
of it. The version was already pinned in Directory.Packages.props with nothing
referencing it. `dotnet test` still runs on Microsoft.Testing.Platform, because
global.json pins the runner and the adapter's targets handle the two coexisting
— the same 281 tests, the same summary, the same program. The cost is a package
in the graph that nothing here runs through, and the comment above it says to
drop it when Rider can discover these tests on its own.
Also renames Feather.GraphQL.sln.DotSettings, which was named for a solution
this repository has not had since it moved to Feather.GraphQL.slnx. Rider looks
for the file beside the solution and under its name, so the settings in it were
being ignored — among them the abbreviation rules that stop a rename turning
GraphQL into GraphQl.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdVrgkAM14vi9JfNBLCc5g
* Write a page size the compiler knows into the document, not a variable
`FirstOrDefaultAsync()` asks the server for one row. One is the compiler's own
number — nothing at the call site can change it — and it was going out as a
variable anyway:
query($v0: Int) { things(take: $v0) { … } } "variables":{"v0":1}
instead of
query { things(take: 1) { … } }
The variable carries the same 1 to the same server on every call, so it buys
nothing, and it costs the one thing the argument is for. A server sizing a query
before it runs it reads `take: $v0` as "up to whatever the schema allows" and
has to budget for that; `take: 1` is one row and it can say so. That is the
argument this library already makes for writing a filter's structure into the
document rather than handing it over as an opaque input object — it just was not
being applied to the figure that actually bounds the query.
The rule is about where the number comes from, not which operator asked for it:
- a terminal's own page — First is 1, Single is 2, Last is 1, Any is 1 — is
always written out, under `take` or `first` as the paging kind decides
- `Take(10)`, `Skip(5)`, `Take(PageSize)` for a const: written out, since
GetConstantValue can read them where the chain is written
- `Take(size)` for one of the method's parameters: still a variable, which is
the one case a variable was ever for
A chain that binds nothing else now posts the constant body rather than building
one, because there is nothing left to write into it at run time.
Two things went dead with this and are gone rather than left to look meaningful:
the generator's payload branch for a terminal's page, which no longer has a
binding to fill, and BoundValue.Last — only a terminal asks for a last page, so
its size is never a variable and there is nothing for that member to name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdVrgkAM14vi9JfNBLCc5g
---------
Co-authored-by: Claude <noreply@anthropic.com>