Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added

- **Standalone clients** (`--standalone`, `-sa`, or `"standalone": true` in the settings file): the
generator writes a copy of the runtime into `Runtime/` under `<namespace>.Runtime`, so the client
references no Linq2GraphQL package. Clients generated with different generator versions can then be
used in one app. `Runtime/RuntimeInfo.cs` records the runtime version and the packages it needs, which
the generator also prints. With `--agent` the copy includes the agent tools from
`Linq2GraphQL.Client.AI` too, in `<namespace>.Runtime.AI`.

- `GraphSubscriptionExecute.StartWithErrorsAsync()` returns `IObservable<GraphResult<T>>`.
Each event carries its data AND its GraphQL `errors`. A `next` event with field errors
no longer terminates the subscription: partial data and later events keep flowing.
Expand Down Expand Up @@ -111,6 +118,9 @@ Behaviour changes that callers may notice. None of them changes a public signatu

### Fixed

- The safe-mode schema cache key includes the schema type, so clients with their own runtime copy
that share an `IMemoryCache` and an endpoint no longer read each other's cached schema
(`InvalidCastException`).
- A server `complete` frame now completes the typed subscription observable. Previously
the observable stayed open after the server finished the stream.
- A `Fault` overflow in one subscriber no longer starves other subscribers of the same
Expand Down
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ Key details that bite:

**T4 workflow (from DEVELOPER.md — read it before touching templates):** each template is three files — `X.tt` (source, edit this), `X.tt.cs` (hand-written partial with constructor params and helpers), and `X.g.cs` (preprocessed output, **generated at build time, gitignored**). The generator's csproj runs the pinned `dotnet-t4` local tool (`.config/dotnet-tools.json`) over every `Templates\**\*.tt` before compiling, so editing a `.tt` and building is the whole loop and stale template logic cannot be built. Compile errors inside template code are reported against the `.tt` file and line. A template in a new folder needs a `T4Template` item with a `TemplateNamespace` in the csproj.

**Standalone mode** (`--standalone`, `StandaloneRuntime.cs`) makes the output carry its own copy of the runtime instead of depending on the packages, so clients generated by different versions can share an app. The csproj embeds `src/Linq2GraphQL.Client`, `.Subscriptions` and `.AI` source (minus `Assembly.cs`) as resources; Subscriptions is copied only with `--subscriptions` and AI only with `--agent`; the generator writes it to `Runtime/` with `Linq2GraphQL.Client` rewritten to `<namespace>.Runtime` everywhere, including the generated client files. So the runtime must stay copyable: no reliance on its own assembly name, no `InternalsVisibleTo` from generated code, no hard-coded `"Linq2GraphQL.Client"` strings, and anything shared across apps (like the safe-mode cache key) must be keyed by type so copies don't collide. `TestClientStandalone` is the checked-in standalone output; `Linq2GraphQL.Tests.Standalone` compiles the main suite's test files against it, with `StandaloneNamespaces.cs` pointing the package namespaces at the copy.

`GeneratorSettings.Current.Nullable` is ambient static state read from inside templates; the nullable and non-nullable clients differ mainly in nullable annotations and `#pragma warning disable CS8618`.

`GeneratorSettings.Current.TypeMapping` is the GraphQL-scalar-to-CLR-type table for the run: `Helpers.DefaultTypeMapping` with any `scalarMappings` from the `--config` settings file applied on top (`GeneratorConfig`). It is read in exactly two places - `BaseType.GetCoreType` for the emitted type name and `Schema.GetCustomScalars`, which treats absence from the table as "generate a `CustomScalar` class for this scalar" - so mapping a scalar to a simple type also suppresses its generated class, and mapping it to `null` does the reverse. Override targets are validated against `Helpers.SupportedTargetTypes`, an allow-list, because a null `CoreType.CSharpType` silently changes nullability and the input-factory template rather than failing.
Expand All @@ -73,6 +75,6 @@ Key details that bite:

`Linq2GraphQL.Tests` is xUnit + Shouldly + Moq. It spins up the real GraphQL server in-process with `WebApplicationFactory<Program>` — `Linq2GraphQL.TestServer` is HotChocolate over the POCOs in `TestServer.Shared`, and `TestServerNullable` is the same schema for the nullable client. `SampleClientFixture` / `SampleClientNullableFixture` wire the generated client to that in-memory host (safe mode on, SSE subscriptions), and test classes take them via `IClassFixture<>`. So most tests are end-to-end: an assertion failure can come from the expression parser, the query text, or the server's own resolvers.

`Linq2GraphQL.TestClient` / `TestClientNullable` hold **checked-in generated output**. They are not regenerated by the build — run `./scripts/regenerate-test-clients.ps1` (it boots both test servers over plain HTTP, regenerates both clients with the flags that produced the committed output, and writes them back) and commit the diff. The `generated-clients` CI job runs the same script with `-Check` and fails if the committed output no longer matches the generator, so a template or schema change that you forget to regenerate is caught in CI rather than passing against stale output.
`Linq2GraphQL.TestClient` / `TestClientNullable` / `TestClientStandalone` hold **checked-in generated output**. They are not regenerated by the build — run `./scripts/regenerate-test-clients.ps1` (it boots the test servers over plain HTTP, regenerates the clients with the flags that produced the committed output, and writes them back) and commit the diff. The `generated-clients` CI job runs the same script with `-Check` and fails if the committed output no longer matches the generator, so a template or schema change that you forget to regenerate is caught in CI rather than passing against stale output. `TestClientStandalone` contains a copy of the runtime, so **any change under `src/Linq2GraphQL.Client`, `.Subscriptions` or `.AI` also needs a regenerate**; `-Check` ignores only the version line in its `RuntimeInfo.cs`.

`docs/` (the Blazor documentation site) and `StartGG/` (a sample client) are outside the CI solution filter and can drift.
4 changes: 3 additions & 1 deletion Linq2GraphQL.CI.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
"test\\Linq2GraphQL.Agent.Sample\\Linq2GraphQL.Agent.Sample.csproj",
"test\\Linq2GraphQL.TestClient\\Linq2GraphQL.TestClient.csproj",
"test\\Linq2GraphQL.TestClientNullable\\Linq2GraphQL.TestClientNullable.csproj",
"test\\Linq2GraphQL.TestClientStandalone\\Linq2GraphQL.TestClientStandalone.csproj",
"test\\Linq2GraphQL.TestServer\\Linq2GraphQL.TestServer.csproj",
"test\\Linq2GraphQL.TestServer.Shared\\Linq2GraphQL.TestServer.Shared.csproj",
"test\\Linq2GraphQL.TestServerNullable\\Linq2GraphQL.TestServerNullable.csproj",
"test\\Linq2GraphQL.Tests\\Linq2GraphQL.Tests.csproj"
"test\\Linq2GraphQL.Tests\\Linq2GraphQL.Tests.csproj",
"test\\Linq2GraphQL.Tests.Standalone\\Linq2GraphQL.Tests.Standalone.csproj"
]
}
}
14 changes: 14 additions & 0 deletions Linq2GraphQL.sln
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Linq2GraphQL.TestServer.Sha
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Linq2GraphQL.TestClientNullable", "test\Linq2GraphQL.TestClientNullable\Linq2GraphQL.TestClientNullable.csproj", "{8083E0AF-8429-4EA1-9BC9-8B6EE5EFB9B2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Linq2GraphQL.TestClientStandalone", "test\Linq2GraphQL.TestClientStandalone\Linq2GraphQL.TestClientStandalone.csproj", "{B6FCB2DB-88F4-44DD-8A77-4675AA6E8D60}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Linq2GraphQL.Tests.Standalone", "test\Linq2GraphQL.Tests.Standalone\Linq2GraphQL.Tests.Standalone.csproj", "{DB460038-5D98-46AC-8551-5F928C2EC76A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Linq2GraphQL.Console", "test\Linq2GraphQL.Console\Linq2GraphQL.Console.csproj", "{058B468F-CE72-4CFF-8847-87CEED642C4E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Linq2GraphQL.Agent.Sample", "test\Linq2GraphQL.Agent.Sample\Linq2GraphQL.Agent.Sample.csproj", "{6E3C2B71-4D8A-4F1E-9B57-2A9C0D4E8F13}"
Expand Down Expand Up @@ -92,6 +96,14 @@ Global
{8083E0AF-8429-4EA1-9BC9-8B6EE5EFB9B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8083E0AF-8429-4EA1-9BC9-8B6EE5EFB9B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8083E0AF-8429-4EA1-9BC9-8B6EE5EFB9B2}.Release|Any CPU.Build.0 = Release|Any CPU
{B6FCB2DB-88F4-44DD-8A77-4675AA6E8D60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B6FCB2DB-88F4-44DD-8A77-4675AA6E8D60}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B6FCB2DB-88F4-44DD-8A77-4675AA6E8D60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B6FCB2DB-88F4-44DD-8A77-4675AA6E8D60}.Release|Any CPU.Build.0 = Release|Any CPU
{DB460038-5D98-46AC-8551-5F928C2EC76A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DB460038-5D98-46AC-8551-5F928C2EC76A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DB460038-5D98-46AC-8551-5F928C2EC76A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DB460038-5D98-46AC-8551-5F928C2EC76A}.Release|Any CPU.Build.0 = Release|Any CPU
{058B468F-CE72-4CFF-8847-87CEED642C4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{058B468F-CE72-4CFF-8847-87CEED642C4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{058B468F-CE72-4CFF-8847-87CEED642C4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -135,6 +147,8 @@ Global
{F0D6061C-3D3F-4323-AE4F-39737F5E2247} = {3D9B8FE2-6906-4190-9AB4-845BAB6E403C}
{C1908816-34E7-4B41-9E39-2283C59DB017} = {5BE1963B-E57D-4CF5-B03E-644D8F39B4D2}
{8083E0AF-8429-4EA1-9BC9-8B6EE5EFB9B2} = {3D9B8FE2-6906-4190-9AB4-845BAB6E403C}
{B6FCB2DB-88F4-44DD-8A77-4675AA6E8D60} = {1F4F9593-3C19-4C2E-AF8C-C269659AA12A}
{DB460038-5D98-46AC-8551-5F928C2EC76A} = {5BE1963B-E57D-4CF5-B03E-644D8F39B4D2}
{058B468F-CE72-4CFF-8847-87CEED642C4E} = {5BE1963B-E57D-4CF5-B03E-644D8F39B4D2}
{6E3C2B71-4D8A-4F1E-9B57-2A9C0D4E8F13} = {5BE1963B-E57D-4CF5-B03E-644D8F39B4D2}
{C0E1A444-A834-4FC0-8436-38066EA8D37D} = {F646B02D-53FC-4C88-8941-2255A2F4DAF6}
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Usage:
-d --deprecated Include Deprecated as Obsolete
-sc --schema Schema file (SDL) to generate from instead of an endpoint
-ag --agent Agent mode: also emit the schema as SDL (see docs/design/agent-mode.md)
-sa --standalone Write a copy of the runtime into the output, see Standalone clients below
-cf --config Json settings file, see Configuration file below

As an example:
Expand Down Expand Up @@ -139,6 +140,7 @@ file is allowed to be absent.
| `nullable` | bool | `false` | `-nu`, `--nullable` |
| `deprecated` | bool | `false` | `-d`, `--deprecated` |
| `agent` | bool | `false` | `-ag`, `--agent` |
| `standalone` | bool | `false` | `-sa`, `--standalone` |
| `scalarMappings` | object | none | *no equivalent* |
| `descriptionAttributes` | bool | `true` | *no equivalent* |

Expand Down Expand Up @@ -181,6 +183,25 @@ Scalar names are matched case insensitively. The target must be one of the suppo
(`System.Int64`). Anything else fails the run with an error rather than generating a client that does
not compile.

### Standalone clients

A generated client normally references the `Linq2GraphQL.Client` package, and an app gets one version
of it however many clients it uses. When clients are shipped as packages of their own by different
teams, an app that references clients made with different generator versions ends up with whichever
runtime NuGet picks. With `--standalone` (`-sa`) the generator instead writes a copy of the runtime into
`Runtime/` in the output, under the namespace `<namespace>.Runtime`, so the client depends on nothing
from Linq2GraphQL:

Linq2GraphQL https://spacex-production.up.railway.app/ -c="SpaceXClient" -n="SpaceX" -o="Generated" --standalone

With `--agent` the copy also includes the agent tools from `Linq2GraphQL.Client.AI`, in
`<namespace>.Runtime.AI`. `Runtime/RuntimeInfo.cs` records the runtime version and the packages the
copy needs, and the generator prints them as `PackageReference` lines to add instead of the
Linq2GraphQL packages. Code using the client does not change; runtime types you name yourself
(`GraphClientOptions`, `GraphQueryExecutionException`) come from `SpaceX.Runtime`. A runtime fix
reaches the client only when you regenerate, and the `Runtime` folder is rewritten on every run (don't
edit it).

## Add Nuget

Latest
Expand Down
67 changes: 67 additions & 0 deletions docs/Linq2GraphQL.Docs/Pages/Generator.razor
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<tr><td class="text-nowrap"><code>-es</code>, <code>--enum-strategy</code></td><td><code>FailIfMissing</code></td><td><code>AddUnknownOption</code> gives every enum an extra <code>Unknown</code> member, so values added to the schema later don't break deserialization.</td></tr>
<tr><td class="text-nowrap"><code>-d</code>, <code>--deprecated</code></td><td><code>false</code></td><td>Include deprecated fields and enum values, marked <code>[Obsolete]</code>. Without it they are left out.</td></tr>
<tr><td class="text-nowrap"><code>-ag</code>, <code>--agent</code></td><td><code>false</code></td><td>Agent mode, in preview: also emit the schema as SDL so <code>Linq2GraphQL.Client.AI</code> can expose it to AI agents.</td></tr>
<tr><td class="text-nowrap"><code>-sa</code>, <code>--standalone</code></td><td><code>false</code></td><td>Write a copy of the runtime into the output, so the client needs no <code>Linq2GraphQL.Client</code> package. See <a href="generator#standalone">standalone clients</a>.</td></tr>
<tr><td class="text-nowrap"><code>-cf</code>, <code>--config</code></td><td><code>linq2graphql.json</code></td><td>A json settings file, see <a href="generator#configuration-file">below</a>.</td></tr>
</tbody>
</table>
Expand Down Expand Up @@ -86,6 +87,7 @@
<tr><td><code>nullable</code></td><td>bool</td><td><code>false</code></td><td><code>-nu</code>, <code>--nullable</code></td></tr>
<tr><td><code>deprecated</code></td><td>bool</td><td><code>false</code></td><td><code>-d</code>, <code>--deprecated</code></td></tr>
<tr><td><code>agent</code></td><td>bool</td><td><code>false</code></td><td><code>-ag</code>, <code>--agent</code></td></tr>
<tr><td><code>standalone</code></td><td>bool</td><td><code>false</code></td><td><code>-sa</code>, <code>--standalone</code></td></tr>
<tr><td><code>scalarMappings</code></td><td>object</td><td>none</td><td>no equivalent</td></tr>
<tr><td><code>descriptionAttributes</code></td><td>bool</td><td><code>true</code></td><td>no equivalent</td></tr>
</tbody>
Expand Down Expand Up @@ -153,6 +155,62 @@
</p>
</section>

<section class="doc-section">
<h2 id="standalone">Standalone clients</h2>
<p>
A generated client normally references the <code>Linq2GraphQL.Client</code> package, and an app gets one
version of that package however many clients it uses. That is a problem when clients are built and
shipped as packages of their own, by different teams: an app that references two clients made with
different generator versions ends up with whichever runtime NuGet picks, which may not be the one either
client was generated against.
</p>
<p>
With <code>--standalone</code> the generator writes a copy of the runtime next to the client instead,
under the namespace <code>&lt;namespace&gt;.Runtime</code>. The client then depends on nothing from
Linq2GraphQL, so any number of standalone clients, of any generator version, can live in one app, next to
clients that use the package.
</p>
<CodeBlock Language="shell" Code="@Standalone"/>
<p>
The runtime lands in a <code>Runtime</code> folder in the output. <code>Runtime/Subscriptions</code> is
only written with <code>--subscriptions</code>, and <code>Runtime/AI</code>, a copy of the agent tools
from <code>Linq2GraphQL.Client.AI</code>, only with <code>--agent</code>. <code>Runtime/RuntimeInfo.cs</code> records the runtime
version the copy was taken from and the packages it needs, and the generator prints them as
<code>PackageReference</code> lines to add to the client's project, in place of
<code>Linq2GraphQL.Client</code>:
</p>
<CodeBlock Language="xml" Code="@StandalonePackages"/>
<p>
Code that uses the client does not change: <code>AddSpaceXClient()</code>, <code>Select</code> and
<code>ExecuteAsync</code> work as before. Runtime types you name yourself, such as
<code>GraphClientOptions</code> or <code>GraphQueryExecutionException</code>, come from
<code>SpaceX.Runtime</code> rather than <code>Linq2GraphQL.Client</code>. In agent mode,
<code>AsAgentTools</code> comes from <code>SpaceX.Runtime.AI</code>, so don't add the
<code>Linq2GraphQL.Client.AI</code> package either.
</p>
<p>Things to know before choosing it:</p>
<ul>
<li>
A runtime fix reaches a standalone client only when the client is regenerated with a newer generator.
That is the point (each client keeps the runtime it was tested with), but nothing updates it for you.
</li>
<li>
Don't edit the files in <code>Runtime</code>. The folder belongs to the generator and is deleted and
rewritten on every run, and turning standalone off removes it.
</li>
<li>
Each client has its own runtime types, so two standalone clients throw different
<code>GraphQueryExecutionException</code> types; catch each one in terms of its own client.
</li>
<li>
The packages the copy needs (<code>Microsoft.Extensions.*</code>, <code>Websocket.Client</code> and,
in agent mode, <code>GraphQL-Parser</code> and <code>Microsoft.Extensions.AI.Abstractions</code>) are
still ordinary NuGet references, so an app gets one version of each. They keep to semantic
versioning, so the highest one requested works for every client.
</li>
</ul>
</section>

@code {
private const string Usage = """
Linq2GraphQL [<endpoint>] [options]
Expand All @@ -178,6 +236,15 @@
Linq2GraphQL --config linq2graphql.json -o=SomewhereElse
""";

private const string Standalone = """
Linq2GraphQL https://spacex-production.up.railway.app/ -c=SpaceXClient -n=SpaceX -o=Generated --standalone
""";

private const string StandalonePackages = """
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.11" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.11" />
""";

private const string ConfigJson = """
{
"endpoint": "https://spacex-production.up.railway.app/",
Expand Down
Loading
Loading