diff --git a/CHANGELOG.md b/CHANGELOG.md index cf55d54b..f2d9bcf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `.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 `.Runtime.AI`. + - `GraphSubscriptionExecute.StartWithErrorsAsync()` returns `IObservable>`. 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. @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 706a8eb2..4b310701 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 `.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. @@ -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` — `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. diff --git a/Linq2GraphQL.CI.slnf b/Linq2GraphQL.CI.slnf index e99fb3e6..be5bfa99 100644 --- a/Linq2GraphQL.CI.slnf +++ b/Linq2GraphQL.CI.slnf @@ -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" ] } } diff --git a/Linq2GraphQL.sln b/Linq2GraphQL.sln index ef044bf5..51b9a7b4 100644 --- a/Linq2GraphQL.sln +++ b/Linq2GraphQL.sln @@ -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}" @@ -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 @@ -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} diff --git a/README.md b/README.md index bf245945..2366282f 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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* | @@ -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 `.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 +`.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 diff --git a/docs/Linq2GraphQL.Docs/Pages/Generator.razor b/docs/Linq2GraphQL.Docs/Pages/Generator.razor index 932f543b..78a7e811 100644 --- a/docs/Linq2GraphQL.Docs/Pages/Generator.razor +++ b/docs/Linq2GraphQL.Docs/Pages/Generator.razor @@ -29,6 +29,7 @@ -es, --enum-strategyFailIfMissingAddUnknownOption gives every enum an extra Unknown member, so values added to the schema later don't break deserialization. -d, --deprecatedfalseInclude deprecated fields and enum values, marked [Obsolete]. Without it they are left out. -ag, --agentfalseAgent mode, in preview: also emit the schema as SDL so Linq2GraphQL.Client.AI can expose it to AI agents. + -sa, --standalonefalseWrite a copy of the runtime into the output, so the client needs no Linq2GraphQL.Client package. See standalone clients. -cf, --configlinq2graphql.jsonA json settings file, see below. @@ -86,6 +87,7 @@ nullableboolfalse-nu, --nullable deprecatedboolfalse-d, --deprecated agentboolfalse-ag, --agent + standaloneboolfalse-sa, --standalone scalarMappingsobjectnoneno equivalent descriptionAttributesbooltrueno equivalent @@ -153,6 +155,62 @@

+
+

Standalone clients

+

+ A generated client normally references the Linq2GraphQL.Client 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. +

+

+ With --standalone the generator writes a copy of the runtime next to the client instead, + under the namespace <namespace>.Runtime. 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. +

+ +

+ The runtime lands in a Runtime folder in the output. Runtime/Subscriptions is + only written with --subscriptions, and Runtime/AI, a copy of the agent tools + from Linq2GraphQL.Client.AI, only with --agent. Runtime/RuntimeInfo.cs records the runtime + version the copy was taken from and the packages it needs, and the generator prints them as + PackageReference lines to add to the client's project, in place of + Linq2GraphQL.Client: +

+ +

+ Code that uses the client does not change: AddSpaceXClient(), Select and + ExecuteAsync work as before. Runtime types you name yourself, such as + GraphClientOptions or GraphQueryExecutionException, come from + SpaceX.Runtime rather than Linq2GraphQL.Client. In agent mode, + AsAgentTools comes from SpaceX.Runtime.AI, so don't add the + Linq2GraphQL.Client.AI package either. +

+

Things to know before choosing it:

+
    +
  • + 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. +
  • +
  • + Don't edit the files in Runtime. The folder belongs to the generator and is deleted and + rewritten on every run, and turning standalone off removes it. +
  • +
  • + Each client has its own runtime types, so two standalone clients throw different + GraphQueryExecutionException types; catch each one in terms of its own client. +
  • +
  • + The packages the copy needs (Microsoft.Extensions.*, Websocket.Client and, + in agent mode, GraphQL-Parser and Microsoft.Extensions.AI.Abstractions) 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. +
  • +
+
+ @code { private const string Usage = """ Linq2GraphQL [] [options] @@ -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 = """ + + + """; + private const string ConfigJson = """ { "endpoint": "https://spacex-production.up.railway.app/", diff --git a/scripts/regenerate-test-clients.ps1 b/scripts/regenerate-test-clients.ps1 index 14ca4728..068690e8 100644 --- a/scripts/regenerate-test-clients.ps1 +++ b/scripts/regenerate-test-clients.ps1 @@ -4,10 +4,11 @@ Regenerates the checked-in test clients from the test servers' live schemas. .DESCRIPTION - test/Linq2GraphQL.TestClient and test/Linq2GraphQL.TestClientNullable hold - generated output that the test suite compiles against. Nothing in the normal - build refreshes it, so it silently drifts whenever a template or the test - schema changes. + test/Linq2GraphQL.TestClient, test/Linq2GraphQL.TestClientNullable and + test/Linq2GraphQL.TestClientStandalone (standalone mode, with its own copy of + the runtime) hold generated output that the test suites compile against. + Nothing in the normal build refreshes it, so it silently drifts whenever a + template, the runtime or the test schema changes. This script boots each test server over plain HTTP, runs the generator against it with the flags that produced the checked-in output, and writes @@ -47,6 +48,15 @@ $clients = @( Port = 5181 Nullable = $true } + @{ + Name = 'SampleClient' + Server = 'test/Linq2GraphQL.TestServer' + Output = 'test/Linq2GraphQL.TestClientStandalone/Generated' + Namespace = 'Linq2GraphQL.TestClientStandalone' + Port = 5182 + Nullable = $false + Standalone = $true + } ) function Wait-ForServer([string]$url, [System.Diagnostics.Process]$process) { @@ -99,6 +109,7 @@ try { '--agent=true' ) if ($client.Nullable) { $arguments += '-nu=true' } + if ($client.Standalone) { $arguments += '--standalone=true' } $sentinel = Join-Path $repoRoot "$($client.Output)/Client/$($client.Name).cs" $generatedAt = Get-Date @@ -131,7 +142,10 @@ try { # so git status reports every regenerated file as modified even when its content is # unchanged. git diff applies the same normalization git would apply on checkin, so # it reports only real drift. - $diff = @(git diff --stat -- @paths) + # + # The standalone client's RuntimeInfo.cs records the generator version, which changes with every + # commit; -I ignores a change made only of lines matching that one. + $diff = @(git diff --stat -I '^\s*public const string Version = ' -- @paths) # git diff only compares tracked files, so a newly generated file needs its own check. $added = @(git ls-files --others --exclude-standard -- @paths) diff --git a/src/Linq2GraphQL.Client/GraphClient.cs b/src/Linq2GraphQL.Client/GraphClient.cs index d340f105..b97f06b0 100644 --- a/src/Linq2GraphQL.Client/GraphClient.cs +++ b/src/Linq2GraphQL.Client/GraphClient.cs @@ -186,7 +186,9 @@ public async Task GetSchemaForSafeModeAsync(CancellationToken can return null; } - var cacheKey = $"Linq2GraphQL_Schema:{HttpClient.BaseAddress}:{includeDeprecated}:{options.Value.SchemaCacheKey}"; + // The schema type is part of the key: clients that carry their own copy of the runtime + // share the app's IMemoryCache but cannot read each other's GraphQLSchema. + var cacheKey = $"Linq2GraphQL_Schema:{typeof(GraphQLSchema).AssemblyQualifiedName}:{HttpClient.BaseAddress}:{includeDeprecated}:{options.Value.SchemaCacheKey}"; return await cache.GetOrCreateAsync(cacheKey, async entry => { diff --git a/src/Linq2GraphQL.Generator/ClientGenerator.cs b/src/Linq2GraphQL.Generator/ClientGenerator.cs index cb4eff5b..45cc8f85 100644 --- a/src/Linq2GraphQL.Generator/ClientGenerator.cs +++ b/src/Linq2GraphQL.Generator/ClientGenerator.cs @@ -19,7 +19,8 @@ public class ClientGenerator( bool includeDeprecated, IReadOnlyDictionary scalarMappings = null, bool descriptionAttributes = true, - bool agent = false) + bool agent = false, + bool standalone = false) { private static readonly Dictionary EmptyScalarMappings = new(); @@ -239,6 +240,17 @@ private List Generate(RootSchema rootSchema) fileName = clientName + "Extensions" + ".cs"; AddFile(clientDirName, fileName, clientExtensionsTemplateText); + if (standalone) + { + Console.WriteLine("Generate Runtime (standalone)..."); + foreach (var entry in entries) + { + entry.Content = StandaloneRuntime.RewriteNamespaces(entry.Content, namespaceName); + } + + entries.AddRange(StandaloneRuntime.GetFiles(namespaceName, includeSubscriptions, agent)); + } + return entries; } diff --git a/src/Linq2GraphQL.Generator/GeneratorConfig.cs b/src/Linq2GraphQL.Generator/GeneratorConfig.cs index 6fb945c1..f04faead 100644 --- a/src/Linq2GraphQL.Generator/GeneratorConfig.cs +++ b/src/Linq2GraphQL.Generator/GeneratorConfig.cs @@ -56,6 +56,12 @@ public class GeneratorConfig /// public bool? Agent { get; set; } + /// + /// Standalone: write a copy of the runtime into the output instead of depending on the + /// Linq2GraphQL.Client packages. + /// + public bool? Standalone { get; set; } + /// /// Emit [Description] attributes from schema descriptions. On by default; set to false to /// opt out. Settings file only, there is no command line flag for it. diff --git a/src/Linq2GraphQL.Generator/Linq2GraphQL.Generator.csproj b/src/Linq2GraphQL.Generator/Linq2GraphQL.Generator.csproj index 209307b2..82496202 100644 --- a/src/Linq2GraphQL.Generator/Linq2GraphQL.Generator.csproj +++ b/src/Linq2GraphQL.Generator/Linq2GraphQL.Generator.csproj @@ -71,4 +71,41 @@ + + + + + + + + + + + $(IntermediateOutputPath)PackageVersions.g.cs + + + <_PackageVersionLine Include="@(PackageVersion->' ["%(Identity)"] = "%(Version)",')"/> + + + + + + + + diff --git a/src/Linq2GraphQL.Generator/Program.cs b/src/Linq2GraphQL.Generator/Program.cs index e8dd1451..69950adc 100644 --- a/src/Linq2GraphQL.Generator/Program.cs +++ b/src/Linq2GraphQL.Generator/Program.cs @@ -28,6 +28,9 @@ private static async Task Main(string[] args) "Schema file (SDL) to generate from instead of an endpoint, relative to current location"); var agent = new Option(new[] { "--agent", "-ag" }, "Agent mode: also emit the schema as SDL and implement IGraphSchemaSource on the client"); + var standalone = new Option(new[] { "--standalone", "-sa" }, + "Standalone: write a copy of the runtime into the output (namespace .Runtime) instead of " + + "depending on the Linq2GraphQL.Client packages"); var rootCommand = new RootCommand("Generate GraphQL client") { @@ -42,7 +45,8 @@ private static async Task Main(string[] args) nullable, includeDeprecated, schemaFile, - agent + agent, + standalone }; rootCommand.SetHandler(async context => @@ -70,10 +74,11 @@ private static async Task Main(string[] args) var descriptionAttributesValue = config.DescriptionAttributes ?? true; var agentValue = Resolve(result, agent, config.Agent, false); + var standaloneValue = Resolve(result, standalone, config.Standalone, false); await GenerateClientAsync(uriValue, schemaFileValue, outputFolderValue, namespaceValue, clientNameValue, includeSubscriptionsValue, authTokenValue, enumStrategyValue, nullableValue, deprecatedValue, - scalarMappings, descriptionAttributesValue, agentValue); + scalarMappings, descriptionAttributesValue, agentValue, standaloneValue); } catch (GeneratorConfigurationException ex) { @@ -156,7 +161,8 @@ private static Uri ResolveEndpoint(ParseResult result, Argument argument, s private static async Task GenerateClientAsync(Uri uri, string schemaFile, string outputFolder, string namespaceName, string name, bool includeSubscriptions, string authToken, string enumStrategy, bool nullable, bool includeDeprecated, - IReadOnlyDictionary scalarMappings, bool descriptionAttributes, bool agent) + IReadOnlyDictionary scalarMappings, bool descriptionAttributes, bool agent, + bool standalone) { var enumStrat = enumStrategy != null && enumStrategy.Equals("AddUnknownOption", StringComparison.InvariantCultureIgnoreCase) @@ -164,7 +170,7 @@ private static async Task GenerateClientAsync(Uri uri, string schemaFile, string : EnumGeneratorStrategy.FailIfMissing; var generator = new ClientGenerator(namespaceName, name, includeSubscriptions, enumStrat, nullable, - includeDeprecated, scalarMappings, descriptionAttributes, agent); + includeDeprecated, scalarMappings, descriptionAttributes, agent, standalone); List entries; if (schemaFile != null) { @@ -184,6 +190,15 @@ private static async Task GenerateClientAsync(Uri uri, string schemaFile, string var outputPath = Path.GetFullPath(outputFolder, Environment.CurrentDirectory); Console.WriteLine($"Output path is set to: {outputPath}"); + + // A runtime copy from an earlier standalone run is the generator's own output: clear it, so files + // dropped from the runtime do not linger and turning standalone off does not leave a second runtime. + var runtimePath = Path.Combine(outputPath, StandaloneRuntime.DirectoryName); + if (File.Exists(Path.Combine(runtimePath, StandaloneRuntime.InfoFileName))) + { + Directory.Delete(runtimePath, true); + } + foreach (var dirName in entries.GroupBy(e => e.DirectoryName)) { var directory = Path.Combine(outputPath, dirName.Key); @@ -195,5 +210,16 @@ private static async Task GenerateClientAsync(Uri uri, string schemaFile, string await File.WriteAllTextAsync(filePath, entry.Content.ReplaceLineEndings("\n")); } } + + if (standalone) + { + Console.WriteLine(""); + Console.WriteLine($"Standalone: runtime {StandaloneRuntime.Version} written to {runtimePath}. " + + "Don't reference the Linq2GraphQL.Client packages; the project needs:"); + foreach (var (packageName, version) in StandaloneRuntime.GetPackages(includeSubscriptions, agent)) + { + Console.WriteLine($" "); + } + } } } diff --git a/src/Linq2GraphQL.Generator/StandaloneRuntime.cs b/src/Linq2GraphQL.Generator/StandaloneRuntime.cs new file mode 100644 index 00000000..21a8ca24 --- /dev/null +++ b/src/Linq2GraphQL.Generator/StandaloneRuntime.cs @@ -0,0 +1,151 @@ +using System.Reflection; +using System.Text.RegularExpressions; + +namespace Linq2GraphQL.Generator; + +/// +/// Standalone mode: instead of depending on the Linq2GraphQL.Client packages, the generated client carries +/// its own copy of the runtime under <namespace>.Runtime. Clients generated by different +/// generator versions can then be used in the same app, because each one brings the runtime it was +/// generated against. The runtime source is embedded in the generator at build time. +/// +internal static class StandaloneRuntime +{ + /// + /// The output folder the runtime is written to. The generator owns it: it is cleared before every run, + /// so files removed from the runtime do not linger. + /// + public const string DirectoryName = "Runtime"; + + /// + /// Written into in standalone mode. Its presence is what marks a Runtime + /// folder as the generator's to clear. + /// + public const string InfoFileName = "RuntimeInfo.cs"; + + private const string ResourcePrefix = DirectoryName + "/"; + private const string SubscriptionsPrefix = ResourcePrefix + "Subscriptions/"; + private const string AgentPrefix = ResourcePrefix + "AI/"; + + private static readonly Regex RuntimeNamespace = new(@"\bLinq2GraphQL\.Client\b", RegexOptions.Compiled); + + private static readonly string[] ClientPackages = ["Microsoft.Extensions.Caching.Memory", "Microsoft.Extensions.Http"]; + private static readonly string[] SubscriptionsPackages = ["Websocket.Client"]; + private static readonly string[] AgentPackages = ["GraphQL-Parser", "Microsoft.Extensions.AI.Abstractions"]; + + /// + /// The runtime project has ImplicitUsings enabled, the project the copy lands in might not. + /// + private static readonly string[] ImplicitUsings = + [ + "System", "System.Collections.Generic", "System.IO", "System.Linq", "System.Net.Http", "System.Threading", + "System.Threading.Tasks" + ]; + + public static string Version { get; } = + typeof(StandaloneRuntime).Assembly.GetCustomAttribute() + ?.InformationalVersion.Split('+')[0] ?? "unknown"; + + public static string GetRuntimeNamespace(string namespaceName) => namespaceName + ".Runtime"; + + /// + /// Points every reference to the runtime packages' namespaces at the copy. + /// + public static string RewriteNamespaces(string content, string namespaceName) => + RuntimeNamespace.Replace(content, GetRuntimeNamespace(namespaceName)); + + /// + /// The packages the copied runtime needs, with the versions it was built against. + /// + public static IReadOnlyList<(string Name, string Version)> GetPackages(bool includeSubscriptions, bool includeAgent) => + ClientPackages + .Concat(includeSubscriptions ? SubscriptionsPackages : []) + .Concat(includeAgent ? AgentPackages : []) + .Select(e => (e, PackageVersions.All.GetValueOrDefault(e, "unknown"))) + .ToList(); + + /// + /// The runtime files: Client always, Subscriptions with subscriptions, and the agent tools + /// (Linq2GraphQL.Client.AI) for a client generated in agent mode. + /// + public static IEnumerable GetFiles(string namespaceName, bool includeSubscriptions, bool includeAgent) + { + var assembly = typeof(StandaloneRuntime).Assembly; + + // RecursiveDir in the resource name uses the build machine's path separator. + var resources = assembly.GetManifestResourceNames() + .Select(e => (Resource: e, Path: e.Replace('\\', '/'))) + .Where(e => e.Path.StartsWith(ResourcePrefix, StringComparison.Ordinal)) + .Where(e => includeSubscriptions || !e.Path.StartsWith(SubscriptionsPrefix, StringComparison.Ordinal)) + .Where(e => includeAgent || !e.Path.StartsWith(AgentPrefix, StringComparison.Ordinal)) + .OrderBy(e => e.Path, StringComparer.Ordinal); + + foreach (var (resource, path) in resources) + { + using var stream = assembly.GetManifestResourceStream(resource); + using var reader = new StreamReader(stream); + var source = reader.ReadToEnd().TrimStart(''); + + yield return new FileEntry + { + DirectoryName = Path.GetDirectoryName(path).Replace('\\', '/'), + FileName = Path.GetFileName(path), + Content = Header + MissingImplicitUsings(source) + RewriteNamespaces(source, namespaceName) + }; + } + + yield return new FileEntry + { + DirectoryName = DirectoryName, FileName = InfoFileName, Content = InfoFile(namespaceName, includeSubscriptions, includeAgent) + }; + } + + private const string Header = """ + //--------------------------------------------------------------------- + // + // Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator + // in standalone mode. Please don't edit this file - regenerate instead. + // The runtime version is in RuntimeInfo.cs. + // Github:https://github.com/linq2graphql/linq2graphql.client + //--------------------------------------------------------------------- + #nullable disable + + """; + + private static string MissingImplicitUsings(string source) + { + var missing = ImplicitUsings + .Where(e => !Regex.IsMatch(source, $@"^\s*using {Regex.Escape(e)};", RegexOptions.Multiline)) + .Select(e => $"using {e};\n"); + + return string.Concat(missing); + } + + private static string InfoFile(string namespaceName, bool includeSubscriptions, bool includeAgent) + { + var packages = string.Join("\n", GetPackages(includeSubscriptions, includeAgent) + .Select(e => $"/// {e.Name} {e.Version}")); + + return $$""" + //--------------------------------------------------------------------- + // + // Written by the Linq2GraphQL generator in standalone mode. + // Please don't edit this file - regenerate instead. + //--------------------------------------------------------------------- + + namespace {{GetRuntimeNamespace(namespaceName)}}; + + /// + /// The Linq2GraphQL runtime this copy was taken from. It needs these packages: + /// + {{packages}} + /// + /// + public static class RuntimeInfo + { + public const string Version = "{{Version}}"; + } + + """; + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Client/MutationMethods.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Client/MutationMethods.cs new file mode 100644 index 00000000..cf2fd928 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Client/MutationMethods.cs @@ -0,0 +1,60 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +/// +/// Implementation of MutationMethods GraphQL operations +/// +public class MutationMethods : IMutationMethods +{ + private readonly GraphClient client; + + /// + /// Initializes a new instance of the MutationMethods class + /// + /// The GraphQL client instance + public MutationMethods(GraphClient client) + { + this.client = client ?? throw new ArgumentNullException(nameof(client)); + } + + /// + /// Executes setName GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphQuery + public GraphQuery SetName(string name = null) + { + var arguments = new List + { + new("name", "String", name), + }; + + return new GraphQuery(client, "setName", OperationType.Mutation, arguments); + } + + /// + /// Executes addCustomer GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphQuery + public GraphQuery AddCustomer(CustomerInput customer = null) + { + var arguments = new List + { + new("customer", "CustomerInput", customer), + }; + + return new GraphQuery(client, "addCustomer", OperationType.Mutation, arguments); + } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Client/QueryMethods.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Client/QueryMethods.cs new file mode 100644 index 00000000..b104c05f --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Client/QueryMethods.cs @@ -0,0 +1,200 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +/// +/// Implementation of QueryMethods GraphQL operations +/// +public class QueryMethods : IQueryMethods +{ + private readonly GraphClient client; + + /// + /// Initializes a new instance of the QueryMethods class + /// + /// The GraphQL client instance + public QueryMethods(GraphClient client) + { + this.client = client ?? throw new ArgumentNullException(nameof(client)); + } + + /// + /// Executes hello GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphQuery + public GraphQuery Hello(string name = null) + { + var arguments = new List + { + new("name", "String", name), + }; + + return new GraphQuery(client, "hello", OperationType.Query, arguments); + } + + /// + /// Executes raiseError GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphQuery + public GraphQuery RaiseError(string message = null) + { + var arguments = new List + { + new("message", "String", message), + }; + + return new GraphQuery(client, "raiseError", OperationType.Query, arguments); + } + + /// + /// Executes raiseAuthError GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphQuery + public GraphQuery RaiseAuthError() + { + var arguments = new List + { + }; + + return new GraphQuery(client, "raiseAuthError", OperationType.Query, arguments); + } + + /// + /// Executes customerReturnNull GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphQuery + public GraphQuery CustomerReturnNull() + { + var arguments = new List + { + }; + + return new GraphQuery(client, "customerReturnNull", OperationType.Query, arguments); + } + + /// + /// Executes customers GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphQuery> + public GraphQuery> Customers() + { + var arguments = new List + { + }; + + return new GraphQuery>(client, "customers", OperationType.Query, arguments); + } + + /// + /// Executes ordersNoBackwardPagination GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphQuery + public GraphQuery OrdersNoBackwardPagination([global::System.ComponentModel.Description("Returns the first _n_ elements from the list.")] int? first = null, [global::System.ComponentModel.Description("Returns the elements in the list that come after the specified cursor.")] string after = null, OrderFilterInput where = null, List order = null) + { + var arguments = new List + { + new("first", "Int", first), + new("after", "String", after), + new("where", "OrderFilterInput", where), + new("order", "[OrderSortInput!]", order), + }; + + return new GraphQuery(client, "ordersNoBackwardPagination", OperationType.Query, arguments); + } + + /// + /// Executes ordersNoTotalCount GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphCursorQuery + public GraphCursorQuery OrdersNoTotalCount([global::System.ComponentModel.Description("Returns the first _n_ elements from the list.")] int? first = null, [global::System.ComponentModel.Description("Returns the elements in the list that come after the specified cursor.")] string after = null, [global::System.ComponentModel.Description("Returns the last _n_ elements from the list.")] int? last = null, [global::System.ComponentModel.Description("Returns the elements in the list that come before the specified cursor.")] string before = null, OrderFilterInput where = null, List order = null) + { + var arguments = new List + { + new("first", "Int", first), + new("after", "String", after), + new("last", "Int", last), + new("before", "String", before), + new("where", "OrderFilterInput", where), + new("order", "[OrderSortInput!]", order), + }; + + return new GraphCursorQuery(client, "ordersNoTotalCount", OperationType.Query, arguments); + } + + /// + /// Executes orders GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphCursorQuery + public GraphCursorQuery Orders([global::System.ComponentModel.Description("Returns the first _n_ elements from the list.")] int? first = null, [global::System.ComponentModel.Description("Returns the elements in the list that come after the specified cursor.")] string after = null, [global::System.ComponentModel.Description("Returns the last _n_ elements from the list.")] int? last = null, [global::System.ComponentModel.Description("Returns the elements in the list that come before the specified cursor.")] string before = null, OrderFilterInput where = null, List order = null) + { + var arguments = new List + { + new("first", "Int", first), + new("after", "String", after), + new("last", "Int", last), + new("before", "String", before), + new("where", "OrderFilterInput", where), + new("order", "[OrderSortInput!]", order), + }; + + return new GraphCursorQuery(client, "orders", OperationType.Query, arguments); + } + + /// + /// Executes animals GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphCursorQuery + public GraphCursorQuery Animals([global::System.ComponentModel.Description("Returns the first _n_ elements from the list.")] int? first = null, [global::System.ComponentModel.Description("Returns the elements in the list that come after the specified cursor.")] string after = null, [global::System.ComponentModel.Description("Returns the last _n_ elements from the list.")] int? last = null, [global::System.ComponentModel.Description("Returns the elements in the list that come before the specified cursor.")] string before = null, IAnimalFilterInput where = null, List order = null) + { + var arguments = new List + { + new("first", "Int", first), + new("after", "String", after), + new("last", "Int", last), + new("before", "String", before), + new("where", "IAnimalFilterInput", where), + new("order", "[IAnimalSortInput!]", order), + }; + + return new GraphCursorQuery(client, "animals", OperationType.Query, arguments); + } + + /// + /// Executes ordersOffsetPaging GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphCursorQuery + public GraphCursorQuery OrdersOffsetPaging([global::System.ComponentModel.Description("Returns the first _n_ elements from the list.")] int? first = null, [global::System.ComponentModel.Description("Returns the elements in the list that come after the specified cursor.")] string after = null, [global::System.ComponentModel.Description("Returns the last _n_ elements from the list.")] int? last = null, [global::System.ComponentModel.Description("Returns the elements in the list that come before the specified cursor.")] string before = null, OrderFilterInput where = null, List order = null) + { + var arguments = new List + { + new("first", "Int", first), + new("after", "String", after), + new("last", "Int", last), + new("before", "String", before), + new("where", "OrderFilterInput", where), + new("order", "[OrderSortInput!]", order), + }; + + return new GraphCursorQuery(client, "ordersOffsetPaging", OperationType.Query, arguments); + } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Client/SampleClient.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Client/SampleClient.cs new file mode 100644 index 00000000..b9d31a1c --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Client/SampleClient.cs @@ -0,0 +1,101 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using Linq2GraphQL.TestClientStandalone.Runtime; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Linq2GraphQL.TestClientStandalone; + +/// +/// GraphQL client for SampleClient operations +/// +/// +/// Provides strongly-typed access to GraphQL queries, mutations, and subscriptions. +/// Supports dependency injection for better testability and flexibility. +/// +public class SampleClient : ISampleClient, IGraphClientSource, IGraphSchemaSource +{ + private readonly GraphClient client; + + /// + /// Constructor with dependency injection support + /// + /// HTTP client for GraphQL requests + /// GraphQL client configuration options + /// Service provider for dependency resolution + /// Optional query methods implementation (uses default if null) + /// Optional mutation methods implementation (uses default if null) + /// Optional subscription methods implementation (uses default if null) + public SampleClient( + HttpClient httpClient, + [FromKeyedServices("SampleClient")] IOptions options, + IServiceProvider provider, + IQueryMethods queryMethods = null, + IMutationMethods mutationMethods = null, + ISubscriptionMethods subscriptionMethods = null) + { + client = new GraphClient(httpClient, options, provider, true); + Query = queryMethods ?? new QueryMethods(client); + Mutation = mutationMethods ?? new MutationMethods(client); + Subscription = subscriptionMethods ?? new SubscriptionMethods(client); + } + + private SampleClient(GraphClient client) + { + this.client = client; + Query = new QueryMethods(client); + Mutation = new MutationMethods(client); + Subscription = new SubscriptionMethods(client); + } + + /// + /// A client that shares this client's connection and settings, and also runs + /// on every request it sends. Use it for per-user or + /// per-conversation headers such as an access token. This client is not changed. + /// + /// + /// The new client uses the generated operation implementations, not ones passed to the constructor. + /// + public SampleClient WithRequestConfiguration(Func configureRequest) + { + return new SampleClient(client.WithRequestConfiguration(configureRequest)); + } + + /// + /// A client that sends Authorization: Bearer <token> on every request, asking + /// for the token each time. This client is not changed. + /// + public SampleClient WithAccessToken(Func> getAccessToken) + { + return WithRequestConfiguration(RequestConfigurations.BearerToken(getAccessToken)); + } + + /// + /// The underlying client, for example to execute a query written as GraphQL text with + /// . + /// + public GraphClient GraphClient => client; + + /// + /// The schema this client was generated from, as SDL. + /// + public static string Sdl => SampleClientSchema.Sdl; + + /// + /// Gets the query methods for this GraphQL client + /// + public IQueryMethods Query { get; private set; } + /// + /// Gets the mutation methods for this GraphQL client + /// + public IMutationMethods Mutation { get; private set; } + /// + /// Gets the subscription methods for this GraphQL client + /// + public ISubscriptionMethods Subscription { get; private set; } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Client/SampleClientExtensions.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Client/SampleClientExtensions.cs new file mode 100644 index 00000000..7abc0ec5 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Client/SampleClientExtensions.cs @@ -0,0 +1,40 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using Linq2GraphQL.TestClientStandalone.Runtime; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Linq2GraphQL.TestClientStandalone; + +public static class SampleClientExtensions +{ + private const string ClientName = "SampleClient"; + + public static IGraphClientBuilder AddSampleClient(this IServiceCollection services) + { + var graphClientOptions = new GraphClientOptions(); + return GraphClientBuilder(services, graphClientOptions); + } + + public static IGraphClientBuilder AddSampleClient(this IServiceCollection services, Action opts) + { + var graphClientOptions = new GraphClientOptions(); + opts(graphClientOptions); + + return GraphClientBuilder(services, graphClientOptions); + } + + private static IGraphClientBuilder GraphClientBuilder(IServiceCollection services, + GraphClientOptions graphClientOptions) + { + var opts = Options.Create(graphClientOptions); + services.AddKeyedSingleton(ClientName, opts); + services.AddMemoryCache(); + return new ClientBuilder(ClientName, services); + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Client/SampleClientSchema.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Client/SampleClientSchema.cs new file mode 100644 index 00000000..1dda5edb --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Client/SampleClientSchema.cs @@ -0,0 +1,588 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +namespace Linq2GraphQL.TestClientStandalone; + +/// +/// The schema SampleClient was generated from, as SDL. +/// +public static partial class SampleClientSchema +{ + public const string Sdl = """ + type Query { + hello(name: String = "World"): String + raiseError(message: String = "Test error"): String + raiseAuthError: String + customerReturnNull: Customer + customers: [Customer] + ordersNoBackwardPagination( + "Returns the first _n_ elements from the list." + first: Int, + "Returns the elements in the list that come after the specified cursor." + after: String, + where: OrderFilterInput, + order: [OrderSortInput!]): OrdersNoBackwardPaginationConnection + ordersNoTotalCount( + "Returns the first _n_ elements from the list." + first: Int, + "Returns the elements in the list that come after the specified cursor." + after: String, + "Returns the last _n_ elements from the list." + last: Int, + "Returns the elements in the list that come before the specified cursor." + before: String, + where: OrderFilterInput, + order: [OrderSortInput!]): OrdersNoTotalCountConnection + orders( + "Returns the first _n_ elements from the list." + first: Int, + "Returns the elements in the list that come after the specified cursor." + after: String, + "Returns the last _n_ elements from the list." + last: Int, + "Returns the elements in the list that come before the specified cursor." + before: String, + where: OrderFilterInput, + order: [OrderSortInput!]): OrdersConnection + animals( + "Returns the first _n_ elements from the list." + first: Int, + "Returns the elements in the list that come after the specified cursor." + after: String, + "Returns the last _n_ elements from the list." + last: Int, + "Returns the elements in the list that come before the specified cursor." + before: String, + where: IAnimalFilterInput, + order: [IAnimalSortInput!]): AnimalsConnection + ordersOffsetPaging( + "Returns the first _n_ elements from the list." + first: Int, + "Returns the elements in the list that come after the specified cursor." + after: String, + "Returns the last _n_ elements from the list." + last: Int, + "Returns the elements in the list that come before the specified cursor." + before: String, + where: OrderFilterInput, + order: [OrderSortInput!]): OrdersOffsetPagingConnection + } + + type Mutation { + setName(name: String): String + addCustomer(customer: CustomerInput): Customer + } + + type Subscription { + customerAdded: Customer + customerNameAdded(name: String): Customer + } + + type Pig implements IAnimal { + spices: String! + name: String! + numberOfLegs: Int! + speed: Int! + } + + type Spider implements IAnimal { + poisonous: Boolean! + name: String! + numberOfLegs: Int! + speed: Int! + } + + input OrderFilterInput { + and: [OrderFilterInput!] + or: [OrderFilterInput!] + orderId: UuidOperationFilterInput + customer: CustomerFilterInput + address: AddressFilterInput + orderDate: DateTimeOperationFilterInput + orderDateTime: DateTimeOperationFilterInput + orderDay: LocalDateOperationFilterInput + lines: ListFilterInputTypeOfOrderLineFilterInput + entryTime: LocalTimeOperationFilterInput + grade: StringOperationFilterInput @deprecated(reason: "This propery is obsolete and should not be used!") + } + + input OrderSortInput { + orderId: SortEnumType + customer: CustomerSortInput + address: AddressSortInput + orderDate: SortEnumType + orderDateTime: SortEnumType + orderDay: SortEnumType + entryTime: SortEnumType + grade: SortEnumType @deprecated(reason: "This propery is obsolete and should not be used!") + } + + input IAnimalFilterInput { + and: [IAnimalFilterInput!] + or: [IAnimalFilterInput!] + name: StringOperationFilterInput + numberOfLegs: IntOperationFilterInput + } + + input IAnimalSortInput { + name: SortEnumType + numberOfLegs: SortEnumType + } + + "A connection to a list of items." + type OrdersNoBackwardPaginationConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [OrdersNoBackwardPaginationEdge!] + "A flattened list of the nodes." + nodes: [Order] + "Identifies the total count of items in the connection." + totalCount: Int! + } + + "A connection to a list of items." + type OrdersNoTotalCountConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [OrdersNoTotalCountEdge!] + "A flattened list of the nodes." + nodes: [Order] + } + + "A connection to a list of items." + type OrdersConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [OrdersEdge!] + "A flattened list of the nodes." + nodes: [Order] + "Identifies the total count of items in the connection." + totalCount: Int! + } + + "A connection to a list of items." + type AnimalsConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [AnimalsEdge!] + "A flattened list of the nodes." + nodes: [IAnimal] + "Identifies the total count of items in the connection." + totalCount: Int! + } + + "A connection to a list of items." + type OrdersOffsetPagingConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [OrdersOffsetPagingEdge!] + "A flattened list of the nodes." + nodes: [Order] + } + + input UuidOperationFilterInput { + eq: UUID + neq: UUID + in: [UUID] + nin: [UUID] + gt: UUID + ngt: UUID + gte: UUID + ngte: UUID + lt: UUID + nlt: UUID + lte: UUID + nlte: UUID + } + + input CustomerFilterInput { + and: [CustomerFilterInput!] + or: [CustomerFilterInput!] + customerId: UuidOperationFilterInput + customerName: StringOperationFilterInput + status: CustomerStatusOperationFilterInput + orders: ListFilterInputTypeOfOrderFilterInput + address: AddressFilterInput + created: DateTimeOperationFilterInput + } + + input AddressFilterInput { + and: [AddressFilterInput!] + or: [AddressFilterInput!] + name: StringOperationFilterInput + street: StringOperationFilterInput + postalCode: StringOperationFilterInput + createdDate: LocalDateOperationFilterInput + } + + input DateTimeOperationFilterInput { + eq: DateTime + neq: DateTime + in: [DateTime] + nin: [DateTime] + gt: DateTime + ngt: DateTime + gte: DateTime + ngte: DateTime + lt: DateTime + nlt: DateTime + lte: DateTime + nlte: DateTime + } + + input LocalDateOperationFilterInput { + eq: LocalDate + neq: LocalDate + in: [LocalDate] + nin: [LocalDate] + gt: LocalDate + ngt: LocalDate + gte: LocalDate + ngte: LocalDate + lt: LocalDate + nlt: LocalDate + lte: LocalDate + nlte: LocalDate + } + + input ListFilterInputTypeOfOrderLineFilterInput { + all: OrderLineFilterInput + none: OrderLineFilterInput + some: OrderLineFilterInput + any: Boolean + } + + input LocalTimeOperationFilterInput { + eq: LocalTime + neq: LocalTime + in: [LocalTime] + nin: [LocalTime] + gt: LocalTime + ngt: LocalTime + gte: LocalTime + ngte: LocalTime + lt: LocalTime + nlt: LocalTime + lte: LocalTime + nlte: LocalTime + } + + input StringOperationFilterInput { + and: [StringOperationFilterInput!] + or: [StringOperationFilterInput!] + eq: String + neq: String + contains: String + ncontains: String + in: [String] + nin: [String] + startsWith: String + nstartsWith: String + endsWith: String + nendsWith: String + } + + enum SortEnumType { + ASC + DESC + } + + input CustomerSortInput { + customerId: SortEnumType + customerName: SortEnumType + status: SortEnumType + address: AddressSortInput + created: SortEnumType + } + + input AddressSortInput { + name: SortEnumType + street: SortEnumType + postalCode: SortEnumType + createdDate: SortEnumType + } + + input IntOperationFilterInput { + eq: Int + neq: Int + in: [Int] + nin: [Int] + gt: Int + ngt: Int + gte: Int + ngte: Int + lt: Int + nlt: Int + lte: Int + nlte: Int + } + + "Information about pagination in a connection." + type PageInfo { + "Indicates whether more edges exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more edges exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! + "When paginating backwards, the cursor to continue." + startCursor: String + "When paginating forwards, the cursor to continue." + endCursor: String + } + + "An edge in a connection." + type OrdersNoBackwardPaginationEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Order + } + + "An edge in a connection." + type OrdersNoTotalCountEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Order + } + + "An edge in a connection." + type OrdersEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Order + } + + "An edge in a connection." + type AnimalsEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: IAnimal + } + + "An edge in a connection." + type OrdersOffsetPagingEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Order + } + + "The `UUID` scalar type represents a Universally Unique Identifier (UUID) as defined by RFC 9562." + scalar UUID + + input CustomerStatusOperationFilterInput { + eq: CustomerStatus + neq: CustomerStatus + in: [CustomerStatus!] + nin: [CustomerStatus!] + } + + input ListFilterInputTypeOfOrderFilterInput { + all: OrderFilterInput + none: OrderFilterInput + some: OrderFilterInput + any: Boolean + } + + "The `DateTime` scalar type represents a date and time with time zone offset information." + scalar DateTime + + "The `LocalDate` scalar type represents a date without time or time zone information." + scalar LocalDate + + input OrderLineFilterInput { + and: [OrderLineFilterInput!] + or: [OrderLineFilterInput!] + lineNumber: IntOperationFilterInput + order: OrderFilterInput + item: ItemFilterInput + price: DecimalOperationFilterInput + quantity: FloatOperationFilterInput + } + + "The `LocalTime` scalar type represents a time of day without date or time zone information." + scalar LocalTime + + input ItemFilterInput { + and: [ItemFilterInput!] + or: [ItemFilterInput!] + itemId: StringOperationFilterInput + itemName: StringOperationFilterInput + data: ListUnsignedByteOperationFilterInput + } + + input DecimalOperationFilterInput { + eq: Decimal + neq: Decimal + in: [Decimal] + nin: [Decimal] + gt: Decimal + ngt: Decimal + gte: Decimal + ngte: Decimal + lt: Decimal + nlt: Decimal + lte: Decimal + nlte: Decimal + } + + input FloatOperationFilterInput { + eq: Float + neq: Float + in: [Float] + nin: [Float] + gt: Float + ngt: Float + gte: Float + ngte: Float + lt: Float + nlt: Float + lte: Float + nlte: Float + } + + input ListUnsignedByteOperationFilterInput { + all: UnsignedByteOperationFilterInput + none: UnsignedByteOperationFilterInput + some: UnsignedByteOperationFilterInput + any: Boolean + } + + "The `Decimal` scalar type represents a decimal floating-point number with high precision." + scalar Decimal + + input UnsignedByteOperationFilterInput { + eq: UnsignedByte + neq: UnsignedByte + in: [UnsignedByte] + nin: [UnsignedByte] + gt: UnsignedByte + ngt: UnsignedByte + gte: UnsignedByte + ngte: UnsignedByte + lt: UnsignedByte + nlt: UnsignedByte + lte: UnsignedByte + nlte: UnsignedByte + } + + "The `UnsignedByte` scalar type represents an unsigned 8-bit integer." + scalar UnsignedByte + + enum CustomerStatus { + ACTIVE + DISABLED + IN_TRANSIT @deprecated(reason: "No longer in use, please use enum value Active instead") + } + + interface IAnimal { + name: String! + numberOfLegs: Int! + } + + type Order { + myGradeMethod(name: String!): String @deprecated(reason: "This method should not be used anymore") + orderHello(name: String!, first: Int! = 0): String! + orderAddress(addressType: AddressType!): Address! + orderId: UUID! + customer: Customer! + address: Address + orderDate: DateTime! + orderDateTime: DateTime! + orderDay: LocalDate! + lines: [OrderLine!]! + entryTime: LocalTime + grade: String @deprecated(reason: "This propery is obsolete and should not be used!") + } + + input CustomerInput { + customerId: UUID! + customerName: String! + status: CustomerStatus! + orders: [OrderInput!]! + address: AddressInput + created: DateTime + } + + type Customer { + customerId: UUID! + customerName: String! + status: CustomerStatus! + orders: [Order!]! + address: Address + created: DateTime + relatedCustomers(relationType: Int!): [Customer] + relatedCustomer(relationType: Int!): Customer + } + + input OrderInput { + orderId: UUID! + customer: CustomerInput! + address: AddressInput + orderDate: DateTime! + orderDateTime: DateTime! + orderDay: LocalDate! + lines: [OrderLineInput!]! + entryTime: LocalTime + grade: String @deprecated(reason: "This propery is obsolete and should not be used!") + } + + type OrderLine { + lineNumber: Int! + order: Order! + item: Item + price: Decimal! + quantity: Float! + } + + enum AddressType { + DELIVERY + INVOICE + } + + type Address { + name: String! + street: String! + postalCode: String! + createdDate: LocalDate! + } + + input AddressInput { + name: String! + street: String! + postalCode: String! + createdDate: LocalDate! + } + + type Item { + itemId: String! + itemName: String! + data: [UnsignedByte!] + } + + input OrderLineInput { + lineNumber: Int! + order: OrderInput! + item: ItemInput + price: Decimal! + quantity: Float! + } + + input ItemInput { + itemId: String! + itemName: String! + data: [UnsignedByte!] + } + """; +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Client/SubscriptionMethods.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Client/SubscriptionMethods.cs new file mode 100644 index 00000000..7d82b560 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Client/SubscriptionMethods.cs @@ -0,0 +1,60 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Subscriptions; + +namespace Linq2GraphQL.TestClientStandalone; + +/// +/// Implementation of SubscriptionMethods GraphQL operations +/// +public class SubscriptionMethods : ISubscriptionMethods +{ + private readonly GraphClient client; + + /// + /// Initializes a new instance of the SubscriptionMethods class + /// + /// The GraphQL client instance + public SubscriptionMethods(GraphClient client) + { + this.client = client ?? throw new ArgumentNullException(nameof(client)); + } + + /// + /// Executes customerAdded GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphSubscription + public GraphSubscription CustomerAdded() + { + var arguments = new List + { + }; + + return new GraphSubscription(client, "customerAdded", OperationType.Subscription, arguments); + } + + /// + /// Executes customerNameAdded GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphSubscription + public GraphSubscription CustomerNameAdded(string name = null) + { + var arguments = new List + { + new("name", "String", name), + }; + + return new GraphSubscription(client, "customerNameAdded", OperationType.Subscription, arguments); + } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Enums/AddressType.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Enums/AddressType.cs new file mode 100644 index 00000000..32050e03 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Enums/AddressType.cs @@ -0,0 +1,22 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using Linq2GraphQL.TestClientStandalone.Runtime; +using System.ComponentModel; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(JsonStringEnumMemberConverter))] +public enum AddressType +{ + [EnumMember(Value = "DELIVERY")] + Delivery, + [EnumMember(Value = "INVOICE")] + Invoice, +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Enums/CustomerStatus.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Enums/CustomerStatus.cs new file mode 100644 index 00000000..a4191df8 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Enums/CustomerStatus.cs @@ -0,0 +1,25 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using Linq2GraphQL.TestClientStandalone.Runtime; +using System.ComponentModel; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(JsonStringEnumMemberConverter))] +public enum CustomerStatus +{ + [EnumMember(Value = "ACTIVE")] + Active, + [EnumMember(Value = "DISABLED")] + Disabled, + [Obsolete("No longer in use, please use enum value Active instead")] + [EnumMember(Value = "IN_TRANSIT")] + InTransit, +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Enums/SortEnumType.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Enums/SortEnumType.cs new file mode 100644 index 00000000..14f2ab61 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Enums/SortEnumType.cs @@ -0,0 +1,22 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using Linq2GraphQL.TestClientStandalone.Runtime; +using System.ComponentModel; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(JsonStringEnumMemberConverter))] +public enum SortEnumType +{ + [EnumMember(Value = "ASC")] + Asc, + [EnumMember(Value = "DESC")] + Desc, +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/AddressFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/AddressFilterInput.cs new file mode 100644 index 00000000..e3295203 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/AddressFilterInput.cs @@ -0,0 +1,66 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class AddressFilterInput : GraphInputBase +{ + [GraphQLMember("and")] + [JsonPropertyName("and")] + public List And + { + get => GetValue>("and"); + set => SetValue("and", value); + } + + [GraphQLMember("or")] + [JsonPropertyName("or")] + public List Or + { + get => GetValue>("or"); + set => SetValue("or", value); + } + + [GraphQLMember("name")] + [JsonPropertyName("name")] + public StringOperationFilterInput Name + { + get => GetValue("name"); + set => SetValue("name", value); + } + + [GraphQLMember("street")] + [JsonPropertyName("street")] + public StringOperationFilterInput Street + { + get => GetValue("street"); + set => SetValue("street", value); + } + + [GraphQLMember("postalCode")] + [JsonPropertyName("postalCode")] + public StringOperationFilterInput PostalCode + { + get => GetValue("postalCode"); + set => SetValue("postalCode", value); + } + + [GraphQLMember("createdDate")] + [JsonPropertyName("createdDate")] + public LocalDateOperationFilterInput CreatedDate + { + get => GetValue("createdDate"); + set => SetValue("createdDate", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/AddressInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/AddressInput.cs new file mode 100644 index 00000000..80e63cb1 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/AddressInput.cs @@ -0,0 +1,50 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class AddressInput : GraphInputBase +{ + [GraphQLMember("name")] + [JsonPropertyName("name")] + public string Name + { + get => GetValue("name"); + set => SetValue("name", value); + } + + [GraphQLMember("street")] + [JsonPropertyName("street")] + public string Street + { + get => GetValue("street"); + set => SetValue("street", value); + } + + [GraphQLMember("postalCode")] + [JsonPropertyName("postalCode")] + public string PostalCode + { + get => GetValue("postalCode"); + set => SetValue("postalCode", value); + } + + [GraphQLMember("createdDate")] + [JsonPropertyName("createdDate")] + public DateOnly CreatedDate + { + get => GetValue("createdDate"); + set => SetValue("createdDate", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/AddressSortInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/AddressSortInput.cs new file mode 100644 index 00000000..3fce4b68 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/AddressSortInput.cs @@ -0,0 +1,50 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class AddressSortInput : GraphInputBase +{ + [GraphQLMember("name")] + [JsonPropertyName("name")] + public SortEnumType? Name + { + get => GetValue("name"); + set => SetValue("name", value); + } + + [GraphQLMember("street")] + [JsonPropertyName("street")] + public SortEnumType? Street + { + get => GetValue("street"); + set => SetValue("street", value); + } + + [GraphQLMember("postalCode")] + [JsonPropertyName("postalCode")] + public SortEnumType? PostalCode + { + get => GetValue("postalCode"); + set => SetValue("postalCode", value); + } + + [GraphQLMember("createdDate")] + [JsonPropertyName("createdDate")] + public SortEnumType? CreatedDate + { + get => GetValue("createdDate"); + set => SetValue("createdDate", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/CustomerFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/CustomerFilterInput.cs new file mode 100644 index 00000000..d458f3a2 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/CustomerFilterInput.cs @@ -0,0 +1,82 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class CustomerFilterInput : GraphInputBase +{ + [GraphQLMember("and")] + [JsonPropertyName("and")] + public List And + { + get => GetValue>("and"); + set => SetValue("and", value); + } + + [GraphQLMember("or")] + [JsonPropertyName("or")] + public List Or + { + get => GetValue>("or"); + set => SetValue("or", value); + } + + [GraphQLMember("customerId")] + [JsonPropertyName("customerId")] + public UuidOperationFilterInput CustomerId + { + get => GetValue("customerId"); + set => SetValue("customerId", value); + } + + [GraphQLMember("customerName")] + [JsonPropertyName("customerName")] + public StringOperationFilterInput CustomerName + { + get => GetValue("customerName"); + set => SetValue("customerName", value); + } + + [GraphQLMember("status")] + [JsonPropertyName("status")] + public CustomerStatusOperationFilterInput Status + { + get => GetValue("status"); + set => SetValue("status", value); + } + + [GraphQLMember("orders")] + [JsonPropertyName("orders")] + public ListFilterInputTypeOfOrderFilterInput Orders + { + get => GetValue("orders"); + set => SetValue("orders", value); + } + + [GraphQLMember("address")] + [JsonPropertyName("address")] + public AddressFilterInput Address + { + get => GetValue("address"); + set => SetValue("address", value); + } + + [GraphQLMember("created")] + [JsonPropertyName("created")] + public DateTimeOperationFilterInput Created + { + get => GetValue("created"); + set => SetValue("created", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/CustomerInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/CustomerInput.cs new file mode 100644 index 00000000..9944ee4d --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/CustomerInput.cs @@ -0,0 +1,66 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class CustomerInput : GraphInputBase +{ + [GraphQLMember("customerId")] + [JsonPropertyName("customerId")] + public Guid CustomerId + { + get => GetValue("customerId"); + set => SetValue("customerId", value); + } + + [GraphQLMember("customerName")] + [JsonPropertyName("customerName")] + public string CustomerName + { + get => GetValue("customerName"); + set => SetValue("customerName", value); + } + + [GraphQLMember("status")] + [JsonPropertyName("status")] + public CustomerStatus Status + { + get => GetValue("status"); + set => SetValue("status", value); + } + + [GraphQLMember("orders")] + [JsonPropertyName("orders")] + public List Orders + { + get => GetValue>("orders"); + set => SetValue("orders", value); + } + + [GraphQLMember("address")] + [JsonPropertyName("address")] + public AddressInput Address + { + get => GetValue("address"); + set => SetValue("address", value); + } + + [GraphQLMember("created")] + [JsonPropertyName("created")] + public DateTimeOffset? Created + { + get => GetValue("created"); + set => SetValue("created", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/CustomerSortInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/CustomerSortInput.cs new file mode 100644 index 00000000..15b56655 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/CustomerSortInput.cs @@ -0,0 +1,58 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class CustomerSortInput : GraphInputBase +{ + [GraphQLMember("customerId")] + [JsonPropertyName("customerId")] + public SortEnumType? CustomerId + { + get => GetValue("customerId"); + set => SetValue("customerId", value); + } + + [GraphQLMember("customerName")] + [JsonPropertyName("customerName")] + public SortEnumType? CustomerName + { + get => GetValue("customerName"); + set => SetValue("customerName", value); + } + + [GraphQLMember("status")] + [JsonPropertyName("status")] + public SortEnumType? Status + { + get => GetValue("status"); + set => SetValue("status", value); + } + + [GraphQLMember("address")] + [JsonPropertyName("address")] + public AddressSortInput Address + { + get => GetValue("address"); + set => SetValue("address", value); + } + + [GraphQLMember("created")] + [JsonPropertyName("created")] + public SortEnumType? Created + { + get => GetValue("created"); + set => SetValue("created", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/CustomerStatusOperationFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/CustomerStatusOperationFilterInput.cs new file mode 100644 index 00000000..2026611d --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/CustomerStatusOperationFilterInput.cs @@ -0,0 +1,50 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class CustomerStatusOperationFilterInput : GraphInputBase +{ + [GraphQLMember("eq")] + [JsonPropertyName("eq")] + public CustomerStatus? Eq + { + get => GetValue("eq"); + set => SetValue("eq", value); + } + + [GraphQLMember("neq")] + [JsonPropertyName("neq")] + public CustomerStatus? Neq + { + get => GetValue("neq"); + set => SetValue("neq", value); + } + + [GraphQLMember("in")] + [JsonPropertyName("in")] + public List In + { + get => GetValue>("in"); + set => SetValue("in", value); + } + + [GraphQLMember("nin")] + [JsonPropertyName("nin")] + public List Nin + { + get => GetValue>("nin"); + set => SetValue("nin", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/DateTimeOperationFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/DateTimeOperationFilterInput.cs new file mode 100644 index 00000000..08f755d4 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/DateTimeOperationFilterInput.cs @@ -0,0 +1,114 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class DateTimeOperationFilterInput : GraphInputBase +{ + [GraphQLMember("eq")] + [JsonPropertyName("eq")] + public DateTimeOffset? Eq + { + get => GetValue("eq"); + set => SetValue("eq", value); + } + + [GraphQLMember("neq")] + [JsonPropertyName("neq")] + public DateTimeOffset? Neq + { + get => GetValue("neq"); + set => SetValue("neq", value); + } + + [GraphQLMember("in")] + [JsonPropertyName("in")] + public List In + { + get => GetValue>("in"); + set => SetValue("in", value); + } + + [GraphQLMember("nin")] + [JsonPropertyName("nin")] + public List Nin + { + get => GetValue>("nin"); + set => SetValue("nin", value); + } + + [GraphQLMember("gt")] + [JsonPropertyName("gt")] + public DateTimeOffset? Gt + { + get => GetValue("gt"); + set => SetValue("gt", value); + } + + [GraphQLMember("ngt")] + [JsonPropertyName("ngt")] + public DateTimeOffset? Ngt + { + get => GetValue("ngt"); + set => SetValue("ngt", value); + } + + [GraphQLMember("gte")] + [JsonPropertyName("gte")] + public DateTimeOffset? Gte + { + get => GetValue("gte"); + set => SetValue("gte", value); + } + + [GraphQLMember("ngte")] + [JsonPropertyName("ngte")] + public DateTimeOffset? Ngte + { + get => GetValue("ngte"); + set => SetValue("ngte", value); + } + + [GraphQLMember("lt")] + [JsonPropertyName("lt")] + public DateTimeOffset? Lt + { + get => GetValue("lt"); + set => SetValue("lt", value); + } + + [GraphQLMember("nlt")] + [JsonPropertyName("nlt")] + public DateTimeOffset? Nlt + { + get => GetValue("nlt"); + set => SetValue("nlt", value); + } + + [GraphQLMember("lte")] + [JsonPropertyName("lte")] + public DateTimeOffset? Lte + { + get => GetValue("lte"); + set => SetValue("lte", value); + } + + [GraphQLMember("nlte")] + [JsonPropertyName("nlte")] + public DateTimeOffset? Nlte + { + get => GetValue("nlte"); + set => SetValue("nlte", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/DecimalOperationFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/DecimalOperationFilterInput.cs new file mode 100644 index 00000000..2fae901e --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/DecimalOperationFilterInput.cs @@ -0,0 +1,114 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class DecimalOperationFilterInput : GraphInputBase +{ + [GraphQLMember("eq")] + [JsonPropertyName("eq")] + public decimal? Eq + { + get => GetValue("eq"); + set => SetValue("eq", value); + } + + [GraphQLMember("neq")] + [JsonPropertyName("neq")] + public decimal? Neq + { + get => GetValue("neq"); + set => SetValue("neq", value); + } + + [GraphQLMember("in")] + [JsonPropertyName("in")] + public List In + { + get => GetValue>("in"); + set => SetValue("in", value); + } + + [GraphQLMember("nin")] + [JsonPropertyName("nin")] + public List Nin + { + get => GetValue>("nin"); + set => SetValue("nin", value); + } + + [GraphQLMember("gt")] + [JsonPropertyName("gt")] + public decimal? Gt + { + get => GetValue("gt"); + set => SetValue("gt", value); + } + + [GraphQLMember("ngt")] + [JsonPropertyName("ngt")] + public decimal? Ngt + { + get => GetValue("ngt"); + set => SetValue("ngt", value); + } + + [GraphQLMember("gte")] + [JsonPropertyName("gte")] + public decimal? Gte + { + get => GetValue("gte"); + set => SetValue("gte", value); + } + + [GraphQLMember("ngte")] + [JsonPropertyName("ngte")] + public decimal? Ngte + { + get => GetValue("ngte"); + set => SetValue("ngte", value); + } + + [GraphQLMember("lt")] + [JsonPropertyName("lt")] + public decimal? Lt + { + get => GetValue("lt"); + set => SetValue("lt", value); + } + + [GraphQLMember("nlt")] + [JsonPropertyName("nlt")] + public decimal? Nlt + { + get => GetValue("nlt"); + set => SetValue("nlt", value); + } + + [GraphQLMember("lte")] + [JsonPropertyName("lte")] + public decimal? Lte + { + get => GetValue("lte"); + set => SetValue("lte", value); + } + + [GraphQLMember("nlte")] + [JsonPropertyName("nlte")] + public decimal? Nlte + { + get => GetValue("nlte"); + set => SetValue("nlte", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/FloatOperationFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/FloatOperationFilterInput.cs new file mode 100644 index 00000000..bce88ad2 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/FloatOperationFilterInput.cs @@ -0,0 +1,114 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class FloatOperationFilterInput : GraphInputBase +{ + [GraphQLMember("eq")] + [JsonPropertyName("eq")] + public double? Eq + { + get => GetValue("eq"); + set => SetValue("eq", value); + } + + [GraphQLMember("neq")] + [JsonPropertyName("neq")] + public double? Neq + { + get => GetValue("neq"); + set => SetValue("neq", value); + } + + [GraphQLMember("in")] + [JsonPropertyName("in")] + public List In + { + get => GetValue>("in"); + set => SetValue("in", value); + } + + [GraphQLMember("nin")] + [JsonPropertyName("nin")] + public List Nin + { + get => GetValue>("nin"); + set => SetValue("nin", value); + } + + [GraphQLMember("gt")] + [JsonPropertyName("gt")] + public double? Gt + { + get => GetValue("gt"); + set => SetValue("gt", value); + } + + [GraphQLMember("ngt")] + [JsonPropertyName("ngt")] + public double? Ngt + { + get => GetValue("ngt"); + set => SetValue("ngt", value); + } + + [GraphQLMember("gte")] + [JsonPropertyName("gte")] + public double? Gte + { + get => GetValue("gte"); + set => SetValue("gte", value); + } + + [GraphQLMember("ngte")] + [JsonPropertyName("ngte")] + public double? Ngte + { + get => GetValue("ngte"); + set => SetValue("ngte", value); + } + + [GraphQLMember("lt")] + [JsonPropertyName("lt")] + public double? Lt + { + get => GetValue("lt"); + set => SetValue("lt", value); + } + + [GraphQLMember("nlt")] + [JsonPropertyName("nlt")] + public double? Nlt + { + get => GetValue("nlt"); + set => SetValue("nlt", value); + } + + [GraphQLMember("lte")] + [JsonPropertyName("lte")] + public double? Lte + { + get => GetValue("lte"); + set => SetValue("lte", value); + } + + [GraphQLMember("nlte")] + [JsonPropertyName("nlte")] + public double? Nlte + { + get => GetValue("nlte"); + set => SetValue("nlte", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/IAnimalFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/IAnimalFilterInput.cs new file mode 100644 index 00000000..8227219d --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/IAnimalFilterInput.cs @@ -0,0 +1,50 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class IAnimalFilterInput : GraphInputBase +{ + [GraphQLMember("and")] + [JsonPropertyName("and")] + public List And + { + get => GetValue>("and"); + set => SetValue("and", value); + } + + [GraphQLMember("or")] + [JsonPropertyName("or")] + public List Or + { + get => GetValue>("or"); + set => SetValue("or", value); + } + + [GraphQLMember("name")] + [JsonPropertyName("name")] + public StringOperationFilterInput Name + { + get => GetValue("name"); + set => SetValue("name", value); + } + + [GraphQLMember("numberOfLegs")] + [JsonPropertyName("numberOfLegs")] + public IntOperationFilterInput NumberOfLegs + { + get => GetValue("numberOfLegs"); + set => SetValue("numberOfLegs", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/IAnimalSortInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/IAnimalSortInput.cs new file mode 100644 index 00000000..712fbd08 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/IAnimalSortInput.cs @@ -0,0 +1,34 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class IAnimalSortInput : GraphInputBase +{ + [GraphQLMember("name")] + [JsonPropertyName("name")] + public SortEnumType? Name + { + get => GetValue("name"); + set => SetValue("name", value); + } + + [GraphQLMember("numberOfLegs")] + [JsonPropertyName("numberOfLegs")] + public SortEnumType? NumberOfLegs + { + get => GetValue("numberOfLegs"); + set => SetValue("numberOfLegs", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/InputFactory.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/InputFactory.cs new file mode 100644 index 00000000..aff3784b --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/InputFactory.cs @@ -0,0 +1,1847 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +public static class IF +{ + public static AddressFilterInput AddressFilter() + { + return new AddressFilterInput(); + } + public static AddressInput Address() + { + return new AddressInput(); + } + public static AddressSortInput AddressSort() + { + return new AddressSortInput(); + } + public static CustomerFilterInput CustomerFilter() + { + return new CustomerFilterInput(); + } + public static CustomerInput Customer() + { + return new CustomerInput(); + } + public static CustomerSortInput CustomerSort() + { + return new CustomerSortInput(); + } + public static CustomerStatusOperationFilterInput CustomerStatusOperationFilter() + { + return new CustomerStatusOperationFilterInput(); + } + public static DateTimeOperationFilterInput DateTimeOperationFilter() + { + return new DateTimeOperationFilterInput(); + } + public static DecimalOperationFilterInput DecimalOperationFilter() + { + return new DecimalOperationFilterInput(); + } + public static FloatOperationFilterInput FloatOperationFilter() + { + return new FloatOperationFilterInput(); + } + public static IAnimalFilterInput IAnimalFilter() + { + return new IAnimalFilterInput(); + } + public static IAnimalSortInput IAnimalSort() + { + return new IAnimalSortInput(); + } + public static IntOperationFilterInput IntOperationFilter() + { + return new IntOperationFilterInput(); + } + public static ItemFilterInput ItemFilter() + { + return new ItemFilterInput(); + } + public static ItemInput Item() + { + return new ItemInput(); + } + public static ListFilterInputTypeOfOrderFilterInput ListFilterInputTypeOfOrderFilter() + { + return new ListFilterInputTypeOfOrderFilterInput(); + } + public static ListFilterInputTypeOfOrderLineFilterInput ListFilterInputTypeOfOrderLineFilter() + { + return new ListFilterInputTypeOfOrderLineFilterInput(); + } + public static ListUnsignedByteOperationFilterInput ListUnsignedByteOperationFilter() + { + return new ListUnsignedByteOperationFilterInput(); + } + public static LocalDateOperationFilterInput LocalDateOperationFilter() + { + return new LocalDateOperationFilterInput(); + } + public static LocalTimeOperationFilterInput LocalTimeOperationFilter() + { + return new LocalTimeOperationFilterInput(); + } + public static OrderFilterInput OrderFilter() + { + return new OrderFilterInput(); + } + public static OrderInput Order() + { + return new OrderInput(); + } + public static OrderLineFilterInput OrderLineFilter() + { + return new OrderLineFilterInput(); + } + public static OrderLineInput OrderLine() + { + return new OrderLineInput(); + } + public static OrderSortInput OrderSort() + { + return new OrderSortInput(); + } + public static StringOperationFilterInput StringOperationFilter() + { + return new StringOperationFilterInput(); + } + public static UnsignedByteOperationFilterInput UnsignedByteOperationFilter() + { + return new UnsignedByteOperationFilterInput(); + } + public static UuidOperationFilterInput UuidOperationFilter() + { + return new UuidOperationFilterInput(); + } +} + + + +public static class AddressFilterInputExtensions +{ + public static AddressFilterInput And(this AddressFilterInput input, Action> mod) + { + var filter = new List(); + mod ??= _ => { }; + mod(filter); + input.And = filter; + return input; + } + + public static AddressFilterInput Or(this AddressFilterInput input, Action> mod) + { + var filter = new List(); + mod ??= _ => { }; + mod(filter); + input.Or = filter; + return input; + } + + public static AddressFilterInput Name(this AddressFilterInput input, Action mod) + { + var filter = new StringOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Name = filter; + return input; + } + + public static AddressFilterInput Street(this AddressFilterInput input, Action mod) + { + var filter = new StringOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Street = filter; + return input; + } + + public static AddressFilterInput PostalCode(this AddressFilterInput input, Action mod) + { + var filter = new StringOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.PostalCode = filter; + return input; + } + + public static AddressFilterInput CreatedDate(this AddressFilterInput input, Action mod) + { + var filter = new LocalDateOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.CreatedDate = filter; + return input; + } + +} + +public static class AddressInputExtensions +{ + + public static AddressInput Name(this AddressInput input, string val) + { + input.Name = val; + return input; + } + + + public static AddressInput Street(this AddressInput input, string val) + { + input.Street = val; + return input; + } + + + public static AddressInput PostalCode(this AddressInput input, string val) + { + input.PostalCode = val; + return input; + } + + + public static AddressInput CreatedDate(this AddressInput input, DateOnly val) + { + input.CreatedDate = val; + return input; + } + +} + +public static class AddressSortInputExtensions +{ + + public static AddressSortInput Name(this AddressSortInput input, SortEnumType? val) + { + input.Name = val; + return input; + } + + + public static AddressSortInput Street(this AddressSortInput input, SortEnumType? val) + { + input.Street = val; + return input; + } + + + public static AddressSortInput PostalCode(this AddressSortInput input, SortEnumType? val) + { + input.PostalCode = val; + return input; + } + + + public static AddressSortInput CreatedDate(this AddressSortInput input, SortEnumType? val) + { + input.CreatedDate = val; + return input; + } + +} + +public static class CustomerFilterInputExtensions +{ + public static CustomerFilterInput And(this CustomerFilterInput input, Action> mod) + { + var filter = new List(); + mod ??= _ => { }; + mod(filter); + input.And = filter; + return input; + } + + public static CustomerFilterInput Or(this CustomerFilterInput input, Action> mod) + { + var filter = new List(); + mod ??= _ => { }; + mod(filter); + input.Or = filter; + return input; + } + + public static CustomerFilterInput CustomerId(this CustomerFilterInput input, Action mod) + { + var filter = new UuidOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.CustomerId = filter; + return input; + } + + public static CustomerFilterInput CustomerName(this CustomerFilterInput input, Action mod) + { + var filter = new StringOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.CustomerName = filter; + return input; + } + + public static CustomerFilterInput Status(this CustomerFilterInput input, Action mod) + { + var filter = new CustomerStatusOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Status = filter; + return input; + } + + public static CustomerFilterInput Orders(this CustomerFilterInput input, Action mod) + { + var filter = new ListFilterInputTypeOfOrderFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Orders = filter; + return input; + } + + public static CustomerFilterInput Address(this CustomerFilterInput input, Action mod) + { + var filter = new AddressFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Address = filter; + return input; + } + + public static CustomerFilterInput Created(this CustomerFilterInput input, Action mod) + { + var filter = new DateTimeOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Created = filter; + return input; + } + +} + +public static class CustomerInputExtensions +{ + + public static CustomerInput CustomerId(this CustomerInput input, Guid val) + { + input.CustomerId = val; + return input; + } + + + public static CustomerInput CustomerName(this CustomerInput input, string val) + { + input.CustomerName = val; + return input; + } + + + public static CustomerInput Status(this CustomerInput input, CustomerStatus val) + { + input.Status = val; + return input; + } + + public static CustomerInput Orders(this CustomerInput input, Action> mod) + { + var filter = new List(); + mod ??= _ => { }; + mod(filter); + input.Orders = filter; + return input; + } + + public static CustomerInput Address(this CustomerInput input, Action mod) + { + var filter = new AddressInput(); + mod ??= _ => { }; + mod(filter); + input.Address = filter; + return input; + } + + + public static CustomerInput Created(this CustomerInput input, DateTimeOffset? val) + { + input.Created = val; + return input; + } + +} + +public static class CustomerSortInputExtensions +{ + + public static CustomerSortInput CustomerId(this CustomerSortInput input, SortEnumType? val) + { + input.CustomerId = val; + return input; + } + + + public static CustomerSortInput CustomerName(this CustomerSortInput input, SortEnumType? val) + { + input.CustomerName = val; + return input; + } + + + public static CustomerSortInput Status(this CustomerSortInput input, SortEnumType? val) + { + input.Status = val; + return input; + } + + public static CustomerSortInput Address(this CustomerSortInput input, Action mod) + { + var filter = new AddressSortInput(); + mod ??= _ => { }; + mod(filter); + input.Address = filter; + return input; + } + + + public static CustomerSortInput Created(this CustomerSortInput input, SortEnumType? val) + { + input.Created = val; + return input; + } + +} + +public static class CustomerStatusOperationFilterInputExtensions +{ + + public static CustomerStatusOperationFilterInput Eq(this CustomerStatusOperationFilterInput input, CustomerStatus? val) + { + input.Eq = val; + return input; + } + + + public static CustomerStatusOperationFilterInput Neq(this CustomerStatusOperationFilterInput input, CustomerStatus? val) + { + input.Neq = val; + return input; + } + + + public static CustomerStatusOperationFilterInput In(this CustomerStatusOperationFilterInput input, List val) + { + input.In = val; + return input; + } + + + public static CustomerStatusOperationFilterInput Nin(this CustomerStatusOperationFilterInput input, List val) + { + input.Nin = val; + return input; + } + +} + +public static class DateTimeOperationFilterInputExtensions +{ + + public static DateTimeOperationFilterInput Eq(this DateTimeOperationFilterInput input, DateTimeOffset? val) + { + input.Eq = val; + return input; + } + + + public static DateTimeOperationFilterInput Neq(this DateTimeOperationFilterInput input, DateTimeOffset? val) + { + input.Neq = val; + return input; + } + + + public static DateTimeOperationFilterInput In(this DateTimeOperationFilterInput input, List val) + { + input.In = val; + return input; + } + + + public static DateTimeOperationFilterInput Nin(this DateTimeOperationFilterInput input, List val) + { + input.Nin = val; + return input; + } + + + public static DateTimeOperationFilterInput Gt(this DateTimeOperationFilterInput input, DateTimeOffset? val) + { + input.Gt = val; + return input; + } + + + public static DateTimeOperationFilterInput Ngt(this DateTimeOperationFilterInput input, DateTimeOffset? val) + { + input.Ngt = val; + return input; + } + + + public static DateTimeOperationFilterInput Gte(this DateTimeOperationFilterInput input, DateTimeOffset? val) + { + input.Gte = val; + return input; + } + + + public static DateTimeOperationFilterInput Ngte(this DateTimeOperationFilterInput input, DateTimeOffset? val) + { + input.Ngte = val; + return input; + } + + + public static DateTimeOperationFilterInput Lt(this DateTimeOperationFilterInput input, DateTimeOffset? val) + { + input.Lt = val; + return input; + } + + + public static DateTimeOperationFilterInput Nlt(this DateTimeOperationFilterInput input, DateTimeOffset? val) + { + input.Nlt = val; + return input; + } + + + public static DateTimeOperationFilterInput Lte(this DateTimeOperationFilterInput input, DateTimeOffset? val) + { + input.Lte = val; + return input; + } + + + public static DateTimeOperationFilterInput Nlte(this DateTimeOperationFilterInput input, DateTimeOffset? val) + { + input.Nlte = val; + return input; + } + +} + +public static class DecimalOperationFilterInputExtensions +{ + + public static DecimalOperationFilterInput Eq(this DecimalOperationFilterInput input, decimal? val) + { + input.Eq = val; + return input; + } + + + public static DecimalOperationFilterInput Neq(this DecimalOperationFilterInput input, decimal? val) + { + input.Neq = val; + return input; + } + + + public static DecimalOperationFilterInput In(this DecimalOperationFilterInput input, List val) + { + input.In = val; + return input; + } + + + public static DecimalOperationFilterInput Nin(this DecimalOperationFilterInput input, List val) + { + input.Nin = val; + return input; + } + + + public static DecimalOperationFilterInput Gt(this DecimalOperationFilterInput input, decimal? val) + { + input.Gt = val; + return input; + } + + + public static DecimalOperationFilterInput Ngt(this DecimalOperationFilterInput input, decimal? val) + { + input.Ngt = val; + return input; + } + + + public static DecimalOperationFilterInput Gte(this DecimalOperationFilterInput input, decimal? val) + { + input.Gte = val; + return input; + } + + + public static DecimalOperationFilterInput Ngte(this DecimalOperationFilterInput input, decimal? val) + { + input.Ngte = val; + return input; + } + + + public static DecimalOperationFilterInput Lt(this DecimalOperationFilterInput input, decimal? val) + { + input.Lt = val; + return input; + } + + + public static DecimalOperationFilterInput Nlt(this DecimalOperationFilterInput input, decimal? val) + { + input.Nlt = val; + return input; + } + + + public static DecimalOperationFilterInput Lte(this DecimalOperationFilterInput input, decimal? val) + { + input.Lte = val; + return input; + } + + + public static DecimalOperationFilterInput Nlte(this DecimalOperationFilterInput input, decimal? val) + { + input.Nlte = val; + return input; + } + +} + +public static class FloatOperationFilterInputExtensions +{ + + public static FloatOperationFilterInput Eq(this FloatOperationFilterInput input, double? val) + { + input.Eq = val; + return input; + } + + + public static FloatOperationFilterInput Neq(this FloatOperationFilterInput input, double? val) + { + input.Neq = val; + return input; + } + + + public static FloatOperationFilterInput In(this FloatOperationFilterInput input, List val) + { + input.In = val; + return input; + } + + + public static FloatOperationFilterInput Nin(this FloatOperationFilterInput input, List val) + { + input.Nin = val; + return input; + } + + + public static FloatOperationFilterInput Gt(this FloatOperationFilterInput input, double? val) + { + input.Gt = val; + return input; + } + + + public static FloatOperationFilterInput Ngt(this FloatOperationFilterInput input, double? val) + { + input.Ngt = val; + return input; + } + + + public static FloatOperationFilterInput Gte(this FloatOperationFilterInput input, double? val) + { + input.Gte = val; + return input; + } + + + public static FloatOperationFilterInput Ngte(this FloatOperationFilterInput input, double? val) + { + input.Ngte = val; + return input; + } + + + public static FloatOperationFilterInput Lt(this FloatOperationFilterInput input, double? val) + { + input.Lt = val; + return input; + } + + + public static FloatOperationFilterInput Nlt(this FloatOperationFilterInput input, double? val) + { + input.Nlt = val; + return input; + } + + + public static FloatOperationFilterInput Lte(this FloatOperationFilterInput input, double? val) + { + input.Lte = val; + return input; + } + + + public static FloatOperationFilterInput Nlte(this FloatOperationFilterInput input, double? val) + { + input.Nlte = val; + return input; + } + +} + +public static class IAnimalFilterInputExtensions +{ + public static IAnimalFilterInput And(this IAnimalFilterInput input, Action> mod) + { + var filter = new List(); + mod ??= _ => { }; + mod(filter); + input.And = filter; + return input; + } + + public static IAnimalFilterInput Or(this IAnimalFilterInput input, Action> mod) + { + var filter = new List(); + mod ??= _ => { }; + mod(filter); + input.Or = filter; + return input; + } + + public static IAnimalFilterInput Name(this IAnimalFilterInput input, Action mod) + { + var filter = new StringOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Name = filter; + return input; + } + + public static IAnimalFilterInput NumberOfLegs(this IAnimalFilterInput input, Action mod) + { + var filter = new IntOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.NumberOfLegs = filter; + return input; + } + +} + +public static class IAnimalSortInputExtensions +{ + + public static IAnimalSortInput Name(this IAnimalSortInput input, SortEnumType? val) + { + input.Name = val; + return input; + } + + + public static IAnimalSortInput NumberOfLegs(this IAnimalSortInput input, SortEnumType? val) + { + input.NumberOfLegs = val; + return input; + } + +} + +public static class IntOperationFilterInputExtensions +{ + + public static IntOperationFilterInput Eq(this IntOperationFilterInput input, int? val) + { + input.Eq = val; + return input; + } + + + public static IntOperationFilterInput Neq(this IntOperationFilterInput input, int? val) + { + input.Neq = val; + return input; + } + + + public static IntOperationFilterInput In(this IntOperationFilterInput input, List val) + { + input.In = val; + return input; + } + + + public static IntOperationFilterInput Nin(this IntOperationFilterInput input, List val) + { + input.Nin = val; + return input; + } + + + public static IntOperationFilterInput Gt(this IntOperationFilterInput input, int? val) + { + input.Gt = val; + return input; + } + + + public static IntOperationFilterInput Ngt(this IntOperationFilterInput input, int? val) + { + input.Ngt = val; + return input; + } + + + public static IntOperationFilterInput Gte(this IntOperationFilterInput input, int? val) + { + input.Gte = val; + return input; + } + + + public static IntOperationFilterInput Ngte(this IntOperationFilterInput input, int? val) + { + input.Ngte = val; + return input; + } + + + public static IntOperationFilterInput Lt(this IntOperationFilterInput input, int? val) + { + input.Lt = val; + return input; + } + + + public static IntOperationFilterInput Nlt(this IntOperationFilterInput input, int? val) + { + input.Nlt = val; + return input; + } + + + public static IntOperationFilterInput Lte(this IntOperationFilterInput input, int? val) + { + input.Lte = val; + return input; + } + + + public static IntOperationFilterInput Nlte(this IntOperationFilterInput input, int? val) + { + input.Nlte = val; + return input; + } + +} + +public static class ItemFilterInputExtensions +{ + public static ItemFilterInput And(this ItemFilterInput input, Action> mod) + { + var filter = new List(); + mod ??= _ => { }; + mod(filter); + input.And = filter; + return input; + } + + public static ItemFilterInput Or(this ItemFilterInput input, Action> mod) + { + var filter = new List(); + mod ??= _ => { }; + mod(filter); + input.Or = filter; + return input; + } + + public static ItemFilterInput ItemId(this ItemFilterInput input, Action mod) + { + var filter = new StringOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.ItemId = filter; + return input; + } + + public static ItemFilterInput ItemName(this ItemFilterInput input, Action mod) + { + var filter = new StringOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.ItemName = filter; + return input; + } + + public static ItemFilterInput Data(this ItemFilterInput input, Action mod) + { + var filter = new ListUnsignedByteOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Data = filter; + return input; + } + +} + +public static class ItemInputExtensions +{ + + public static ItemInput ItemId(this ItemInput input, string val) + { + input.ItemId = val; + return input; + } + + + public static ItemInput ItemName(this ItemInput input, string val) + { + input.ItemName = val; + return input; + } + + + public static ItemInput Data(this ItemInput input, List val) + { + input.Data = val; + return input; + } + +} + +public static class ListFilterInputTypeOfOrderFilterInputExtensions +{ + public static ListFilterInputTypeOfOrderFilterInput All(this ListFilterInputTypeOfOrderFilterInput input, Action mod) + { + var filter = new OrderFilterInput(); + mod ??= _ => { }; + mod(filter); + input.All = filter; + return input; + } + + public static ListFilterInputTypeOfOrderFilterInput None(this ListFilterInputTypeOfOrderFilterInput input, Action mod) + { + var filter = new OrderFilterInput(); + mod ??= _ => { }; + mod(filter); + input.None = filter; + return input; + } + + public static ListFilterInputTypeOfOrderFilterInput Some(this ListFilterInputTypeOfOrderFilterInput input, Action mod) + { + var filter = new OrderFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Some = filter; + return input; + } + + + public static ListFilterInputTypeOfOrderFilterInput Any(this ListFilterInputTypeOfOrderFilterInput input, bool? val) + { + input.Any = val; + return input; + } + +} + +public static class ListFilterInputTypeOfOrderLineFilterInputExtensions +{ + public static ListFilterInputTypeOfOrderLineFilterInput All(this ListFilterInputTypeOfOrderLineFilterInput input, Action mod) + { + var filter = new OrderLineFilterInput(); + mod ??= _ => { }; + mod(filter); + input.All = filter; + return input; + } + + public static ListFilterInputTypeOfOrderLineFilterInput None(this ListFilterInputTypeOfOrderLineFilterInput input, Action mod) + { + var filter = new OrderLineFilterInput(); + mod ??= _ => { }; + mod(filter); + input.None = filter; + return input; + } + + public static ListFilterInputTypeOfOrderLineFilterInput Some(this ListFilterInputTypeOfOrderLineFilterInput input, Action mod) + { + var filter = new OrderLineFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Some = filter; + return input; + } + + + public static ListFilterInputTypeOfOrderLineFilterInput Any(this ListFilterInputTypeOfOrderLineFilterInput input, bool? val) + { + input.Any = val; + return input; + } + +} + +public static class ListUnsignedByteOperationFilterInputExtensions +{ + public static ListUnsignedByteOperationFilterInput All(this ListUnsignedByteOperationFilterInput input, Action mod) + { + var filter = new UnsignedByteOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.All = filter; + return input; + } + + public static ListUnsignedByteOperationFilterInput None(this ListUnsignedByteOperationFilterInput input, Action mod) + { + var filter = new UnsignedByteOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.None = filter; + return input; + } + + public static ListUnsignedByteOperationFilterInput Some(this ListUnsignedByteOperationFilterInput input, Action mod) + { + var filter = new UnsignedByteOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Some = filter; + return input; + } + + + public static ListUnsignedByteOperationFilterInput Any(this ListUnsignedByteOperationFilterInput input, bool? val) + { + input.Any = val; + return input; + } + +} + +public static class LocalDateOperationFilterInputExtensions +{ + + public static LocalDateOperationFilterInput Eq(this LocalDateOperationFilterInput input, DateOnly? val) + { + input.Eq = val; + return input; + } + + + public static LocalDateOperationFilterInput Neq(this LocalDateOperationFilterInput input, DateOnly? val) + { + input.Neq = val; + return input; + } + + + public static LocalDateOperationFilterInput In(this LocalDateOperationFilterInput input, List val) + { + input.In = val; + return input; + } + + + public static LocalDateOperationFilterInput Nin(this LocalDateOperationFilterInput input, List val) + { + input.Nin = val; + return input; + } + + + public static LocalDateOperationFilterInput Gt(this LocalDateOperationFilterInput input, DateOnly? val) + { + input.Gt = val; + return input; + } + + + public static LocalDateOperationFilterInput Ngt(this LocalDateOperationFilterInput input, DateOnly? val) + { + input.Ngt = val; + return input; + } + + + public static LocalDateOperationFilterInput Gte(this LocalDateOperationFilterInput input, DateOnly? val) + { + input.Gte = val; + return input; + } + + + public static LocalDateOperationFilterInput Ngte(this LocalDateOperationFilterInput input, DateOnly? val) + { + input.Ngte = val; + return input; + } + + + public static LocalDateOperationFilterInput Lt(this LocalDateOperationFilterInput input, DateOnly? val) + { + input.Lt = val; + return input; + } + + + public static LocalDateOperationFilterInput Nlt(this LocalDateOperationFilterInput input, DateOnly? val) + { + input.Nlt = val; + return input; + } + + + public static LocalDateOperationFilterInput Lte(this LocalDateOperationFilterInput input, DateOnly? val) + { + input.Lte = val; + return input; + } + + + public static LocalDateOperationFilterInput Nlte(this LocalDateOperationFilterInput input, DateOnly? val) + { + input.Nlte = val; + return input; + } + +} + +public static class LocalTimeOperationFilterInputExtensions +{ + + public static LocalTimeOperationFilterInput Eq(this LocalTimeOperationFilterInput input, TimeOnly? val) + { + input.Eq = val; + return input; + } + + + public static LocalTimeOperationFilterInput Neq(this LocalTimeOperationFilterInput input, TimeOnly? val) + { + input.Neq = val; + return input; + } + + + public static LocalTimeOperationFilterInput In(this LocalTimeOperationFilterInput input, List val) + { + input.In = val; + return input; + } + + + public static LocalTimeOperationFilterInput Nin(this LocalTimeOperationFilterInput input, List val) + { + input.Nin = val; + return input; + } + + + public static LocalTimeOperationFilterInput Gt(this LocalTimeOperationFilterInput input, TimeOnly? val) + { + input.Gt = val; + return input; + } + + + public static LocalTimeOperationFilterInput Ngt(this LocalTimeOperationFilterInput input, TimeOnly? val) + { + input.Ngt = val; + return input; + } + + + public static LocalTimeOperationFilterInput Gte(this LocalTimeOperationFilterInput input, TimeOnly? val) + { + input.Gte = val; + return input; + } + + + public static LocalTimeOperationFilterInput Ngte(this LocalTimeOperationFilterInput input, TimeOnly? val) + { + input.Ngte = val; + return input; + } + + + public static LocalTimeOperationFilterInput Lt(this LocalTimeOperationFilterInput input, TimeOnly? val) + { + input.Lt = val; + return input; + } + + + public static LocalTimeOperationFilterInput Nlt(this LocalTimeOperationFilterInput input, TimeOnly? val) + { + input.Nlt = val; + return input; + } + + + public static LocalTimeOperationFilterInput Lte(this LocalTimeOperationFilterInput input, TimeOnly? val) + { + input.Lte = val; + return input; + } + + + public static LocalTimeOperationFilterInput Nlte(this LocalTimeOperationFilterInput input, TimeOnly? val) + { + input.Nlte = val; + return input; + } + +} + +public static class OrderFilterInputExtensions +{ + public static OrderFilterInput And(this OrderFilterInput input, Action> mod) + { + var filter = new List(); + mod ??= _ => { }; + mod(filter); + input.And = filter; + return input; + } + + public static OrderFilterInput Or(this OrderFilterInput input, Action> mod) + { + var filter = new List(); + mod ??= _ => { }; + mod(filter); + input.Or = filter; + return input; + } + + public static OrderFilterInput OrderId(this OrderFilterInput input, Action mod) + { + var filter = new UuidOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.OrderId = filter; + return input; + } + + public static OrderFilterInput Customer(this OrderFilterInput input, Action mod) + { + var filter = new CustomerFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Customer = filter; + return input; + } + + public static OrderFilterInput Address(this OrderFilterInput input, Action mod) + { + var filter = new AddressFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Address = filter; + return input; + } + + public static OrderFilterInput OrderDate(this OrderFilterInput input, Action mod) + { + var filter = new DateTimeOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.OrderDate = filter; + return input; + } + + public static OrderFilterInput OrderDateTime(this OrderFilterInput input, Action mod) + { + var filter = new DateTimeOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.OrderDateTime = filter; + return input; + } + + public static OrderFilterInput OrderDay(this OrderFilterInput input, Action mod) + { + var filter = new LocalDateOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.OrderDay = filter; + return input; + } + + public static OrderFilterInput Lines(this OrderFilterInput input, Action mod) + { + var filter = new ListFilterInputTypeOfOrderLineFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Lines = filter; + return input; + } + + public static OrderFilterInput EntryTime(this OrderFilterInput input, Action mod) + { + var filter = new LocalTimeOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.EntryTime = filter; + return input; + } + + [Obsolete("This propery is obsolete and should not be used!")] + public static OrderFilterInput Grade(this OrderFilterInput input, Action mod) + { + var filter = new StringOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Grade = filter; + return input; + } + +} + +public static class OrderInputExtensions +{ + + public static OrderInput OrderId(this OrderInput input, Guid val) + { + input.OrderId = val; + return input; + } + + public static OrderInput Customer(this OrderInput input, Action mod) + { + var filter = new CustomerInput(); + mod ??= _ => { }; + mod(filter); + input.Customer = filter; + return input; + } + + public static OrderInput Address(this OrderInput input, Action mod) + { + var filter = new AddressInput(); + mod ??= _ => { }; + mod(filter); + input.Address = filter; + return input; + } + + + public static OrderInput OrderDate(this OrderInput input, DateTimeOffset val) + { + input.OrderDate = val; + return input; + } + + + public static OrderInput OrderDateTime(this OrderInput input, DateTimeOffset val) + { + input.OrderDateTime = val; + return input; + } + + + public static OrderInput OrderDay(this OrderInput input, DateOnly val) + { + input.OrderDay = val; + return input; + } + + public static OrderInput Lines(this OrderInput input, Action> mod) + { + var filter = new List(); + mod ??= _ => { }; + mod(filter); + input.Lines = filter; + return input; + } + + + public static OrderInput EntryTime(this OrderInput input, TimeOnly? val) + { + input.EntryTime = val; + return input; + } + + + [Obsolete("This propery is obsolete and should not be used!")] + public static OrderInput Grade(this OrderInput input, string val) + { + input.Grade = val; + return input; + } + +} + +public static class OrderLineFilterInputExtensions +{ + public static OrderLineFilterInput And(this OrderLineFilterInput input, Action> mod) + { + var filter = new List(); + mod ??= _ => { }; + mod(filter); + input.And = filter; + return input; + } + + public static OrderLineFilterInput Or(this OrderLineFilterInput input, Action> mod) + { + var filter = new List(); + mod ??= _ => { }; + mod(filter); + input.Or = filter; + return input; + } + + public static OrderLineFilterInput LineNumber(this OrderLineFilterInput input, Action mod) + { + var filter = new IntOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.LineNumber = filter; + return input; + } + + public static OrderLineFilterInput Order(this OrderLineFilterInput input, Action mod) + { + var filter = new OrderFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Order = filter; + return input; + } + + public static OrderLineFilterInput Item(this OrderLineFilterInput input, Action mod) + { + var filter = new ItemFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Item = filter; + return input; + } + + public static OrderLineFilterInput Price(this OrderLineFilterInput input, Action mod) + { + var filter = new DecimalOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Price = filter; + return input; + } + + public static OrderLineFilterInput Quantity(this OrderLineFilterInput input, Action mod) + { + var filter = new FloatOperationFilterInput(); + mod ??= _ => { }; + mod(filter); + input.Quantity = filter; + return input; + } + +} + +public static class OrderLineInputExtensions +{ + + public static OrderLineInput LineNumber(this OrderLineInput input, int val) + { + input.LineNumber = val; + return input; + } + + public static OrderLineInput Order(this OrderLineInput input, Action mod) + { + var filter = new OrderInput(); + mod ??= _ => { }; + mod(filter); + input.Order = filter; + return input; + } + + public static OrderLineInput Item(this OrderLineInput input, Action mod) + { + var filter = new ItemInput(); + mod ??= _ => { }; + mod(filter); + input.Item = filter; + return input; + } + + + public static OrderLineInput Price(this OrderLineInput input, decimal val) + { + input.Price = val; + return input; + } + + + public static OrderLineInput Quantity(this OrderLineInput input, double val) + { + input.Quantity = val; + return input; + } + +} + +public static class OrderSortInputExtensions +{ + + public static OrderSortInput OrderId(this OrderSortInput input, SortEnumType? val) + { + input.OrderId = val; + return input; + } + + public static OrderSortInput Customer(this OrderSortInput input, Action mod) + { + var filter = new CustomerSortInput(); + mod ??= _ => { }; + mod(filter); + input.Customer = filter; + return input; + } + + public static OrderSortInput Address(this OrderSortInput input, Action mod) + { + var filter = new AddressSortInput(); + mod ??= _ => { }; + mod(filter); + input.Address = filter; + return input; + } + + + public static OrderSortInput OrderDate(this OrderSortInput input, SortEnumType? val) + { + input.OrderDate = val; + return input; + } + + + public static OrderSortInput OrderDateTime(this OrderSortInput input, SortEnumType? val) + { + input.OrderDateTime = val; + return input; + } + + + public static OrderSortInput OrderDay(this OrderSortInput input, SortEnumType? val) + { + input.OrderDay = val; + return input; + } + + + public static OrderSortInput EntryTime(this OrderSortInput input, SortEnumType? val) + { + input.EntryTime = val; + return input; + } + + + [Obsolete("This propery is obsolete and should not be used!")] + public static OrderSortInput Grade(this OrderSortInput input, SortEnumType? val) + { + input.Grade = val; + return input; + } + +} + +public static class StringOperationFilterInputExtensions +{ + public static StringOperationFilterInput And(this StringOperationFilterInput input, Action> mod) + { + var filter = new List(); + mod ??= _ => { }; + mod(filter); + input.And = filter; + return input; + } + + public static StringOperationFilterInput Or(this StringOperationFilterInput input, Action> mod) + { + var filter = new List(); + mod ??= _ => { }; + mod(filter); + input.Or = filter; + return input; + } + + + public static StringOperationFilterInput Eq(this StringOperationFilterInput input, string val) + { + input.Eq = val; + return input; + } + + + public static StringOperationFilterInput Neq(this StringOperationFilterInput input, string val) + { + input.Neq = val; + return input; + } + + + public static StringOperationFilterInput Contains(this StringOperationFilterInput input, string val) + { + input.Contains = val; + return input; + } + + + public static StringOperationFilterInput Ncontains(this StringOperationFilterInput input, string val) + { + input.Ncontains = val; + return input; + } + + + public static StringOperationFilterInput In(this StringOperationFilterInput input, List val) + { + input.In = val; + return input; + } + + + public static StringOperationFilterInput Nin(this StringOperationFilterInput input, List val) + { + input.Nin = val; + return input; + } + + + public static StringOperationFilterInput StartsWith(this StringOperationFilterInput input, string val) + { + input.StartsWith = val; + return input; + } + + + public static StringOperationFilterInput NstartsWith(this StringOperationFilterInput input, string val) + { + input.NstartsWith = val; + return input; + } + + + public static StringOperationFilterInput EndsWith(this StringOperationFilterInput input, string val) + { + input.EndsWith = val; + return input; + } + + + public static StringOperationFilterInput NendsWith(this StringOperationFilterInput input, string val) + { + input.NendsWith = val; + return input; + } + +} + +public static class UnsignedByteOperationFilterInputExtensions +{ + + public static UnsignedByteOperationFilterInput Eq(this UnsignedByteOperationFilterInput input, byte? val) + { + input.Eq = val; + return input; + } + + + public static UnsignedByteOperationFilterInput Neq(this UnsignedByteOperationFilterInput input, byte? val) + { + input.Neq = val; + return input; + } + + + public static UnsignedByteOperationFilterInput In(this UnsignedByteOperationFilterInput input, List val) + { + input.In = val; + return input; + } + + + public static UnsignedByteOperationFilterInput Nin(this UnsignedByteOperationFilterInput input, List val) + { + input.Nin = val; + return input; + } + + + public static UnsignedByteOperationFilterInput Gt(this UnsignedByteOperationFilterInput input, byte? val) + { + input.Gt = val; + return input; + } + + + public static UnsignedByteOperationFilterInput Ngt(this UnsignedByteOperationFilterInput input, byte? val) + { + input.Ngt = val; + return input; + } + + + public static UnsignedByteOperationFilterInput Gte(this UnsignedByteOperationFilterInput input, byte? val) + { + input.Gte = val; + return input; + } + + + public static UnsignedByteOperationFilterInput Ngte(this UnsignedByteOperationFilterInput input, byte? val) + { + input.Ngte = val; + return input; + } + + + public static UnsignedByteOperationFilterInput Lt(this UnsignedByteOperationFilterInput input, byte? val) + { + input.Lt = val; + return input; + } + + + public static UnsignedByteOperationFilterInput Nlt(this UnsignedByteOperationFilterInput input, byte? val) + { + input.Nlt = val; + return input; + } + + + public static UnsignedByteOperationFilterInput Lte(this UnsignedByteOperationFilterInput input, byte? val) + { + input.Lte = val; + return input; + } + + + public static UnsignedByteOperationFilterInput Nlte(this UnsignedByteOperationFilterInput input, byte? val) + { + input.Nlte = val; + return input; + } + +} + +public static class UuidOperationFilterInputExtensions +{ + + public static UuidOperationFilterInput Eq(this UuidOperationFilterInput input, Guid? val) + { + input.Eq = val; + return input; + } + + + public static UuidOperationFilterInput Neq(this UuidOperationFilterInput input, Guid? val) + { + input.Neq = val; + return input; + } + + + public static UuidOperationFilterInput In(this UuidOperationFilterInput input, List val) + { + input.In = val; + return input; + } + + + public static UuidOperationFilterInput Nin(this UuidOperationFilterInput input, List val) + { + input.Nin = val; + return input; + } + + + public static UuidOperationFilterInput Gt(this UuidOperationFilterInput input, Guid? val) + { + input.Gt = val; + return input; + } + + + public static UuidOperationFilterInput Ngt(this UuidOperationFilterInput input, Guid? val) + { + input.Ngt = val; + return input; + } + + + public static UuidOperationFilterInput Gte(this UuidOperationFilterInput input, Guid? val) + { + input.Gte = val; + return input; + } + + + public static UuidOperationFilterInput Ngte(this UuidOperationFilterInput input, Guid? val) + { + input.Ngte = val; + return input; + } + + + public static UuidOperationFilterInput Lt(this UuidOperationFilterInput input, Guid? val) + { + input.Lt = val; + return input; + } + + + public static UuidOperationFilterInput Nlt(this UuidOperationFilterInput input, Guid? val) + { + input.Nlt = val; + return input; + } + + + public static UuidOperationFilterInput Lte(this UuidOperationFilterInput input, Guid? val) + { + input.Lte = val; + return input; + } + + + public static UuidOperationFilterInput Nlte(this UuidOperationFilterInput input, Guid? val) + { + input.Nlte = val; + return input; + } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/IntOperationFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/IntOperationFilterInput.cs new file mode 100644 index 00000000..55a1fd51 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/IntOperationFilterInput.cs @@ -0,0 +1,114 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class IntOperationFilterInput : GraphInputBase +{ + [GraphQLMember("eq")] + [JsonPropertyName("eq")] + public int? Eq + { + get => GetValue("eq"); + set => SetValue("eq", value); + } + + [GraphQLMember("neq")] + [JsonPropertyName("neq")] + public int? Neq + { + get => GetValue("neq"); + set => SetValue("neq", value); + } + + [GraphQLMember("in")] + [JsonPropertyName("in")] + public List In + { + get => GetValue>("in"); + set => SetValue("in", value); + } + + [GraphQLMember("nin")] + [JsonPropertyName("nin")] + public List Nin + { + get => GetValue>("nin"); + set => SetValue("nin", value); + } + + [GraphQLMember("gt")] + [JsonPropertyName("gt")] + public int? Gt + { + get => GetValue("gt"); + set => SetValue("gt", value); + } + + [GraphQLMember("ngt")] + [JsonPropertyName("ngt")] + public int? Ngt + { + get => GetValue("ngt"); + set => SetValue("ngt", value); + } + + [GraphQLMember("gte")] + [JsonPropertyName("gte")] + public int? Gte + { + get => GetValue("gte"); + set => SetValue("gte", value); + } + + [GraphQLMember("ngte")] + [JsonPropertyName("ngte")] + public int? Ngte + { + get => GetValue("ngte"); + set => SetValue("ngte", value); + } + + [GraphQLMember("lt")] + [JsonPropertyName("lt")] + public int? Lt + { + get => GetValue("lt"); + set => SetValue("lt", value); + } + + [GraphQLMember("nlt")] + [JsonPropertyName("nlt")] + public int? Nlt + { + get => GetValue("nlt"); + set => SetValue("nlt", value); + } + + [GraphQLMember("lte")] + [JsonPropertyName("lte")] + public int? Lte + { + get => GetValue("lte"); + set => SetValue("lte", value); + } + + [GraphQLMember("nlte")] + [JsonPropertyName("nlte")] + public int? Nlte + { + get => GetValue("nlte"); + set => SetValue("nlte", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/ItemFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/ItemFilterInput.cs new file mode 100644 index 00000000..68511b1e --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/ItemFilterInput.cs @@ -0,0 +1,58 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class ItemFilterInput : GraphInputBase +{ + [GraphQLMember("and")] + [JsonPropertyName("and")] + public List And + { + get => GetValue>("and"); + set => SetValue("and", value); + } + + [GraphQLMember("or")] + [JsonPropertyName("or")] + public List Or + { + get => GetValue>("or"); + set => SetValue("or", value); + } + + [GraphQLMember("itemId")] + [JsonPropertyName("itemId")] + public StringOperationFilterInput ItemId + { + get => GetValue("itemId"); + set => SetValue("itemId", value); + } + + [GraphQLMember("itemName")] + [JsonPropertyName("itemName")] + public StringOperationFilterInput ItemName + { + get => GetValue("itemName"); + set => SetValue("itemName", value); + } + + [GraphQLMember("data")] + [JsonPropertyName("data")] + public ListUnsignedByteOperationFilterInput Data + { + get => GetValue("data"); + set => SetValue("data", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/ItemInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/ItemInput.cs new file mode 100644 index 00000000..b8aa83f8 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/ItemInput.cs @@ -0,0 +1,42 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class ItemInput : GraphInputBase +{ + [GraphQLMember("itemId")] + [JsonPropertyName("itemId")] + public string ItemId + { + get => GetValue("itemId"); + set => SetValue("itemId", value); + } + + [GraphQLMember("itemName")] + [JsonPropertyName("itemName")] + public string ItemName + { + get => GetValue("itemName"); + set => SetValue("itemName", value); + } + + [GraphQLMember("data")] + [JsonPropertyName("data")] + public List Data + { + get => GetValue>("data"); + set => SetValue("data", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/ListFilterInputTypeOfOrderFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/ListFilterInputTypeOfOrderFilterInput.cs new file mode 100644 index 00000000..0ef9181c --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/ListFilterInputTypeOfOrderFilterInput.cs @@ -0,0 +1,50 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class ListFilterInputTypeOfOrderFilterInput : GraphInputBase +{ + [GraphQLMember("all")] + [JsonPropertyName("all")] + public OrderFilterInput All + { + get => GetValue("all"); + set => SetValue("all", value); + } + + [GraphQLMember("none")] + [JsonPropertyName("none")] + public OrderFilterInput None + { + get => GetValue("none"); + set => SetValue("none", value); + } + + [GraphQLMember("some")] + [JsonPropertyName("some")] + public OrderFilterInput Some + { + get => GetValue("some"); + set => SetValue("some", value); + } + + [GraphQLMember("any")] + [JsonPropertyName("any")] + public bool? Any + { + get => GetValue("any"); + set => SetValue("any", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/ListFilterInputTypeOfOrderLineFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/ListFilterInputTypeOfOrderLineFilterInput.cs new file mode 100644 index 00000000..8dd56f0a --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/ListFilterInputTypeOfOrderLineFilterInput.cs @@ -0,0 +1,50 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class ListFilterInputTypeOfOrderLineFilterInput : GraphInputBase +{ + [GraphQLMember("all")] + [JsonPropertyName("all")] + public OrderLineFilterInput All + { + get => GetValue("all"); + set => SetValue("all", value); + } + + [GraphQLMember("none")] + [JsonPropertyName("none")] + public OrderLineFilterInput None + { + get => GetValue("none"); + set => SetValue("none", value); + } + + [GraphQLMember("some")] + [JsonPropertyName("some")] + public OrderLineFilterInput Some + { + get => GetValue("some"); + set => SetValue("some", value); + } + + [GraphQLMember("any")] + [JsonPropertyName("any")] + public bool? Any + { + get => GetValue("any"); + set => SetValue("any", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/ListUnsignedByteOperationFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/ListUnsignedByteOperationFilterInput.cs new file mode 100644 index 00000000..ecae3b8b --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/ListUnsignedByteOperationFilterInput.cs @@ -0,0 +1,50 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class ListUnsignedByteOperationFilterInput : GraphInputBase +{ + [GraphQLMember("all")] + [JsonPropertyName("all")] + public UnsignedByteOperationFilterInput All + { + get => GetValue("all"); + set => SetValue("all", value); + } + + [GraphQLMember("none")] + [JsonPropertyName("none")] + public UnsignedByteOperationFilterInput None + { + get => GetValue("none"); + set => SetValue("none", value); + } + + [GraphQLMember("some")] + [JsonPropertyName("some")] + public UnsignedByteOperationFilterInput Some + { + get => GetValue("some"); + set => SetValue("some", value); + } + + [GraphQLMember("any")] + [JsonPropertyName("any")] + public bool? Any + { + get => GetValue("any"); + set => SetValue("any", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/LocalDateOperationFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/LocalDateOperationFilterInput.cs new file mode 100644 index 00000000..037699f8 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/LocalDateOperationFilterInput.cs @@ -0,0 +1,114 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class LocalDateOperationFilterInput : GraphInputBase +{ + [GraphQLMember("eq")] + [JsonPropertyName("eq")] + public DateOnly? Eq + { + get => GetValue("eq"); + set => SetValue("eq", value); + } + + [GraphQLMember("neq")] + [JsonPropertyName("neq")] + public DateOnly? Neq + { + get => GetValue("neq"); + set => SetValue("neq", value); + } + + [GraphQLMember("in")] + [JsonPropertyName("in")] + public List In + { + get => GetValue>("in"); + set => SetValue("in", value); + } + + [GraphQLMember("nin")] + [JsonPropertyName("nin")] + public List Nin + { + get => GetValue>("nin"); + set => SetValue("nin", value); + } + + [GraphQLMember("gt")] + [JsonPropertyName("gt")] + public DateOnly? Gt + { + get => GetValue("gt"); + set => SetValue("gt", value); + } + + [GraphQLMember("ngt")] + [JsonPropertyName("ngt")] + public DateOnly? Ngt + { + get => GetValue("ngt"); + set => SetValue("ngt", value); + } + + [GraphQLMember("gte")] + [JsonPropertyName("gte")] + public DateOnly? Gte + { + get => GetValue("gte"); + set => SetValue("gte", value); + } + + [GraphQLMember("ngte")] + [JsonPropertyName("ngte")] + public DateOnly? Ngte + { + get => GetValue("ngte"); + set => SetValue("ngte", value); + } + + [GraphQLMember("lt")] + [JsonPropertyName("lt")] + public DateOnly? Lt + { + get => GetValue("lt"); + set => SetValue("lt", value); + } + + [GraphQLMember("nlt")] + [JsonPropertyName("nlt")] + public DateOnly? Nlt + { + get => GetValue("nlt"); + set => SetValue("nlt", value); + } + + [GraphQLMember("lte")] + [JsonPropertyName("lte")] + public DateOnly? Lte + { + get => GetValue("lte"); + set => SetValue("lte", value); + } + + [GraphQLMember("nlte")] + [JsonPropertyName("nlte")] + public DateOnly? Nlte + { + get => GetValue("nlte"); + set => SetValue("nlte", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/LocalTimeOperationFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/LocalTimeOperationFilterInput.cs new file mode 100644 index 00000000..72f47b51 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/LocalTimeOperationFilterInput.cs @@ -0,0 +1,114 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class LocalTimeOperationFilterInput : GraphInputBase +{ + [GraphQLMember("eq")] + [JsonPropertyName("eq")] + public TimeOnly? Eq + { + get => GetValue("eq"); + set => SetValue("eq", value); + } + + [GraphQLMember("neq")] + [JsonPropertyName("neq")] + public TimeOnly? Neq + { + get => GetValue("neq"); + set => SetValue("neq", value); + } + + [GraphQLMember("in")] + [JsonPropertyName("in")] + public List In + { + get => GetValue>("in"); + set => SetValue("in", value); + } + + [GraphQLMember("nin")] + [JsonPropertyName("nin")] + public List Nin + { + get => GetValue>("nin"); + set => SetValue("nin", value); + } + + [GraphQLMember("gt")] + [JsonPropertyName("gt")] + public TimeOnly? Gt + { + get => GetValue("gt"); + set => SetValue("gt", value); + } + + [GraphQLMember("ngt")] + [JsonPropertyName("ngt")] + public TimeOnly? Ngt + { + get => GetValue("ngt"); + set => SetValue("ngt", value); + } + + [GraphQLMember("gte")] + [JsonPropertyName("gte")] + public TimeOnly? Gte + { + get => GetValue("gte"); + set => SetValue("gte", value); + } + + [GraphQLMember("ngte")] + [JsonPropertyName("ngte")] + public TimeOnly? Ngte + { + get => GetValue("ngte"); + set => SetValue("ngte", value); + } + + [GraphQLMember("lt")] + [JsonPropertyName("lt")] + public TimeOnly? Lt + { + get => GetValue("lt"); + set => SetValue("lt", value); + } + + [GraphQLMember("nlt")] + [JsonPropertyName("nlt")] + public TimeOnly? Nlt + { + get => GetValue("nlt"); + set => SetValue("nlt", value); + } + + [GraphQLMember("lte")] + [JsonPropertyName("lte")] + public TimeOnly? Lte + { + get => GetValue("lte"); + set => SetValue("lte", value); + } + + [GraphQLMember("nlte")] + [JsonPropertyName("nlte")] + public TimeOnly? Nlte + { + get => GetValue("nlte"); + set => SetValue("nlte", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/OrderFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/OrderFilterInput.cs new file mode 100644 index 00000000..344fd8b3 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/OrderFilterInput.cs @@ -0,0 +1,107 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class OrderFilterInput : GraphInputBase +{ + [GraphQLMember("and")] + [JsonPropertyName("and")] + public List And + { + get => GetValue>("and"); + set => SetValue("and", value); + } + + [GraphQLMember("or")] + [JsonPropertyName("or")] + public List Or + { + get => GetValue>("or"); + set => SetValue("or", value); + } + + [GraphQLMember("orderId")] + [JsonPropertyName("orderId")] + public UuidOperationFilterInput OrderId + { + get => GetValue("orderId"); + set => SetValue("orderId", value); + } + + [GraphQLMember("customer")] + [JsonPropertyName("customer")] + public CustomerFilterInput Customer + { + get => GetValue("customer"); + set => SetValue("customer", value); + } + + [GraphQLMember("address")] + [JsonPropertyName("address")] + public AddressFilterInput Address + { + get => GetValue("address"); + set => SetValue("address", value); + } + + [GraphQLMember("orderDate")] + [JsonPropertyName("orderDate")] + public DateTimeOperationFilterInput OrderDate + { + get => GetValue("orderDate"); + set => SetValue("orderDate", value); + } + + [GraphQLMember("orderDateTime")] + [JsonPropertyName("orderDateTime")] + public DateTimeOperationFilterInput OrderDateTime + { + get => GetValue("orderDateTime"); + set => SetValue("orderDateTime", value); + } + + [GraphQLMember("orderDay")] + [JsonPropertyName("orderDay")] + public LocalDateOperationFilterInput OrderDay + { + get => GetValue("orderDay"); + set => SetValue("orderDay", value); + } + + [GraphQLMember("lines")] + [JsonPropertyName("lines")] + public ListFilterInputTypeOfOrderLineFilterInput Lines + { + get => GetValue("lines"); + set => SetValue("lines", value); + } + + [GraphQLMember("entryTime")] + [JsonPropertyName("entryTime")] + public LocalTimeOperationFilterInput EntryTime + { + get => GetValue("entryTime"); + set => SetValue("entryTime", value); + } + + [Obsolete("This propery is obsolete and should not be used!")] + [GraphQLMember("grade")] + [JsonPropertyName("grade")] + public StringOperationFilterInput Grade + { + get => GetValue("grade"); + set => SetValue("grade", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/OrderInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/OrderInput.cs new file mode 100644 index 00000000..367ca158 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/OrderInput.cs @@ -0,0 +1,91 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class OrderInput : GraphInputBase +{ + [GraphQLMember("orderId")] + [JsonPropertyName("orderId")] + public Guid OrderId + { + get => GetValue("orderId"); + set => SetValue("orderId", value); + } + + [GraphQLMember("customer")] + [JsonPropertyName("customer")] + public CustomerInput Customer + { + get => GetValue("customer"); + set => SetValue("customer", value); + } + + [GraphQLMember("address")] + [JsonPropertyName("address")] + public AddressInput Address + { + get => GetValue("address"); + set => SetValue("address", value); + } + + [GraphQLMember("orderDate")] + [JsonPropertyName("orderDate")] + public DateTimeOffset OrderDate + { + get => GetValue("orderDate"); + set => SetValue("orderDate", value); + } + + [GraphQLMember("orderDateTime")] + [JsonPropertyName("orderDateTime")] + public DateTimeOffset OrderDateTime + { + get => GetValue("orderDateTime"); + set => SetValue("orderDateTime", value); + } + + [GraphQLMember("orderDay")] + [JsonPropertyName("orderDay")] + public DateOnly OrderDay + { + get => GetValue("orderDay"); + set => SetValue("orderDay", value); + } + + [GraphQLMember("lines")] + [JsonPropertyName("lines")] + public List Lines + { + get => GetValue>("lines"); + set => SetValue("lines", value); + } + + [GraphQLMember("entryTime")] + [JsonPropertyName("entryTime")] + public TimeOnly? EntryTime + { + get => GetValue("entryTime"); + set => SetValue("entryTime", value); + } + + [Obsolete("This propery is obsolete and should not be used!")] + [GraphQLMember("grade")] + [JsonPropertyName("grade")] + public string Grade + { + get => GetValue("grade"); + set => SetValue("grade", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/OrderLineFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/OrderLineFilterInput.cs new file mode 100644 index 00000000..a4682277 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/OrderLineFilterInput.cs @@ -0,0 +1,74 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class OrderLineFilterInput : GraphInputBase +{ + [GraphQLMember("and")] + [JsonPropertyName("and")] + public List And + { + get => GetValue>("and"); + set => SetValue("and", value); + } + + [GraphQLMember("or")] + [JsonPropertyName("or")] + public List Or + { + get => GetValue>("or"); + set => SetValue("or", value); + } + + [GraphQLMember("lineNumber")] + [JsonPropertyName("lineNumber")] + public IntOperationFilterInput LineNumber + { + get => GetValue("lineNumber"); + set => SetValue("lineNumber", value); + } + + [GraphQLMember("order")] + [JsonPropertyName("order")] + public OrderFilterInput Order + { + get => GetValue("order"); + set => SetValue("order", value); + } + + [GraphQLMember("item")] + [JsonPropertyName("item")] + public ItemFilterInput Item + { + get => GetValue("item"); + set => SetValue("item", value); + } + + [GraphQLMember("price")] + [JsonPropertyName("price")] + public DecimalOperationFilterInput Price + { + get => GetValue("price"); + set => SetValue("price", value); + } + + [GraphQLMember("quantity")] + [JsonPropertyName("quantity")] + public FloatOperationFilterInput Quantity + { + get => GetValue("quantity"); + set => SetValue("quantity", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/OrderLineInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/OrderLineInput.cs new file mode 100644 index 00000000..10f60393 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/OrderLineInput.cs @@ -0,0 +1,58 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class OrderLineInput : GraphInputBase +{ + [GraphQLMember("lineNumber")] + [JsonPropertyName("lineNumber")] + public int LineNumber + { + get => GetValue("lineNumber"); + set => SetValue("lineNumber", value); + } + + [GraphQLMember("order")] + [JsonPropertyName("order")] + public OrderInput Order + { + get => GetValue("order"); + set => SetValue("order", value); + } + + [GraphQLMember("item")] + [JsonPropertyName("item")] + public ItemInput Item + { + get => GetValue("item"); + set => SetValue("item", value); + } + + [GraphQLMember("price")] + [JsonPropertyName("price")] + public decimal Price + { + get => GetValue("price"); + set => SetValue("price", value); + } + + [GraphQLMember("quantity")] + [JsonPropertyName("quantity")] + public double Quantity + { + get => GetValue("quantity"); + set => SetValue("quantity", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/OrderSortInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/OrderSortInput.cs new file mode 100644 index 00000000..16a7024c --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/OrderSortInput.cs @@ -0,0 +1,83 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class OrderSortInput : GraphInputBase +{ + [GraphQLMember("orderId")] + [JsonPropertyName("orderId")] + public SortEnumType? OrderId + { + get => GetValue("orderId"); + set => SetValue("orderId", value); + } + + [GraphQLMember("customer")] + [JsonPropertyName("customer")] + public CustomerSortInput Customer + { + get => GetValue("customer"); + set => SetValue("customer", value); + } + + [GraphQLMember("address")] + [JsonPropertyName("address")] + public AddressSortInput Address + { + get => GetValue("address"); + set => SetValue("address", value); + } + + [GraphQLMember("orderDate")] + [JsonPropertyName("orderDate")] + public SortEnumType? OrderDate + { + get => GetValue("orderDate"); + set => SetValue("orderDate", value); + } + + [GraphQLMember("orderDateTime")] + [JsonPropertyName("orderDateTime")] + public SortEnumType? OrderDateTime + { + get => GetValue("orderDateTime"); + set => SetValue("orderDateTime", value); + } + + [GraphQLMember("orderDay")] + [JsonPropertyName("orderDay")] + public SortEnumType? OrderDay + { + get => GetValue("orderDay"); + set => SetValue("orderDay", value); + } + + [GraphQLMember("entryTime")] + [JsonPropertyName("entryTime")] + public SortEnumType? EntryTime + { + get => GetValue("entryTime"); + set => SetValue("entryTime", value); + } + + [Obsolete("This propery is obsolete and should not be used!")] + [GraphQLMember("grade")] + [JsonPropertyName("grade")] + public SortEnumType? Grade + { + get => GetValue("grade"); + set => SetValue("grade", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/StringOperationFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/StringOperationFilterInput.cs new file mode 100644 index 00000000..92fc4169 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/StringOperationFilterInput.cs @@ -0,0 +1,114 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class StringOperationFilterInput : GraphInputBase +{ + [GraphQLMember("and")] + [JsonPropertyName("and")] + public List And + { + get => GetValue>("and"); + set => SetValue("and", value); + } + + [GraphQLMember("or")] + [JsonPropertyName("or")] + public List Or + { + get => GetValue>("or"); + set => SetValue("or", value); + } + + [GraphQLMember("eq")] + [JsonPropertyName("eq")] + public string Eq + { + get => GetValue("eq"); + set => SetValue("eq", value); + } + + [GraphQLMember("neq")] + [JsonPropertyName("neq")] + public string Neq + { + get => GetValue("neq"); + set => SetValue("neq", value); + } + + [GraphQLMember("contains")] + [JsonPropertyName("contains")] + public string Contains + { + get => GetValue("contains"); + set => SetValue("contains", value); + } + + [GraphQLMember("ncontains")] + [JsonPropertyName("ncontains")] + public string Ncontains + { + get => GetValue("ncontains"); + set => SetValue("ncontains", value); + } + + [GraphQLMember("in")] + [JsonPropertyName("in")] + public List In + { + get => GetValue>("in"); + set => SetValue("in", value); + } + + [GraphQLMember("nin")] + [JsonPropertyName("nin")] + public List Nin + { + get => GetValue>("nin"); + set => SetValue("nin", value); + } + + [GraphQLMember("startsWith")] + [JsonPropertyName("startsWith")] + public string StartsWith + { + get => GetValue("startsWith"); + set => SetValue("startsWith", value); + } + + [GraphQLMember("nstartsWith")] + [JsonPropertyName("nstartsWith")] + public string NstartsWith + { + get => GetValue("nstartsWith"); + set => SetValue("nstartsWith", value); + } + + [GraphQLMember("endsWith")] + [JsonPropertyName("endsWith")] + public string EndsWith + { + get => GetValue("endsWith"); + set => SetValue("endsWith", value); + } + + [GraphQLMember("nendsWith")] + [JsonPropertyName("nendsWith")] + public string NendsWith + { + get => GetValue("nendsWith"); + set => SetValue("nendsWith", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/UnsignedByteOperationFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/UnsignedByteOperationFilterInput.cs new file mode 100644 index 00000000..82782521 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/UnsignedByteOperationFilterInput.cs @@ -0,0 +1,114 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class UnsignedByteOperationFilterInput : GraphInputBase +{ + [GraphQLMember("eq")] + [JsonPropertyName("eq")] + public byte? Eq + { + get => GetValue("eq"); + set => SetValue("eq", value); + } + + [GraphQLMember("neq")] + [JsonPropertyName("neq")] + public byte? Neq + { + get => GetValue("neq"); + set => SetValue("neq", value); + } + + [GraphQLMember("in")] + [JsonPropertyName("in")] + public List In + { + get => GetValue>("in"); + set => SetValue("in", value); + } + + [GraphQLMember("nin")] + [JsonPropertyName("nin")] + public List Nin + { + get => GetValue>("nin"); + set => SetValue("nin", value); + } + + [GraphQLMember("gt")] + [JsonPropertyName("gt")] + public byte? Gt + { + get => GetValue("gt"); + set => SetValue("gt", value); + } + + [GraphQLMember("ngt")] + [JsonPropertyName("ngt")] + public byte? Ngt + { + get => GetValue("ngt"); + set => SetValue("ngt", value); + } + + [GraphQLMember("gte")] + [JsonPropertyName("gte")] + public byte? Gte + { + get => GetValue("gte"); + set => SetValue("gte", value); + } + + [GraphQLMember("ngte")] + [JsonPropertyName("ngte")] + public byte? Ngte + { + get => GetValue("ngte"); + set => SetValue("ngte", value); + } + + [GraphQLMember("lt")] + [JsonPropertyName("lt")] + public byte? Lt + { + get => GetValue("lt"); + set => SetValue("lt", value); + } + + [GraphQLMember("nlt")] + [JsonPropertyName("nlt")] + public byte? Nlt + { + get => GetValue("nlt"); + set => SetValue("nlt", value); + } + + [GraphQLMember("lte")] + [JsonPropertyName("lte")] + public byte? Lte + { + get => GetValue("lte"); + set => SetValue("lte", value); + } + + [GraphQLMember("nlte")] + [JsonPropertyName("nlte")] + public byte? Nlte + { + get => GetValue("nlte"); + set => SetValue("nlte", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/UuidOperationFilterInput.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/UuidOperationFilterInput.cs new file mode 100644 index 00000000..d3ac4551 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Inputs/UuidOperationFilterInput.cs @@ -0,0 +1,114 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +[JsonConverter(typeof(GraphInputConverter))] +public partial class UuidOperationFilterInput : GraphInputBase +{ + [GraphQLMember("eq")] + [JsonPropertyName("eq")] + public Guid? Eq + { + get => GetValue("eq"); + set => SetValue("eq", value); + } + + [GraphQLMember("neq")] + [JsonPropertyName("neq")] + public Guid? Neq + { + get => GetValue("neq"); + set => SetValue("neq", value); + } + + [GraphQLMember("in")] + [JsonPropertyName("in")] + public List In + { + get => GetValue>("in"); + set => SetValue("in", value); + } + + [GraphQLMember("nin")] + [JsonPropertyName("nin")] + public List Nin + { + get => GetValue>("nin"); + set => SetValue("nin", value); + } + + [GraphQLMember("gt")] + [JsonPropertyName("gt")] + public Guid? Gt + { + get => GetValue("gt"); + set => SetValue("gt", value); + } + + [GraphQLMember("ngt")] + [JsonPropertyName("ngt")] + public Guid? Ngt + { + get => GetValue("ngt"); + set => SetValue("ngt", value); + } + + [GraphQLMember("gte")] + [JsonPropertyName("gte")] + public Guid? Gte + { + get => GetValue("gte"); + set => SetValue("gte", value); + } + + [GraphQLMember("ngte")] + [JsonPropertyName("ngte")] + public Guid? Ngte + { + get => GetValue("ngte"); + set => SetValue("ngte", value); + } + + [GraphQLMember("lt")] + [JsonPropertyName("lt")] + public Guid? Lt + { + get => GetValue("lt"); + set => SetValue("lt", value); + } + + [GraphQLMember("nlt")] + [JsonPropertyName("nlt")] + public Guid? Nlt + { + get => GetValue("nlt"); + set => SetValue("nlt", value); + } + + [GraphQLMember("lte")] + [JsonPropertyName("lte")] + public Guid? Lte + { + get => GetValue("lte"); + set => SetValue("lte", value); + } + + [GraphQLMember("nlte")] + [JsonPropertyName("nlte")] + public Guid? Nlte + { + get => GetValue("nlte"); + set => SetValue("nlte", value); + } + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Interfaces/IAnimal.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Interfaces/IAnimal.cs new file mode 100644 index 00000000..27f7b53b --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Interfaces/IAnimal.cs @@ -0,0 +1,120 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Converters; + +namespace Linq2GraphQL.TestClientStandalone; + +/// +/// Extension methods for IAnimal interface type casting +/// +public static class IAnimalExtensions +{ + /// + /// Casts IAnimal to Pig if the runtime type matches + /// + /// The interface value to cast + /// Pig instance or null if type doesn't match + [GraphQLMember("Pig", true)] + public static Pig Pig(this IAnimal value) + { + if (value.__TypeName == "Pig") + { + return (Pig)value; + } + return null; + } + + /// + /// Casts IAnimal to Spider if the runtime type matches + /// + /// The interface value to cast + /// Spider instance or null if type doesn't match + [GraphQLMember("Spider", true)] + public static Spider Spider(this IAnimal value) + { + if (value.__TypeName == "Spider") + { + return (Spider)value; + } + return null; + } + +} + +/// +/// JSON converter for IAnimal interface deserialization +/// +internal class IAnimalConverter : InterfaceJsonConverter +{ + /// + /// Deserializes JSON to the appropriate concrete type based on __typename + /// + /// The GraphQL type name from __typename field + /// The JSON object to deserialize + /// Deserialized instance of the appropriate concrete type + public override IAnimal Deserialize(string typeName, JsonObject json) => typeName switch + { + "Pig" => json.Deserialize(), + "Spider" => json.Deserialize(), + _ => json.Deserialize() + }; +} + +/// +/// GraphQL interface IAnimal with all common fields +/// +[JsonConverter(typeof(IAnimalConverter))] +public interface IAnimal +{ + /// + /// name field from GraphQL schema + /// + [GraphQLMember("name")] + string Name { get; set; } + + /// + /// numberOfLegs field from GraphQL schema + /// + [GraphQLMember("numberOfLegs")] + int NumberOfLegs { get; set; } + + /// + /// GraphQL __typename field for runtime type resolution + /// + [GraphQLMember("__typename")] + string __TypeName { get; set; } +} + +/// +/// Concrete implementation of IAnimal interface for fallback deserialization +/// +internal class IAnimal__Concrete : IAnimal +{ + /// + /// name field from GraphQL schema + /// + [GraphQLMember("name")] + public string Name { get; set; } + + /// + /// numberOfLegs field from GraphQL schema + /// + [GraphQLMember("numberOfLegs")] + public int NumberOfLegs { get; set; } + + /// + /// GraphQL __typename field for runtime type resolution + /// + [GraphQLMember("__typename")] + public string __TypeName { get; set; } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Interfaces/IMutationMethods.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Interfaces/IMutationMethods.cs new file mode 100644 index 00000000..c632acad --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Interfaces/IMutationMethods.cs @@ -0,0 +1,33 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +/// +/// Interface for MutationMethods GraphQL operations +/// +public interface IMutationMethods +{ + /// + /// Executes setName GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphQuery + GraphQuery SetName(string name = null); + + /// + /// Executes addCustomer GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphQuery + GraphQuery AddCustomer(CustomerInput customer = null); + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Interfaces/IQueryMethods.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Interfaces/IQueryMethods.cs new file mode 100644 index 00000000..8a385f14 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Interfaces/IQueryMethods.cs @@ -0,0 +1,89 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +/// +/// Interface for QueryMethods GraphQL operations +/// +public interface IQueryMethods +{ + /// + /// Executes hello GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphQuery + GraphQuery Hello(string name = null); + + /// + /// Executes raiseError GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphQuery + GraphQuery RaiseError(string message = null); + + /// + /// Executes raiseAuthError GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphQuery + GraphQuery RaiseAuthError(); + + /// + /// Executes customerReturnNull GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphQuery + GraphQuery CustomerReturnNull(); + + /// + /// Executes customers GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphQuery> + GraphQuery> Customers(); + + /// + /// Executes ordersNoBackwardPagination GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphQuery + GraphQuery OrdersNoBackwardPagination([global::System.ComponentModel.Description("Returns the first _n_ elements from the list.")] int? first = null, [global::System.ComponentModel.Description("Returns the elements in the list that come after the specified cursor.")] string after = null, OrderFilterInput where = null, List order = null); + + /// + /// Executes ordersNoTotalCount GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphCursorQuery + GraphCursorQuery OrdersNoTotalCount([global::System.ComponentModel.Description("Returns the first _n_ elements from the list.")] int? first = null, [global::System.ComponentModel.Description("Returns the elements in the list that come after the specified cursor.")] string after = null, [global::System.ComponentModel.Description("Returns the last _n_ elements from the list.")] int? last = null, [global::System.ComponentModel.Description("Returns the elements in the list that come before the specified cursor.")] string before = null, OrderFilterInput where = null, List order = null); + + /// + /// Executes orders GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphCursorQuery + GraphCursorQuery Orders([global::System.ComponentModel.Description("Returns the first _n_ elements from the list.")] int? first = null, [global::System.ComponentModel.Description("Returns the elements in the list that come after the specified cursor.")] string after = null, [global::System.ComponentModel.Description("Returns the last _n_ elements from the list.")] int? last = null, [global::System.ComponentModel.Description("Returns the elements in the list that come before the specified cursor.")] string before = null, OrderFilterInput where = null, List order = null); + + /// + /// Executes animals GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphCursorQuery + GraphCursorQuery Animals([global::System.ComponentModel.Description("Returns the first _n_ elements from the list.")] int? first = null, [global::System.ComponentModel.Description("Returns the elements in the list that come after the specified cursor.")] string after = null, [global::System.ComponentModel.Description("Returns the last _n_ elements from the list.")] int? last = null, [global::System.ComponentModel.Description("Returns the elements in the list that come before the specified cursor.")] string before = null, IAnimalFilterInput where = null, List order = null); + + /// + /// Executes ordersOffsetPaging GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphCursorQuery + GraphCursorQuery OrdersOffsetPaging([global::System.ComponentModel.Description("Returns the first _n_ elements from the list.")] int? first = null, [global::System.ComponentModel.Description("Returns the elements in the list that come after the specified cursor.")] string after = null, [global::System.ComponentModel.Description("Returns the last _n_ elements from the list.")] int? last = null, [global::System.ComponentModel.Description("Returns the elements in the list that come before the specified cursor.")] string before = null, OrderFilterInput where = null, List order = null); + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Interfaces/ISampleClient.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Interfaces/ISampleClient.cs new file mode 100644 index 00000000..9b47543d --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Interfaces/ISampleClient.cs @@ -0,0 +1,17 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone; + +public interface ISampleClient +{ + IQueryMethods Query { get; } + IMutationMethods Mutation { get; } + ISubscriptionMethods Subscription { get; } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Interfaces/ISubscriptionMethods.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Interfaces/ISubscriptionMethods.cs new file mode 100644 index 00000000..56f3c878 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Interfaces/ISubscriptionMethods.cs @@ -0,0 +1,34 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Subscriptions; + +namespace Linq2GraphQL.TestClientStandalone; + +/// +/// Interface for SubscriptionMethods GraphQL operations +/// +public interface ISubscriptionMethods +{ + /// + /// Executes customerAdded GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphSubscription + GraphSubscription CustomerAdded(); + + /// + /// Executes customerNameAdded GraphQL operation + /// + /// The operation parameters + /// GraphQL query result of type GraphSubscription + GraphSubscription CustomerNameAdded(string name = null); + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/AgentConfigurationException.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/AgentConfigurationException.cs new file mode 100644 index 00000000..c96dc9bb --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/AgentConfigurationException.cs @@ -0,0 +1,32 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime.AI; + +/// +/// The agent configuration does not match the schema, for example an allowed operation or an omitted field +/// that does not exist. Thrown when the agent tools are set up, so mistakes surface at startup rather than +/// on the agent's first call. +/// +public class AgentConfigurationException : Exception +{ + public AgentConfigurationException(string message) : base(message) + { + } + + public AgentConfigurationException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/AgentOptions.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/AgentOptions.cs new file mode 100644 index 00000000..71e4046a --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/AgentOptions.cs @@ -0,0 +1,226 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Linq.Expressions; +using System.Reflection; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.AI; + +/// +/// What the agent may use and what it is told: allowed operations, omitted fields, descriptions and +/// instructions. Everything is referenced through the generated client, so a schema rename breaks the build, +/// and is checked against the schema when the tools are set up. +/// +public sealed class AgentOptions +{ + internal List AllowedQueries { get; } = []; + internal AllowAllQueriesOptions AllowAll { get; private set; } + internal List Omitted { get; } = []; + internal List Descriptions { get; } = []; + internal List InstructionTexts { get; } = []; + internal List> Examples { get; } = []; + internal List> QueryLoggers { get; } = []; + + /// + /// The access token for the user or conversation the agent acts for, asked for on every request, for + /// example an on-behalf-of token from your token acquisition library. Leave it unset to use the client's + /// own configuration. + /// + public Func> AccessToken { get; set; } + + /// + /// An example of a common task, written with the typed client, for example + /// o.Example("Recent orders", "A customer's most recent orders", c => c.Query.Customer(id: "4711").Select(...)). + /// It is checked by the compiler, validated against the scope when the tools are set up, and shown to the + /// agent as GraphQL. A task that always runs the same way is better as a dedicated tool. + /// + public AgentOptions Example(string name, string description, + Func> query) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentException.ThrowIfNullOrWhiteSpace(description); + ArgumentNullException.ThrowIfNull(query); + Examples.Add(new ExampleDefinition(name, description, client => query(client).GetRequest())); + return this; + } + + /// + /// Called for every query the agent runs or tries to run: the query, its variables, validation errors if it + /// was rejected, server errors and duration. Use it to see what the agent actually does; repeated queries + /// are candidates for dedicated tools. + /// + public AgentOptions OnQuery(Action log) + { + ArgumentNullException.ThrowIfNull(log); + QueryLoggers.Add(log); + return this; + } + + /// + /// Allows the agent to call a query, for example o.AllowQuery(c => c.Query.Customers). + /// + public OperationOptions AllowQuery(Expression> operation) + { + var options = new OperationOptions(ExpressionReferences.Method(operation)); + AllowedQueries.Add(options); + return options; + } + + /// + /// Allows every query in the schema, for the fastest start. Never the default; narrow it with + /// . Rendering, validation, omitted fields and + /// descriptions still apply. + /// + public AllowAllQueriesOptions AllowAllQueries() + { + AllowAll ??= new AllowAllQueriesOptions(); + return AllowAll; + } + + /// + /// Describes a type to the agent, replacing the schema's description unless is set. + /// + public AgentOptions Describe(string description, bool append = false) + { + Descriptions.Add(new DescriptionOverride(new MemberReference(typeof(T), null, null), description, append)); + return this; + } + + /// + /// Describes a field or input field to the agent, for example o.Describe<Customer>(c => c.Status, "..."). + /// + public AgentOptions Describe(Expression> field, string description, bool append = false) + { + Descriptions.Add(new DescriptionOverride(ExpressionReferences.Member(field), description, append)); + return this; + } + + /// + /// Describes an enum value to the agent, for example o.Describe(CustomerStatus.Active, "..."). + /// + public AgentOptions Describe(TEnum value, string description, bool append = false) + where TEnum : struct, Enum + { + Descriptions.Add(new DescriptionOverride(ExpressionReferences.EnumValue(value), description, append)); + return this; + } + + /// + /// Removes a field or input field from the agent's scope: it is neither described nor accepted in queries. + /// Use it for noise and for data the agent shouldn't touch or its token can't read. The server and the + /// token remain the security boundary for everything else. + /// + public AgentOptions Omit(Expression> field) + { + Omitted.Add(ExpressionReferences.Member(field)); + return this; + } + + /// + /// General guidance for the agent, for example "All dates are UTC." + /// + public AgentOptions Instructions(string instructions) + { + ArgumentException.ThrowIfNullOrWhiteSpace(instructions); + InstructionTexts.Add(instructions); + return this; + } +} + +/// +/// Settings for one allowed operation. +/// +public sealed class OperationOptions +{ + internal OperationOptions(MethodInfo method) + { + Method = method; + } + + internal MethodInfo Method { get; } + internal (string Text, bool Append)? Description { get; private set; } + internal List<(string Argument, string Text, bool Append)> ArgumentDescriptions { get; } = []; + + /// + /// Describes the operation to the agent, replacing the schema's description unless is set. + /// + public OperationOptions Describe(string description, bool append = false) + { + ArgumentException.ThrowIfNullOrWhiteSpace(description); + Description = (description, append); + return this; + } + + /// + /// Describes one of the operation's arguments by its GraphQL name, for example .Argument("first", "Max 50."). + /// + public OperationOptions Argument(string name, string description, bool append = false) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentException.ThrowIfNullOrWhiteSpace(description); + ArgumentDescriptions.Add((name, description, append)); + return this; + } +} + +/// +/// Settings for . +/// +public sealed class AllowAllQueriesOptions +{ + internal List Excluded { get; } = []; + + /// + /// Keeps these queries out of the allow-list, for example .Except(c => c.Query.AuditLog). + /// + public AllowAllQueriesOptions Except(params Expression>[] operations) + { + Excluded.AddRange(operations.Select(ExpressionReferences.Method)); + return this; + } +} + +/// +/// A reference to a generated type, or one of its members or enum values, resolved against the schema later. +/// +internal sealed record MemberReference(Type ClrType, MemberInfo Member, string EnumValue) +{ + public override string ToString() => Member != null ? $"{ClrType.Name}.{Member.Name}" + : EnumValue != null ? $"{ClrType.Name}.{EnumValue}" + : ClrType.Name; +} + +internal sealed record DescriptionOverride(MemberReference Target, string Text, bool Append); + +internal sealed record ExampleDefinition(string Name, string Description, Func Request); + +/// +/// One query the agent ran or tried to run, for . +/// +/// The GraphQL text the agent wrote. +/// The variables as JSON, or null. +/// Why the query was rejected before sending; empty when it was sent. +/// GraphQL or request errors from the server; empty when there were none. +/// Time spent validating and, if sent, executing. +public sealed record AgentQueryLog( + string Query, + string Variables, + IReadOnlyList ValidationErrors, + IReadOnlyList Errors, + TimeSpan Duration) +{ + /// Whether the query passed validation and was sent. + public bool WasSent => ValidationErrors.Count == 0; +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/AgentToolsExtensions.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/AgentToolsExtensions.cs new file mode 100644 index 00000000..92a69354 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/AgentToolsExtensions.cs @@ -0,0 +1,113 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.ComponentModel; +using System.Text.Json; +using Linq2GraphQL.TestClientStandalone.Runtime.AI.Schema; +using Linq2GraphQL.TestClientStandalone.Runtime.AI.Tools; +using Microsoft.Extensions.AI; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.AI; + +/// +/// Turns a generated client into agent tools: graphql_query, describe_operation, +/// describe_type and search_schema, as Microsoft.Extensions.AI s that +/// plug into any agent built on it. The agent writes GraphQL; the tools keep it inside the configured scope. +/// +public static class AgentToolsExtensions +{ + /// + /// The agent tools for a client generated with --agent, using the schema it was generated from. + /// Create them per user or conversation when is set. + /// + /// The configuration does not match the schema. + public static IList AsAgentTools(this TClient client, Action> configure) + where TClient : IGraphClientSource, IGraphSchemaSource + { + return client.AsAgentTools(TClient.Sdl, configure); + } + + /// + /// The agent tools for any generated client, with the schema given as SDL, for example a schema file + /// exported from the server. + /// + /// The configuration does not match the schema. + public static IList AsAgentTools(this TClient client, string sdl, + Action> configure) + where TClient : IGraphClientSource + { + ArgumentNullException.ThrowIfNull(client); + ArgumentException.ThrowIfNullOrWhiteSpace(sdl); + ArgumentNullException.ThrowIfNull(configure); + + var options = new AgentOptions(); + configure(options); + + var schema = ScopedSchema.Create(SdlReader.Read(sdl), options); + + // Everything the agent causes goes through this client, so the user's token applies to raw and + // typed queries alike. + var agentClient = options.AccessToken == null + ? client + : client.WithRequestConfiguration(RequestConfigurations.BearerToken(options.AccessToken)); + + schema.SetExamples(options.Examples.Select(example => + { + var request = example.Request(agentClient); + var (query, tidyVariables) = ExampleFormatter.Tidy(request.Query, request.Variables); + var variables = tidyVariables is { Count: > 0 } + ? JsonSerializer.Serialize(tidyVariables, agentClient.GraphClient.SerializerOptions) + : null; + return (example.Name, example.Description, query, variables); + })); + + var tools = new AgentToolset(schema, agentClient.GraphClient, options.QueryLoggers.ToList()); + return Create(tools); + } + + private static List Create(AgentToolset tools) + { + return + [ + AIFunctionFactory.Create( + ([Description("A GraphQL query document, for example `query { customers { name } }`. Only queries are allowed.")] + string query, + [Description("Values for the variables the query declares, as a JSON object. Omit when there are none.")] + JsonElement? variables = null, + CancellationToken cancellationToken = default) => + tools.QueryAsync(query, variables, cancellationToken), + "graphql_query", + "Runs a read-only GraphQL query and returns the data as JSON. The query is checked first; if it is " + + "invalid nothing is sent and the errors say how to fix it. Use describe_operation for an operation's " + + "arguments and a starter query, describe_type for a type's fields, and search_schema to find a name.\n\n" + + tools.Index), + AIFunctionFactory.Create( + ([Description("Names of the operations to describe, as listed under Queries.")] string[] names) => + tools.DescribeOperations(names), + "describe_operation", + "Describes operations: arguments with filters and sorting, the return type, a starter query to adapt, " + + "and examples."), + AIFunctionFactory.Create( + ([Description("Names of the types to describe.")] string[] names) => tools.DescribeTypes(names), + "describe_type", + "Describes types in full: fields, arguments and descriptions, plus the types they use."), + AIFunctionFactory.Create( + ([Description("A word to search for in names and descriptions.")] string text) => tools.Search(text), + "search_schema", + "Finds operations, examples, types, fields and enum values by name or description, when you don't " + + "know what something is called.") + ]; + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/ExpressionReferences.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/ExpressionReferences.cs new file mode 100644 index 00000000..89915576 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/ExpressionReferences.cs @@ -0,0 +1,83 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.Serialization; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.AI; + +/// +/// Turns the typed expressions of the agent configuration into references to generated members. +/// +internal static class ExpressionReferences +{ + /// + /// The operation method behind c => c.Query.Customers. A method group in an expression tree + /// becomes a call to on a constant MethodInfo. + /// + public static MethodInfo Method(Expression> operation) + { + ArgumentNullException.ThrowIfNull(operation); + + var body = operation.Body is UnaryExpression { NodeType: ExpressionType.Convert } convert + ? convert.Operand + : operation.Body; + + if (body is MethodCallExpression { Method.Name: nameof(MethodInfo.CreateDelegate) } call) + { + var method = (call.Object as ConstantExpression ?? call.Arguments.OfType().LastOrDefault()) + ?.Value as MethodInfo; + if (method != null) + { + return method; + } + } + + throw new ArgumentException( + $"Expected an operation such as c => c.Query.Customers, but got {operation}.", nameof(operation)); + } + + /// + /// The property behind c => c.Status. + /// + public static MemberReference Member(Expression> field) + { + ArgumentNullException.ThrowIfNull(field); + + var body = field.Body is UnaryExpression { NodeType: ExpressionType.Convert } convert + ? convert.Operand + : field.Body; + + if (body is MemberExpression { Member: PropertyInfo property } member && member.Expression is ParameterExpression) + { + return new MemberReference(typeof(T), property, null); + } + + throw new ArgumentException( + $"Expected a field of {typeof(T).Name} such as c => c.Name, but got {field}.", nameof(field)); + } + + /// + /// The GraphQL name of a generated enum value, taken from its . + /// + public static MemberReference EnumValue(TEnum value) where TEnum : struct, Enum + { + var name = value.ToString(); + var member = typeof(TEnum).GetField(name); + var graphQLName = member?.GetCustomAttribute()?.Value ?? name; + return new MemberReference(typeof(TEnum), null, graphQLName); + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/ExampleFormatter.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/ExampleFormatter.cs new file mode 100644 index 00000000..57ee7fb9 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/ExampleFormatter.cs @@ -0,0 +1,111 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Text; +using System.Text.RegularExpressions; +using GraphQLParser; +using GraphQLParser.AST; +using GraphQLParser.Visitors; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.AI.Schema; + +/// +/// Makes a query built by the typed client read like one a person would write, so the agent can copy and adapt +/// it: variables lose the client's uniqueness suffixes ($name_1_1 becomes $name) where that stays +/// unambiguous, and the document is re-printed on one line with regular spacing. +/// +internal static partial class ExampleFormatter +{ + [GeneratedRegex(@"(_\d+)+$")] + private static partial Regex UniquenessSuffix(); + + public static (string Query, Dictionary Variables) Tidy(string query, + IReadOnlyDictionary variables) + { + var document = Parser.Parse(query); + var declared = document.Definitions.OfType() + .SelectMany(e => e.Variables?.Items ?? []) + .Select(e => e.Variable.Name.StringValue) + .ToList(); + + var renames = new Dictionary(); + foreach (var name in declared) + { + var plain = UniquenessSuffix().Replace(name, ""); + var clashes = declared.Count(e => UniquenessSuffix().Replace(e, "") == plain) > 1 || + (plain != name && declared.Contains(plain)); + if (plain != name && plain.Length > 0 && !clashes) + { + renames[name] = plain; + } + } + + foreach (var (from, to) in renames) + { + query = Regex.Replace(query, $@"\${Regex.Escape(from)}\b", "$" + to); + } + + var tidyVariables = variables?.ToDictionary(e => renames.GetValueOrDefault(e.Key, e.Key), e => e.Value); + var printed = new SDLPrinter().Print(Parser.Parse(query)); + return (OneLine(printed), tidyVariables); + } + + /// + /// Collapses whitespace to single spaces outside string literals. + /// + private static string OneLine(string text) + { + var result = new StringBuilder(); + var inString = false; + var pendingSpace = false; + + for (var i = 0; i < text.Length; i++) + { + var c = text[i]; + if (inString) + { + result.Append(c); + if (c == '\\' && i + 1 < text.Length) + { + result.Append(text[++i]); + } + else if (c == '"') + { + inString = false; + } + + continue; + } + + if (char.IsWhiteSpace(c)) + { + pendingSpace = true; + continue; + } + + var spaceBefore = c == '{' || pendingSpace && c is not ('(' or ')' or ']' or ':' or ','); + if (spaceBefore && result.Length > 0 && result[^1] is not ('(' or '[' or '$' or ' ')) + { + result.Append(' '); + } + + result.Append(c); + inString = c == '"'; + pendingSpace = c is ':' or ','; + } + + return result.ToString(); + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/NameSuggestions.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/NameSuggestions.cs new file mode 100644 index 00000000..7936d0e6 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/NameSuggestions.cs @@ -0,0 +1,87 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime.AI.Schema; + +/// +/// "Did you mean" suggestions and capped name lists, so every message to the agent says what it can use instead. +/// +internal static class NameSuggestions +{ + private const int MaxListedNames = 15; + + /// + /// " Did you mean `x`?" for a near miss, followed by the available names. + /// + public static string Suggest(string name, IEnumerable candidates) + { + var list = candidates.ToList(); + var closest = Closest(name, list); + var suggestion = closest == null ? "" : $" Did you mean `{closest}`?"; + return list.Count == 0 ? suggestion : $"{suggestion} Available: {List(list)}."; + } + + /// + /// A near miss by spelling, or else a name that ends with the guessed one as a camelCase word: agents + /// often ask for name or id where the schema has customerName or customerId. + /// + public static string Closest(string name, IEnumerable candidates) + { + var list = candidates as IList ?? candidates.ToList(); + return list + .Select(e => (Name: e, Distance: Distance(name.ToLowerInvariant(), e.ToLowerInvariant()))) + .Where(e => e.Distance <= Math.Max(2, name.Length / 3)) + .OrderBy(e => e.Distance) + .Select(e => e.Name) + .FirstOrDefault() + ?? list + .Where(e => EndsWithWord(e, name)) + .OrderBy(e => e.Length) + .FirstOrDefault(); + } + + private static bool EndsWithWord(string candidate, string word) => + word.Length > 0 + && candidate.Length > word.Length + && candidate.EndsWith(word, StringComparison.OrdinalIgnoreCase) + && (char.IsUpper(candidate[^word.Length]) || candidate[^(word.Length + 1)] == '_'); + + public static string List(IEnumerable names) + { + var list = names.ToList(); + return list.Count <= MaxListedNames + ? string.Join(", ", list) + : string.Join(", ", list.Take(MaxListedNames)) + $" and {list.Count - MaxListedNames} more"; + } + + private static int Distance(string a, string b) + { + var previous = Enumerable.Range(0, b.Length + 1).ToArray(); + for (var i = 1; i <= a.Length; i++) + { + var current = new int[b.Length + 1]; + current[0] = i; + for (var j = 1; j <= b.Length; j++) + { + var cost = a[i - 1] == b[j - 1] ? 0 : 1; + current[j] = Math.Min(Math.Min(current[j - 1] + 1, previous[j] + 1), previous[j - 1] + cost); + } + + previous = current; + } + + return previous[b.Length]; + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/QueryValidator.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/QueryValidator.cs new file mode 100644 index 00000000..1c22b94e --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/QueryValidator.cs @@ -0,0 +1,629 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; +using GraphQLParser; +using GraphQLParser.AST; +using GraphQLParser.Exceptions; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.AI.Schema; + +/// +/// One problem with a query, at a path such as orders(where).customer.status. +/// +internal sealed record ValidationError(string Path, string Message) +{ + public override string ToString() => string.IsNullOrEmpty(Path) ? Message : $"{Path}: {Message}"; +} + +internal sealed class ValidationResult(IReadOnlyList errors) +{ + public IReadOnlyList Errors { get; } = errors; + + public bool IsValid => Errors.Count == 0; + + public override string ToString() => string.Join("\n", Errors); +} + +/// +/// Checks a query the agent wrote against the scoped schema before it is sent: one query operation, allowed +/// roots, fields that exist and aren't omitted, arguments, input objects (filters included), enum values and +/// variables, with fragments followed through their type conditions. Aliases don't matter because the check +/// uses real field names. Variable values are checked too, so an omitted field cannot be passed in through +/// variables. All errors are returned at once, each saying where and what to do instead. +/// +internal sealed class QueryValidator(ScopedSchema schema) +{ + + + private readonly SchemaModel model = schema.Schema; + private readonly SchemaShape shape = new(schema.Schema); + + /// + /// The root fields a valid query selects, following fragments at the root. + /// + public static IReadOnlyList RootFields(string query) + { + var document = Parser.Parse(query); + var fragments = document.Definitions.OfType() + .ToDictionary(e => Name(e.FragmentName.Name)); + var names = new List(); + + void Collect(GraphQLSelectionSet selectionSet, HashSet seen) + { + foreach (var selection in selectionSet.Selections) + { + switch (selection) + { + case GraphQLField field when Name(field.Name) != "__typename": + names.Add(Name(field.Name)); + break; + case GraphQLInlineFragment inline: + Collect(inline.SelectionSet, seen); + break; + case GraphQLFragmentSpread spread when fragments.TryGetValue(Name(spread.FragmentName.Name), out var fragment) && + seen.Add(Name(spread.FragmentName.Name)): + Collect(fragment.SelectionSet, seen); + break; + } + } + } + + Collect(document.Definitions.OfType().Single().SelectionSet, []); + return names.Distinct().ToList(); + } + + public ValidationResult Validate(string query, JsonElement? variables = null) + { + var run = new Run(this); + run.Validate(query, variables); + return new ValidationResult(run.Errors); + } + + private sealed class Run(QueryValidator validator) + { + private readonly SchemaModel model = validator.model; + private readonly SchemaShape shape = validator.shape; + private readonly Dictionary fragments = new(); + private readonly Dictionary declaredVariables = new(); + + public List Errors { get; } = []; + + public void Validate(string query, JsonElement? variables) + { + if (string.IsNullOrWhiteSpace(query)) + { + Error("", "The query is empty."); + return; + } + + GraphQLDocument document; + try + { + document = Parser.Parse(query); + } + catch (GraphQLParserException ex) + { + Error("", $"Syntax error: {ex.Message}"); + return; + } + + foreach (var fragment in document.Definitions.OfType()) + { + fragments[Name(fragment.FragmentName.Name)] = fragment; + } + + var operations = document.Definitions.OfType().ToList(); + if (document.Definitions.Any(e => e is not (GraphQLOperationDefinition or GraphQLFragmentDefinition))) + { + Error("", "Only a query and its fragments can be sent; schema definitions are not allowed."); + } + + if (operations.Count != 1) + { + Error("", operations.Count == 0 + ? "The document has no operation. Send one query, for example `query { ... }`." + : "Send one operation per call."); + return; + } + + var operation = operations[0]; + if (operation.Operation != GraphQLParser.AST.OperationType.Query) + { + Error("", $"Only queries are allowed; this is a {operation.Operation.ToString().ToLowerInvariant()}."); + return; + } + + ValidateVariableDefinitions(operation, variables); + ValidateSelections(model.Type(model.QueryType), operation.SelectionSet, "", []); + } + + private void ValidateVariableDefinitions(GraphQLOperationDefinition operation, JsonElement? variables) + { + foreach (var definition in operation.Variables?.Items ?? []) + { + var name = Name(definition.Variable.Name); + var type = TypeRefOf(definition.Type); + var path = "$" + name; + + if (model.Type(type.NamedType) is not { } namedType) + { + Error(path, $"Type {type.NamedType} is not available.{NameSuggestions.Suggest(type.NamedType, InputTypeNames())}"); + continue; + } + + if (namedType.Kind is not (SchemaTypeKind.Scalar or SchemaTypeKind.Enum or SchemaTypeKind.InputObject)) + { + Error(path, $"{namedType.Name} is an output type and cannot be a variable."); + continue; + } + + declaredVariables[name] = type; + + if (definition.DefaultValue != null) + { + ValidateValue(definition.DefaultValue, type, path); + } + + if (variables is { ValueKind: JsonValueKind.Object } values && values.TryGetProperty(name, out var value)) + { + ValidateJson(value, type, path); + } + else if (type.IsNonNull && definition.DefaultValue == null) + { + Error(path, $"Variable ${name} is required ({type}) but no value was given."); + } + } + + if (variables is { ValueKind: JsonValueKind.Object } given) + { + foreach (var property in given.EnumerateObject().Where(e => !declaredVariables.ContainsKey(e.Name))) + { + Error("$" + property.Name, $"Variable ${property.Name} is not declared in the query."); + } + } + } + + private void ValidateSelections(SchemaType parent, GraphQLSelectionSet selectionSet, string path, + HashSet fragmentsInUse) + { + foreach (var selection in selectionSet.Selections) + { + switch (selection) + { + case GraphQLField field: + ValidateField(parent, field, path, fragmentsInUse); + break; + case GraphQLFragmentSpread spread: + ValidateFragmentSpread(parent, spread, path, fragmentsInUse); + break; + case GraphQLInlineFragment inline: + ValidateDirectives(inline.Directives, path); + var type = inline.TypeCondition == null + ? parent + : FragmentType(parent, Name(inline.TypeCondition.Type.Name), path); + if (type != null) + { + ValidateSelections(type, inline.SelectionSet, path, fragmentsInUse); + } + + break; + } + } + } + + private void ValidateFragmentSpread(SchemaType parent, GraphQLFragmentSpread spread, string path, + HashSet fragmentsInUse) + { + ValidateDirectives(spread.Directives, path); + + var name = Name(spread.FragmentName.Name); + if (!fragments.TryGetValue(name, out var fragment)) + { + Error(path, $"Fragment {name} is not defined."); + return; + } + + if (!fragmentsInUse.Add(name)) + { + Error(path, $"Fragment {name} spreads itself."); + return; + } + + if (FragmentType(parent, Name(fragment.TypeCondition.Type.Name), path) is { } type) + { + ValidateSelections(type, fragment.SelectionSet, path, fragmentsInUse); + } + + fragmentsInUse.Remove(name); + } + + /// + /// The type a fragment selects on, if it exists in scope and can apply where it is spread. + /// + private SchemaType FragmentType(SchemaType parent, string typeName, string path) + { + if (model.Type(typeName) is not { } type) + { + Error(path, $"Type {typeName} is not available."); + return null; + } + + var applies = type.Name == parent.Name || + parent.PossibleTypes.Contains(type.Name) || + type.PossibleTypes.Contains(parent.Name) || + type.PossibleTypes.Intersect(parent.PossibleTypes).Any(); + if (!applies && shape.ConnectionOf(parent.Name) is { } connection && + (connection.NodeType == type.Name || model.Type(connection.NodeType)?.PossibleTypes.Contains(type.Name) == true)) + { + var pageField = connection.HasNodes ? "nodes" : "edges { node"; + var close = connection.HasNodes ? "}" : "} }"; + Error(path, $"This field is paged; put `... on {type.Name}` inside `{pageField} {{ ... on {type.Name} {{ ... }} {close}`, " + + "for example `{ nodes { __typename ... on " + type.Name + " { ... } } pageInfo { hasNextPage endCursor } }`."); + return null; + } + + if (!applies) + { + Error(path, $"A fragment on {type.Name} cannot apply to {parent.Name}."); + return null; + } + + return type; + } + + private void ValidateField(SchemaType parent, GraphQLField field, string parentPath, + HashSet fragmentsInUse) + { + var name = Name(field.Name); + var path = Join(parentPath, name); + ValidateDirectives(field.Directives, path); + + if (name == "__typename") + { + return; + } + + if (name is "__schema" or "__type") + { + Error(path, "Introspection is not available. Use describe_operation, describe_type and search_schema to explore the schema."); + return; + } + + var definition = parent.Field(name); + if (definition == null) + { + Error(path, MissingField(parent, name)); + return; + } + + ValidateArguments(definition, field, path); + + var type = model.Type(definition.Type.NamedType); + var isLeaf = type.Kind is SchemaTypeKind.Scalar or SchemaTypeKind.Enum; + + if (isLeaf && field.SelectionSet != null) + { + Error(path, $"{name} is a {type.Name} and has no fields to select. Remove `{{ ... }}`."); + } + else if (!isLeaf && field.SelectionSet == null) + { + Error(path, $"{name} returns {SelectionHint(type)}; select fields, for example `{name} {{ {ExampleFields(type)} }}`."); + } + else if (!isLeaf) + { + ValidateSelections(type, field.SelectionSet, path, fragmentsInUse); + } + } + + private string MissingField(SchemaType parent, string name) + { + if (parent.Name == model.QueryType) + { + return $"Query {name} is not available.{NameSuggestions.Suggest(name, parent.Fields.Select(e => e.Name))}"; + } + + if (shape.ConnectionOf(parent.Name) is { } connection) + { + var pageField = connection.HasNodes ? "nodes" : "edges { node"; + var close = connection.HasNodes ? "}" : "} }"; + return $"This field is paged; select {name} inside `{pageField} {{ {name} {close}`, " + + "for example `{ nodes { ... } pageInfo { hasNextPage endCursor } }`."; + } + + return parent.Kind == SchemaTypeKind.Union + ? $"{parent.Name} is a union; select fields inside `... on {string.Join(" / ", parent.PossibleTypes)} {{ ... }}`." + : $"{parent.Name} has no field {name}.{NameSuggestions.Suggest(name, parent.Fields.Select(e => e.Name))}"; + } + + private void ValidateArguments(SchemaField definition, GraphQLField field, string path) + { + var given = field.Arguments?.Items ?? []; + foreach (var argument in given) + { + var name = Name(argument.Name); + var argumentPath = $"{path}({name})"; + var argumentDefinition = definition.Arguments.FirstOrDefault(e => e.Name == name); + if (argumentDefinition == null) + { + Error(argumentPath, $"{definition.Name} has no argument {name}." + + NameSuggestions.Suggest(name, definition.Arguments.Select(e => e.Name))); + continue; + } + + ValidateValue(argument.Value, argumentDefinition.Type, argumentPath); + } + + foreach (var required in definition.Arguments.Where(e => + e.Type.IsNonNull && e.DefaultValue == null && given.All(g => Name(g.Name) != e.Name))) + { + Error(path, $"Argument {required.Name} ({required.Type}) is required."); + } + } + + private void ValidateDirectives(GraphQLDirectives directives, string path) + { + foreach (var directive in directives?.Items ?? []) + { + var name = Name(directive.Name); + if (name is not ("include" or "skip")) + { + Error(path, $"Directive @{name} is not supported; only @include and @skip are."); + } + } + } + + private void ValidateValue(GraphQLValue value, TypeRef expected, string path) + { + if (value is GraphQLVariable variable) + { + var name = Name(variable.Name); + if (!declaredVariables.TryGetValue(name, out var declared)) + { + Error(path, $"Variable ${name} is not declared. Declare it, for example `query(${name}: {expected})`."); + } + else if (declared.NamedType != expected.NamedType) + { + Error(path, $"Variable ${name} is declared as {declared} but {expected} is expected here."); + } + + return; + } + + if (value is GraphQLNullValue) + { + if (expected.IsNonNull) + { + Error(path, $"A value is required ({expected})."); + } + + return; + } + + if (expected.IsNonNull) + { + ValidateValue(value, expected.OfType, path); + return; + } + + if (expected.IsList) + { + if (value is GraphQLListValue list) + { + var index = 0; + foreach (var item in list.Values ?? []) + { + ValidateValue(item, expected.OfType, $"{path}[{index++}]"); + } + } + else + { + ValidateValue(value, expected.OfType, path); + } + + return; + } + + var type = model.Type(expected.Name); + switch (type.Kind) + { + case SchemaTypeKind.InputObject when value is GraphQLObjectValue objectValue: + var fields = objectValue.Fields ?? []; + foreach (var field in fields) + { + ValidateInputField(type, Name(field.Name), path, fieldPath => ValidateValue(field.Value, type.Field(Name(field.Name)).Type, fieldPath)); + } + + RequireInputFields(type, fields.Select(e => Name(e.Name)), path); + break; + case SchemaTypeKind.InputObject: + Error(path, $"Expected an object for {type.Name}, for example `{{ {ExampleFields(type)} }}`."); + break; + case SchemaTypeKind.Enum when value is GraphQLEnumValue enumValue: + ValidateEnumValue(type, Name(enumValue.Name), path); + break; + case SchemaTypeKind.Enum when value is GraphQLStringValue text: + Error(path, $"{type.Name} values are written without quotes, for example `{text.Value.ToString()}`." + + NameSuggestions.Suggest(text.Value.ToString(), type.EnumValues.Select(e => e.Name))); + break; + case SchemaTypeKind.Enum: + Error(path, $"Expected a {type.Name} value: {NameSuggestions.List(type.EnumValues.Select(e => e.Name))}."); + break; + case SchemaTypeKind.Scalar when !ScalarAccepts(type.Name, value): + Error(path, $"Expected a {type.Name} value."); + break; + } + } + + private void ValidateJson(JsonElement value, TypeRef expected, string path) + { + if (value.ValueKind == JsonValueKind.Null) + { + if (expected.IsNonNull) + { + Error(path, $"A value is required ({expected})."); + } + + return; + } + + if (expected.IsNonNull) + { + ValidateJson(value, expected.OfType, path); + return; + } + + if (expected.IsList) + { + if (value.ValueKind == JsonValueKind.Array) + { + var index = 0; + foreach (var item in value.EnumerateArray()) + { + ValidateJson(item, expected.OfType, $"{path}[{index++}]"); + } + } + else + { + ValidateJson(value, expected.OfType, path); + } + + return; + } + + var type = model.Type(expected.Name); + switch (type.Kind) + { + case SchemaTypeKind.InputObject when value.ValueKind == JsonValueKind.Object: + var names = new List(); + foreach (var property in value.EnumerateObject()) + { + names.Add(property.Name); + ValidateInputField(type, property.Name, path, fieldPath => ValidateJson(property.Value, type.Field(property.Name).Type, fieldPath)); + } + + RequireInputFields(type, names, path); + break; + case SchemaTypeKind.InputObject: + Error(path, $"Expected an object for {type.Name}."); + break; + case SchemaTypeKind.Enum when value.ValueKind == JsonValueKind.String: + ValidateEnumValue(type, value.GetString(), path); + break; + case SchemaTypeKind.Enum: + Error(path, $"Expected a {type.Name} value as a string: {NameSuggestions.List(type.EnumValues.Select(e => e.Name))}."); + break; + case SchemaTypeKind.Scalar when !ScalarAccepts(type.Name, value.ValueKind): + Error(path, $"Expected a {type.Name} value."); + break; + } + } + + private void ValidateInputField(SchemaType type, string name, string path, Action validateValue) + { + var fieldPath = Join(path, name); + if (type.Field(name) == null) + { + Error(fieldPath, $"{type.Name} has no field {name}.{NameSuggestions.Suggest(name, type.Fields.Select(e => e.Name))}"); + return; + } + + validateValue(fieldPath); + } + + private void RequireInputFields(SchemaType type, IEnumerable given, string path) + { + var givenNames = given.ToHashSet(); + foreach (var required in type.Fields.Where(e => + e.Type.IsNonNull && e.DefaultValue == null && !givenNames.Contains(e.Name))) + { + Error(path, $"{type.Name}.{required.Name} ({required.Type}) is required."); + } + } + + private void ValidateEnumValue(SchemaType type, string value, string path) + { + if (type.EnumValues.All(e => e.Name != value)) + { + Error(path, $"{type.Name} has no value {value}.{NameSuggestions.Suggest(value, type.EnumValues.Select(e => e.Name))}"); + } + } + + private static bool ScalarAccepts(string scalar, GraphQLValue value) => scalar switch + { + "Int" => value is GraphQLIntValue, + "Float" => value is GraphQLIntValue or GraphQLFloatValue, + "Boolean" => value is GraphQLBooleanValue, + "String" => value is GraphQLStringValue, + "ID" => value is GraphQLStringValue or GraphQLIntValue, + _ => value is not (GraphQLObjectValue or GraphQLListValue) + }; + + private static bool ScalarAccepts(string scalar, JsonValueKind kind) => scalar switch + { + "Int" or "Float" => kind == JsonValueKind.Number, + "Boolean" => kind is JsonValueKind.True or JsonValueKind.False, + "String" => kind == JsonValueKind.String, + "ID" => kind is JsonValueKind.String or JsonValueKind.Number, + _ => kind is not (JsonValueKind.Object or JsonValueKind.Array) + }; + + private string SelectionHint(SchemaType type) => + shape.ConnectionOf(type.Name) is { } connection ? $"a page of {connection.NodeType}" : type.Name; + + private string ExampleFields(SchemaType type) + { + if (shape.ConnectionOf(type.Name) is { } connection) + { + var node = model.Type(connection.NodeType); + return connection.HasNodes + ? $"nodes {{ {ExampleFields(node)} }} pageInfo {{ hasNextPage endCursor }}" + : $"edges {{ node {{ {ExampleFields(node)} }} }} pageInfo {{ hasNextPage endCursor }}"; + } + + if (type.Kind == SchemaTypeKind.Union) + { + return $"... on {type.PossibleTypes.FirstOrDefault()} {{ ... }}"; + } + + var leaves = type.Fields + .Where(e => e.Arguments.Count == 0 && + model.Type(e.Type.NamedType).Kind is SchemaTypeKind.Scalar or SchemaTypeKind.Enum) + .Take(3) + .Select(e => e.Name) + .ToList(); + return leaves.Count > 0 ? string.Join(" ", leaves) : "..."; + } + + private IEnumerable InputTypeNames() => model.Types.Values + .Where(e => e.Kind is SchemaTypeKind.Scalar or SchemaTypeKind.Enum or SchemaTypeKind.InputObject) + .Select(e => e.Name); + + private void Error(string path, string message) => Errors.Add(new ValidationError(path, message)); + } + + + private static string Join(string path, string name) => string.IsNullOrEmpty(path) ? name : $"{path}.{name}"; + + private static string Name(GraphQLName name) => name.StringValue; + + private static TypeRef TypeRefOf(GraphQLType type) => type switch + { + GraphQLNonNullType e => TypeRef.NonNull(TypeRefOf(e.Type)), + GraphQLListType e => TypeRef.List(TypeRefOf(e.Type)), + GraphQLNamedType e => TypeRef.Named(Name(e.Name)), + _ => TypeRef.Named(type.Kind.ToString()) + }; +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/SchemaModel.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/SchemaModel.cs new file mode 100644 index 00000000..da508ccc --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/SchemaModel.cs @@ -0,0 +1,121 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime.AI.Schema; + +internal enum SchemaTypeKind +{ + Object, + Interface, + Union, + Enum, + InputObject, + Scalar +} + +/// +/// A type reference as written in the schema: a named type, possibly wrapped in lists and non-null markers. +/// +internal sealed record TypeRef(string Name, TypeRef OfType, bool IsList, bool IsNonNull) +{ + public static TypeRef Named(string name) => new(name, null, false, false); + public static TypeRef List(TypeRef ofType) => new(null, ofType, true, false); + public static TypeRef NonNull(TypeRef ofType) => new(null, ofType, false, true); + + /// + /// The named type at the bottom of the wrappers. + /// + public string NamedType => OfType == null ? Name : OfType.NamedType; + + public override string ToString() => + IsNonNull ? $"{OfType}!" : IsList ? $"[{OfType}]" : Name; +} + +internal sealed class SchemaArgument +{ + public required string Name { get; init; } + public string Description { get; set; } + public required TypeRef Type { get; init; } + public string DefaultValue { get; init; } +} + +internal sealed class SchemaField +{ + public required string Name { get; init; } + public string Description { get; set; } + public required TypeRef Type { get; init; } + public List Arguments { get; init; } = []; + public string DefaultValue { get; init; } + public bool IsDeprecated { get; init; } + + public SchemaField Copy() => new() + { + Name = Name, + Description = Description, + Type = Type, + Arguments = Arguments.Select(e => new SchemaArgument + { + Name = e.Name, Description = e.Description, Type = e.Type, DefaultValue = e.DefaultValue + }).ToList(), + DefaultValue = DefaultValue, + IsDeprecated = IsDeprecated + }; +} + +internal sealed class SchemaEnumValue +{ + public required string Name { get; init; } + public string Description { get; set; } + public bool IsDeprecated { get; init; } +} + +internal sealed class SchemaType +{ + public required string Name { get; init; } + public required SchemaTypeKind Kind { get; init; } + public string Description { get; set; } + + /// + /// Output fields of object and interface types, or input fields of input types, in schema order. + /// + public List Fields { get; init; } = []; + + public List EnumValues { get; init; } = []; + public List Interfaces { get; init; } = []; + + /// + /// Member types of a union, or implementing object types of an interface. + /// + public List PossibleTypes { get; init; } = []; + + public SchemaField Field(string name) => Fields.FirstOrDefault(e => e.Name == name); +} + +/// +/// An example for a common task, as the agent sees it: GraphQL text, variables as JSON, and the root +/// operations it uses. +/// +internal sealed record AgentExample(string Name, string Description, string Query, string Variables, + IReadOnlyList Operations); + +internal sealed class SchemaModel +{ + public required Dictionary Types { get; init; } + public required string QueryType { get; init; } + public string MutationType { get; init; } + public string SubscriptionType { get; init; } + + public SchemaType Type(string name) => Types.GetValueOrDefault(name); +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/SchemaRenderer.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/SchemaRenderer.cs new file mode 100644 index 00000000..57f0e17c --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/SchemaRenderer.cs @@ -0,0 +1,462 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Text; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.AI.Schema; + +/// +/// How much of the scoped schema the agent is shown at once. +/// +internal enum Detail +{ + /// Allowed queries with one-line descriptions, and type names. + Index, + + /// Adds argument types, every type's fields, and filter and sort summaries. + Outline, + + /// Adds full descriptions, enum value meanings and argument descriptions. + Full +} + +/// +/// Turns the scoped schema into the text the agent reads. It stays close to SDL, because models know it and the +/// agent writes GraphQL anyway, and is simplified by collapsing plumbing: connections become +/// [Node] @paged with one shared paging rule, and filter and sort inputs become per-type summaries with +/// one shared syntax rule. Output is deterministic, so it can be snapshot-tested. +/// +internal static class SchemaRenderer +{ + private const string Indent = " "; + + /// + /// The operator sets HotChocolate generates, named once in the conventions so each filterable field needs + /// only the set's name. Any other set is listed in full where it is used. + /// + private static readonly (string Name, string[] Operators)[] OperatorSets = + [ + ("text", ["eq", "neq", "contains", "ncontains", "in", "nin", "startsWith", "nstartsWith", "endsWith", "nendsWith"]), + ("comparable", ["eq", "neq", "in", "nin", "gt", "ngt", "gte", "ngte", "lt", "nlt", "lte", "nlte"]), + ("equality", ["eq", "neq", "in", "nin"]) + ]; + + public static string Render(ScopedSchema schema, Detail detail) + { + var shape = new SchemaShape(schema.Schema); + var text = new StringBuilder(); + + RenderInstructions(text, schema); + RenderConventions(text, shape, detail == Detail.Index ? [] : UsedOperatorSets(schema.Schema, shape)); + + text.AppendLine("Queries:"); + foreach (var root in schema.QueryType.Fields) + { + RenderRoot(text, root, shape, detail); + } + + if (schema.Examples.Count > 0) + { + text.AppendLine(); + text.AppendLine("Examples (describe_operation shows their queries):"); + foreach (var example in schema.Examples) + { + text.Append(Indent).AppendLine($"{example.Name} – {example.Description}"); + } + } + + var types = schema.Schema.Types.Values + .Where(e => e.Name != schema.Schema.QueryType && IsListed(e, shape)) + .OrderBy(e => KindOrder(e.Kind)) + .ToList(); + + if (detail == Detail.Index) + { + if (types.Count > 0) + { + text.AppendLine(); + text.AppendLine("Types: " + string.Join(", ", types.Select(e => e.Name))); + } + + return Finish(text); + } + + if (types.Count > 0) + { + text.AppendLine(); + text.AppendLine("Types:"); + foreach (var type in types) + { + RenderType(text, type, shape, detail, Indent); + } + } + + RenderFilters(text, schema.Schema, shape); + RenderSorting(text, schema.Schema, shape); + + return Finish(text); + } + + /// + /// The named types at Full detail, for describing them on demand. Plumbing types are explained by the + /// conventions instead, and unknown names are skipped. + /// + public static string RenderTypes(ScopedSchema schema, IEnumerable typeNames, Detail detail = Detail.Full) + { + var shape = new SchemaShape(schema.Schema); + var text = new StringBuilder(); + + foreach (var name in typeNames.Distinct()) + { + if (schema.Schema.Type(name) is { } type && IsListed(type, shape)) + { + RenderType(text, type, shape, detail, ""); + } + } + + return Finish(text); + } + + internal static bool IsListed(SchemaType type, SchemaShape shape) => + !shape.IsPlumbing(type.Name) && !IsBuiltInScalar(type); + + /// + /// Types are grouped by kind, each group in schema order: the shapes the agent selects from first, + /// scalars last. + /// + internal static int KindOrder(SchemaTypeKind kind) => kind switch + { + SchemaTypeKind.Interface => 0, + SchemaTypeKind.Object => 1, + SchemaTypeKind.Union => 2, + SchemaTypeKind.Enum => 3, + SchemaTypeKind.InputObject => 4, + _ => 5 + }; + + private static List<(string Name, string[] Operators)> UsedOperatorSets(SchemaModel schema, SchemaShape shape) + { + var used = schema.Types.Values + .Where(e => shape.IsOperatorFilter(e) || shape.IsListFilter(e)) + .Select(e => shape.IsListFilter(e) ? ListElement(schema, e) : e) + .Where(e => e != null && shape.IsOperatorFilter(e)) + .Select(e => OperatorSetName(Operators(e))) + .ToHashSet(); + + return OperatorSets.Where(e => used.Contains(e.Name)).ToList(); + } + + private static string[] Operators(SchemaType operatorFilter) => + operatorFilter.Fields.Where(e => e.Name is not ("and" or "or")).Select(e => e.Name).ToArray(); + + private static string OperatorSetName(string[] operators) => + OperatorSets.FirstOrDefault(e => e.Operators.SequenceEqual(operators)).Name; + + private static string OperatorText(SchemaType operatorFilter) + { + var operators = Operators(operatorFilter); + return OperatorSetName(operators) ?? string.Join(" ", operators); + } + + /// + /// The filter a list filter applies to each element, through some, all or none. + /// + private static SchemaType ListElement(SchemaModel schema, SchemaType listFilter) => + listFilter.Fields.FirstOrDefault(e => e.Name != "any") is { } element ? schema.Type(element.Type.NamedType) : null; + + private static bool IsBuiltInScalar(SchemaType type) => + type.Kind == SchemaTypeKind.Scalar && type.Name is "String" or "Int" or "Float" or "Boolean" or "ID"; + + private static void RenderInstructions(StringBuilder text, ScopedSchema schema) + { + if (schema.Instructions.Count == 0) + { + return; + } + + text.AppendLine("Instructions:"); + foreach (var instruction in schema.Instructions) + { + text.AppendLine("- " + instruction); + } + + text.AppendLine(); + } + + private static void RenderConventions(StringBuilder text, SchemaShape shape, + List<(string Name, string[] Operators)> operatorSets) + { + if (!shape.HasConnections && !shape.HasFilters && !shape.HasSorting) + { + return; + } + + text.AppendLine("Conventions:"); + if (shape.HasConnections) + { + text.AppendLine("- @paged fields return a page: select `nodes { ... }` and `pageInfo { hasNextPage endCursor }`, " + + "and pass `after: ` for the next page. @paged(totalCount) also has `totalCount`. " + + "@paged(edges) has no `nodes`: select `edges { node { ... } }`."); + } + + if (shape.HasFilters) + { + text.AppendLine("- `where` filters: `where: { field: { operator: value } }`, combined with " + + "`and: [ ... ]` and `or: [ ... ]`. A field of a related type nests: `{ customer: { status: { eq: ACTIVE } } }`. " + + "List fields take `some`, `all` or `none: { ... }`, or `any: true`."); + + if (operatorSets.Count > 0) + { + text.AppendLine("- Operator sets: " + + string.Join("; ", operatorSets.Select(e => $"{e.Name} = {string.Join(" ", e.Operators)}")) + "."); + } + } + + if (shape.HasSorting) + { + text.AppendLine("- `order` sorts: `order: [{ field: ASC }]`, or DESC."); + } + + text.AppendLine(); + } + + private static void RenderRoot(StringBuilder text, SchemaField root, SchemaShape shape, Detail detail) + { + if (detail == Detail.Index) + { + var arguments = root.Arguments.Count == 0 ? "" : $"({string.Join(", ", root.Arguments.Select(e => e.Name))})"; + text.Append(Indent).Append(root.Name).Append(arguments).Append(": ").Append(TypeText(root.Type, shape)); + if (FirstLine(root.Description) is { } summary) + { + text.Append(" – ").Append(summary); + } + + text.AppendLine(); + return; + } + + if (detail == Detail.Full) + { + RenderDescription(text, root.Description, Indent); + } + + RenderField(text, root, shape, detail, Indent); + } + + private static void RenderType(StringBuilder text, SchemaType type, SchemaShape shape, Detail detail, + string indent) + { + if (detail == Detail.Full) + { + RenderDescription(text, type.Description, indent); + } + + switch (type.Kind) + { + case SchemaTypeKind.Scalar: + text.Append(indent).AppendLine($"scalar {type.Name}"); + return; + case SchemaTypeKind.Union: + text.Append(indent).AppendLine($"union {type.Name} = {string.Join(" | ", type.PossibleTypes)}"); + return; + case SchemaTypeKind.Enum when detail == Detail.Outline: + text.Append(indent).AppendLine($"enum {type.Name} {{ {string.Join(" ", type.EnumValues.Select(e => e.Name))} }}"); + return; + case SchemaTypeKind.Enum: + text.Append(indent).AppendLine($"enum {type.Name} {{"); + foreach (var value in type.EnumValues) + { + RenderDescription(text, value.Description, indent + Indent); + text.Append(indent).Append(Indent).AppendLine(value.Name); + } + + text.Append(indent).AppendLine("}"); + return; + } + + var keyword = type.Kind switch + { + SchemaTypeKind.Interface => "interface", + SchemaTypeKind.InputObject => "input", + _ => "type" + }; + var implements = type.Interfaces.Count == 0 ? "" : " implements " + string.Join(" & ", type.Interfaces); + + if (detail == Detail.Outline) + { + var fields = type.Fields.Select(e => FieldText(e, shape, Detail.Outline)); + text.Append(indent).AppendLine($"{keyword} {type.Name}{implements} {{ {string.Join(" ", fields)} }}"); + return; + } + + text.Append(indent).AppendLine($"{keyword} {type.Name}{implements} {{"); + foreach (var field in type.Fields) + { + RenderDescription(text, field.Description, indent + Indent); + RenderField(text, field, shape, detail, indent + Indent); + } + + text.Append(indent).AppendLine("}"); + } + + /// + /// A field on its own line. At Full detail, arguments with descriptions get a line each, like SDL. + /// + private static void RenderField(StringBuilder text, SchemaField field, SchemaShape shape, Detail detail, + string indent) + { + if (detail != Detail.Full || field.Arguments.All(e => string.IsNullOrWhiteSpace(e.Description))) + { + text.Append(indent).AppendLine(FieldText(field, shape, detail)); + return; + } + + text.Append(indent).Append(field.Name).AppendLine("("); + foreach (var argument in field.Arguments) + { + RenderDescription(text, argument.Description, indent + Indent); + text.Append(indent).Append(Indent).AppendLine(ArgumentText(argument)); + } + + text.Append(indent).Append("): ").AppendLine(TypeText(field.Type, shape)); + } + + private static string FieldText(SchemaField field, SchemaShape shape, Detail detail) + { + var arguments = field.Arguments.Count == 0 + ? "" + : $"({string.Join(", ", field.Arguments.Select(ArgumentText))})"; + var defaultValue = field.DefaultValue == null || detail == Detail.Index ? "" : $" = {field.DefaultValue}"; + return $"{field.Name}{arguments}: {TypeText(field.Type, shape)}{defaultValue}"; + } + + private static string ArgumentText(SchemaArgument argument) => + argument.DefaultValue == null + ? $"{argument.Name}: {argument.Type}" + : $"{argument.Name}: {argument.Type} = {argument.DefaultValue}"; + + /// + /// The type as the agent should read it: a connection becomes its node type marked @paged. + /// + internal static string TypeText(TypeRef type, SchemaShape shape) + { + if (shape.ConnectionOf(type.NamedType) is not { } connection) + { + return type.ToString(); + } + + var marker = connection.HasNodes switch + { + false => "@paged(edges)", + true when connection.HasTotalCount => "@paged(totalCount)", + true => "@paged" + }; + return $"[{connection.NodeType}] {marker}"; + } + + private static void RenderFilters(StringBuilder text, SchemaModel schema, SchemaShape shape) + { + var filters = schema.Types.Values.Where(shape.IsEntityFilter).ToList(); + if (filters.Count == 0) + { + return; + } + + text.AppendLine(); + text.AppendLine("Filters:"); + foreach (var filter in filters) + { + text.Append(Indent).AppendLine($"{filter.Name}: {FilterSummary(schema, shape, filter)}"); + } + } + + /// + /// One entity filter as a line, for example customerName (text), orders (list of OrderFilterInput: …). + /// + internal static string FilterSummary(SchemaModel schema, SchemaShape shape, SchemaType filter) => + string.Join(", ", filter.Fields + .Where(e => e.Name is not ("and" or "or")) + .Select(e => $"{e.Name} {FilterOperatorsOf(schema, shape, e)}")); + + /// + /// One sort input as a line, for example orderId, customer (CustomerSortInput). + /// + internal static string SortSummary(SchemaModel schema, SchemaShape shape, SchemaType sort) => + string.Join(", ", sort.Fields.Select(e => shape.IsSortEnum(schema.Type(e.Type.NamedType)) + ? e.Name + : $"{e.Name} ({e.Type.NamedType})")); + + private static string FilterOperatorsOf(SchemaModel schema, SchemaShape shape, SchemaField field) + { + var type = schema.Type(field.Type.NamedType); + if (shape.IsOperatorFilter(type)) + { + return $"({OperatorText(type)})"; + } + + if (shape.IsListFilter(type)) + { + return ListElement(schema, type) switch + { + null => "(list: any)", + var element when shape.IsOperatorFilter(element) => $"(list of {OperatorText(element)}: some/all/none, any)", + var element => $"(list of {element.Name}: some/all/none, any)" + }; + } + + return $"({type.Name})"; + } + + private static void RenderSorting(StringBuilder text, SchemaModel schema, SchemaShape shape) + { + var sorts = schema.Types.Values.Where(shape.IsSortInput).ToList(); + if (sorts.Count == 0) + { + return; + } + + text.AppendLine(); + text.AppendLine("Sorting:"); + foreach (var sort in sorts) + { + text.Append(Indent).AppendLine($"{sort.Name}: {SortSummary(schema, shape, sort)}"); + } + } + + private static void RenderDescription(StringBuilder text, string description, string indent) + { + if (string.IsNullOrWhiteSpace(description)) + { + return; + } + + var escaped = description.Trim().Replace("\\", "\\\\").Replace("\"", "\\\"").ReplaceLineEndings("\\n"); + text.Append(indent).Append('"').Append(escaped).AppendLine("\""); + } + + internal static string FirstLine(string description) + { + if (string.IsNullOrWhiteSpace(description)) + { + return null; + } + + var trimmed = description.Trim(); + var end = trimmed.IndexOfAny(['\r', '\n']); + return end < 0 ? trimmed : trimmed[..end].TrimEnd(); + } + + private static string Finish(StringBuilder text) => text.ToString().ReplaceLineEndings("\n").TrimEnd() + "\n"; +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/SchemaShape.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/SchemaShape.cs new file mode 100644 index 00000000..fc3dca1c --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/SchemaShape.cs @@ -0,0 +1,145 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime.AI.Schema; + +/// +/// Recognises the plumbing types the renderer collapses: cursor connections (connection, edge, PageInfo) and +/// HotChocolate-style filter and sort inputs. Detection is by shape, not by name alone, so schemas from other +/// servers that follow the same conventions are collapsed too, and anything that doesn't fit is rendered as is. +/// +internal sealed class SchemaShape +{ + private static readonly HashSet FilterOperators = + [ + "eq", "neq", "contains", "ncontains", "in", "nin", "startsWith", "nstartsWith", "endsWith", "nendsWith", + "gt", "ngt", "gte", "ngte", "lt", "nlt", "lte", "nlte" + ]; + + private static readonly HashSet ListFilterOperators = ["all", "none", "some", "any"]; + + private readonly SchemaModel schema; + private readonly Dictionary connections = new(); + private readonly HashSet plumbing = []; + + public SchemaShape(SchemaModel schema) + { + this.schema = schema; + + foreach (var type in schema.Types.Values.Where(e => e.Kind == SchemaTypeKind.Object)) + { + if (DetectConnection(type) is { } connection) + { + connections[type.Name] = connection; + plumbing.Add(type.Name); + if (connection.EdgeType != null) + { + plumbing.Add(connection.EdgeType); + } + + plumbing.Add("PageInfo"); + } + } + + foreach (var type in schema.Types.Values) + { + if (IsEntityFilter(type) || IsOperatorFilter(type) || IsListFilter(type) || IsSortInput(type) || + IsSortEnum(type)) + { + plumbing.Add(type.Name); + } + } + } + + public sealed record Connection(string NodeType, string EdgeType, bool HasNodes, bool HasTotalCount); + + public bool HasConnections => connections.Count > 0; + + public bool HasFilters => schema.Types.Values.Any(IsEntityFilter); + + public bool HasSorting => schema.Types.Values.Any(IsSortInput); + + public Connection ConnectionOf(string typeName) => connections.GetValueOrDefault(typeName); + + /// + /// Connection, edge, PageInfo, filter and sort types: described through conventions and summaries, not + /// listed as types. + /// + public bool IsPlumbing(string typeName) => plumbing.Contains(typeName); + + /// + /// A filter on an entity (OrderFilterInput): and/or lists of itself plus one filter + /// per filterable field. + /// + public bool IsEntityFilter(SchemaType type) => + type.Kind == SchemaTypeKind.InputObject && + IsSelfList(type, "and") && IsSelfList(type, "or") && + type.Fields.Any(e => e.Name is not ("and" or "or")) && + type.Fields.Where(e => e.Name is not ("and" or "or")).Any(e => !FilterOperators.Contains(e.Name)); + + /// + /// A filter on one value (StringOperationFilterInput): only operators such as eq and in. + /// + public bool IsOperatorFilter(SchemaType type) => + type.Kind == SchemaTypeKind.InputObject && + type.Fields.Any(e => FilterOperators.Contains(e.Name)) && + type.Fields.All(e => FilterOperators.Contains(e.Name) || e.Name is "and" or "or"); + + /// + /// A filter on a list field (ListFilterInputTypeOf…): some, all, none, any. + /// + public bool IsListFilter(SchemaType type) => + type.Kind == SchemaTypeKind.InputObject && + type.Fields.Count > 0 && + type.Fields.All(e => ListFilterOperators.Contains(e.Name)); + + public bool IsSortEnum(SchemaType type) => + type.Kind == SchemaTypeKind.Enum && + type.EnumValues.Select(e => e.Name).OrderBy(e => e).SequenceEqual(["ASC", "DESC"]); + + /// + /// A sort input (OrderSortInput): every field is the sort enum or another sort input. + /// + public bool IsSortInput(SchemaType type) => + type.Kind == SchemaTypeKind.InputObject && + type.Fields.Count > 0 && + type.Fields.All(e => schema.Type(e.Type.NamedType) is { } fieldType && + (IsSortEnum(fieldType) || fieldType.Kind == SchemaTypeKind.InputObject && + fieldType.Name != type.Name && fieldType.Name.EndsWith("SortInput"))); + + private bool IsSelfList(SchemaType type, string fieldName) => + type.Field(fieldName) is { Type.OfType: not null } field && field.Type.NamedType == type.Name; + + private Connection DetectConnection(SchemaType type) + { + if (type.Field("pageInfo")?.Type.NamedType != "PageInfo") + { + return null; + } + + var nodes = type.Field("nodes"); + var edges = type.Field("edges"); + var edgeType = edges == null ? null : schema.Type(edges.Type.NamedType); + var edgeNode = edgeType?.Field("node"); + + var nodeType = nodes?.Type.NamedType ?? edgeNode?.Type.NamedType; + if (nodeType == null) + { + return null; + } + + return new Connection(nodeType, edgeType?.Name, nodes != null, type.Field("totalCount") != null); + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/ScopedSchema.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/ScopedSchema.cs new file mode 100644 index 00000000..c99e6df3 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/ScopedSchema.cs @@ -0,0 +1,419 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Reflection; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.AI.Schema; + +/// +/// The schema as the agent sees it: only the allowed queries, the types reachable from them, no omitted or +/// deprecated members, and the configured descriptions applied. The renderer, the validator and the tools all +/// work from this, never from the full schema. Mutations and subscriptions are left out in this version. +/// +internal sealed class ScopedSchema +{ + private ScopedSchema(SchemaModel schema, IReadOnlyList instructions) + { + Schema = schema; + Instructions = instructions; + } + + public SchemaModel Schema { get; } + + public SchemaType QueryType => Schema.Type(Schema.QueryType); + + public IReadOnlyList Instructions { get; } + + /// + /// Examples for common tasks, already validated against this schema. + /// + public IReadOnlyList Examples { get; private set; } = []; + + /// + /// Validates the examples against this schema and attaches them. Every problem is reported in one + /// exception, so a broken example surfaces when the tools are set up, not when the agent copies it. + /// + internal void SetExamples(IEnumerable<(string Name, string Description, string Query, string Variables)> examples) + { + var validator = new QueryValidator(this); + var list = new List(); + var errors = new List(); + + foreach (var (name, description, query, variables) in examples) + { + var result = validator.Validate(query, + variables == null ? null : System.Text.Json.JsonDocument.Parse(variables).RootElement); + if (!result.IsValid) + { + errors.AddRange(result.Errors.Select(e => $"Example \"{name}\": {e}")); + continue; + } + + list.Add(new AgentExample(name, description, query, variables, QueryValidator.RootFields(query))); + } + + if (errors.Count > 0) + { + throw new AgentConfigurationException( + "An example does not match the agent's scope:" + Environment.NewLine + + string.Join(Environment.NewLine, errors.Select(e => "- " + e))); + } + + Examples = list; + } + + public static ScopedSchema Create(string sdl, Action> configure) + { + ArgumentException.ThrowIfNullOrWhiteSpace(sdl); + ArgumentNullException.ThrowIfNull(configure); + + var options = new AgentOptions(); + configure(options); + return Create(SdlReader.Read(sdl), options); + } + + public static ScopedSchema Create(SchemaModel full, AgentOptions options) + { + var resolver = new Resolver(full); + + var allowedRoots = AllowedRoots(full, options, resolver); + var omitted = WithFilterAndSortFields(full, options.Omitted + .Select(e => resolver.Field(e)) + .Where(e => e != null) + .Select(e => e!.Value) + .ToHashSet()); + var descriptions = Descriptions(full, options, resolver, omitted); + + if (allowedRoots.Count == 0 && resolver.Errors.Count == 0) + { + resolver.Errors.Add("No queries are allowed. Use AllowQuery, or AllowAllQueries for every query."); + } + + if (resolver.Errors.Count > 0) + { + throw new AgentConfigurationException( + "The agent configuration does not match the schema:" + Environment.NewLine + + string.Join(Environment.NewLine, resolver.Errors.Select(e => "- " + e))); + } + + var reachable = Reachable(full, allowedRoots, omitted); + var types = new Dictionary(); + foreach (var type in full.Types.Values.Where(e => reachable.Contains(e.Name))) + { + types[type.Name] = Scope(type, full, allowedRoots, omitted, reachable, descriptions); + } + + return new ScopedSchema(new SchemaModel { Types = types, QueryType = full.QueryType }, + options.InstructionTexts.ToList()); + } + + /// + /// An omitted field must not stay usable through the type's filter or sort input, or the agent could + /// still infer its values by filtering or sorting on it. HotChocolate names those inputs after the type. + /// + private static HashSet<(string Type, string Field)> WithFilterAndSortFields(SchemaModel full, + HashSet<(string Type, string Field)> omitted) + { + foreach (var (type, field) in omitted.ToList()) + { + foreach (var input in new[] { type + "FilterInput", type + "SortInput" }) + { + if (full.Type(input) is { Kind: SchemaTypeKind.InputObject } inputType && inputType.Field(field) != null) + { + omitted.Add((input, field)); + } + } + } + + return omitted; + } + + private static List AllowedRoots(SchemaModel full, AgentOptions options, + Resolver resolver) + { + var allowed = new HashSet(); + + if (options.AllowAll != null) + { + var excluded = options.AllowAll.Excluded.Select(resolver.Query).Where(e => e != null).ToHashSet(); + allowed.UnionWith(full.Type(full.QueryType).Fields.Select(e => e.Name).Where(e => !excluded.Contains(e))); + } + + foreach (var operation in options.AllowedQueries) + { + if (resolver.Query(operation.Method) is { } name) + { + allowed.Add(name); + } + } + + // Schema order, not configuration order, so the agent always sees the same layout. + return full.Type(full.QueryType).Fields.Select(e => e.Name).Where(allowed.Contains).ToList(); + } + + private static Dictionary<(string Type, string Member, string Argument), (string Text, bool Append)> Descriptions( + SchemaModel full, AgentOptions options, Resolver resolver, HashSet<(string Type, string Field)> omitted) + { + var descriptions = new Dictionary<(string, string, string), (string, bool)>(); + + foreach (var operation in options.AllowedQueries) + { + if (resolver.Query(operation.Method) is not { } name) + { + continue; + } + + if (operation.Description is { } description) + { + descriptions[(full.QueryType, name, null)] = description; + } + + var field = full.Type(full.QueryType).Field(name); + foreach (var (argument, text, append) in operation.ArgumentDescriptions) + { + if (field.Arguments.All(e => e.Name != argument)) + { + resolver.Errors.Add($"Query {name} has no argument {argument}. " + + $"Arguments: {Names(field.Arguments.Select(e => e.Name))}."); + continue; + } + + descriptions[(full.QueryType, name, argument)] = (text, append); + } + } + + foreach (var (target, text, append) in options.Descriptions) + { + if (target.Member != null) + { + if (resolver.Field(target) is not { } field) + { + continue; + } + + if (omitted.Contains(field)) + { + resolver.Errors.Add($"{target} is described but also omitted; the agent never sees it."); + continue; + } + + if (full.Type(field.Type).Field(field.Field).IsDeprecated) + { + resolver.Errors.Add($"{target} is described but deprecated, so it is not in the agent's scope."); + continue; + } + + descriptions[(field.Type, field.Field, null)] = (text, append); + } + else if (target.EnumValue != null) + { + if (resolver.EnumValue(target) is not { } value) + { + continue; + } + + descriptions[(value.Type, value.Value, null)] = (text, append); + } + else if (resolver.Type(target.ClrType) is { } type) + { + descriptions[(type.Name, null, null)] = (text, append); + } + } + + return descriptions; + } + + /// + /// Every type the agent can reach from the allowed queries, following field types, argument types, + /// input fields, union members and interface implementations. + /// + private static HashSet Reachable(SchemaModel full, List allowedRoots, + HashSet<(string Type, string Field)> omitted) + { + var reachable = new HashSet { full.QueryType }; + var pending = new Queue(); + + void Visit(string name) + { + if (reachable.Add(name)) + { + pending.Enqueue(name); + } + } + + foreach (var root in allowedRoots.Select(e => full.Type(full.QueryType).Field(e))) + { + Visit(root.Type.NamedType); + root.Arguments.ForEach(e => Visit(e.Type.NamedType)); + } + + while (pending.TryDequeue(out var name)) + { + var type = full.Type(name); + foreach (var field in type.Fields.Where(e => !e.IsDeprecated && !omitted.Contains((name, e.Name)))) + { + Visit(field.Type.NamedType); + field.Arguments.ForEach(e => Visit(e.Type.NamedType)); + } + + type.PossibleTypes.ForEach(Visit); + } + + return reachable; + } + + private static SchemaType Scope(SchemaType type, SchemaModel full, List allowedRoots, + HashSet<(string Type, string Field)> omitted, HashSet reachable, + Dictionary<(string Type, string Member, string Argument), (string Text, bool Append)> descriptions) + { + var isQuery = type.Name == full.QueryType; + var fields = type.Fields + .Where(e => isQuery ? allowedRoots.Contains(e.Name) : !e.IsDeprecated && !omitted.Contains((type.Name, e.Name))) + .Select(e => + { + var field = e.Copy(); + field.Description = Apply(field.Description, descriptions, (type.Name, e.Name, null)); + foreach (var argument in field.Arguments) + { + argument.Description = Apply(argument.Description, descriptions, (type.Name, e.Name, argument.Name)); + } + + return field; + }) + .ToList(); + + return new SchemaType + { + Name = type.Name, + Kind = type.Kind, + Description = Apply(type.Description, descriptions, (type.Name, null, null)), + Fields = fields, + EnumValues = type.EnumValues.Where(e => !e.IsDeprecated).Select(e => new SchemaEnumValue + { + Name = e.Name, + Description = Apply(e.Description, descriptions, (type.Name, e.Name, null)) + }).ToList(), + Interfaces = type.Interfaces.Where(reachable.Contains).ToList(), + PossibleTypes = type.PossibleTypes.Where(reachable.Contains).ToList() + }; + } + + private static string Apply(string original, + Dictionary<(string Type, string Member, string Argument), (string Text, bool Append)> descriptions, + (string, string, string) key) + { + if (!descriptions.TryGetValue(key, out var description)) + { + return original; + } + + return description.Append && !string.IsNullOrWhiteSpace(original) + ? original + "\n" + description.Text + : description.Text; + } + + private static string Names(IEnumerable names) + { + var list = names.ToList(); + return list.Count == 0 ? "none" : string.Join(", ", list); + } + + /// + /// Maps generated CLR members to schema names. Generated types keep their GraphQL names, properties carry + /// , and other members follow the generator's rule of upper-casing the + /// first letter. Problems are collected rather than thrown, so one exception can list them all. + /// + private sealed class Resolver(SchemaModel full) + { + public List Errors { get; } = []; + + public string Query(MethodInfo method) + { + var name = Match(full.Type(full.QueryType).Fields.Select(e => e.Name), method.Name); + if (name != null) + { + return name; + } + + if (full.MutationType != null && Match(full.Type(full.MutationType).Fields.Select(e => e.Name), method.Name) != null) + { + Errors.Add($"{method.Name} is a mutation. Only queries can be allowed in this version."); + } + else + { + Errors.Add($"The schema has no query {method.Name}."); + } + + return null; + } + + public SchemaType Type(Type clrType) + { + var type = full.Type(clrType.Name); + if (type == null) + { + Errors.Add($"The schema has no type {clrType.Name}."); + } + + return type; + } + + public (string Type, string Field)? Field(MemberReference reference) + { + if (Type(reference.ClrType) is not { } type) + { + return null; + } + + var name = reference.Member.GetCustomAttribute()?.GraphQLName + ?? Match(type.Fields.Select(e => e.Name), reference.Member.Name); + if (name == null || type.Field(name) == null) + { + Errors.Add($"Type {type.Name} has no field {name ?? reference.Member.Name}."); + return null; + } + + return (type.Name, name); + } + + public (string Type, string Value)? EnumValue(MemberReference reference) + { + if (Type(reference.ClrType) is not { } type) + { + return null; + } + + if (type.EnumValues.All(e => e.Name != reference.EnumValue)) + { + Errors.Add($"Enum {type.Name} has no value {reference.EnumValue}."); + return null; + } + + if (type.EnumValues.First(e => e.Name == reference.EnumValue).IsDeprecated) + { + Errors.Add($"{type.Name}.{reference.EnumValue} is described but deprecated, so it is not in the agent's scope."); + return null; + } + + return (type.Name, reference.EnumValue); + } + + private static string Match(IEnumerable schemaNames, string clrName) => + schemaNames.FirstOrDefault(e => ToPascalCase(e) == clrName); + + private static string ToPascalCase(string name) => + char.IsUpper(name[0]) ? name : char.ToUpperInvariant(name[0]) + name[1..]; + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/SdlReader.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/SdlReader.cs new file mode 100644 index 00000000..205b88a6 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Schema/SdlReader.cs @@ -0,0 +1,215 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using GraphQLParser; +using GraphQLParser.AST; +using GraphQLParser.Exceptions; +using GraphQLParser.Visitors; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.AI.Schema; + +/// +/// Reads SDL into the agent package's own schema model. It accepts the SDL the generator emits in agent mode +/// as well as hand-written or exported schema files, including type extensions. +/// +internal static class SdlReader +{ + private static readonly string[] BuiltInScalars = ["String", "Int", "Float", "Boolean", "ID"]; + + public static SchemaModel Read(string sdl) + { + GraphQLDocument document; + try + { + document = Parser.Parse(sdl); + } + catch (GraphQLParserException ex) + { + throw new AgentConfigurationException($"The schema is not valid SDL: {ex.Message}", ex); + } + + var types = new Dictionary(); + foreach (var scalar in BuiltInScalars) + { + types[scalar] = new SchemaType { Name = scalar, Kind = SchemaTypeKind.Scalar }; + } + + foreach (var definition in document.Definitions) + { + var type = definition switch + { + GraphQLObjectTypeDefinition e => NewType(e.Name, SchemaTypeKind.Object, e.Description), + GraphQLInterfaceTypeDefinition e => NewType(e.Name, SchemaTypeKind.Interface, e.Description), + GraphQLUnionTypeDefinition e => NewType(e.Name, SchemaTypeKind.Union, e.Description), + GraphQLEnumTypeDefinition e => NewType(e.Name, SchemaTypeKind.Enum, e.Description), + GraphQLInputObjectTypeDefinition e => NewType(e.Name, SchemaTypeKind.InputObject, e.Description), + GraphQLScalarTypeDefinition e => NewType(e.Name, SchemaTypeKind.Scalar, e.Description), + _ => null + }; + + if (type != null) + { + types[type.Name] = type; + } + } + + foreach (var definition in document.Definitions) + { + switch (definition) + { + case GraphQLObjectTypeDefinition e: + AddMembers(types, e.Name, e.Interfaces, e.Fields); + break; + case GraphQLObjectTypeExtension e: + AddMembers(types, e.Name, e.Interfaces, e.Fields); + break; + case GraphQLInterfaceTypeDefinition e: + AddMembers(types, e.Name, e.Interfaces, e.Fields); + break; + case GraphQLInterfaceTypeExtension e: + AddMembers(types, e.Name, e.Interfaces, e.Fields); + break; + case GraphQLInputObjectTypeDefinition e: + AddInputFields(types, e.Name, e.Fields); + break; + case GraphQLInputObjectTypeExtension e: + AddInputFields(types, e.Name, e.Fields); + break; + case GraphQLEnumTypeDefinition e: + AddEnumValues(types, e.Name, e.Values); + break; + case GraphQLEnumTypeExtension e: + AddEnumValues(types, e.Name, e.Values); + break; + case GraphQLUnionTypeDefinition e: + Existing(types, e.Name).PossibleTypes.AddRange(e.Types?.Items.Select(t => Name(t.Name)) ?? []); + break; + case GraphQLUnionTypeExtension e: + Existing(types, e.Name).PossibleTypes.AddRange(e.Types?.Items.Select(t => Name(t.Name)) ?? []); + break; + } + } + + foreach (var interfaceType in types.Values.Where(e => e.Kind == SchemaTypeKind.Interface)) + { + interfaceType.PossibleTypes.AddRange(types.Values + .Where(e => e.Kind == SchemaTypeKind.Object && e.Interfaces.Contains(interfaceType.Name)) + .Select(e => e.Name)); + } + + var schemaDefinition = document.Definitions.OfType().FirstOrDefault(); + var model = new SchemaModel + { + Types = types, + QueryType = RootType(schemaDefinition, types, GraphQLParser.AST.OperationType.Query, "Query") + ?? throw new AgentConfigurationException("The schema has no query type."), + MutationType = RootType(schemaDefinition, types, GraphQLParser.AST.OperationType.Mutation, "Mutation"), + SubscriptionType = RootType(schemaDefinition, types, GraphQLParser.AST.OperationType.Subscription, "Subscription") + }; + + var unknown = types.Values + .SelectMany(t => t.Fields.SelectMany(f => f.Arguments.Select(a => a.Type).Append(f.Type))) + .Select(e => e.NamedType) + .FirstOrDefault(e => !types.ContainsKey(e)); + if (unknown != null) + { + throw new AgentConfigurationException($"The schema references type {unknown}, which is not defined."); + } + + return model; + } + + private static SchemaType NewType(GraphQLName name, SchemaTypeKind kind, GraphQLDescription description) => + new() { Name = Name(name), Kind = kind, Description = Text(description) }; + + private static SchemaType Existing(Dictionary types, GraphQLName name) => + types.TryGetValue(Name(name), out var type) + ? type + : throw new AgentConfigurationException($"The schema extends type {Name(name)}, which is not defined."); + + private static void AddMembers(Dictionary types, GraphQLName name, + GraphQLImplementsInterfaces interfaces, GraphQLFieldsDefinition fields) + { + var type = Existing(types, name); + type.Interfaces.AddRange(interfaces?.Items.Select(e => Name(e.Name)) ?? []); + type.Fields.AddRange((fields?.Items ?? []).Select(field => new SchemaField + { + Name = Name(field.Name), + Description = Text(field.Description), + Type = TypeRefOf(field.Type), + IsDeprecated = IsDeprecated(field.Directives), + Arguments = (field.Arguments?.Items ?? []).Select(e => new SchemaArgument + { + Name = Name(e.Name), + Description = Text(e.Description), + Type = TypeRefOf(e.Type), + DefaultValue = Print(e.DefaultValue) + }).ToList() + })); + } + + private static void AddInputFields(Dictionary types, GraphQLName name, + GraphQLInputFieldsDefinition fields) + { + Existing(types, name).Fields.AddRange((fields?.Items ?? []).Select(field => new SchemaField + { + Name = Name(field.Name), + Description = Text(field.Description), + Type = TypeRefOf(field.Type), + DefaultValue = Print(field.DefaultValue), + IsDeprecated = IsDeprecated(field.Directives) + })); + } + + private static void AddEnumValues(Dictionary types, GraphQLName name, + GraphQLEnumValuesDefinition values) + { + Existing(types, name).EnumValues.AddRange((values?.Items ?? []).Select(value => new SchemaEnumValue + { + Name = Name(value.EnumValue.Name), + Description = Text(value.Description), + IsDeprecated = IsDeprecated(value.Directives) + })); + } + + private static string RootType(GraphQLSchemaDefinition schemaDefinition, Dictionary types, + GraphQLParser.AST.OperationType operation, string defaultName) + { + if (schemaDefinition != null) + { + var root = schemaDefinition.OperationTypes.FirstOrDefault(e => e.Operation == operation); + return root == null ? null : Name(root.Type.Name); + } + + return types.ContainsKey(defaultName) ? defaultName : null; + } + + private static TypeRef TypeRefOf(GraphQLType type) => type switch + { + GraphQLNonNullType e => TypeRef.NonNull(TypeRefOf(e.Type)), + GraphQLListType e => TypeRef.List(TypeRefOf(e.Type)), + GraphQLNamedType e => TypeRef.Named(Name(e.Name)), + _ => throw new AgentConfigurationException($"Unsupported type reference {type.Kind}.") + }; + + private static bool IsDeprecated(GraphQLDirectives directives) => + directives?.Items.Any(e => Name(e.Name) == "deprecated") == true; + + private static string Print(GraphQLValue value) => value == null ? null : new SDLPrinter().Print(value); + + private static string Name(GraphQLName name) => name.StringValue; + + private static string Text(GraphQLDescription description) => description?.Value.ToString(); +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Tools/AgentToolset.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Tools/AgentToolset.cs new file mode 100644 index 00000000..8e6443c5 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/AI/Tools/AgentToolset.cs @@ -0,0 +1,449 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Text; +using System.Text.Json; +using Linq2GraphQL.TestClientStandalone.Runtime.AI.Schema; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.AI.Tools; + +/// +/// The four agent tools as plain methods, with no AI framework dependency: run a query, describe operations, +/// describe types and search the schema. Everything works from the scoped schema; only +/// talks to the server, and only after the query has passed validation. Results are text, because that is what +/// a tool hands back to the model. +/// +internal sealed class AgentToolset +{ + private const int StarterFieldCount = 6; + private const int MaxSearchResults = 20; + + private static readonly JsonSerializerOptions ResultJson = new() + { + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }; + + private readonly ScopedSchema schema; + private readonly GraphClient client; + private readonly QueryValidator validator; + private readonly SchemaShape shape; + private readonly SchemaModel model; + private readonly IReadOnlyList> queryLoggers; + + public AgentToolset(ScopedSchema schema, GraphClient client, IReadOnlyList> queryLoggers = null) + { + this.schema = schema; + this.client = client; + this.queryLoggers = queryLoggers ?? []; + validator = new QueryValidator(schema); + shape = new SchemaShape(schema.Schema); + model = schema.Schema; + Index = SchemaRenderer.Render(schema, Detail.Index); + } + + /// + /// The Index: the allowed operations and type names, for the query tool's description. + /// + public string Index { get; } + + /// + /// graphql_query: validates the query and its variables, and only sends a valid query. Returns JSON with + /// data and errors, or validationErrors when nothing was sent. + /// + public async Task QueryAsync(string query, JsonElement? variables = null, + CancellationToken cancellationToken = default) + { + var started = System.Diagnostics.Stopwatch.StartNew(); + var validation = validator.Validate(query, variables); + if (!validation.IsValid) + { + var validationErrors = validation.Errors.Select(e => e.ToString()).ToList(); + Log(query, variables, validationErrors, [], started.Elapsed); + return JsonSerializer.Serialize(new + { + validationErrors, + note = "The query was not sent. Fix every error listed and try again." + }, ResultJson); + } + + try + { + var result = await client.ExecuteAsync(query, Variables(variables), cancellationToken); + Log(query, variables, [], result.Errors?.Select(e => e.Message).ToList() ?? [], started.Elapsed); + return JsonSerializer.Serialize(new + { + data = result.Data.ValueKind == JsonValueKind.Undefined ? (JsonElement?)null : result.Data, + errors = result.Errors?.Select(e => new { message = e.Message, path = e.Path, hint = Hint(e) }) + }, ResultJson); + } + catch (GraphQueryRequestException ex) + { + Log(query, variables, [], [ex.Message], started.Elapsed); + return JsonSerializer.Serialize(new { requestError = ex.Message }, ResultJson); + } + } + + /// + /// What to do about a server error the schema cannot predict. The page-size limit is the common one: it is + /// not in the SDL, so agents guess a page size and retry with smaller guesses unless told the limit. + /// + private static string Hint(GraphQueryError error) + { + if (error.Extensions?.TryGetValue("code", out var code) != true || code?.ToString() != "HC0051") + { + return null; + } + + var max = error.Extensions.TryGetValue("maxAllowedItems", out var value) ? value?.ToString() : null; + var field = error.Extensions.TryGetValue("coordinate", out var coordinate) ? $" for {coordinate}" : ""; + return max == null + ? $"The page size{field} is limited. Leave out first/last to get the server's default page, then page with after: endCursor." + : $"At most {max} items per page{field}. Use first: {max} or leave it out, then page with after: endCursor while hasNextPage is true."; + } + + /// + /// Reports a query to the OnQuery loggers. A failing logger must not break the agent's call. + /// + private void Log(string query, JsonElement? variables, IReadOnlyList validationErrors, + IReadOnlyList errors, TimeSpan duration) + { + if (queryLoggers.Count == 0) + { + return; + } + + var entry = new AgentQueryLog(query, variables?.GetRawText(), validationErrors, errors, duration); + foreach (var logger in queryLoggers) + { + try + { + logger(entry); + } + catch (Exception) + { + // Logging is best effort; the agent's query result matters more. + } + } + } + + /// + /// describe_operation: everything needed to call each named operation, including a starter query. + /// + public string DescribeOperations(IEnumerable names) + { + var text = new StringBuilder(); + foreach (var name in names.Distinct()) + { + if (text.Length > 0) + { + text.AppendLine(); + } + + var operation = schema.QueryType.Field(name); + if (operation == null) + { + text.AppendLine($"Operation {name} is not available." + + NameSuggestions.Suggest(name, schema.QueryType.Fields.Select(e => e.Name))); + continue; + } + + DescribeOperation(text, operation); + } + + return Finish(text); + } + + /// + /// describe_type: full detail for each named type, plus the types its fields use at Outline level. Filter + /// and sort inputs are described by their summary line. + /// + public string DescribeTypes(IEnumerable names) + { + var requested = names.Distinct().ToList(); + var text = new StringBuilder(); + var described = new List(); + + foreach (var name in requested) + { + var type = model.Type(name); + if (type == null) + { + text.AppendLine($"Type {name} is not available." + NameSuggestions.Suggest(name, ListedTypeNames())); + } + else if (shape.IsEntityFilter(type)) + { + text.AppendLine($"{type.Name} (filter): {SchemaRenderer.FilterSummary(model, shape, type)}"); + } + else if (shape.IsSortInput(type)) + { + text.AppendLine($"{type.Name} (sorting): {SchemaRenderer.SortSummary(model, shape, type)}"); + } + else if (shape.ConnectionOf(type.Name) is { } connection) + { + text.AppendLine($"{type.Name} is a page of {connection.NodeType}; see the @paged convention. " + + $"Describe {connection.NodeType} for its fields."); + } + else if (SchemaRenderer.IsListed(type, shape)) + { + described.Add(type.Name); + text.Append(SchemaRenderer.RenderTypes(schema, [type.Name])); + } + else + { + text.AppendLine($"{type.Name} is a built-in scalar."); + } + } + + if (described.Count > 0) + { + var related = described + .SelectMany(e => RelatedTypes(model.Type(e))) + .Where(e => !described.Contains(e)) + .Distinct() + .ToList(); + if (related.Count > 0) + { + text.AppendLine(); + text.AppendLine("Related types:"); + text.Append(SchemaRenderer.RenderTypes(schema, related, Detail.Outline)); + } + } + + return Finish(text); + } + + /// + /// search_schema: operations, examples, types, fields and enum values matching the text, names before descriptions. + /// + public string Search(string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return "Give a word to search for, such as a field or type name.\n"; + } + + var term = text.Trim(); + var matches = new List<(int Score, int Kind, int Order, string Line)>(); + var order = 0; + + void Consider(int kind, string name, string description, string line) + { + var score = name.Equals(term, StringComparison.OrdinalIgnoreCase) ? 0 + : name.StartsWith(term, StringComparison.OrdinalIgnoreCase) ? 1 + : name.Contains(term, StringComparison.OrdinalIgnoreCase) ? 2 + : description?.Contains(term, StringComparison.OrdinalIgnoreCase) == true ? 3 + : -1; + if (score >= 0) + { + matches.Add((score, kind, order++, line)); + } + } + + foreach (var example in schema.Examples) + { + Consider(1, example.Name, example.Description, $"example {example.Name} – {example.Description}"); + } + + foreach (var operation in schema.QueryType.Fields) + { + Consider(0, operation.Name, operation.Description, + $"operation {operation.Name}: {SchemaRenderer.TypeText(operation.Type, shape)}{Summary(operation.Description)}"); + } + + foreach (var type in model.Types.Values.Where(e => e.Name != model.QueryType && SchemaRenderer.IsListed(e, shape))) + { + Consider(2, type.Name, type.Description, $"type {type.Name}{Summary(type.Description)}"); + + foreach (var field in type.Fields) + { + Consider(3, field.Name, field.Description, + $"field {type.Name}.{field.Name}: {SchemaRenderer.TypeText(field.Type, shape)}{Summary(field.Description)}"); + } + + foreach (var value in type.EnumValues) + { + Consider(4, value.Name, value.Description, $"value {type.Name}.{value.Name}{Summary(value.Description)}"); + } + } + + if (matches.Count == 0) + { + return $"No operations, types or fields match \"{term}\". Try a shorter or broader word.\n"; + } + + var result = new StringBuilder(); + foreach (var match in matches.OrderBy(e => e.Score).ThenBy(e => e.Kind).ThenBy(e => e.Order).Take(MaxSearchResults)) + { + result.AppendLine(match.Line); + } + + if (matches.Count > MaxSearchResults) + { + result.AppendLine($"… and {matches.Count - MaxSearchResults} more. Use a more specific word."); + } + + return Finish(result); + } + + private void DescribeOperation(StringBuilder text, SchemaField operation) + { + text.AppendLine($"{operation.Name}{Summary(operation.Description, full: true)}"); + + if (operation.Arguments.Count > 0) + { + text.AppendLine("Arguments:"); + foreach (var argument in operation.Arguments) + { + var line = new StringBuilder($" {argument.Name}: {argument.Type}"); + if (argument.DefaultValue != null) + { + line.Append($" = {argument.DefaultValue}"); + } + + if (!string.IsNullOrWhiteSpace(argument.Description)) + { + line.Append($" – {argument.Description.Trim().ReplaceLineEndings(" ")}"); + } + + var argumentType = model.Type(argument.Type.NamedType); + if (shape.IsEntityFilter(argumentType)) + { + line.Append($" – filterable: {SchemaRenderer.FilterSummary(model, shape, argumentType)}"); + } + else if (shape.IsSortInput(argumentType)) + { + line.Append($" – sortable: {SchemaRenderer.SortSummary(model, shape, argumentType)}"); + } + + text.AppendLine(line.ToString()); + } + } + + text.AppendLine($"Returns: {SchemaRenderer.TypeText(operation.Type, shape)}"); + var returnType = ReturnedType(operation); + if (SchemaRenderer.IsListed(returnType, shape) && returnType.Kind is not (SchemaTypeKind.Scalar or SchemaTypeKind.Enum)) + { + foreach (var line in SchemaRenderer.RenderTypes(schema, [returnType.Name], Detail.Outline).TrimEnd().Split('\n')) + { + text.AppendLine(" " + line); + } + } + + text.AppendLine("Starter query:"); + text.AppendLine(" " + StarterQuery(operation)); + + var examples = schema.Examples.Where(e => e.Operations.Contains(operation.Name)).ToList(); + if (examples.Count > 0) + { + text.AppendLine("Examples:"); + foreach (var example in examples) + { + text.AppendLine($" {example.Name} – {example.Description}"); + text.AppendLine(" " + example.Query.ReplaceLineEndings(" ").Trim()); + if (example.Variables != null) + { + text.AppendLine(" variables: " + example.Variables); + } + } + } + } + + /// + /// A valid query to adapt: required arguments as variables and the first few plain fields of the result. + /// Paged operations get no page size, because the schema doesn't publish the server's maximum; the server + /// then uses its own default. + /// + internal string StarterQuery(SchemaField operation) + { + var required = operation.Arguments.Where(e => e.Type.IsNonNull && e.DefaultValue == null).ToList(); + var declarations = required.Count == 0 ? "" : $"({string.Join(", ", required.Select(e => $"${e.Name}: {e.Type}"))})"; + var arguments = required.Select(e => $"{e.Name}: ${e.Name}").ToList(); + + var call = arguments.Count == 0 ? operation.Name : $"{operation.Name}({string.Join(", ", arguments)})"; + return $"query{declarations} {{ {call}{Selection(model.Type(operation.Type.NamedType))} }}"; + } + + private string Selection(SchemaType type) + { + if (type.Kind is SchemaTypeKind.Scalar or SchemaTypeKind.Enum) + { + return ""; + } + + if (shape.ConnectionOf(type.Name) is { } connection) + { + var node = Selection(model.Type(connection.NodeType)); + return connection.HasNodes + ? $" {{ nodes{node} pageInfo {{ hasNextPage endCursor }} }}" + : $" {{ edges {{ node{node} }} pageInfo {{ hasNextPage endCursor }} }}"; + } + + if (type.Kind == SchemaTypeKind.Union) + { + var first = type.PossibleTypes.FirstOrDefault(); + return first == null ? " { __typename }" : $" {{ __typename ... on {first}{Selection(model.Type(first))} }}"; + } + + var fields = type.Fields + .Where(e => e.Arguments.All(a => !a.Type.IsNonNull || a.DefaultValue != null) && + model.Type(e.Type.NamedType).Kind is SchemaTypeKind.Scalar or SchemaTypeKind.Enum) + .Take(StarterFieldCount) + .Select(e => e.Name) + .ToList(); + return $" {{ {(fields.Count == 0 ? "__typename" : string.Join(" ", fields))} }}"; + } + + private SchemaType ReturnedType(SchemaField operation) => + shape.ConnectionOf(operation.Type.NamedType) is { } connection + ? model.Type(connection.NodeType) + : model.Type(operation.Type.NamedType); + + /// + /// The listed types a type's fields and arguments use, with connections resolved to their node types. + /// + private IEnumerable RelatedTypes(SchemaType type) => + type.Fields + .SelectMany(e => e.Arguments.Select(a => a.Type.NamedType).Append(e.Type.NamedType)) + .Select(e => shape.ConnectionOf(e)?.NodeType ?? e) + .Concat(type.PossibleTypes) + .Concat(type.Interfaces) + .Where(e => model.Type(e) is { } related && SchemaRenderer.IsListed(related, shape) && + related.Kind != SchemaTypeKind.Scalar); + + private IEnumerable ListedTypeNames() => + model.Types.Values + .Where(e => e.Name != model.QueryType && SchemaRenderer.IsListed(e, shape)) + .OrderBy(e => SchemaRenderer.KindOrder(e.Kind)) + .Select(e => e.Name); + + private static Dictionary Variables(JsonElement? variables) => + variables is { ValueKind: JsonValueKind.Object } values + ? values.EnumerateObject().ToDictionary(e => e.Name, e => (object)e.Value.Clone()) + : null; + + private static string Summary(string description, bool full = false) + { + if (string.IsNullOrWhiteSpace(description)) + { + return ""; + } + + return full + ? " – " + description.Trim().ReplaceLineEndings(" ") + : " – " + SchemaRenderer.FirstLine(description); + } + + private static string Finish(StringBuilder text) => text.ToString().ReplaceLineEndings("\n").TrimEnd() + "\n"; +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/ArgumentValue.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/ArgumentValue.cs new file mode 100644 index 00000000..8f0584a8 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/ArgumentValue.cs @@ -0,0 +1,32 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public class ArgumentValue +{ + public ArgumentValue(string graphName, string graphType, object value) + { + GraphType = graphType; + Value = value; + GraphName = graphName; + VariableName = graphName; + } + + public string GraphName { get; set; } + public string GraphType { get; set; } + public object Value { get; set; } + public string VariableName { get; set; } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphArgumentAttribute.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphArgumentAttribute.cs new file mode 100644 index 00000000..84e370be --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphArgumentAttribute.cs @@ -0,0 +1,27 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +[AttributeUsage(AttributeTargets.Parameter)] +public class GraphArgumentAttribute : Attribute +{ + public GraphArgumentAttribute(string graphType) + { + GraphType = graphType; + } + + public string GraphType { get; private set; } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphInterfaceAttribute.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphInterfaceAttribute.cs new file mode 100644 index 00000000..00193535 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphInterfaceAttribute.cs @@ -0,0 +1,27 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime; + + +/// +/// Will be removed in future versions +/// Only here for backward compatibility +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] +public class GraphInterfaceAttribute : Attribute +{ + +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphMethodAttribute.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphMethodAttribute.cs new file mode 100644 index 00000000..a0d89f83 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphMethodAttribute.cs @@ -0,0 +1,27 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +[AttributeUsage(AttributeTargets.Method)] +public class GraphMethodAttribute : Attribute +{ + public GraphMethodAttribute(string graphName) + { + GraphName = graphName; + } + + public string GraphName { get; private set; } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphQLArgumentAttribute.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphQLArgumentAttribute.cs new file mode 100644 index 00000000..e3c4f826 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphQLArgumentAttribute.cs @@ -0,0 +1,23 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +[AttributeUsage(AttributeTargets.Parameter)] +public class GraphQLArgumentAttribute(string graphQLName, string graphQLType) : Attribute +{ + public string GraphQLType { get; private set; } = graphQLType; + public string GraphQLName { get; private set; } = graphQLName; +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphQLMemberAttribute.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphQLMemberAttribute.cs new file mode 100644 index 00000000..ad03f223 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphQLMemberAttribute.cs @@ -0,0 +1,35 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] +public class GraphQLMemberAttribute : Attribute +{ + public GraphQLMemberAttribute(string graphQLName) + { + GraphQLName = graphQLName; + } + + public GraphQLMemberAttribute(string graphQLName, bool interfaceProperty) + { + GraphQLName = graphQLName; + InterfaceProperty = interfaceProperty; + } + + public string GraphQLName { get; private set; } + + public bool InterfaceProperty { get; set; } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphShadowPropertyAttribute.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphShadowPropertyAttribute.cs new file mode 100644 index 00000000..474594b9 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Attributes/GraphShadowPropertyAttribute.cs @@ -0,0 +1,21 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +[AttributeUsage(AttributeTargets.Property)] +public class GraphShadowPropertyAttribute : Attribute +{ +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/ClientBuilder.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/ClientBuilder.cs new file mode 100644 index 00000000..b5dfcb3e --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/ClientBuilder.cs @@ -0,0 +1,32 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public class ClientBuilder : IGraphClientBuilder where T : class +{ + public ClientBuilder( + string clientName, + IServiceCollection services) + { + ClientName = clientName; + Services = services; + } + + public string ClientName { get; } + public IServiceCollection Services { get; } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/ClientJson.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/ClientJson.cs new file mode 100644 index 00000000..a52aa275 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/ClientJson.cs @@ -0,0 +1,41 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Common; + +/// +/// Shared, frozen JsonSerializerOptions for all GraphClient instances. +/// System.Text.Json caches type metadata per options instance, so a single shared +/// instance avoids rebuilding converters and property metadata on every request. +/// Values are identical to the historical per-client configuration. +/// +internal static class ClientJson +{ + internal static readonly JsonSerializerOptions Default = CreateDefault(); + + private static JsonSerializerOptions CreateDefault() + { + var options = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { }, + }; + options.MakeReadOnly(populateMissingResolver: true); + return options; + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/CustomScalar.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/CustomScalar.cs new file mode 100644 index 00000000..b773d577 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/CustomScalar.cs @@ -0,0 +1,33 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public class CustomScalar +{ + internal object InternalValue { get; set; } + + public virtual object Value + { + get => InternalValue; + + set => InternalValue = value; + } + + public override string ToString() + { + return InternalValue?.ToString(); + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/GraphEnvelopeReader.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/GraphEnvelopeReader.cs new file mode 100644 index 00000000..b52223f6 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/GraphEnvelopeReader.cs @@ -0,0 +1,154 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Buffers; +using System.Text.Json; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Common; + +/// +/// The single parser for the GraphQL response envelope (data / errors / +/// extensions). Reads directly from UTF-8 bytes: only the requested field under +/// "data" is deserialized; sibling root fields are skipped without materializing a typed +/// value. No JsonDocument and no UTF-16 round trip. Every response path (typed queries, +/// raw GraphClient.ExecuteAsync, the public ProcessResponse overloads and subscription +/// payloads) goes through here, so they cannot drift apart. +/// +internal static class GraphEnvelopeReader +{ + /// + /// Reads the envelope and deserializes only data.{name} as . + /// An empty body yields a result with no data and no errors. + /// + internal static GraphResult Read(ReadOnlySpan utf8, string name, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(name); + return ReadCore(utf8, name, options, out _); + } + + /// + /// Reads the envelope and returns the whole data value as a standalone + /// (for queries written as GraphQL text). A JSON null + /// data is returned as a element and a missing one as + /// default. reports whether the body carried a + /// data or errors member, i.e. whether it is a GraphQL response at all. + /// + internal static GraphResult ReadDocument(ReadOnlySpan utf8, JsonSerializerOptions options, + out bool isEnvelope) + { + return ReadCore(utf8, null, options, out isEnvelope); + } + + /// Root field to read, or null to read the whole data value. + private static GraphResult ReadCore(ReadOnlySpan utf8, string name, JsonSerializerOptions options, + out bool isEnvelope) + { + isEnvelope = false; + + if (utf8.Length == 0) + { + return new GraphResult { Data = default }; + } + + var reader = new Utf8JsonReader(utf8, isFinalBlock: true, state: default); + + if (reader.Read() && reader.TokenType == JsonTokenType.StartObject) + { + List errors = null; + Dictionary extensions = null; + T data = default; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + var propertyName = reader.GetString(); + reader.Read(); + + switch (propertyName) + { + case "data": + isEnvelope = true; + data = name == null + ? JsonSerializer.Deserialize(ref reader, options) + : ReadData(ref reader, name, options); + break; + case "errors": + isEnvelope = true; + errors = JsonSerializer.Deserialize>(ref reader, options); + break; + case "extensions": + extensions = + JsonSerializer.Deserialize>(ref reader, options); + break; + default: + reader.Skip(); + break; + } + } + + return new GraphResult { Data = data, Errors = errors, Extensions = extensions }; + } + + throw new JsonException("GraphQL response envelope must be a JSON object."); + } + + private static TData ReadData(ref Utf8JsonReader reader, string name, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return default; + } + + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("The 'data' property must be a JSON object or null."); + } + + TData result = default; + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + var fieldName = reader.GetString(); + reader.Read(); + + if (fieldName == name) + { + if (reader.TokenType == JsonTokenType.Null) + { + result = default; + } + else + { + result = JsonSerializer.Deserialize(ref reader, options); + } + } + else + { + // Multi-root query: never materialize fields we were not asked for. + reader.Skip(); + } + } + + return result; + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/GraphQLTypeBase.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/GraphQLTypeBase.cs new file mode 100644 index 00000000..35b818c7 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/GraphQLTypeBase.cs @@ -0,0 +1,58 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System.IO; +using System.Net.Http; +using System.Threading; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Text.Json; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Common +{ + public abstract class GraphQLTypeBase + { + + public T GetMethodValue(string methodName, params object[] arguments) + { + var id = Utilities.GetArgumentsId(arguments); + + if (__AdditionalProperties?.TryGetValue(methodName + id, out var value) != true) + { + return default; + } + + return value.Deserialize(ClientJson.Default); + } + + public T GetFirstMethodValue(string methodName) + { + var keyValue = __AdditionalProperties?.FirstOrDefault(e => e.Key.StartsWith(methodName)); + if (keyValue == null || keyValue.Value.Key == null) + { + return default; + } + + return keyValue.Value.Value.Deserialize(ClientJson.Default); + } + + + + /// + /// Unknown properties captured during deserialization. Null until the serializer + /// actually meets an unknown property — no dictionary is allocated per object. + /// + [JsonExtensionData] + public Dictionary __AdditionalProperties { get; set; } + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/ICursorPaging.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/ICursorPaging.cs new file mode 100644 index 00000000..d7495469 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/ICursorPaging.cs @@ -0,0 +1,28 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System.IO; +using System.Net.Http; +using System.Threading; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Common +{ + public interface ICursorPaging + { + [GraphQLMember("pageInfo")] + [JsonPropertyName("pageInfo")] + public PageInfo PageInfo { get; set; } + + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/IPageInfo.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/IPageInfo.cs new file mode 100644 index 00000000..21789f4a --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/IPageInfo.cs @@ -0,0 +1,40 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System.IO; +using System.Net.Http; +using System.Threading; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Common +{ + public class PageInfo + { + [GraphQLMember("hasNextPage")] + [JsonPropertyName("hasNextPage")] + public bool HasNextPage { get; set; } + + [GraphQLMember("hasPreviousPage")] + [JsonPropertyName("hasPreviousPage")] + public bool HasPreviousPage { get; set; } + + [GraphQLMember("startCursor")] + [JsonPropertyName("startCursor")] + public string StartCursor { get; set; } + + [GraphQLMember("endCursor")] + [JsonPropertyName("endCursor")] + public string EndCursor { get; set; } + + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/LazyProperty.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/LazyProperty.cs new file mode 100644 index 00000000..fe1ec1f5 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Common/LazyProperty.cs @@ -0,0 +1,35 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System.IO; +using System.Net.Http; +using System.Threading; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Common +{ + public class LazyProperty + { + bool _initialized = false; + T _result; + + public T Value(Func fn) + { + if (!_initialized) + { + _result = fn(); + _initialized = true; + } + return _result; + } + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Converters/CustomScalarConverter.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Converters/CustomScalarConverter.cs new file mode 100644 index 00000000..26460c87 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Converters/CustomScalarConverter.cs @@ -0,0 +1,56 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public class CustomScalarConverter : JsonConverter + where TScalar : CustomScalar, new() +{ + public override TScalar Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + object value = null; + switch (reader.TokenType) + { + case JsonTokenType.Null: + case JsonTokenType.None: + return null; + case JsonTokenType.String: + value = reader.GetString(); + break; + case JsonTokenType.Number: + value = reader.GetDecimal(); + break; + case JsonTokenType.True: + value = true; + break; + case JsonTokenType.False: + value = false; + break; + default: + value = JsonDocument.ParseValue(ref reader); + break; + } + + return new() { Value = value }; + } + + public override void Write(Utf8JsonWriter writer, TScalar value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.InternalValue?.ToString()); + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Converters/EnumConverter.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Converters/EnumConverter.cs new file mode 100644 index 00000000..f99f6cad --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Converters/EnumConverter.cs @@ -0,0 +1,359 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + + +public class JsonStringEnumMemberConverter : JsonConverterFactory +{ + private readonly bool _allowIntegerValues; + private readonly JsonNamingPolicy _namingPolicy; + + public JsonStringEnumMemberConverter() + : this(null, true) { } + + public JsonStringEnumMemberConverter(JsonNamingPolicy namingPolicy = null, bool allowIntegerValues = true) + { + _namingPolicy = namingPolicy; + _allowIntegerValues = allowIntegerValues; + } + + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsEnum; + } + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return (JsonConverter)Activator.CreateInstance( + typeof(Converter<>).MakeGenericType(typeToConvert), + BindingFlags.Instance | BindingFlags.Public, + null, + new object[] { _namingPolicy, _allowIntegerValues }, + null); + } + + private class Converter : JsonConverter + where T : struct, Enum + { + private static readonly Type s_enumType = typeof(T); + private static readonly TypeCode s_enumTypeCode = Type.GetTypeCode(s_enumType); + + private readonly bool _allowIntegerValues; + private readonly bool _isFlags; + private readonly Dictionary _rawToTransformed; + private readonly Dictionary _transformedToRaw; + + public Converter(JsonNamingPolicy namingPolicy = null, bool allowIntegerValues = true) + { + _allowIntegerValues = allowIntegerValues; + + _isFlags = s_enumType.IsDefined(typeof(FlagsAttribute), true); + + var builtInNames = s_enumType.GetEnumNames(); + var builtInValues = s_enumType.GetEnumValues(); + + _rawToTransformed = new Dictionary(); + _transformedToRaw = new Dictionary(); + + for (var i = 0; i < builtInNames.Length; i++) + { + var enumValue = (T)builtInValues.GetValue(i); + var rawValue = GetEnumValue(enumValue); + + var name = builtInNames[i]; + + string transformedName; + if (namingPolicy == null) + { + var field = s_enumType.GetField(name, + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static)!; + var enumMemberAttribute = field.GetCustomAttribute(true); + transformedName = enumMemberAttribute?.Value ?? name; + } + else + { + transformedName = namingPolicy.ConvertName(name) ?? name; + } + + _rawToTransformed[rawValue] = new EnumInfo + { + Name = transformedName, + EnumValue = enumValue, + RawValue = rawValue + }; + _transformedToRaw[transformedName] = new EnumInfo + { + Name = name, + EnumValue = enumValue, + RawValue = rawValue + }; + } + } + + private static ulong GetEnumValue(object value) + { + switch (s_enumTypeCode) + { + case TypeCode.Int32: + return (ulong)(int)value; + case TypeCode.UInt32: + return (uint)value; + case TypeCode.UInt64: + return (ulong)value; + case TypeCode.Int64: + return (ulong)(long)value; + + case TypeCode.SByte: + return (ulong)(sbyte)value; + case TypeCode.Byte: + return (byte)value; + case TypeCode.Int16: + return (ulong)(short)value; + case TypeCode.UInt16: + return (ushort)value; + } + + throw new NotSupportedException(); + } + + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var token = reader.TokenType; + + if (token == JsonTokenType.String) + { + var enumString = reader.GetString(); + + // Case sensitive search attempted first. + if (_transformedToRaw.TryGetValue(enumString, out var enumInfo)) + { + return (T)Enum.ToObject(s_enumType, enumInfo.RawValue); + } + + if (_isFlags) + { + ulong calculatedValue = 0; + + var flagValues = enumString.Split(", "); + foreach (var flagValue in flagValues) + { + // Case sensitive search attempted first. + if (_transformedToRaw.TryGetValue(flagValue, out enumInfo)) + { + calculatedValue |= enumInfo.RawValue; + } + else + { + // Case insensitive search attempted second. + + var matched = false; + foreach (var enumItem in _transformedToRaw) + { + if (string.Equals(enumItem.Key, flagValue, StringComparison.OrdinalIgnoreCase)) + { + calculatedValue |= enumItem.Value.RawValue; + matched = true; + break; + } + } + + if (!matched) + { + throw new NotSupportedException(); + } + } + } + + return (T)Enum.ToObject(s_enumType, calculatedValue); + } + + // Case insensitive search attempted second. + foreach (var enumItem in _transformedToRaw) + { + if (string.Equals(enumItem.Key, enumString, StringComparison.OrdinalIgnoreCase)) + { + return (T)Enum.ToObject(s_enumType, enumItem.Value.RawValue); + } + } + + if (_transformedToRaw.TryGetValue("", out var value)) + { + return value.EnumValue; + } + + throw new NotSupportedException(); + } + + if (token != JsonTokenType.Number || !_allowIntegerValues) + { + throw new NotSupportedException(); + } + + switch (s_enumTypeCode) + { + // Switch cases ordered by expected frequency + + case TypeCode.Int32: + if (reader.TryGetInt32(out var int32)) + { + return (T)Enum.ToObject(s_enumType, int32); + } + + break; + case TypeCode.UInt32: + if (reader.TryGetUInt32(out var uint32)) + { + return (T)Enum.ToObject(s_enumType, uint32); + } + + break; + case TypeCode.UInt64: + if (reader.TryGetUInt64(out var uint64)) + { + return (T)Enum.ToObject(s_enumType, uint64); + } + + break; + case TypeCode.Int64: + if (reader.TryGetInt64(out var int64)) + { + return (T)Enum.ToObject(s_enumType, int64); + } + + break; + + case TypeCode.SByte: + if (reader.TryGetSByte(out var byte8)) + { + return (T)Enum.ToObject(s_enumType, byte8); + } + + break; + case TypeCode.Byte: + if (reader.TryGetByte(out var ubyte8)) + { + return (T)Enum.ToObject(s_enumType, ubyte8); + } + + break; + case TypeCode.Int16: + if (reader.TryGetInt16(out var int16)) + { + return (T)Enum.ToObject(s_enumType, int16); + } + + break; + case TypeCode.UInt16: + if (reader.TryGetUInt16(out var uint16)) + { + return (T)Enum.ToObject(s_enumType, uint16); + } + + break; + } + + throw new NotSupportedException(); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + var rawValue = GetEnumValue(value); + + if (_rawToTransformed.TryGetValue(rawValue, out var enumInfo)) + { + writer.WriteStringValue(enumInfo.Name); + return; + } + + if (_isFlags) + { + ulong calculatedValue = 0; + + var Builder = new StringBuilder(); + foreach (var enumItem in _rawToTransformed) + { + enumInfo = enumItem.Value; + if (!value.HasFlag(enumInfo.EnumValue) + || enumInfo.RawValue == 0) // Definitions with 'None' should hit the cache case. + { + continue; + } + + // Track the value to make sure all bits are represented. + calculatedValue |= enumInfo.RawValue; + + if (Builder.Length > 0) + Builder.Append(", "); + Builder.Append(enumInfo.Name); + } + + if (calculatedValue == rawValue) + { + writer.WriteStringValue(Builder.ToString()); + return; + } + } + + if (!_allowIntegerValues) + { + throw new NotSupportedException(); + } + + switch (s_enumTypeCode) + { + case TypeCode.Int32: + writer.WriteNumberValue((int)rawValue); + break; + case TypeCode.UInt32: + writer.WriteNumberValue((uint)rawValue); + break; + case TypeCode.UInt64: + writer.WriteNumberValue(rawValue); + break; + case TypeCode.Int64: + writer.WriteNumberValue((long)rawValue); + break; + case TypeCode.Int16: + writer.WriteNumberValue((short)rawValue); + break; + case TypeCode.UInt16: + writer.WriteNumberValue((ushort)rawValue); + break; + case TypeCode.Byte: + writer.WriteNumberValue((byte)rawValue); + break; + case TypeCode.SByte: + writer.WriteNumberValue((sbyte)rawValue); + break; + default: + throw new NotSupportedException(); + } + } + + private class EnumInfo + { + public T EnumValue; + public string Name; + public ulong RawValue; + } + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Converters/GraphInputConverter.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Converters/GraphInputConverter.cs new file mode 100644 index 00000000..d6facca6 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Converters/GraphInputConverter.cs @@ -0,0 +1,33 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public class GraphInputConverter : JsonConverter + where ToGraphInputBase : GraphInputBase +{ + public override ToGraphInputBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write(Utf8JsonWriter writer, ToGraphInputBase value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value.GetValues(), options); + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Converters/InterfaceConverter.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Converters/InterfaceConverter.cs new file mode 100644 index 00000000..6a569161 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Converters/InterfaceConverter.cs @@ -0,0 +1,76 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Converters; + +public class InterfaceConverter : JsonConverter + where TImplementation : class, TInterface +{ + public override TInterface Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return JsonSerializer.Deserialize(ref reader, options); + } + + public override void Write(Utf8JsonWriter writer, TInterface value, JsonSerializerOptions options) + { + } +} + +public abstract class InterfaceJsonConverter : JsonConverter +{ + public override T Read(ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + var json = JsonSerializer.Deserialize(ref reader); + if (json is null) + { + return default; + } + + if (!json.ContainsKey("__typename")) + { + return default; + } + + var type = json["__typename"]!.ToString(); + var value = Deserialize(type, json); + return value; + } + + public abstract T Deserialize(string typeName, JsonObject json); + + public override void Write( + Utf8JsonWriter writer, + T value, + JsonSerializerOptions options) + { + switch (value) + { + case null: + JsonSerializer.Serialize(writer, default!, options); + break; + default: + { + JsonSerializer.Serialize(writer, value, value.GetType(), options); + break; + } + } + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Exceptions/GraphErrorCode.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Exceptions/GraphErrorCode.cs new file mode 100644 index 00000000..ad72992a --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Exceptions/GraphErrorCode.cs @@ -0,0 +1,70 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public enum GraphErrorCode +{ + Unknown, + Authentication, + Authorization, + Forbidden, + Validation, + BadRequest, + NotFound, + RateLimited, + InternalServerError, + Timeout, + Conflict, + PersistedQueryNotFound, + PersistedQueryMismatch +} + +internal static class GraphErrorCodeClassifier +{ + private static readonly Dictionary CodeMap = new(StringComparer.OrdinalIgnoreCase) + { + // Apollo / GraphQL-over-HTTP codes + { "UNAUTHENTICATED", GraphErrorCode.Authentication }, + { "FORBIDDEN", GraphErrorCode.Forbidden }, + { "BAD_USER_INPUT", GraphErrorCode.BadRequest }, + { "VALIDATION_FAILED", GraphErrorCode.Validation }, + { "GRAPHQL_VALIDATION_FAILED", GraphErrorCode.Validation }, + { "INTERNAL_SERVER_ERROR", GraphErrorCode.InternalServerError }, + { "PERSISTED_QUERY_NOT_FOUND", GraphErrorCode.PersistedQueryNotFound }, + { "PERSISTED_QUERY_NOT_SUPPORTED", GraphErrorCode.PersistedQueryMismatch }, + // Hot Chocolate codes + { "AUTH_NOT_AUTHENTICATED", GraphErrorCode.Authentication }, + { "AUTH_NOT_AUTHORIZED", GraphErrorCode.Authorization }, + { "HC0018", GraphErrorCode.Validation }, + { "HC0008", GraphErrorCode.BadRequest }, + { "HC0012", GraphErrorCode.NotFound }, + { "HC0001", GraphErrorCode.InternalServerError }, + // Common patterns + { "RATE_LIMITED", GraphErrorCode.RateLimited }, + { "RATE_LIMIT_EXCEEDED", GraphErrorCode.RateLimited }, + { "THROTTLED", GraphErrorCode.RateLimited }, + { "TIMEOUT", GraphErrorCode.Timeout }, + { "REQUEST_TIMEOUT", GraphErrorCode.Timeout }, + { "CONFLICT", GraphErrorCode.Conflict }, + { "NOT_FOUND", GraphErrorCode.NotFound }, + }; + + internal static GraphErrorCode Classify(string code) + { + if (string.IsNullOrWhiteSpace(code)) return GraphErrorCode.Unknown; + return CodeMap.TryGetValue(code, out var errorCode) ? errorCode : GraphErrorCode.Unknown; + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Exceptions/GraphQueryExecutionException.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Exceptions/GraphQueryExecutionException.cs new file mode 100644 index 00000000..b8052a3f --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Exceptions/GraphQueryExecutionException.cs @@ -0,0 +1,65 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json.Serialization; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public class GraphQueryExecutionException : Exception +{ + public GraphQueryExecutionException(string query, Dictionary variables) : base( + "Unexpected error response received from server.") + { + GraphQLQuery = query; + GraphQLVariables = variables; + } + + public GraphQueryExecutionException(IEnumerable errors, string query, + Dictionary variables) + : base($"{errors.FirstOrDefault()?.Message} - Check {nameof(Errors)} property for details") + { + Errors = errors; + GraphQLQuery = query; + GraphQLVariables = variables; + } + + public string GraphQLQuery { get; private set; } + public Dictionary GraphQLVariables { get; private set; } + + public IEnumerable Errors { get; private set; } +} + +public class GraphQueryError +{ + [JsonPropertyName("message")] public string Message { get; set; } + + [JsonPropertyName("locations")] public ErrorLocation[] Locations { get; set; } + + [JsonPropertyName("path")] public List Path { get; set; } + + [JsonPropertyName("extensions")] public Dictionary Extensions { get; set; } + + public GraphErrorCode ErrorCode => + Extensions?.TryGetValue("code", out var codeObj) == true + ? GraphErrorCodeClassifier.Classify(codeObj?.ToString()) + : GraphErrorCode.Unknown; +} + +public class ErrorLocation +{ + [JsonPropertyName("line")] public int Line { get; set; } + + [JsonPropertyName("column")] public int Column { get; set; } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Exceptions/GraphQueryRequestException.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Exceptions/GraphQueryRequestException.cs new file mode 100644 index 00000000..eb680963 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Exceptions/GraphQueryRequestException.cs @@ -0,0 +1,29 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public class GraphQueryRequestException : Exception +{ + public GraphQueryRequestException(string message, string query, Dictionary variables) : + base(message) + { + GraphQLQuery = query; + GraphQLVariables = variables; + } + + public string GraphQLQuery { get; private set; } + public Dictionary GraphQLVariables { get; private set; } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Exceptions/GraphQueryTranslationException.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Exceptions/GraphQueryTranslationException.cs new file mode 100644 index 00000000..27a1409d --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Exceptions/GraphQueryTranslationException.cs @@ -0,0 +1,50 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Linq.Expressions; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +/// +/// Thrown when an expression passed to Include or Select cannot be turned into a GraphQL +/// selection, either because it uses a construct that has no GraphQL equivalent or because it reads +/// something that is not part of the query. +/// +/// +/// The query text does not exist yet when translation fails, so unlike +/// and this +/// exception carries the offending instead. +/// It derives from , which is what the parser threw before this type +/// existed, so callers that catch that keep working. +/// +public class GraphQueryTranslationException : NotSupportedException +{ + public GraphQueryTranslationException(string message, Expression expression, string memberName = null) + : base(message) + { + Expression = expression; + MemberName = memberName; + } + + /// + /// The part of the expression that could not be translated. + /// + public Expression Expression { get; } + + /// + /// The LINQ operator or GraphQL member the failure is about, when it is about a single one. + /// + public string MemberName { get; } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Extensions.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Extensions.cs new file mode 100644 index 00000000..3291cc34 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Extensions.cs @@ -0,0 +1,80 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Reflection; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +internal static class Extensions +{ + internal static bool IsValueTypeOrString(this Type type) + { + if (type == null) + { + return false; + } + + return type.IsValueType || type == typeof(string) || type.IsSubclassOf(typeof(CustomScalar)); + ; + } + + internal static bool IsListOfPrimitiveTypeOrString(this Type type) + { + return GetTypeOrListType(type).IsValueTypeOrString(); + } + + private static bool IsList(this Type type) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>); + } + + internal static Type GetTypeOrListType(this Type type) + { + if (type.IsList()) + { + var genericArguments = type.GetGenericArguments(); + + return genericArguments[0].GetTypeOrListType(); + } + + return type; + } + + internal static string ToCamelCase(this string input) + { + if (char.IsLower(input[0])) + { + return input; + } + + return string.Concat(input[..1].ToLower(), input.AsSpan(1)); + } + + internal static Type GetUnderlyingType(this MemberInfo member) + { + return member.MemberType switch + { + MemberTypes.TypeInfo => (Type)member, + MemberTypes.Event => ((EventInfo)member).EventHandlerType, + MemberTypes.Field => ((FieldInfo)member).FieldType, + MemberTypes.Method => ((MethodInfo)member).ReturnType, + MemberTypes.Property => ((PropertyInfo)member).PropertyType, + _ => throw new ArgumentException + ( + "Input MemberInfo must be if type TypeInfo, EventInfo, FieldInfo, MethodInfo, or PropertyInfo" + ) + }; + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphBase.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphBase.cs new file mode 100644 index 00000000..4b8b6c77 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphBase.cs @@ -0,0 +1,55 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Linq.Expressions; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public abstract class GraphBase +{ + protected readonly GraphClient client; + protected readonly OperationType operationType; + + public GraphBase(GraphClient client, string name, OperationType operationType, List arguments) + { + this.client = client; + this.operationType = operationType; + QueryNode = new(typeof(T), name, arguments, null, true); + } + + public QueryNode QueryNode { get; } + + + /// + /// Include top node + /// + /// + public TGraph Include() + { + QueryNode.IncludePrimitive = true; + return (TGraph)(object)this; + } + + public TGraph Include(Expression> path) + { + Utilities.ParseExpression(path, QueryNode); + return (TGraph)(object)this; + } + + public void ParseExpression(Expression body) + { + Utilities.ParseExpression(body, QueryNode); + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphBaseExecute.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphBaseExecute.cs new file mode 100644 index 00000000..3ec6e302 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphBaseExecute.cs @@ -0,0 +1,212 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Linq2GraphQL.TestClientStandalone.Runtime.Schema; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; +using System.Text.Json; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public abstract class GraphBaseExecute +{ + protected readonly GraphClient client; + protected readonly OperationType operationType; + protected readonly QueryExecutor queryExecutor; + public readonly QueryNode QueryNode; + protected readonly Func mapper; + protected readonly Expression> selector; + + private readonly SemaphoreSlim _lock = new(1, 1); + private volatile bool intialized; + private GraphQLSchema schema; + + public GraphBaseExecute(GraphClient client, OperationType operationType, QueryNode queryNode, + Expression> selector) + { + this.client = client; + this.operationType = operationType; + this.QueryNode = queryNode; + // Identity selector (r => r) is the common case — skip the expression compilation; + // ConvertResult already handles a null mapper as identity. + this.mapper = selector == null || selector.Body == selector.Parameters[0] ? null : selector.Compile(); + this.selector = selector; + queryExecutor = new QueryExecutor(client); + } + + private async Task InitQueryAsync(CancellationToken cancellationToken) + { + // Fast path: initialization is one-shot; skip the semaphore once done. + if (intialized) + { + return; + } + + // Outside the try: a cancelled wait never acquired the lock, so it must not release it. + await _lock.WaitAsync(cancellationToken); + try + { + if (!intialized) + { + // A cancelled schema fetch throws before intialized is set, so the next caller retries. + schema = await client.GetSchemaForSafeModeAsync(cancellationToken); + QueryNode.SetAllUniqueVariableNames(); + QueryNode.AddPrimitiveChildren(true, schema); + intialized = true; + } + + } + finally + { + _lock.Release(); + } + } + + public GraphQLSchema Schema => schema; + + private string GetQueryVariablesString(IReadOnlyList args) + { + if (args.Count == 0) + { + return ""; + } + + var text = new StringBuilder(args.Count * 32); + foreach (var argument in args) + { + text.Append('$').Append(argument.VariableName).Append(": ").Append(argument.GraphType).Append(' '); + } + + return $"({text})"; + } + + public Task GetRequestAsJsonAsync() + { + return GetRequestAsJsonAsync(CancellationToken.None); + } + + /// + /// Like , but the one-time safe-mode setup (waiting for + /// initialization and the schema query) stops when is cancelled. + /// + public async Task GetRequestAsJsonAsync(CancellationToken cancellationToken) + { + var request = await GetRequestAsync(cancellationToken); + + + var result = request.Query; + + result += Environment.NewLine; + result += Environment.NewLine; + result += JsonSerializer.Serialize(request.Variables, client.SerializerOptions); + + return result; + } + + + /// + /// The request this query would send, built without contacting the server: safe mode's schema check is + /// skipped, so automatically included fields are not validated against the live schema. Use it to show or + /// log a query; builds the request the query actually sends. + /// + public GraphQLRequest GetRequest() + { + _lock.Wait(); + try + { + if (!intialized) + { + QueryNode.SetAllUniqueVariableNames(); + QueryNode.AddPrimitiveChildren(true, null); + intialized = true; + } + } + finally + { + _lock.Release(); + } + + var args = QueryNode.GetAllActiveArguments(); + + return new GraphQLRequest + { + Query = GetGraphQLQuery(args), + Variables = args.ToDictionary(x => x.VariableName, e => e.Value) + }; + } + + public Task GetRequestAsync() + { + return GetRequestAsync(CancellationToken.None); + } + + /// + /// Builds the request this query sends. The first call does one-time setup: with + /// on, that includes the schema query (unless the schema is + /// already cached). Cancelling stops waiting for that setup and cancels + /// this caller's schema query; nothing is cached, so a later call starts the setup again. + /// + public async Task GetRequestAsync(CancellationToken cancellationToken) + { + await InitQueryAsync(cancellationToken); + + // Compute active arguments once for both the query string and the variables. + var args = QueryNode.GetAllActiveArguments(); + + return new GraphQLRequest + { + Query = GetGraphQLQuery(args), + Variables = args.ToDictionary(x => x.VariableName, e => e.Value) + }; + } + + private string GetGraphQLQuery(IReadOnlyList args) + { + return GetOperationType() + GetQueryVariablesString(args) + "{ " + Environment.NewLine + + QueryNode.GetQueryString() + Environment.NewLine + "}"; + } + + private string GetOperationType() + { + return operationType.ToString().ToLower(); + } + + public TResult ConvertResult(T result) + { + if (Equals(result, default(T))) + { + return default; + } + + if (mapper == null) + { + return (TResult)(object)result; + } + else + { + return mapper.Invoke(result); + } + } + + public GraphResult ConvertResultFull(GraphResult result) + { + return new GraphResult + { + Data = ConvertResult(result.Data), + Errors = result.Errors, + Extensions = result.Extensions + }; + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphClient.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphClient.cs new file mode 100644 index 00000000..7c3c75f2 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphClient.cs @@ -0,0 +1,226 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; +using Linq2GraphQL.TestClientStandalone.Runtime.Converters; +using Linq2GraphQL.TestClientStandalone.Runtime.Schema; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public class GraphClient +{ + private readonly IMemoryCache cache; + private readonly IOptions options; + private readonly bool includeDeprecated; + + /// + /// Constructor for backward compatibility with code compiled against 0.13.83 and earlier. + /// This constructor calls the 4-parameter constructor with includeDeprecated = false. + /// + public GraphClient(HttpClient httpClient, IOptions options, IServiceProvider provider) + : this(httpClient, options, provider, false) + { + } + + /// + /// Constructor with optional includeDeprecated parameter. + /// + public GraphClient(HttpClient httpClient, IOptions options, IServiceProvider provider, + bool includeDeprecated = false) + { + this.options = options; + this.includeDeprecated = includeDeprecated; + if (options.Value.UseSafeMode) + { + cache = provider.GetRequiredService(); + } + + HttpClient = httpClient; + + // Shared frozen options: one metadata cache for all clients instead of one per request. + SerializerOptions = Common.ClientJson.Default; + + SubscriptionUrl = GetSubscriptionUrl(); + } + + private GraphClient(GraphClient source, Func configureRequest) + { + options = source.options; + includeDeprecated = source.includeDeprecated; + cache = source.cache; + HttpClient = source.HttpClient; + SerializerOptions = source.SerializerOptions; + SubscriptionUrl = source.SubscriptionUrl; + requestConfigurations = [..source.requestConfigurations, configureRequest]; + } + + /// + /// Hooks added with , run in order after + /// . + /// + private readonly Func[] requestConfigurations = []; + + /// + /// A client that shares this client's HttpClient, options, schema cache and serializer settings, and also + /// runs on every request it sends, after the hooks already in place. + /// Use it for per-user or per-conversation headers such as an access token. Creating one is cheap, and + /// this client is not changed. + /// + public GraphClient WithRequestConfiguration(Func configureRequest) + { + ArgumentNullException.ThrowIfNull(configureRequest); + return new GraphClient(this, configureRequest); + } + + /// + /// Executes a query written as GraphQL text and returns the whole data object as JSON, together + /// with any errors and extensions. GraphQL errors, including a server rejecting the document, are + /// returned in the result rather than thrown; only transport failures throw + /// . The request goes through the same hooks as typed queries. + /// + public Task> ExecuteAsync(string query, IDictionary variables = null, + CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(query); + + var request = new GraphQLRequest + { + Query = query, + Variables = variables == null ? null : new Dictionary(variables) + }; + return new QueryExecutor(this).ExecuteDocumentAsync(request, cancellationToken); + } + + public string SubscriptionUrl { get; } + public SubscriptionProtocol SubscriptionProtocol => options.Value.SubscriptionProtocol; + public HttpClient HttpClient { get; } + public JsonSerializerOptions SerializerOptions { get; } + + internal GraphClientOptions ClientOptions => options.Value; + + + /// + /// The connection_init payload delegate from , + /// exposed for callers that build transports by hand. The obsolete + /// is the fallback when this is null. + /// + public Func> ConnectionInitPayload => + options.Value.ConnectionInitPayload; + + [Obsolete("Use ConnectionInitPayload instead: it can return any JSON object (an authorization payload, " + + "for example), not just a GraphQLRequest.")] + public Func> WSConnectionInitPayload => options.Value.WSConnectionInitPayload; + + internal async ValueTask GetConnectionInitPayloadAsync(CancellationToken cancellationToken) + { + if (options.Value.ConnectionInitPayload is { } payload) + { + return await payload(this, cancellationToken).ConfigureAwait(false); + } + +#pragma warning disable CS0618 // WSConnectionInitPayload is the legacy shape, kept for source compatibility. + if (options.Value.WSConnectionInitPayload is { } legacy) + { + return await legacy(this).ConfigureAwait(false); + } +#pragma warning restore CS0618 + + return null; + } + + internal async ValueTask ConfigureRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (options.Value.ConfigureRequest is { } configureRequest) + { + await configureRequest(request, cancellationToken); + } + + foreach (var requestConfiguration in requestConfigurations) + { + await requestConfiguration(request, cancellationToken); + } + } + + private string GetSubscriptionUrl() + { + var baseUrl = HttpClient?.BaseAddress.ToString(); + if (string.IsNullOrWhiteSpace(baseUrl)) + { + throw new ArgumentNullException("HttpClient BaseAddress must be set"); + } + + if (SubscriptionProtocol == SubscriptionProtocol.ServerSentEvents) + { + return baseUrl; + } + + return HttpClient.BaseAddress.Scheme switch + { + "http" => new UriBuilder(HttpClient.BaseAddress) { Scheme = "ws" }.Uri.ToString(), + "https" => new UriBuilder(HttpClient.BaseAddress) { Scheme = "wss" }.Uri.ToString(), + _ => throw new Exception($"BaseAddress Scheme {HttpClient.BaseAddress.Scheme} is unknown") + }; + } + + public Task GetSchemaForSafeModeAsync() + { + return GetSchemaForSafeModeAsync(CancellationToken.None); + } + + /// + /// The schema safe mode validates against, fetched once and cached per base address, deprecated-fields + /// flag and schema cache key. Returns null when is off. + /// + /// + /// cancels only this caller's schema query. That is safe with + /// IMemoryCache.GetOrCreateAsync: it does not share one in-flight fetch between callers (each + /// cache miss runs its own factory), and a factory that throws, including on cancellation, stores + /// nothing, so a cancelled fetch is never cached. If the fetch is ever made shared (single-flight), + /// it must not run on one caller's token; each caller would then stop waiting with + /// Task.WaitAsync(cancellationToken) instead. + /// + public async Task GetSchemaForSafeModeAsync(CancellationToken cancellationToken) + { + if (!options.Value.UseSafeMode) + { + return null; + } + + // The schema type is part of the key: clients that carry their own copy of the runtime + // share the app's IMemoryCache but cannot read each other's GraphQLSchema. + var cacheKey = $"Linq2GraphQL_Schema:{typeof(GraphQLSchema).AssemblyQualifiedName}:{HttpClient.BaseAddress}:{includeDeprecated}:{options.Value.SchemaCacheKey}"; + + return await cache.GetOrCreateAsync(cacheKey, async entry => + { + var executor = new QueryExecutor(this); + + string query; + if (includeDeprecated) + { + query = Helpers.SchemaQueryIncludeDeprecated; + } + else + { + query = Helpers.SchemaQuery; + } + + var graphRequest = new GraphQLRequest { Query = query }; + return await executor.ExecuteRequestAsync("__schema", graphRequest, cancellationToken); + }); + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphClientOptions.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphClientOptions.cs new file mode 100644 index 00000000..41246f1c --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphClientOptions.cs @@ -0,0 +1,115 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +/// +/// What happens when the subscription event buffer (SubscriptionMaxBufferedEvents) +/// overflows because the observer cannot keep up. +/// +public enum SubscriptionOverflowPolicy +{ + /// Fault the subscription with an InvalidOperationException (default - never drops events silently). + Fault = 0, + + /// Drop the oldest queued events to make room; the newest events are always delivered. + DropOldest, + + /// Keep only the most recent event; observers that only need the latest value. + KeepLatest, +} + +public class GraphClientOptions +{ + public bool UseSafeMode { get; set; } = false; + public SubscriptionProtocol SubscriptionProtocol { get; set; } = default; + + /// + /// Sets the connection_init payload of the WebSocket subscription transports. Return any + /// JSON object the server expects as connection parameters, for example + /// new Dictionary<string, object> { ["Authorization"] = $"Bearer {token}" } (the + /// graphql-ws and Apollo convention) or { { "token", token } }. Called before every + /// (re)connect, before the socket opens, so a refreshed token is picked up and a slow token + /// fetch cannot run into the server's connection_init wait; it counts within the connect + /// timeout (30 s) together with the request hooks. Overrides the obsolete + /// when set. When + /// is enabled the payload must serialize to a + /// JSON object (a dictionary, an anonymous object, a POCO, a JsonObject...) so the + /// Authorization parameter can be merged. + /// + public Func> ConnectionInitPayload { get; set; } + + [Obsolete("Use ConnectionInitPayload instead: it can return any JSON object (an authorization payload, " + + "for example), not just a GraphQLRequest. Superseded when ConnectionInitPayload is set.")] + public Func> WSConnectionInitPayload { get; set; } = + _ => Task.FromResult(null!); + + /// + /// Copies the Authorization header produced by and + /// derived clients (WithAccessToken on a generated client) into the connection_init + /// payload. This is how a per-user token reaches the server on Blazor WebAssembly, where the + /// browser does not allow headers on the WebSocket upgrade request. An Authorization in + /// is used when no hook sets one. The header is + /// merged into the JSON object from and wins on key conflicts. Like + /// any connection_init payload, the token is only as protected as the transport: prefer + /// wss:// so it is not sent in clear text. + /// + public bool SendAuthorizationInConnectionInit { get; set; } + + /// + /// Called for every request the client sends (queries, mutations, the safe-mode schema query, + /// server-sent-event subscriptions and the WebSocket subscription upgrade) just before it is + /// sent. On the WebSocket upgrade the hook's headers, together with the + /// (a hook header wins on a name conflict), are + /// copied to the upgrade request. Use it + /// to set per-request headers, such as an access token, without a + /// . On Blazor WebAssembly the browser ignores headers on + /// WebSocket requests; set to send the token + /// in the connection_init payload instead. + /// + public Func ConfigureRequest { get; set; } + + /// + /// Separates the safe-mode schema cache entry of this client from other clients that use the same base + /// address. Set it when such clients see different schemas, for example because they use different + /// credentials. + /// + public string SchemaCacheKey { get; set; } + + /// + /// Maximum number of subscription events buffered between the transport and the + /// observer. Overflow behavior is controlled by SubscriptionOverflowPolicy. + /// + public int SubscriptionMaxBufferedEvents { get; set; } = 128; + + /// Behavior when the subscription event buffer overflows. + public SubscriptionOverflowPolicy SubscriptionOverflowPolicy { get; set; } = SubscriptionOverflowPolicy.Fault; + + /// + /// Optional idle timeout for SSE subscriptions. If no bytes (including keep-alive + /// comments) arrive within this window, the subscription fails. The window is measured + /// per read while waiting for data, so time spent handling an event never counts as idle. + /// Null disables it. + /// + public TimeSpan? SubscriptionIdleTimeout { get; set; } + + /// + /// Maximum number of WebSocket reconnect attempts after a dropped connection before + /// the subscription fails with an error. Attempts use exponential backoff with jitter + /// (graphql-ws default: 2^n seconds plus 300-3000 ms). A successful connection resets + /// the count. Fatal close codes never retry. Default 5; 0 disables reconnection. + /// + public int SubscriptionRetryAttempts { get; set; } = 5; +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphCursorPager.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphCursorPager.cs new file mode 100644 index 00000000..300848ca --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphCursorPager.cs @@ -0,0 +1,179 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public class GraphCursorPager where T : ICursorPaging +{ + private GraphCursorQueryExecute query; + + // The page size the query was built with. Moving forward pages with `first`, moving + // back with `last`; a server only counts back from `before` when `last` is set. + private readonly object pageSize; + + public GraphCursorPager(GraphCursorQueryExecute graphCursorQueryExecute) + { + this.query = graphCursorQueryExecute; + pageSize = GetArgumentValue("first") ?? GetArgumentValue("last"); + IncludeT(e => e.PageInfo); + } + + public T PagerResult => query.BaseResult; + + private GraphCursorPager IncludeT(Expression> path) + { + Utilities.ParseExpression(path, query.QueryNode); + return this; + } + + private async Task ExecutePagerAsync(CancellationToken cancellationToken = default) + { + var baseType = await query.ExecuteBaseAsync(cancellationToken); + return query.ConvertResult(baseType); + } + + private async Task> ExecutePagerWithResultAsync(CancellationToken cancellationToken = default) + { + var result = await query.ExecuteBaseWithResultAsync(cancellationToken); + return query.ConvertResultFull(result); + } + + private object GetArgumentValue(string graphName) + { + return query.QueryNode.Arguments.FirstOrDefault(e => e.GraphName == graphName)?.Value; + } + + private void SetNextPageArguments() + { + query.QueryNode.SetArgumentValue("first", pageSize); + query.QueryNode.SetArgumentValue("after", query.BaseResult?.PageInfo?.EndCursor); + query.QueryNode.SetArgumentValue("last", null); + query.QueryNode.SetArgumentValue("before", null); + } + + private void SetPreviousPageArguments() + { + query.QueryNode.SetArgumentValue("first", null); + query.QueryNode.SetArgumentValue("after", null); + query.QueryNode.SetArgumentValue("last", pageSize); + query.QueryNode.SetArgumentValue("before", query.BaseResult?.PageInfo?.StartCursor); + } + + public async Task NextPageAsync(CancellationToken cancellationToken = default) + { + SetNextPageArguments(); + return await ExecutePagerAsync(cancellationToken); + } + + public async Task> NextPageWithResultAsync(CancellationToken cancellationToken = default) + { + SetNextPageArguments(); + return await ExecutePagerWithResultAsync(cancellationToken); + } + + public async Task PreviousPageAsync(CancellationToken cancellationToken = default) + { + SetPreviousPageArguments(); + return await ExecutePagerAsync(cancellationToken); + } + + public async Task> PreviousPageWithResultAsync(CancellationToken cancellationToken = default) + { + SetPreviousPageArguments(); + return await ExecutePagerWithResultAsync(cancellationToken); + } + + /// + /// Pages forward from the first page until the server reports no next page, yielding one result per page. + /// Each page is requested only when the caller asks for it, so breaking out of the loop stops paging; + /// the pager is then left on the last page that was read. + /// + /// Stop after this many pages even if the server reports more. + /// is less than 1. + public async IAsyncEnumerable GetAllPagesAsync(int? maxPages = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + ValidateMaxPages(maxPages); + SetFirstPageArguments(); + + for (var pageCount = 1; ; pageCount++) + { + var previousEndCursor = query.BaseResult?.PageInfo?.EndCursor; + yield return await ExecutePagerAsync(cancellationToken); + + if (!HasMorePages(previousEndCursor, pageCount, maxPages)) + { + yield break; + } + + SetNextPageArguments(); + } + } + + /// + /// Like , but yields each page's so errors and + /// extensions are kept. Paging stops after the first page that has errors or no data. + /// + /// Stop after this many pages even if the server reports more. + /// is less than 1. + public async IAsyncEnumerable> GetAllPagesWithResultAsync(int? maxPages = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + ValidateMaxPages(maxPages); + SetFirstPageArguments(); + + for (var pageCount = 1; ; pageCount++) + { + var previousEndCursor = query.BaseResult?.PageInfo?.EndCursor; + var result = await ExecutePagerWithResultAsync(cancellationToken); + yield return result; + + if (result.HasErrors || !result.HasData || !HasMorePages(previousEndCursor, pageCount, maxPages)) + { + yield break; + } + + SetNextPageArguments(); + } + } + + private void SetFirstPageArguments() + { + query.BaseResult = default; + SetNextPageArguments(); + } + + // Also stops when the end cursor does not move, so a server that keeps reporting a next page + // without advancing cannot keep the loop going forever. + private bool HasMorePages(string previousEndCursor, int pageCount, int? maxPages) + { + var pageInfo = query.BaseResult?.PageInfo; + return pageInfo is { HasNextPage: true, EndCursor: not null } + && pageInfo.EndCursor != previousEndCursor + && (maxPages == null || pageCount < maxPages); + } + + private static void ValidateMaxPages(int? maxPages) + { + if (maxPages < 1) + { + throw new ArgumentOutOfRangeException(nameof(maxPages), maxPages, "Must be at least 1."); + } + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphCursorQuery.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphCursorQuery.cs new file mode 100644 index 00000000..7a65efff --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphCursorQuery.cs @@ -0,0 +1,39 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Linq.Expressions; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public class GraphCursorQuery : GraphBase> where T : ICursorPaging +{ + public GraphCursorQuery(GraphClient client, string name, OperationType operationType, List arguments) + : base(client, name, operationType, arguments) + { + } + + public GraphCursorQueryExecute Select() + { + QueryNode.IncludePrimitiveIfNoChildPrimitive(); + return new(client, operationType, QueryNode, null); + } + + public GraphCursorQueryExecute Select(Expression> selector) + { + ParseExpression(selector); + return new(client, operationType, QueryNode, selector); + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphInputBase.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphInputBase.cs new file mode 100644 index 00000000..8a4ecc01 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphInputBase.cs @@ -0,0 +1,50 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Concurrent; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public abstract class GraphInputBase +{ + private ConcurrentDictionary Values { get; } = new(); + + internal Dictionary GetValues() + { + return Values.ToDictionary(e => e.Key, e => e.Value); + } + + protected void ClearValues() + { + Values.Clear(); + } + + protected T GetValue(string name) + { + if (Values.TryGetValue(name, out var value)) + { + return (T)value; + } + + return default; + } + + + protected void SetValue(string name, object value) + { + Values.TryRemove(name, out _); + Values.TryAdd(name, value); + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphQLRequest.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphQLRequest.cs new file mode 100644 index 00000000..808d1b03 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphQLRequest.cs @@ -0,0 +1,25 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json.Serialization; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public class GraphQLRequest +{ + [JsonPropertyName("query")] public string Query { get; set; } + + [JsonPropertyName("variables")] public Dictionary Variables { get; set; } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphQuery.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphQuery.cs new file mode 100644 index 00000000..486c6765 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphQuery.cs @@ -0,0 +1,37 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Linq.Expressions; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public class GraphQuery : GraphBase> +{ + public GraphQuery(GraphClient client, string name, OperationType operationType, List arguments) : base(client, name, operationType, arguments) + { + } + + public GraphQueryExecute Select() + { + QueryNode.IncludePrimitiveIfNoChildPrimitive(); + return new GraphQueryExecute(client, operationType, QueryNode, null); + } + + public GraphQueryExecute Select(Expression> selector) + { + ParseExpression(selector); + return new GraphQueryExecute(client, operationType, QueryNode, selector); + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphQueryExecute.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphQueryExecute.cs new file mode 100644 index 00000000..03a58ea8 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphQueryExecute.cs @@ -0,0 +1,64 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; +using System.Linq.Expressions; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public class GraphCursorQueryExecute : GraphQueryExecute where T : ICursorPaging +{ + public GraphCursorQueryExecute(GraphClient client, OperationType operationType, QueryNode queryNode, + Expression> selector) : base(client, operationType, queryNode, selector) { } + + public GraphCursorPager AsPager() + { + return new GraphCursorPager(this); + } +} + +public class GraphQueryExecute : GraphBaseExecute +{ + public GraphQueryExecute(GraphClient client, OperationType operationType, QueryNode queryNode, + Expression> selector) : base(client, operationType, queryNode, selector) { } + + public T BaseResult { get; set; } + + public async Task ExecuteBaseAsync(CancellationToken cancellationToken = default) + { + BaseResult = + await queryExecutor.ExecuteRequestAsync(QueryNode.Name, await GetRequestAsync(cancellationToken), + cancellationToken); + return BaseResult; + } + + public async Task> ExecuteBaseWithResultAsync(CancellationToken cancellationToken = default) + { + var result = await queryExecutor.ExecuteRawAsync(QueryNode.Name, await GetRequestAsync(cancellationToken), + cancellationToken); + BaseResult = result.Data; + return result; + } + + public async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + return ConvertResult(await ExecuteBaseAsync(cancellationToken)); + } + + public async Task> ExecuteWithResultAsync(CancellationToken cancellationToken = default) + { + return ConvertResultFull(await ExecuteBaseWithResultAsync(cancellationToken)); + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphResult.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphResult.cs new file mode 100644 index 00000000..13e29bdc --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/GraphResult.cs @@ -0,0 +1,34 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public class GraphResult +{ + public T Data { get; init; } + public List Errors { get; init; } + public Dictionary Extensions { get; init; } + + public bool HasErrors => Errors is { Count: > 0 }; + public bool HasData => Data is not null; + + public void EnsureNoErrors() + { + if (HasErrors) + { + throw new GraphQueryExecutionException(Errors, string.Empty, null); + } + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/HttpClientBuilderExtensions.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/HttpClientBuilderExtensions.cs new file mode 100644 index 00000000..5bed7b82 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/HttpClientBuilderExtensions.cs @@ -0,0 +1,72 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Net.Http.Headers; +using Microsoft.Extensions.DependencyInjection; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public static class HttpClientBuilderExtensions +{ + private const string userAgentName = "Linq2GraphQL"; + private static readonly string userAgentVersion = ThisAssembly.AssemblyFileVersion; + + public static IHttpClientBuilder WithHttpClient( + this IGraphClientBuilder clientBuilder, + Action configureClient, + Action configureClientBuilder = null) where T : class + { + var builder = clientBuilder.Services + .AddHttpClient( + clientBuilder.ClientName, + client => + { + client.DefaultRequestHeaders.UserAgent.Add( + new ProductInfoHeaderValue( + new ProductHeaderValue( + userAgentName, + userAgentVersion))); + configureClient(client); + }); + + configureClientBuilder?.Invoke(builder); + + return builder; + } + + public static IHttpClientBuilder WithHttpClient( + this IGraphClientBuilder clientBuilder, + string httpClientName, + Action configureClient, + Action configureClientBuilder = null) where T : class + { + var builder = clientBuilder.Services + .AddHttpClient( + httpClientName, + client => + { + client.DefaultRequestHeaders.UserAgent.Add( + new ProductInfoHeaderValue( + new ProductHeaderValue( + userAgentName, + userAgentVersion))); + configureClient(client); + }); + + configureClientBuilder?.Invoke(builder); + + return builder; + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/IGraphClientBuilder.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/IGraphClientBuilder.cs new file mode 100644 index 00000000..71fcede2 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/IGraphClientBuilder.cs @@ -0,0 +1,24 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public interface IGraphClientBuilder where T : class +{ + public string ClientName { get; } + public IServiceCollection Services { get; } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/IGraphClientSource.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/IGraphClientSource.cs new file mode 100644 index 00000000..67ee58bd --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/IGraphClientSource.cs @@ -0,0 +1,28 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +/// +/// Implemented by every generated client: access to its , and a way to derive a +/// client of the same type with an extra request hook. Lets other packages work with any generated client +/// generically, for example to execute queries with a per-user token. +/// +public interface IGraphClientSource where TSelf : IGraphClientSource +{ + GraphClient GraphClient { get; } + + TSelf WithRequestConfiguration(Func configureRequest); +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/IGraphSchemaSource.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/IGraphSchemaSource.cs new file mode 100644 index 00000000..82804c58 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/IGraphSchemaSource.cs @@ -0,0 +1,25 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +/// +/// Implemented by clients generated with --agent: the schema the client was generated from, as SDL. +/// Lets other packages require such a client at compile time. +/// +public interface IGraphSchemaSource +{ + static abstract string Sdl { get; } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/OperationType.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/OperationType.cs new file mode 100644 index 00000000..79cb1bb2 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/OperationType.cs @@ -0,0 +1,23 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public enum OperationType +{ + Query, + Mutation, + Subscription +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/QueryExecutor.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/QueryExecutor.cs new file mode 100644 index 00000000..a36c525a --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/QueryExecutor.cs @@ -0,0 +1,226 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Net.Http.Json; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +/// +/// Every response path parses the GraphQL envelope with ; +/// do not add a second envelope parser here. +/// +public class QueryExecutor +{ + private readonly GraphClient client; + + internal QueryExecutor(GraphClient client) + { + this.client = client; + } + + internal async Task ExecuteRequestAsync(string name, GraphQLRequest graphRequest, + CancellationToken cancellationToken = default) + { + var result = await ExecuteRawAsync(name, graphRequest, cancellationToken); + + if (result.HasErrors) + { + throw new GraphQueryExecutionException(result.Errors, graphRequest.Query, graphRequest.Variables); + } + + return result.Data; + } + + internal async Task> ExecuteRawAsync(string name, GraphQLRequest graphRequest, + CancellationToken cancellationToken = default) + { + using var request = new HttpRequestMessage(HttpMethod.Post, ""); + request.Content = JsonContent.Create(graphRequest, options: client.SerializerOptions); + await client.ConfigureRequestAsync(request, cancellationToken); + + // With ResponseHeadersRead, HttpClient.Timeout only covers the time until the + // headers arrive. Extend it over reading the body with a linked token source. + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + if (client.HttpClient.Timeout != Timeout.InfiniteTimeSpan) + { + timeoutCts.CancelAfter(client.HttpClient.Timeout); + } + + try + { + using var response = await client.HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, + timeoutCts.Token); + + if (!response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(timeoutCts.Token); + content = Truncate(content, MaxErrorMessageLength); + throw new GraphQueryRequestException($"Http error! Status code {response.StatusCode} Error: {content}", + graphRequest.Query, graphRequest.Variables); + } + + await using var stream = await response.Content.ReadAsStreamAsync(timeoutCts.Token); + using var memory = new MemoryStream(); + await stream.CopyToAsync(memory, timeoutCts.Token); + + // Parse in place — GetBuffer avoids copying the body a second time. + return Common.GraphEnvelopeReader.Read( + memory.GetBuffer().AsSpan(0, (int)memory.Length), name, client.SerializerOptions); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + // Only the timeout fired — surface it the way HttpClient does. + throw new TaskCanceledException("The request timed out.", new TimeoutException()); + } + } + + private const int MaxErrorMessageLength = 4096; + + private static string Truncate(string value, int maxLength) + { + if (value == null || value.Length <= maxLength) + { + return value; + } + + return value.Substring(0, maxLength) + "...(truncated)"; + } + + /// + /// Sends a request and returns the whole data object as JSON, for queries written as text rather + /// than built from expressions. + /// + internal async Task> ExecuteDocumentAsync(GraphQLRequest graphRequest, + CancellationToken cancellationToken = default) + { + using var request = new HttpRequestMessage(HttpMethod.Post, "") + { + Content = JsonContent.Create(graphRequest, options: client.SerializerOptions) + }; + await client.ConfigureRequestAsync(request, cancellationToken); + + using var response = await client.HttpClient.SendAsync(request, cancellationToken); + var content = await response.Content.ReadAsByteArrayAsync(cancellationToken); + + if (response.IsSuccessStatusCode) + { + return Common.GraphEnvelopeReader.ReadDocument(content, client.SerializerOptions, out _); + } + + // GraphQL servers answer a request they reject (for example a validation error) with a + // non-success status but a regular GraphQL body; that body is a result, not a transport failure. + GraphResult result = null; + var isEnvelope = false; + try + { + result = Common.GraphEnvelopeReader.ReadDocument(content, client.SerializerOptions, out isEnvelope); + } + catch (JsonException) + { + // Not JSON (HTML error page, proxy text, ...): a transport failure, reported below. + } + + if (!isEnvelope) + { + throw new GraphQueryRequestException( + $"Http error! Status code {response.StatusCode} Error: {Encoding.UTF8.GetString(content)}", + graphRequest.Query, graphRequest.Variables); + } + + return result; + } + + public T ProcessResponse(string con, string name, GraphQLRequest request) + { + var result = ProcessResponseFull(con, name); + + if (result.HasErrors) + { + throw new GraphQueryExecutionException(result.Errors, request.Query, request.Variables); + } + + return result.Data; + } + + /// + /// Parses a response from an already-materialized root + /// (the full envelope) and throws on errors. + /// + public T ProcessResponse(JsonElement root, string name, GraphQLRequest request) + { + var result = ProcessResponseFull(root, name); + + if (result.HasErrors) + { + throw new GraphQueryExecutionException(result.Errors, request.Query, request.Variables); + } + + return result.Data; + } + + /// + /// Parses a response envelope from raw UTF-8 bytes (used by the SSE byte path): + /// only the requested root field is deserialized — no JsonDocument and no UTF-16 + /// round trip, matching the query path. + /// + public T ProcessResponse(byte[] utf8Json, string name, GraphQLRequest request) + { + var result = Common.GraphEnvelopeReader.Read(utf8Json, name, client.SerializerOptions); + + if (result.HasErrors) + { + throw new GraphQueryExecutionException(result.Errors, request.Query, request.Variables); + } + + return result.Data; + } + + /// + /// Full-envelope parse from raw UTF-8 bytes (SSE/WS byte path). Returns data and + /// errors together and never throws on GraphQL errors. + /// + public GraphResult ProcessResponseFull(byte[] utf8Json, string name) + { + return Common.GraphEnvelopeReader.Read(utf8Json, name, client.SerializerOptions); + } + + /// + /// Full-envelope parse from a JSON string. Transcodes to UTF-8 and goes through the same + /// parser as every other response path. An empty string yields a result with no data and + /// no errors. + /// + public GraphResult ProcessResponseFull(string con, string name) + { + ArgumentNullException.ThrowIfNull(con); + return Common.GraphEnvelopeReader.Read(Encoding.UTF8.GetBytes(con), name, client.SerializerOptions); + } + + /// + /// Full-envelope parse from an already-materialized root. The + /// element's raw UTF-8 text is handed to the same parser as every other response path. + /// + public GraphResult ProcessResponseFull(JsonElement root, string name) + { + if (root.ValueKind != JsonValueKind.Object) + { + throw new JsonException("GraphQL response envelope must be a JSON object."); + } + + return Common.GraphEnvelopeReader.Read(JsonMarshal.GetRawUtf8Value(root), name, client.SerializerOptions); + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/QueryNode.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/QueryNode.cs new file mode 100644 index 00000000..2c0193a0 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/QueryNode.cs @@ -0,0 +1,260 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Reflection; +using Linq2GraphQL.TestClientStandalone.Runtime.Schema; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public class QueryNode +{ + private readonly bool mustHaveChildren; + private readonly Type underlyingMemberType; + private string argumentHashCodeId; + + public QueryNode(MemberInfo member, string name = null, List arguments = null, + bool? interfaceProperty = null, bool topLevel = false) + { + var memberAttribute = member.GetCustomAttribute(); + + Name = name ?? memberAttribute?.GraphQLName ?? member.Name.ToCamelCase(); + Member = member; + Arguments = arguments ?? new List(); + underlyingMemberType = member.GetUnderlyingType(); + mustHaveChildren = MustHaveChildren(underlyingMemberType); + InterfaceProperty = memberAttribute?.InterfaceProperty == true; + + if (!topLevel) + { + SetArgumentHashCodeId(); + } + } + + public bool InterfaceProperty { get; internal set; } + public string Name { get; internal set; } + public string Alias { get; internal set; } + + public MemberInfo Member { get; internal set; } + public List ChildNodes { get; internal set; } = new(); + public List Arguments { get; internal set; } = new(); + public bool IncludePrimitive { get; internal set; } + + public QueryNode Parent { get; private set; } + + public int Level => Parent?.Level + 1 ?? 1; + public int Leaf { get; internal set; } = 1; + + + internal void IncludePrimitiveIfNoChildPrimitive() + { + if (!ChildNodes.Any(e => e.underlyingMemberType.IsValueTypeOrString())) + { + IncludePrimitive = true; + } + } + + private void SetArgumentHashCodeId() + { + argumentHashCodeId = Utilities.GetArgumentsId(Arguments?.Select(e => e.Value)); + if (argumentHashCodeId != null) + { + Alias = Name + argumentHashCodeId; + } + } + + + private static bool MustHaveChildren(Type type) + { + return !type.IsValueTypeOrString() && + !type.IsListOfPrimitiveTypeOrString(); + } + + + public void AddChildNode(MemberInfo member, string name = null) + { + AddChildNode(new(member, name)); + } + + public QueryNode AddChildNode(QueryNode childNode) + { + var currentNode = ChildNodes.FirstOrDefault(e => + e.Name == childNode.Name && e.argumentHashCodeId == childNode.argumentHashCodeId); + if (currentNode == null) + { + childNode.Parent = this; + childNode.Leaf = ChildNodes.Count + 1; + ChildNodes.Add(childNode); + return childNode; + } + + if (childNode.IncludePrimitive) + { + currentNode.IncludePrimitive = true; + } + + foreach (var child in childNode.ChildNodes) + { + currentNode.AddChildNode(child); + } + + return currentNode; + } + + public void SetArgumentValue(string graphName, object value) + { + var argument = Arguments.FirstOrDefault(e => e.GraphName == graphName); + if (argument != null) + { + argument.Value = value; + } + } + + public void AddPrimitiveChildren(bool recursive, GraphQLSchema schema) + { + if (recursive) + { + foreach (var child in ChildNodes.Where(e => e.mustHaveChildren)) + { + child.AddPrimitiveChildren(recursive, schema); + } + } + + if (mustHaveChildren && (!ChildNodes.Any() || IncludePrimitive)) + { + var typeOrListType = underlyingMemberType.GetTypeOrListType(); + foreach (var propertyInfo in typeOrListType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (!propertyInfo.PropertyType.IsValueTypeOrString()) + { + continue; + } + + + var memberAttribute = propertyInfo.GetCustomAttribute(); + if (memberAttribute == null) + { + continue; + } + + + if (schema != null) + { + if (schema.TypePropertyExists(typeOrListType.Name, memberAttribute.GraphQLName)) + { + AddChildNode(propertyInfo, memberAttribute.GraphQLName); + } + else + { + Console.WriteLine( + $"Property: {propertyInfo.Name} Type: {Member.Name} was not present in active schema"); + } + } + else + { + AddChildNode(propertyInfo); + } + } + } + } + + internal void SetAllUniqueVariableNames() + { + foreach (var argument in Arguments) + { + argument.VariableName += $"_{Level}_{Leaf}"; + } + + foreach (var child in ChildNodes) + { + child.SetAllUniqueVariableNames(); + } + } + + public List GetActiveArguments() + { + return Arguments.Where(e => e.Value != null).ToList(); + } + + public List GetAllActiveArguments() + { + var allArguments = GetActiveArguments(); + foreach (var childNode in ChildNodes) + { + allArguments.AddRange(childNode.GetAllActiveArguments()); + } + + return allArguments; + } + + private string GetArgumentString() + { + var args = GetActiveArguments(); + ; + if (!args.Any()) + { + return ""; + } + + var argString = "("; + foreach (var arg in args) + { + argString += arg.GraphName + ":$" + arg.VariableName + " "; + } + + argString.TrimEnd(); + argString += ")"; + + return argString; + } + + + internal string GetQueryString(string indent = "") + { + string query; + var memberType = Member.GetUnderlyingType(); + + if (InterfaceProperty) + { + query = "... on " + Name + GetArgumentString() + Environment.NewLine; + } + else if (!string.IsNullOrWhiteSpace(Alias)) + { + query = Alias + ":" + Name + GetArgumentString() + Environment.NewLine; + } + else + { + query = Name + GetArgumentString() + Environment.NewLine; + } + + if (memberType.IsListOfPrimitiveTypeOrString()) + { + return indent + query; + } + + query += indent + "{" + Environment.NewLine; + + if (ChildNodes.Any()) + { + var newIndent = " " + indent; + foreach (var childNode in ChildNodes) + { + query += childNode.GetQueryString(newIndent); + } + } + + query += indent + "}" + Environment.NewLine; + return query; + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/RequestConfigurations.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/RequestConfigurations.cs new file mode 100644 index 00000000..75d85e83 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/RequestConfigurations.cs @@ -0,0 +1,42 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Net.Http.Headers; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +/// +/// Ready-made request hooks for and +/// . +/// +public static class RequestConfigurations +{ + /// + /// Sets Authorization: Bearer <token> on every request, asking + /// for the token each time. Caching and refreshing the token is the delegate's job, as token acquisition + /// libraries already do. + /// + public static Func BearerToken( + Func> getAccessToken) + { + ArgumentNullException.ThrowIfNull(getAccessToken); + + return async (request, cancellationToken) => + { + var token = await getAccessToken(cancellationToken); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + }; + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Schema/GraphQLSchema.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Schema/GraphQLSchema.cs new file mode 100644 index 00000000..cce3f821 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Schema/GraphQLSchema.cs @@ -0,0 +1,51 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics; +using System.Text.Json.Serialization; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Schema; + +public class GraphQLSchema +{ + [JsonPropertyName("types")] public List Types { get; set; } + + + public bool TypePropertyExists(string typeName, string fieldName) + { + if (fieldName.ToLower() == "__typename" ) { return true; } + + var type = Types.FirstOrDefault(x => x.Name == typeName); + if (type?.Fields.FirstOrDefault(x => x.Name == fieldName) == null) + { + return false; + } + + return true; + } +} + +[DebuggerDisplay("{Name}")] +public class GraphQLType +{ + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("fields")] public List Fields { get; set; } +} + +public class GraphQLField +{ + [JsonPropertyName("name")] public string Name { get; set; } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Schema/Helpers.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Schema/Helpers.cs new file mode 100644 index 00000000..7cbefc62 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Schema/Helpers.cs @@ -0,0 +1,41 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime.Schema; + +internal static class Helpers +{ + internal const string SchemaQuery = @"{ + __schema { + types { + name + fields { + name + } + } + } + }"; + + internal const string SchemaQueryIncludeDeprecated = @"{ + __schema { + types { + name + fields(includeDeprecated: true) { + name + } + } + } + }"; +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/SubscriptionSubprotocol.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/SubscriptionSubprotocol.cs new file mode 100644 index 00000000..4c725261 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/SubscriptionSubprotocol.cs @@ -0,0 +1,37 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public enum SubscriptionProtocol +{ + /// + /// Default protocol + /// actually named graphql-ws just to make things even more confusing + /// sends: graphql-transport-ws as subprotocol + /// + GraphQLWebSocket, + + /// + /// Old apollo protocol + /// sends: graphql-ws as subprotocol + /// + ApolloWebSocket, + + /// + /// SSE + /// + ServerSentEvents +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Utilities.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Utilities.cs new file mode 100644 index 00000000..6db5848e --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Utilities.cs @@ -0,0 +1,200 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Collections; +using System.Globalization; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using Linq2GraphQL.TestClientStandalone.Runtime.Visitors; + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +public static class Utilities +{ + private const int MaxArgumentDepth = 8; + + /// Separates one argument from the next so that adjacent values cannot run together. + private const char Separator = (char)31; + + /// + /// Identifies a set of field arguments so that the same field requested with different arguments becomes + /// several aliased selections. The result is derived from the argument values only, because the alias has + /// to be recomputed from the same values when the response is read back. + /// + /// An alias suffix, or null when there are no arguments to distinguish. + public static string GetArgumentsId(IEnumerable objects) + { + if (objects == null) + { + return null; + } + + var values = objects.Where(o => o != null).ToList(); + if (values.Count == 0) + { + return null; + } + + var builder = new StringBuilder(); + foreach (var value in values) + { + AppendValue(builder, value, 0); + builder.Append(Separator); + } + + return "_" + Hash(builder).ToString("x16"); + } + + public static void ParseExpression(Expression body, QueryNode parent) + { + QueryExpressionVisitor.Parse(body, parent); + } + + /// + /// Writes a value in a form that is identical for equal values and stable between processes, which + /// is not for reference types. + /// + private static void AppendValue(StringBuilder builder, object value, int depth) + { + switch (value) + { + case null: + builder.Append("null"); + return; + + case string text: + builder.Append('"').Append(text).Append('"'); + return; + + case bool flag: + builder.Append(flag ? "true" : "false"); + return; + + case Enum enumValue: + builder.Append(enumValue.GetType().FullName).Append('.').Append(enumValue.ToString("D")); + return; + + case DateTime dateTime: + builder.Append(dateTime.ToString("O", CultureInfo.InvariantCulture)); + return; + + case DateTimeOffset dateTimeOffset: + builder.Append(dateTimeOffset.ToString("O", CultureInfo.InvariantCulture)); + return; + + case TimeSpan timeSpan: + builder.Append(timeSpan.ToString("c", CultureInfo.InvariantCulture)); + return; + + case CustomScalar scalar: + AppendValue(builder, scalar.Value, depth + 1); + return; + + case IFormattable formattable: + // Numbers, Guid, DateOnly, TimeOnly. + builder.Append(formattable.ToString(null, CultureInfo.InvariantCulture)); + return; + + case IEnumerable items: + AppendItems(builder, items, depth); + return; + + default: + AppendProperties(builder, value, depth); + return; + } + } + + private static void AppendItems(StringBuilder builder, IEnumerable items, int depth) + { + if (depth >= MaxArgumentDepth) + { + builder.Append("[...]"); + return; + } + + builder.Append('['); + foreach (var item in items) + { + AppendValue(builder, item, depth + 1); + builder.Append(','); + } + + builder.Append(']'); + } + + /// Input objects are compared by their contents, in a fixed property order. + private static void AppendProperties(StringBuilder builder, object value, int depth) + { + var type = value.GetType(); + builder.Append(type.FullName); + + if (depth >= MaxArgumentDepth) + { + builder.Append("{...}"); + return; + } + + builder.Append('{'); + + var properties = type + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(e => e.CanRead && e.GetIndexParameters().Length == 0) + .OrderBy(e => e.Name, StringComparer.Ordinal); + + foreach (var property in properties) + { + builder.Append(property.Name).Append('='); + + object propertyValue; + try + { + propertyValue = property.GetValue(value); + } + catch (Exception) + { + // A property that throws cannot contribute to the identity of the arguments. + builder.Append(";"); + continue; + } + + AppendValue(builder, propertyValue, depth + 1); + builder.Append(';'); + } + + builder.Append('}'); + } + + /// FNV-1a, for a 64 bit value that is the same in every process. + private static ulong Hash(StringBuilder builder) + { + const ulong offsetBasis = 14695981039346656037; + const ulong prime = 1099511628211; + + var hash = offsetBasis; + + foreach (var chunk in builder.GetChunks()) + { + foreach (var character in chunk.Span) + { + hash = (hash ^ (byte)character) * prime; + hash = (hash ^ (byte)(character >> 8)) * prime; + } + } + + return hash; + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Visitors/ExpressionEvaluator.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Visitors/ExpressionEvaluator.cs new file mode 100644 index 00000000..de4214bb --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Visitors/ExpressionEvaluator.cs @@ -0,0 +1,117 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Linq.Expressions; +using System.Reflection; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Visitors; + +/// +/// Turns the argument expressions of a GraphQL field into the values that are sent as query variables. +/// Constants and captured locals are read directly; everything else falls back to compiling the +/// expression, which is comparatively expensive. +/// +internal static class ExpressionEvaluator +{ + internal static object Evaluate(Expression expression) + { + return TryEvaluateFast(expression, out var value) ? value : Compile(expression); + } + + private static bool TryEvaluateFast(Expression expression, out object value) + { + switch (expression) + { + case null: + value = null; + return true; + + case ConstantExpression constant: + value = constant.Value; + return true; + + case MemberExpression member: + return TryEvaluateMember(member, out value); + + case UnaryExpression unary when IsTransparentConversion(unary): + return TryEvaluateFast(unary.Operand, out value); + + default: + value = null; + return false; + } + } + + private static bool TryEvaluateMember(MemberExpression member, out object value) + { + // A static member, or a captured local, which the compiler turns into a field on a closure object + // that is held by a ConstantExpression. + object target = null; + + if (member.Expression != null && !TryEvaluateFast(member.Expression, out target)) + { + value = null; + return false; + } + + switch (member.Member) + { + case FieldInfo field: + value = field.GetValue(target); + return true; + + case PropertyInfo property when property.CanRead && property.GetIndexParameters().Length == 0: + value = property.GetValue(target); + return true; + + default: + value = null; + return false; + } + } + + /// + /// True for a conversion that cannot change the value, so the operand can be read instead: boxing, + /// an upcast, or a lift into . + /// + private static bool IsTransparentConversion(UnaryExpression unary) + { + if (unary.NodeType != ExpressionType.Convert && + unary.NodeType != ExpressionType.ConvertChecked && + unary.NodeType != ExpressionType.TypeAs) + { + return false; + } + + if (unary.Method != null) + { + return false; + } + + var from = unary.Operand.Type; + var to = Nullable.GetUnderlyingType(unary.Type) ?? unary.Type; + + return to.IsAssignableFrom(from); + } + + private static object Compile(Expression expression) + { + var body = expression.Type == typeof(object) + ? expression + : Expression.Convert(expression, typeof(object)); + + return Expression.Lambda>(body).Compile()(); + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Visitors/LinqOperator.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Visitors/LinqOperator.cs new file mode 100644 index 00000000..bd24b87e --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Visitors/LinqOperator.cs @@ -0,0 +1,140 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Visitors; + +internal enum LinqOperatorKind +{ + /// Not a LINQ operator at all. + None, + + /// + /// The operator replaces the element with something else (Select, SelectMany). + /// The selection continues at whatever the projection lambda returns. + /// + Projection, + + /// + /// The operator keeps the source element type, or reduces the sequence to one of its elements, to a + /// scalar computed from them, or to another container holding them (Where, OrderBy, + /// First, Count, ToDictionary, GroupBy, ...). + /// The selection stays on the source node; any lambda is a client side predicate or key selector whose + /// members still have to be fetched for it to work. + /// + PassThrough, + + /// + /// A known LINQ operator that cannot be translated, because it combines several sequences + /// (Concat, Join, Zip, ...) or folds the elements into an accumulator that the + /// selection cannot follow (Aggregate). + /// + Unsupported +} + +internal static class LinqOperator +{ + private static readonly HashSet ProjectionOperators = + [ + "Select", + "SelectMany" + ]; + + private static readonly HashSet PassThroughOperators = + [ + "All", + "Any", + "Append", + "AsEnumerable", + "AsQueryable", + "Average", + "Cast", + "Chunk", + "Contains", + "Count", + "CountBy", + "DefaultIfEmpty", + "Distinct", + "DistinctBy", + "ElementAt", + "ElementAtOrDefault", + "First", + "FirstOrDefault", + "GroupBy", + "Index", + "Last", + "LastOrDefault", + "LongCount", + "Max", + "MaxBy", + "Min", + "MinBy", + "OfType", + "Order", + "OrderBy", + "OrderByDescending", + "OrderDescending", + "Prepend", + "Reverse", + "Shuffle", + "Single", + "SingleOrDefault", + "Skip", + "SkipLast", + "SkipWhile", + "Sum", + "Take", + "TakeLast", + "TakeWhile", + "ThenBy", + "ThenByDescending", + "ToArray", + "ToDictionary", + "ToHashSet", + "ToList", + "ToLookup", + "TryGetNonEnumeratedCount", + "Where" + ]; + + internal static LinqOperatorKind Classify(MethodInfo method) + { + if (method.DeclaringType != typeof(Queryable) && method.DeclaringType != typeof(Enumerable)) + { + return LinqOperatorKind.None; + } + + if (Attribute.GetCustomAttribute(method, typeof(ExtensionAttribute)) == null) + { + return LinqOperatorKind.None; + } + + if (ProjectionOperators.Contains(method.Name)) + { + return LinqOperatorKind.Projection; + } + + return PassThroughOperators.Contains(method.Name) + ? LinqOperatorKind.PassThrough + : LinqOperatorKind.Unsupported; + } + + internal static bool IsOperator(MethodInfo method) + { + return Classify(method) != LinqOperatorKind.None; + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Visitors/QueryExpressionVisitor.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Visitors/QueryExpressionVisitor.cs new file mode 100644 index 00000000..977f1b9e --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Client/Visitors/QueryExpressionVisitor.cs @@ -0,0 +1,384 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Linq.Expressions; +using System.Reflection; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Visitors; + +/// +/// Translates the LINQ expressions passed to Include and Select into the +/// tree that the GraphQL query text is generated from. +/// +/// +/// +/// The visitor works on two levels. handles expressions that name a +/// field — a chain of members, a GraphQL method call, or a LINQ operator over one of those — and returns +/// the node that expression selects, creating it if needed. Everything else (object initialisers, +/// comparisons, string calls, ...) is walked by the inherited so that +/// every field mentioned anywhere inside it still ends up in the query. +/// +/// +/// Lambda parameters are bound to the node they iterate in , keyed by reference, so +/// nested lambdas that reuse a parameter name stay distinct and a parameter that was never bound is +/// reported instead of silently attaching its fields to the root. +/// +/// +internal class QueryExpressionVisitor : ExpressionVisitor +{ + private readonly QueryNode root; + private readonly Dictionary scopes = new(); + + private QueryExpressionVisitor(QueryNode root) + { + this.root = root; + } + + internal static void Parse(Expression expression, QueryNode root) + { + ArgumentNullException.ThrowIfNull(expression); + ArgumentNullException.ThrowIfNull(root); + + new QueryExpressionVisitor(root).ParseRoot(expression); + } + + private void ParseRoot(Expression expression) + { + var body = Unwrap(expression); + + if (body is LambdaExpression lambda) + { + if (lambda.Parameters.Count > 0) + { + Bind(lambda.Parameters[0], root); + } + + body = Unwrap(lambda.Body); + } + + Select(body); + } + + /// + /// Adds everything selects to the query. When the expression names a + /// single field, that field is the end of the path the caller wrote and therefore also gets its + /// primitive properties. + /// + private void Select(Expression expression) + { + var node = ResolvePath(expression); + if (node != null) + { + node.IncludePrimitive = true; + } + } + + /// + /// Resolves an expression that names a field to its node, or returns null when the expression is not a + /// single path — in which case any fields inside it have been added as a side effect. + /// + private QueryNode ResolvePath(Expression expression) + { + switch (Unwrap(expression)) + { + case null: + return null; + + case ParameterExpression parameter: + return Scope(parameter); + + case MemberExpression member: + return ResolveMember(member); + + case MethodCallExpression call: + return ResolveCall(call); + + case var other: + // Not a path: an initialiser, a comparison, a literal. Walk it so the fields it mentions are + // still fetched, but there is no single node to hand back. + Visit(other); + return null; + } + } + + private QueryNode ResolveMember(MemberExpression member) + { + if (member.Member.GetCustomAttribute() == null) + { + // Not a GraphQL field: Nullable.Value, a plain CLR property, a captured local. The selection is + // whatever it was read from. + return ResolvePath(member.Expression); + } + + var parent = ResolvePath(member.Expression); + + // A GraphQL field read off something that is not part of the query - a captured entity, for instance - + // selects nothing. + return parent?.AddChildNode(new QueryNode(member.Member)); + } + + private QueryNode ResolveCall(MethodCallExpression call) + { + if (call.Method.GetCustomAttribute() != null) + { + return ResolveGraphMethod(call); + } + + var kind = LinqOperator.Classify(call.Method); + + switch (kind) + { + case LinqOperatorKind.Projection: + case LinqOperatorKind.PassThrough: + return ResolveLinqOperator(call, kind); + + case LinqOperatorKind.Unsupported: + throw Unsupported(call, + $"the LINQ operator '{call.Method.Name}' has no GraphQL equivalent and the selection cannot " + + "follow what it returns", + $"Select the fields the query needs and call '{call.Method.Name}' on the result of " + + "ExecuteAsync instead.", + call.Method.Name); + + default: + // An ordinary method such as string.ToUpper(). It selects nothing itself, but its target and + // arguments may mention fields that have to be fetched for it to run on the result. + Visit(call.Object); + foreach (var argument in call.Arguments) + { + Visit(argument); + } + + return null; + } + } + + private QueryNode ResolveGraphMethod(MethodCallExpression call) + { + // Extension methods carry their target as the first argument; instance methods have it as Object. + var target = call.Object ?? call.Arguments.FirstOrDefault(); + var parent = ResolvePath(target); + + if (parent == null) + { + throw Unsupported(call, + $"the target of '{call.Method.Name}' is not part of the query", + memberName: call.Method.Name); + } + + return parent.AddChildNode(new QueryNode(call.Method, arguments: GetArguments(call))); + } + + private QueryNode ResolveLinqOperator(MethodCallExpression call, LinqOperatorKind kind) + { + var source = ResolvePath(call.Arguments[0]); + var lambdas = GetLambdas(call); + + if (source == null) + { + // The sequence is not part of the query, so there is nothing to bind the lambdas to. + foreach (var argument in call.Arguments) + { + Visit(argument); + } + + return null; + } + + if (kind == LinqOperatorKind.PassThrough) + { + // Predicates and key selectors run on the client, so the members they touch must be fetched, but + // the selection itself stays on the sequence. + var elementType = GetElementType(call.Arguments[0].Type); + + foreach (var lambda in lambdas) + { + Bind(lambda.Parameters[0], source); + + // GroupBy's result selector takes (key, elements): the elements are the sequence itself. + for (var i = 1; i < lambda.Parameters.Count; i++) + { + if (elementType != null && GetElementType(lambda.Parameters[i].Type) == elementType) + { + Bind(lambda.Parameters[i], source); + } + } + + Select(lambda.Body); + } + + return source; + } + + // Select / SelectMany: the selection moves to whatever the projection returns. + QueryNode collection = null; + QueryNode projected = null; + + for (var i = 0; i < lambdas.Count; i++) + { + var lambda = lambdas[i]; + + Bind(lambda.Parameters[0], source); + + // SelectMany's result selector takes (element, collectionElement). + if (i > 0 && lambda.Parameters.Count > 1 && collection != null) + { + Bind(lambda.Parameters[1], collection); + } + + projected = ResolvePath(lambda.Body); + + if (i == 0) + { + collection = projected; + } + } + + return projected; + } + + /// + /// The element type of a sequence type, or null when the type is not a sequence. + /// + private static Type GetElementType(Type type) + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + return type.GetGenericArguments()[0]; + } + + return type.GetInterfaces() + .FirstOrDefault(e => e.IsGenericType && e.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + ?.GetGenericArguments()[0]; + } + + private static List GetLambdas(MethodCallExpression call) + { + var lambdas = new List(); + + for (var i = 1; i < call.Arguments.Count; i++) + { + if (Unwrap(call.Arguments[i]) is LambdaExpression { Parameters.Count: > 0 } lambda) + { + lambdas.Add(lambda); + } + } + + return lambdas; + } + + private static List GetArguments(MethodCallExpression call) + { + var parameters = call.Method.GetParameters(); + var arguments = new List(); + + for (var i = 0; i < parameters.Length && i < call.Arguments.Count; i++) + { + var attribute = parameters[i].GetCustomAttribute(); + if (attribute == null) + { + continue; + } + + arguments.Add(new ArgumentValue(attribute.GraphQLName, attribute.GraphQLType, + ExpressionEvaluator.Evaluate(call.Arguments[i]))); + } + + return arguments; + } + + protected override Expression VisitMember(MemberExpression node) + { + if (node.Member.GetCustomAttribute() != null) + { + Select(node); + return node; + } + + return base.VisitMember(node); + } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.GetCustomAttribute() != null || + LinqOperator.IsOperator(node.Method)) + { + Select(node); + return node; + } + + return base.VisitMethodCall(node); + } + + protected override Expression VisitParameter(ParameterExpression node) + { + // The whole element is projected, so take all of its primitive fields. + if (scopes.TryGetValue(node, out var scopeNode)) + { + scopeNode.IncludePrimitive = true; + } + + return node; + } + + private void Bind(ParameterExpression parameter, QueryNode node) + { + scopes[parameter] = node; + } + + private QueryNode Scope(ParameterExpression parameter) + { + if (scopes.TryGetValue(parameter, out var node)) + { + return node; + } + + throw new GraphQueryTranslationException( + $"Cannot translate '{parameter.Name}' of type '{parameter.Type.Name}': it is not bound to a field of " + + "the query. Only the parameters of the lambdas passed to Include and Select can be used to select fields.", + parameter); + } + + private static Expression Unwrap(Expression expression) + { + while (expression != null) + { + switch (expression.NodeType) + { + case ExpressionType.Quote: + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + case ExpressionType.TypeAs: + case ExpressionType.Unbox: + expression = ((UnaryExpression)expression).Operand; + continue; + + default: + return expression; + } + } + + return null; + } + + private static GraphQueryTranslationException Unsupported(Expression expression, string reason, + string hint = null, string memberName = null) + { + var message = $"Cannot translate '{expression}' into a GraphQL selection: {reason}."; + + return new GraphQueryTranslationException(hint == null ? message : message + " " + hint, expression, + memberName); + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/RuntimeInfo.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/RuntimeInfo.cs new file mode 100644 index 00000000..37f14373 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/RuntimeInfo.cs @@ -0,0 +1,22 @@ +//--------------------------------------------------------------------- +// +// Written by the Linq2GraphQL generator in standalone mode. +// Please don't edit this file - regenerate instead. +//--------------------------------------------------------------------- + +namespace Linq2GraphQL.TestClientStandalone.Runtime; + +/// +/// The Linq2GraphQL runtime this copy was taken from. It needs these packages: +/// +/// Microsoft.Extensions.Caching.Memory 10.0.11 +/// Microsoft.Extensions.Http 10.0.11 +/// Websocket.Client 5.3.0 +/// GraphQL-Parser 9.5.1 +/// Microsoft.Extensions.AI.Abstractions 10.10.0 +/// +/// +public static class RuntimeInfo +{ + public const string Version = "1.0.84-prerelease"; +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/GraphSubscription.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/GraphSubscription.cs new file mode 100644 index 00000000..0f22ccf4 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/GraphSubscription.cs @@ -0,0 +1,36 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Linq.Expressions; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Subscriptions; + +public class GraphSubscription : GraphBase> +{ + public GraphSubscription(GraphClient client, string name, OperationType operationType, List arguments) : base(client, name, operationType, arguments) + { + } + + public GraphSubscriptionExecute Select() + { + return new GraphSubscriptionExecute(client, operationType, QueryNode, null); + } + + public GraphSubscriptionExecute Select(Expression> selector) + { + ParseExpression(selector); + return new GraphSubscriptionExecute(client, operationType, QueryNode, selector); + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/GraphSubscriptionExecute.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/GraphSubscriptionExecute.cs new file mode 100644 index 00000000..44f60729 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/GraphSubscriptionExecute.cs @@ -0,0 +1,312 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Linq.Expressions; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Text.Json; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Subscriptions; + +public class GraphSubscriptionExecute : GraphBaseExecute +{ + private TaskCompletionSource connectedTcs; + + public GraphSubscriptionExecute(GraphClient client, OperationType operationType, QueryNode queryNode, + Expression> selector) : base(client, operationType, queryNode, selector) { } + + /// + /// Completes when the underlying transport is connected (SSE response accepted, + /// WebSocket init/ack received) — lets callers trigger server events deterministically. + /// + internal Task WhenConnected => connectedTcs?.Task ?? Task.CompletedTask; + + public async Task> StartAsync() + { + var results = await StartCoreAsync(continueOnFieldErrors: false); + return results.Select(result => result.Data); + } + + /// + /// Like , but each event is delivered as a + /// carrying its data AND its GraphQL errors. + /// A next event with field errors does NOT end the subscription: the + /// partial data and all later events keep flowing. Only transport and protocol + /// failures terminate the stream. + /// + public async Task>> StartWithErrorsAsync() + { + return await StartCoreAsync(continueOnFieldErrors: true); + } + + private async Task>> StartCoreAsync(bool continueOnFieldErrors) + { + var request = await GetRequestAsync(); + + if (string.IsNullOrWhiteSpace(client.SubscriptionUrl)) + { + throw new Exception("Subscription url is not set"); + } + + var options = client.ClientOptions; + + return Observable.Create>(observer => + { + // Per-subscriber state: each Subscribe() gets its own buffer, transport and + // connection signal. Sharing these across subscribers let one overflow poison + // the others. + var buffer = new SubscriptionEventBuffer( + options.SubscriptionMaxBufferedEvents, options.SubscriptionOverflowPolicy); + var connectedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + connectedTcs = connectedSource; + var cancellation = new CancellationTokenSource(); + var queueLock = new object(); + var signal = new SemaphoreSlim(0); + var completed = false; + var processorExited = false; + Exception failure = null; + IDisposable transportSubscription = Disposable.Empty; + IDisposable transportFailureSubscription = Disposable.Empty; + SSEClient sseClient = null; + WSClient wsClient = null; + + void OnNext(PendingPayload payload) + { + lock (queueLock) + { + if (completed || processorExited || cancellation.IsCancellationRequested) + { + return; + } + + if (!buffer.TryEnqueue(payload, out var overflowFault)) + { + if (overflowFault is not null) + { + failure = overflowFault; + completed = true; + signal.Release(); + } + + return; + } + + signal.Release(); + } + } + + void OnFailure(Exception exception) + { + lock (queueLock) + { + // Terminal transitions are idempotent: a late transport failure must + // not override normal completion, and no callback may signal the + // semaphore once the processor has exited and disposed it. + if (completed || processorExited) + { + return; + } + + failure = exception; + completed = true; + signal.Release(); + } + } + + void OnCompleted() + { + lock (queueLock) + { + if (completed || processorExited) + { + return; + } + + completed = true; + signal.Release(); + } + } + + if (client.SubscriptionProtocol == SubscriptionProtocol.ServerSentEvents) + { + sseClient = new SSEClient(client, request); + transportSubscription = sseClient.Payloads.Subscribe( + bytes => OnNext(new PendingPayload(bytes, default)), OnFailure, OnCompleted); + transportFailureSubscription = sseClient.Failures.Subscribe(OnFailure); + _ = ForwardConnectedAsync(sseClient.Connected, connectedSource); + _ = sseClient.Start(cancellation.Token); + } + else + { + wsClient = new WSClient(client, request); + transportSubscription = wsClient.Payloads.Subscribe( + element => OnNext(new PendingPayload(null, element)), OnFailure, OnCompleted); + transportFailureSubscription = wsClient.Failures.Subscribe(OnFailure); + _ = StartWebSocketAsync(wsClient, OnFailure, connectedSource); + } + + _ = ProcessEventsAsync(); + + return Disposable.Create(() => + { + try + { + cancellation.Cancel(); + } + catch (ObjectDisposedException) + { + // Processor may already have exited and disposed the source. + } + + transportSubscription.Dispose(); + transportFailureSubscription.Dispose(); + sseClient?.Dispose(); + if (wsClient is not null) + { + _ = wsClient.DisposeAsync(); + } + + // The processor owns signal/cancellation cleanup; never block the disposing observer. + }); + + async Task ProcessEventsAsync() + { + try + { + while (true) + { + await signal.WaitAsync(cancellation.Token).ConfigureAwait(false); + + // Drain everything currently queued before waiting again. + while (true) + { + byte[] jsonBytes = null; + JsonElement element = default; + var hasPayload = false; + var shouldComplete = false; + Exception terminalFailure = null; + + lock (queueLock) + { + if (buffer.TryDequeue(out var payload)) + { + jsonBytes = payload.JsonBytes; + element = payload.Element; + hasPayload = true; + } + else if (completed) + { + shouldComplete = true; + terminalFailure = failure; + } + } + + if (!hasPayload) + { + if (shouldComplete) + { + if (terminalFailure is not null) + { + observer.OnError(terminalFailure); + } + else + { + observer.OnCompleted(); + } + + return; + } + + break; + } + + try + { + var full = jsonBytes is not null + ? queryExecutor.ProcessResponseFull(jsonBytes, QueryNode.Name) + : queryExecutor.ProcessResponseFull(element, QueryNode.Name); + + if (full.HasErrors && !continueOnFieldErrors) + { + throw new GraphQueryExecutionException(full.Errors, request.Query, request.Variables); + } + + observer.OnNext(new GraphResult + { + Data = ConvertResult(full.Data), + Errors = full.Errors, + Extensions = full.Extensions + }); + } + catch (Exception exception) + { + observer.OnError(exception); + return; + } + } + } + } + catch (OperationCanceledException) when (cancellation.IsCancellationRequested) + { + // Unsubscription ends the processor without notifying the observer. + } + finally + { + lock (queueLock) + { + processorExited = true; + } + + signal.Dispose(); + cancellation.Dispose(); + } + } + }); + } + + private readonly record struct PendingPayload(byte[] JsonBytes, JsonElement Element); + + private static async Task StartWebSocketAsync(WSClient wsClient, Action onFailure, + TaskCompletionSource connectedTcs) + { + try + { + await wsClient.Start().ConfigureAwait(false); + connectedTcs.TrySetResult(null); + } + catch (Exception exception) + { + connectedTcs.TrySetException(exception); + onFailure(exception); + } + } + + private static async Task ForwardConnectedAsync(Task connected, TaskCompletionSource connectedTcs) + { + try + { + await connected.ConfigureAwait(false); + connectedTcs.TrySetResult(null); + } + catch (OperationCanceledException) + { + connectedTcs.TrySetCanceled(); + } + catch (Exception exception) + { + connectedTcs.TrySetException(exception); + } + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/IdleTimeoutStream.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/IdleTimeoutStream.cs new file mode 100644 index 00000000..3e3cb048 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/IdleTimeoutStream.cs @@ -0,0 +1,127 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime.Subscriptions; + +/// +/// Stream decorator that fails a read with an IOException once no bytes have +/// arrived within the idle timeout. Each read gets its own timer that runs only while +/// that read waits for data, so keep-alives (including WHATWG : comments, which +/// the SSE parser consumes invisibly) keep the stream alive, and time the consumer spends +/// between reads (processing an event, waiting for a thread) never counts as idle. +/// +internal sealed class IdleTimeoutStream : Stream +{ + private readonly Stream inner; + private readonly TimeSpan idleTimeout; + private readonly TimeProvider timeProvider; + private bool disposed; + + /// Clock driving the idle timer; when null. + /// Tests pass a fake provider so the timer only advances when they say so. + internal IdleTimeoutStream(Stream inner, TimeSpan idleTimeout, TimeProvider timeProvider = null) + { + if (idleTimeout <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(idleTimeout), "Idle timeout must be positive."); + } + + this.inner = inner ?? throw new ArgumentNullException(nameof(inner)); + this.idleTimeout = idleTimeout; + this.timeProvider = timeProvider ?? TimeProvider.System; + } + + public override bool CanRead => inner.CanRead; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(disposed, this); + + // A fresh timer per read, disposed when the read completes: it runs only while waiting + // for data, and a timer that fired during a read which still returned bytes cannot leak + // into the next read (a CancellationTokenSource cannot be reset once cancelled). + // The timer source is bound to the TimeProvider (a linked source cannot be), then linked + // with the caller's token for the read itself. + using var timerCts = new CancellationTokenSource(idleTimeout, timeProvider); + using var readCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timerCts.Token); + + try + { + return await inner.ReadAsync(buffer, readCts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when ( + timerCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) + { + // The idle timer fired and the caller did not cancel, so no bytes arrived within the + // window: the connection is dead. An OperationCanceledException the inner stream + // raised on its own propagates unchanged. + throw new IOException($"SSE idle timeout: no data received within {FormatTimeout(idleTimeout)}."); + } + } + + /// + /// Whole seconds as "N s", anything else as "N ms" - never rounds up (750 ms is not "1s"); + /// a positive timeout under 1 ms is "<1 ms", not "0 s". Expects a positive timeout, as + /// the constructor enforces. + /// + internal static string FormatTimeout(TimeSpan timeout) + { + var milliseconds = (long)timeout.TotalMilliseconds; + if (milliseconds == 0) + { + return "<1 ms"; + } + + return milliseconds % 1000 == 0 + ? FormattableString.Invariant($"{milliseconds / 1000} s") + : FormattableString.Invariant($"{milliseconds} ms"); + } + + public override void Flush() => throw new NotSupportedException(); + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + protected override void Dispose(bool disposing) + { + if (Interlocked.Exchange(ref disposed, true)) + { + return; + } + + if (disposing) + { + inner.Dispose(); + } + + base.Dispose(disposing); + } + + public override async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref disposed, true)) + { + return; + } + + await inner.DisposeAsync().ConfigureAwait(false); + GC.SuppressFinalize(this); + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/SSEClient.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/SSEClient.cs new file mode 100644 index 00000000..3b15ad93 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/SSEClient.cs @@ -0,0 +1,325 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Net.Http.Headers; +using System.Net.ServerSentEvents; +using System.Net.Mime; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Text; +using System.Text.Json; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Subscriptions; + +public class SSEClient : IDisposable +{ + private readonly GraphClient graphClient; + private readonly GraphQLRequest payload; + private readonly ISubject payloadSubject = Subject.Synchronize(new Subject()); + private readonly ISubject errorSubject = + Subject.Synchronize(new Subject()); + private readonly ISubject failureSubject = Subject.Synchronize(new Subject()); + private readonly CancellationTokenSource cancellationSource = new(); + private readonly object resourceLock = new(); + private readonly TaskCompletionSource connectedTcs = + new(TaskCreationOptions.RunContinuationsAsynchronously); + private HttpResponseMessage response; + private Stream eventStream; + private int disposed; + private int failureReported; + private int completedSent; + + public SSEClient(GraphClient graphClient, GraphQLRequest payload) + { + this.graphClient = graphClient; + this.payload = payload; + } + + /// Clock for the idle timeout; tests substitute a fake provider. + internal TimeProvider TimeProvider { get; init; } = TimeProvider.System; + + /// + /// UTF-8 payloads of "next" events decoded to strings — deferred per subscriber: + /// the decode only happens for listeners actually attached here. + /// + public IObservable Subscription => payloadSubject + .Select(static data => Encoding.UTF8.GetString(data)); + + /// + /// Completes when the server has accepted the SSE POST (status + content type + /// verified); faults/cancels if the connection fails or is disposed pre-connect. + /// + internal Task Connected => connectedTcs.Task; + + public IObservable Errors => errorSubject.AsObservable(); + internal IObservable Failures => failureSubject.AsObservable(); + + /// + /// Raw UTF-8 payloads of "next" events — the byte-level feed used by + /// GraphSubscriptionExecute to avoid a string round trip per message. + /// + internal IObservable Payloads => payloadSubject.AsObservable(); + + public void Dispose() + { + if (Interlocked.Exchange(ref disposed, 1) != 0) + { + return; + } + + try + { + cancellationSource.Cancel(); + } + catch (ObjectDisposedException) + { + // The reader has already completed and disposed the token source. + } + + lock (resourceLock) + { + eventStream?.Dispose(); + response?.Dispose(); + } + payloadSubject.OnCompleted(); + errorSubject.OnCompleted(); + failureSubject.OnCompleted(); + cancellationSource.Dispose(); + } + + public Task Start() + { + if (Volatile.Read(ref disposed) != 0) + { + connectedTcs.TrySetCanceled(); + return Task.CompletedTask; + } + + return Start(cancellationSource.Token); + } + + internal async Task Start(CancellationToken cancellationToken) + { + if (Volatile.Read(ref disposed) != 0) + { + // Disposed before start: a no-op, not an ObjectDisposedException. + connectedTcs.TrySetCanceled(); + return; + } + + using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource( + cancellationSource.Token, cancellationToken); + var token = linkedSource.Token; + + using var request = new HttpRequestMessage(HttpMethod.Post, ""); + + try + { + var json = JsonSerializer.Serialize(payload, graphClient.SerializerOptions); + request.Content = new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream")); + await graphClient.ConfigureRequestAsync(request, token).ConfigureAwait(false); + var sentResponse = await graphClient.HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token) + .ConfigureAwait(false); + lock (resourceLock) + { + response = sentResponse; + if (Volatile.Read(ref disposed) != 0) + { + response.Dispose(); + response = null; + connectedTcs.TrySetCanceled(); + return; + } + } + + if (!sentResponse.IsSuccessStatusCode) + { + var content = await sentResponse.Content.ReadAsStringAsync(token).ConfigureAwait(false); + throw new GraphQueryRequestException( + $"SSE connection failed with status {sentResponse.StatusCode}: {content}", + payload.Query, payload.Variables); + } + + if (!string.Equals(sentResponse.Content.Headers.ContentType?.MediaType, "text/event-stream", + StringComparison.OrdinalIgnoreCase)) + { + throw new GraphQueryRequestException( + $"SSE connection returned unsupported content type '{sentResponse.Content.Headers.ContentType}'.", + payload.Query, payload.Variables); + } + + var rawStream = await sentResponse.Content.ReadAsStreamAsync(token).ConfigureAwait(false); + Stream source = rawStream; + + // The server has accepted the subscription POST — the transport is live. + connectedTcs.TrySetResult(null); + + // Optional idle timeout: any bytes (including ':' keep-alive comments, which + // the SSE parser consumes invisibly) reset the timer. + var idleTimeout = graphClient.ClientOptions?.SubscriptionIdleTimeout; + if (idleTimeout is { } timeout) + { + source = new IdleTimeoutStream(rawStream, timeout, TimeProvider); + } + + lock (resourceLock) + { + eventStream = source; + if (Volatile.Read(ref disposed) != 0) + { + eventStream.Dispose(); + eventStream = null; + connectedTcs.TrySetCanceled(); + return; + } + } + + // SseParser (System.Net.ServerSentEvents) implements the WHATWG framing: + // multi-line data joined with '\n', comments ignored, dispatch on blank line, + // incomplete trailing event discarded at EOF. Data is kept as UTF-8 bytes. + var parser = SseParser.Create(source, static (_, data) => data.ToArray()); + await foreach (var item in parser.EnumerateAsync(token).ConfigureAwait(false)) + { + // Older GraphQL-over-SSE servers omit `event: next`. SseParser reports the + // SSE default event type "message" when no event field was sent — map both + // to "next"; Dispatch ignores unknown types (keep-alives etc.) otherwise. + var eventType = item.EventType; + if (string.IsNullOrEmpty(eventType) || eventType == "message") + { + eventType = "next"; + } + + var sseEvent = new SseEvent(eventType, item.Data); + + if (Dispatch(sseEvent)) + { + return; + } + } + + token.ThrowIfCancellationRequested(); + throw new IOException("SSE stream ended before a complete event was received."); + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + // Disposal/unsubscription is the normal way to stop an open stream. + connectedTcs.TrySetCanceled(); + } + catch (Exception exception) + { + ReportFailure(exception is HttpRequestException or IOException or OperationCanceledException + ? new GraphQueryRequestException($"SSE connection failed: {exception.Message}", + payload.Query, payload.Variables) + : exception); + } + finally + { + lock (resourceLock) + { + response?.Dispose(); + response = null; + eventStream?.Dispose(); + eventStream = null; + } + + // Complete only if neither a failure nor an explicit server complete + // already terminated the stream (prevents a double OnCompleted). + if (Volatile.Read(ref failureReported) == 0 + && Volatile.Read(ref completedSent) == 0 + && Volatile.Read(ref disposed) == 0) + { + payloadSubject.OnCompleted(); + } + } + } + + private bool Dispatch(SseEvent message) + { + switch (message.EventType) + { + case "next": + payloadSubject.OnNext(message.Data); + return false; + case "error": + ReportFailure(ParseSseError(message.Data)); + return true; + case "complete": + // Guard against a duplicate complete. + if (Interlocked.Exchange(ref completedSent, 1) == 0) + { + payloadSubject.OnCompleted(); + } + + return true; + default: + // WHATWG SSE spec: unknown event names dispatch to no listener. Servers + // and proxies legitimately add event types (e.g. HotChocolate "ka" + // keep-alives); ignoring them keeps the stream alive. + return false; + } + } + + private Exception ParseSseError(byte[] json) + { + try + { + using var document = JsonDocument.Parse(json); + var root = document.RootElement; + var errors = root.ValueKind == JsonValueKind.Array + ? root.Deserialize>(graphClient.SerializerOptions) + : root.TryGetProperty("errors", out var errorsElement) + ? errorsElement.Deserialize>(graphClient.SerializerOptions) + : new List { new() { Message = Encoding.UTF8.GetString(json) } }; + return new GraphQueryExecutionException(errors, payload.Query, payload.Variables); + } + catch (JsonException) + { + return new GraphQueryExecutionException( + new List { new() { Message = Encoding.UTF8.GetString(json) } }, + payload.Query, payload.Variables); + } + catch (InvalidOperationException) + { + return new GraphQueryExecutionException( + new List { new() { Message = Encoding.UTF8.GetString(json) } }, + payload.Query, payload.Variables); + } + } + + private void ReportFailure(Exception exception) + { + // Idempotent: only the first failure emits. This uses a dedicated flag so a + // later Dispose() still runs (it owns resource cleanup via the disposed flag). + if (Interlocked.Exchange(ref failureReported, 1) != 0) + { + return; + } + + if (exception is GraphQueryExecutionException executionException) + { + errorSubject.OnNext(executionException); + } + + // WhenConnected waiters learn about connect failures; a no-op once connected. + connectedTcs.TrySetException(exception); + + failureSubject.OnNext(exception); + failureSubject.OnCompleted(); + payloadSubject.OnCompleted(); + } +} + +internal readonly record struct SseEvent(string EventType, byte[] Data); diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/SubscribeCommands.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/SubscribeCommands.cs new file mode 100644 index 00000000..ed0aa5ae --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/SubscribeCommands.cs @@ -0,0 +1,24 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime.Subscriptions +{ + internal class SubscribeCommands + { + internal const string SUBSCRIBE = "subscribe"; + internal const string START = "start"; + internal const string STOP = "stop"; + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/SubscriptionEventBuffer.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/SubscriptionEventBuffer.cs new file mode 100644 index 00000000..192b211e --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/SubscriptionEventBuffer.cs @@ -0,0 +1,96 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Linq2GraphQL.TestClientStandalone.Runtime; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Subscriptions; + +/// +/// Bounded event buffer between a subscription transport and its observer. The +/// overflow behavior is controlled by . +/// Callers own synchronization. +/// +internal sealed class SubscriptionEventBuffer +{ + private readonly int maximumBufferedEvents; + private readonly SubscriptionOverflowPolicy policy; + private readonly Queue queue; + + public SubscriptionEventBuffer(int maximumBufferedEvents, SubscriptionOverflowPolicy policy) + { + if (maximumBufferedEvents <= 0) + { + throw new ArgumentOutOfRangeException(nameof(maximumBufferedEvents), + "SubscriptionMaxBufferedEvents must be greater than zero."); + } + + this.maximumBufferedEvents = maximumBufferedEvents; + this.policy = policy; + queue = new Queue(Math.Min(maximumBufferedEvents, 16)); + } + + public int Count => queue.Count; + + /// + /// Adds an event, applying the overflow policy when full. Returns false when + /// the buffer is in the faulted state (Fault policy with a full buffer); the + /// fault is returned via and the buffer then + /// refuses all further enqueues. + /// + public bool TryEnqueue(T payload, out Exception overflowFault) + { + overflowFault = null; + + if (faulted) + { + return false; + } + + if (queue.Count >= maximumBufferedEvents) + { + switch (policy) + { + case SubscriptionOverflowPolicy.DropOldest: + queue.Dequeue(); + break; + case SubscriptionOverflowPolicy.KeepLatest: + queue.Clear(); + break; + default: + overflowFault = new InvalidOperationException( + $"Subscription event buffer exceeded {maximumBufferedEvents} messages."); + faulted = true; + return false; + } + } + + queue.Enqueue(payload); + return true; + } + + public bool TryDequeue(out T payload) + { + if (queue.Count == 0) + { + payload = default; + return false; + } + + payload = queue.Dequeue(); + return true; + } + + private bool faulted; +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/SubscriptionProtocols.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/SubscriptionProtocols.cs new file mode 100644 index 00000000..fc0cfb66 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/SubscriptionProtocols.cs @@ -0,0 +1,23 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime.Subscriptions +{ + internal static class SubscriptionProtocols + { + internal const string GRAPGQL_TRANSPORT_WS = "graphql-transport-ws"; + internal const string GRAPHQL_WS = "graphql-ws"; + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/WSClient.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/WSClient.cs new file mode 100644 index 00000000..026a2614 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/WSClient.cs @@ -0,0 +1,843 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Net.WebSockets; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Runtime.ExceptionServices; +using System.Text.Json; +using System.Text.Json.Nodes; +using Websocket.Client; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Subscriptions; + +public class WSClient : IAsyncDisposable +{ + private readonly GraphClient graphClient; + private readonly GraphQLRequest payload; + // Raw string feed kept for backward compatibility; emissions are serialized by the + // transport's single receive loop, so the unsynchronized Subject is safe. + private readonly Subject subscriptionSubject = new(); + private readonly ISubject payloadSubject = Subject.Synchronize(new Subject()); + private readonly ISubject errorSubject = + Subject.Synchronize(new Subject()); + private readonly ISubject failureSubject = Subject.Synchronize(new Subject()); + private readonly WebsocketClient client; + // Shared frozen options (same instance as GraphClient.SerializerOptions) — outgoing + // protocol messages only, but reusing it keeps one metadata cache process-wide. + private readonly JsonSerializerOptions jsonOptions = Linq2GraphQL.TestClientStandalone.Runtime.Common.ClientJson.Default; + private readonly string subscriptionId = Guid.NewGuid().ToString(); + private readonly object stateLock = new(); + private TaskCompletionSource ackSource; + private readonly CancellationTokenSource cancellationSource = new(); + private readonly IDisposable messageSubscription; + private readonly IDisposable disconnectionSubscription; + private Task initializationTask; + private bool acknowledged; + private int retryAttemptsUsed; + private readonly int maxRetryAttempts; + private bool retryLoopRunning; + private bool subscriptionSent; + private bool initSent; + private bool disposed; + private bool intentionalDisconnection; + private int terminalFailure; + private string subprotocol; + // Own no-message watchdog: Websocket.Client's ReconnectTimeout only acts when its own + // reconnection is enabled, which this class disables (verified: no NoMessageReceived event). + private Timer watchdog; + private long lastMessageTicks; + // connection_init payload of the CURRENT connection. Resolved in the connection factory, + // before the socket connects, so the payload delegate runs inside ConnectTimeout and cannot + // hold an open socket past the server's connection_init wait (graphql-ws closes with 4408). + // Read by SendConnectionInit, which runs after the factory for the same generation. + private volatile object connectionInitPayload; + // The last failure of the pre-connect preparation (request hooks, payload delegate). + // Websocket.Client wraps any factory exception in its own WebsocketException; this lets + // Start() and the retry loop surface the caller's original exception instead. + private volatile ExceptionDispatchInfo preparationFailure; + + // Headers the WebSocket handshake owns; a hook setting them must not break the upgrade. + private static readonly HashSet HandshakeOwnedHeaders = new(StringComparer.OrdinalIgnoreCase) + { + "Host", + "Connection", + "Upgrade", + "Sec-WebSocket-Key", + "Sec-WebSocket-Version", + "Sec-WebSocket-Protocol", + "Sec-WebSocket-Extensions", + }; + + public WSClient(GraphClient graphClient, GraphQLRequest payload) + { + this.graphClient = graphClient; + this.payload = payload; + maxRetryAttempts = Math.Max(0, graphClient.ClientOptions.SubscriptionRetryAttempts); + + // The async connection factory runs for EVERY connect, including each reconnect attempt + // inside the retry loop — so request hooks (WithAccessToken etc.) run per connection and + // a refreshed token is picked up on reconnect. It must return an already-connected socket. + var factory = new Func>(async (uri, cancellationToken) => + { + var socket = new ClientWebSocket(); + try + { + try + { + socket.Options.AddSubProtocol(subprotocol ?? GetSubprotocolString()); + var authorization = await ApplyRequestHooksAsync(socket.Options, cancellationToken) + .ConfigureAwait(false); + connectionInitPayload = await BuildConnectionInitPayloadAsync(authorization, cancellationToken) + .ConfigureAwait(false); + preparationFailure = null; + } + catch (Exception exception) when (exception is not OperationCanceledException) + { + // Cancellation (ConnectTimeout elapsed, disposal) stays wrapped: a bare + // OperationCanceledException from Start() would read as the caller's own cancel. + preparationFailure = ExceptionDispatchInfo.Capture(exception); + throw; + } + + await socket.ConnectAsync(uri, cancellationToken).ConfigureAwait(false); + return socket; + } + catch + { + socket.Dispose(); + throw; + } + }); + + client = new WebsocketClient(new Uri(graphClient.SubscriptionUrl), null, factory) + { + // This class owns reconnection (bounded, with backoff) and silence detection. The + // library's own loop retries forever and goes silent after an Error. + ReconnectTimeout = null, + IsReconnectionEnabled = false, + // The default 5 s budget covers only the socket connect; the request hooks (token + // fetch, e.g. an IdP round-trip) run inside the same window, so give them room. + ConnectTimeout = TimeSpan.FromSeconds(30), + }; + + messageSubscription = client.MessageReceived.Subscribe(HandleMessage, ReportFailure); + disconnectionSubscription = client.DisconnectionHappened.Subscribe(HandleDisconnection, ReportFailure); + } + + /// + /// Runs the request hooks for one upgrade request and copies the resulting headers onto the + /// socket. Returns the effective Authorization value (for + /// ), or null. + /// + private async Task ApplyRequestHooksAsync(ClientWebSocketOptions socketOptions, + CancellationToken cancellationToken) + { + // The hooks are HttpRequestMessage-based (same shape as queries and SSE); run them on a + // dummy request for the subscription URL, then copy the resulting headers onto the socket. + using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(graphClient.SubscriptionUrl)); + await graphClient.ConfigureRequestAsync(request, cancellationToken).ConfigureAwait(false); + + // Queries get HttpClient.DefaultRequestHeaders from HttpClient.SendAsync; the upgrade does + // not go through HttpClient, so merge them here. Same precedence as SendAsync: a header + // already on the request (set by a hook) wins over the default of the same name. + var defaults = graphClient.HttpClient?.DefaultRequestHeaders; + var headers = new List<(string Name, string Value)>(); + if (defaults is not null) + { + foreach (var (key, values) in defaults) + { + if (!request.Headers.Contains(key)) + { + headers.Add((key, string.Join(", ", values))); + } + } + } + + foreach (var (key, values) in request.Headers) + { + headers.Add((key, string.Join(", ", values))); + } + + var authorization = request.Headers.Authorization?.ToString() ?? defaults?.Authorization?.ToString(); + + // Browsers do not allow custom headers on WebSocket requests: the Authorization value + // returned here travels in the connection_init payload instead (Blazor WebAssembly). + if (OperatingSystem.IsBrowser()) + { + return authorization; + } + + foreach (var (name, value) in headers) + { + if (!HandshakeOwnedHeaders.Contains(name) && !string.IsNullOrEmpty(value)) + { + socketOptions.SetRequestHeader(name, value); + } + } + + return authorization; + } + + public IObservable Subscription => subscriptionSubject.AsObservable(); + internal IObservable Payloads => payloadSubject.AsObservable(); + public IObservable Errors => errorSubject.AsObservable(); + internal IObservable Failures => failureSubject.AsObservable(); + + /// + /// A connection that delivers no frame (data, ping or keep-alive) for this long is treated + /// as lost and goes through the reconnect loop. Mirrors the previous 30 s ReconnectTimeout. + /// + internal TimeSpan NoMessageTimeout { get; set; } = TimeSpan.FromSeconds(30); + + /// + /// Delay before reconnect attempt n (0-based). Tests shorten it so "no background retry" + /// assertions do not depend on outwaiting the real backoff. + /// + internal Func RetryDelay { get; set; } = BackoffDelay; + + public async ValueTask DisposeAsync() + { + lock (stateLock) + { + if (disposed) + { + return; + } + + disposed = true; + intentionalDisconnection = true; + } + + cancellationSource.Cancel(); + watchdog?.Dispose(); + messageSubscription.Dispose(); + disconnectionSubscription.Dispose(); + + try + { + if (client.IsRunning) + { + await client.SendInstant(JsonSerializer.Serialize( + new WebsocketRequest(GetCompleteCommand()) { Id = subscriptionId }, jsonOptions)).ConfigureAwait(false); + } + + await client.Stop(WebSocketCloseStatus.NormalClosure, string.Empty).ConfigureAwait(false); + } + catch (Exception) + { + // Disposal must still release client resources if the transport already failed. + } + + client.Dispose(); + payloadSubject.OnCompleted(); + subscriptionSubject.OnCompleted(); + errorSubject.OnCompleted(); + failureSubject.OnCompleted(); + // cancellationSource is intentionally not disposed: a retry loop still unwinding after + // Cancel() may read its token, and an undisposed CTS without timers holds no resources. + } + + public Task Start() + { + lock (stateLock) + { + if (disposed) + { + throw new ObjectDisposedException(nameof(WSClient)); + } + + initializationTask ??= InitializeAsync(); + return initializationTask; + } + } + + private async Task InitializeAsync() + { + // Created before StartOrFail so an eager ack arriving during the handshake + // completes this generation's source instead of racing a null field (L2). + ackSource = CreateAckSource(); + try + { + await StartTransportAsync().ConfigureAwait(false); + + // Initial connect: a missing subprotocol is a protocol violation and stays terminal. + var negotiated = client.NativeClient?.SubProtocol; + if (string.IsNullOrEmpty(negotiated)) + { + throw new InvalidDataException("WebSocket server did not negotiate a GraphQL subprotocol."); + } + + subprotocol = ValidateNegotiatedSubprotocol(negotiated); + SendConnectionInit(); + await WaitForAckAsync().ConfigureAwait(false); + } + catch + { + // Any failure of the initial connect/handshake propagates to the caller from Start(). + // The transport may still surface the same failure later (e.g. the server closing on + // its own ack timeout); without marking it terminal the retry loop would run the + // full bounded budget in the background and report a second, delayed error for a + // failure the caller already handled. + Interlocked.Exchange(ref terminalFailure, 1); + throw; + } + + StartWatchdog(); + } + + /// + /// , but a failure of our own pre-connect step + /// (a request hook or the connection_init payload delegate) is rethrown as the original + /// exception, with its stack, instead of Websocket.Client's WebsocketException + /// wrapper - so callers can catch their own types (e.g. a token service's exception). + /// + private async Task StartTransportAsync() + { + preparationFailure = null; + try + { + await client.StartOrFail().ConfigureAwait(false); + } + catch (Exception exception) when (preparationFailure is { } failure + && ReferenceEquals(exception.InnerException, failure.SourceException)) + { + failure.Throw(); + } + } + + private void StartWatchdog() + { + Volatile.Write(ref lastMessageTicks, Environment.TickCount64); + var period = TimeSpan.FromMilliseconds(Math.Max(100, NoMessageTimeout.TotalMilliseconds / 4)); + lock (stateLock) + { + if (disposed || watchdog is not null) + { + return; + } + + watchdog = new Timer(_ => CheckSilence(), null, period, period); + } + } + + private void CheckSilence() + { + var silentMs = Environment.TickCount64 - Volatile.Read(ref lastMessageTicks); + if (silentMs < NoMessageTimeout.TotalMilliseconds) + { + return; + } + + lock (stateLock) + { + if (disposed || retryLoopRunning || Volatile.Read(ref terminalFailure) != 0) + { + return; + } + } + + StartRetryLoop(new IOException( + $"No WebSocket message received for {NoMessageTimeout.TotalSeconds:0.#} s; connection treated as lost.")); + } + + private async Task WaitForAckAsync() + { + var source = ackSource; + using var registration = cancellationSource.Token.Register( + () => source.TrySetCanceled(cancellationSource.Token)); + try + { + await source.Task.WaitAsync(TimeSpan.FromSeconds(10), cancellationSource.Token).ConfigureAwait(false); + } + catch (TimeoutException) + { + throw new GraphQueryRequestException( + "WebSocket connection_ack was not received within 10 seconds.", + payload.Query, payload.Variables); + } + } + + private static TaskCompletionSource CreateAckSource() + { + return new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + + private void SendConnectionInit() + { + // The payload was resolved by the connection factory for this connection (before the + // socket opened), so nothing slow runs between the open socket and connection_init. + var initRequest = new WebsocketRequest(WebsocketRequestTypes.CONNECTION_INIT) + { + Payload = connectionInitPayload + }; + + SendRequest(initRequest); + lock (stateLock) + { + initSent = true; + } + + // An eager server may ack before init was queued; if so, subscribe now. + // SendSubscription is idempotent (subscriptionSent) and no-ops otherwise. + SendSubscription(); + } + + private async ValueTask BuildConnectionInitPayloadAsync(string authorization, + CancellationToken cancellationToken) + { + // New option wins; the obsolete WSConnectionInitPayload is the fallback (resolved inside + // GraphClient so the deprecation pragma lives in exactly one place). + var payload = await graphClient.GetConnectionInitPayloadAsync(cancellationToken).ConfigureAwait(false); + + if (!graphClient.ClientOptions.SendAuthorizationInConnectionInit || string.IsNullOrEmpty(authorization)) + { + return payload; + } + + // Blazor WebAssembly path: the browser drops headers on WebSocket requests, so the token + // produced by the request hooks travels inside connection_init (graphql-ws/Apollo style + // "Authorization" connection parameter). The hook value wins on key conflicts. + if (payload is null) + { + return new JsonObject { ["Authorization"] = authorization }; + } + + // Merge on the JSON the payload serializes to - with the same options it would be sent + // with - so any shape that is a JSON object merges: dictionaries of any value type, + // anonymous objects, POCOs ([JsonPropertyName] honored), JsonObject and JsonElement. + if (JsonSerializer.SerializeToNode(payload, payload.GetType(), jsonOptions) is JsonObject merged) + { + merged["Authorization"] = authorization; + return merged; + } + + throw new InvalidOperationException( + "SendAuthorizationInConnectionInit requires the ConnectionInitPayload to serialize to a JSON " + + "object (or be null) so the Authorization connection parameter can be merged; " + + $"it was {payload.GetType()}."); + } + + private string ValidateNegotiatedSubprotocol(string negotiatedProtocol) + { + // Null on reconnect can mean the handshake has not completed yet. Skipping + // (instead of failing) lets the library's reconnect cycle retry; a wrong + // non-null protocol is a real mismatch and stays terminal. + if (string.IsNullOrEmpty(negotiatedProtocol)) + { + return null; + } + + var expected = GetSubprotocolString(); + if (!string.Equals(negotiatedProtocol, expected, StringComparison.Ordinal)) + { + throw new InvalidDataException($"WebSocket server negotiated '{negotiatedProtocol}', expected '{expected}'."); + } + + return negotiatedProtocol; + } + + private void HandleMessage(ResponseMessage message) + { + Volatile.Write(ref lastMessageTicks, Environment.TickCount64); + if (Volatile.Read(ref terminalFailure) != 0 || disposed) + { + return; + } + + if (string.IsNullOrWhiteSpace(message.Text)) + { + ReportFailure(new InvalidDataException("Received an empty WebSocket frame.")); + return; + } + + try + { + using var document = JsonDocument.Parse(message.Text); + var root = document.RootElement; + var type = root.TryGetProperty("type", out var typeElement) ? typeElement.GetString() : null; + var id = root.TryGetProperty("id", out var idElement) ? idElement.GetString() : null; + + switch (type) + { + case null: + return; + case WebsocketRequestTypes.CONNECTION_ACK: + lock (stateLock) + { + if (disposed) + { + return; + } + + acknowledged = true; + retryAttemptsUsed = 0; + } + + ackSource?.TrySetResult(true); + SendSubscription(); + return; + case WebsocketRequestTypes.CONNECTION_ERROR: + ReportFailure(CreateServerException(root.TryGetProperty("payload", out var connectionError) + ? connectionError + : root)); + return; + case WebsocketRequestTypes.PING: + SendPong(root.TryGetProperty("payload", out var pingPayload) ? pingPayload : null); + return; + case WebsocketRequestTypes.PONG: + return; + case "ka": + return; + case WebsocketRequestTypes.ERROR: + if (MatchesActiveSubscription(id)) + { + ReportFailure(CreateServerException(root.TryGetProperty("payload", out var errorPayload) + ? errorPayload + : default)); + } + + return; + case WebsocketRequestTypes.COMPLETE: + if (MatchesActiveSubscription(id)) + { + // Typed consumers (GraphSubscriptionExecute) listen to payloadSubject; + // completing only the string feed left their observable open forever. + payloadSubject.OnCompleted(); + subscriptionSubject.OnCompleted(); + } + + return; + case WebsocketRequestTypes.NEXT: + case WebsocketRequestTypes.DATA: + if (MatchesActiveSubscription(id) && root.TryGetProperty("payload", out var nextPayload)) + { + // Cloned so the element outlives the disposed JsonDocument. + payloadSubject.OnNext(nextPayload.Clone()); + if (subscriptionSubject.HasObservers) + { + subscriptionSubject.OnNext(nextPayload.GetRawText()); + } + } + + return; + default: + // Unknown message types (proxy injections, forward-compatible server + // additions like graphql-ws "ping"/"pong") are ignored, matching the + // SSE transport's tolerance for unknown event types — a single + // unknown type must not kill the subscription. + return; + } + } + catch (Exception exception) + { + ReportFailure(exception); + } + } + + // graphql-ws reference client (client.ts shouldRetryConnectOrThrow): these close codes are + // fatal and never retried. 4403 Forbidden, 4408 init timeout, 4499 and 4504 are retryable. + private static readonly int[] FatalCloseCodes = + { + 4004, 4005, 4400, 4401, 4406, 4409, 4429, 4500, + }; + + // Internal WebSocket close codes in [1000, 1999] are fatal except the normal/transient ones. + private static readonly int[] NonFatalInternalCloseCodes = { 1000, 1001, 1005, 1006, 1012, 1013, 1014 }; + + internal static bool IsFatalCloseCodeForTests(int code) => IsFatalCloseCode(code); + + private static bool IsFatalCloseCode(int code) + { + if (Array.IndexOf(FatalCloseCodes, code) >= 0) + { + return true; + } + + return code is >= 1000 and <= 1999 && Array.IndexOf(NonFatalInternalCloseCodes, code) < 0; + } + + private void HandleDisconnection(DisconnectionInfo info) + { + if (info.Type == DisconnectionType.Exit || info.Type == DisconnectionType.ByUser) + { + return; + } + + lock (stateLock) + { + if (disposed || intentionalDisconnection || Volatile.Read(ref terminalFailure) != 0) + { + return; + } + } + + if (info.Type == DisconnectionType.Error) + { + // A failed connect/reconnect attempt: the retry loop below decides whether to continue. + StartRetryLoop(info.Exception); + return; + } + + // A protocol-level close code means the server rejected this client or operation; + // reconnecting would retry into the same rejection (spec: fatal, never retried). + if (info.CloseStatus is { } closeStatus && IsFatalCloseCode((int)closeStatus)) + { + // Stop the transport from reconnecting in the background after a terminal close. + info.CancelReconnection = true; + ReportFailure(new IOException( + $"WebSocket closed with fatal protocol code {(int)closeStatus}: {info.CloseStatusDescription}")); + return; + } + + // Recoverable disconnect (Lost / NoMessageReceived / ByServer): reconnect through the + // bounded retry loop so a transient outage does not terminate the subscription. + StartRetryLoop(info.Exception); + } + + private void StartRetryLoop(Exception cause) + { + lock (stateLock) + { + if (retryLoopRunning) + { + return; + } + + retryLoopRunning = true; + acknowledged = false; + subscriptionSent = false; + initSent = false; + } + + _ = RunRetryLoopAsync(cause); + } + + // graphql-ws reference client behaviour: bounded retries, exponential backoff with jitter + // (2^n seconds + 300-3000 ms), a fatal close or an exhausted budget reports ONE terminal error. + private async Task RunRetryLoopAsync(Exception lastException) + { + try + { + while (true) + { + int attempt; + bool exhausted; + lock (stateLock) + { + if (disposed || Volatile.Read(ref terminalFailure) != 0) + { + return; + } + + attempt = retryAttemptsUsed; + exhausted = attempt >= maxRetryAttempts; + if (!exhausted) + { + retryAttemptsUsed++; + } + } + + if (exhausted) + { + // Reported outside stateLock: subscribers may dispose synchronously from OnError, + // and DisposeAsync takes stateLock. Keeps the last cause as the inner exception. + ReportFailure(new IOException( + $"WebSocket reconnect failed after {attempt} attempts; giving up.", lastException)); + return; + } + + var delay = RetryDelay(attempt); + await Task.Delay(delay, cancellationSource.Token).ConfigureAwait(false); + + // Re-check after the backoff: a terminal failure (e.g. the initial connect's + // Start() already threw) can be flagged while this loop is sleeping, and the + // attempt below must not run hooks for a client that already reported failure. + if (Volatile.Read(ref terminalFailure) != 0) + { + return; + } + + try + { + // Websocket.Client keeps a failed/aborted socket as its current one: a bare StartOrFail() + // after a failed attempt returns at once with State=Aborted and no connection (verified). + // Stop() resets it so every attempt opens a real socket. + await client.Stop(WebSocketCloseStatus.NormalClosure, string.Empty).ConfigureAwait(false); + await StartTransportAsync().ConfigureAwait(false); + // A fresh ack source per handshake: the previous one is already completed and would + // make WaitForAckAsync return without waiting for this connection's connection_ack. + subprotocol = ValidateNegotiatedSubprotocol(client.NativeClient?.SubProtocol); + ackSource = CreateAckSource(); + SendConnectionInit(); + await WaitForAckAsync().ConfigureAwait(false); + Volatile.Write(ref lastMessageTicks, Environment.TickCount64); + return; + } + catch (OperationCanceledException) when (cancellationSource.IsCancellationRequested) + { + return; + } + catch (Exception exception) + { + lastException = exception; + } + } + } + catch (OperationCanceledException) when (cancellationSource.IsCancellationRequested) + { + // Disposal ends the loop without reporting. + } + finally + { + lock (stateLock) + { + retryLoopRunning = false; + } + } + } + + private static TimeSpan BackoffDelay(int attempt) + { + // attempt 0 -> 1 s, 1 -> 2 s, 2 -> 4 s ... capped at 30 s; plus 300-3000 ms jitter. + var seconds = Math.Min(30, Math.Pow(2, attempt)); + var jitterMs = Random.Shared.Next(300, 3001); + return TimeSpan.FromSeconds(seconds) + TimeSpan.FromMilliseconds(jitterMs); + } + + private void SendSubscription() + { + lock (stateLock) + { + if (disposed || !initSent || !acknowledged || subscriptionSent + || Volatile.Read(ref terminalFailure) != 0) + { + return; + } + + subscriptionSent = true; + } + + SendRequest(new WebsocketRequest(GetSubscribeCommand()) + { + Id = subscriptionId, + Payload = payload + }); + } + + private bool MatchesActiveSubscription(string id) + { + return !string.IsNullOrEmpty(id) && string.Equals(id, subscriptionId, StringComparison.Ordinal); + } + + private Exception CreateServerException(JsonElement payloadElement) + { + List errors; + if (payloadElement.ValueKind == JsonValueKind.Object + && payloadElement.TryGetProperty("errors", out var errorsElement)) + { + errors = errorsElement.Deserialize>(graphClient.SerializerOptions); + } + else if (payloadElement.ValueKind == JsonValueKind.Array) + { + errors = payloadElement.Deserialize>(graphClient.SerializerOptions); + } + else + { + errors = new List + { + new() { Message = payloadElement.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null + ? "Unknown subscription error" + : payloadElement.GetRawText() } + }; + } + + return new GraphQueryExecutionException(errors, payload.Query, payload.Variables); + } + + private void ReportFailure(Exception exception) + { + // Idempotent: the first failure terminates the stream; late/concurrent + // callbacks (message pump, reconnect, disconnect) must not re-emit after + // OnCompleted, and the recoverable paths must not resubscribe post-failure. + if (Interlocked.Exchange(ref terminalFailure, 1) != 0) + { + return; + } + + intentionalDisconnection = true; + + if (exception is GraphQueryExecutionException executionException) + { + errorSubject.OnNext(executionException); + } + + failureSubject.OnNext(exception); + failureSubject.OnCompleted(); + // Failure was already published above, so the typed pipeline keeps the error + // (its terminal transition is first-wins); this only releases the feed. + payloadSubject.OnCompleted(); + subscriptionSubject.OnCompleted(); + ackSource?.TrySetException(exception); + } + + private void SendPong(JsonElement? payloadElement) + { + var response = new WebsocketRequest(WebsocketRequestTypes.PONG); + if (payloadElement is { } value && value.ValueKind is not (JsonValueKind.Null or JsonValueKind.Undefined)) + { + // Serialize the element directly (valid while the parsed document is alive). + response.Payload = value; + } + + SendRequest(response); + } + + private string GetSubprotocolString() + { + return graphClient.SubscriptionProtocol switch + { + SubscriptionProtocol.GraphQLWebSocket => SubscriptionProtocols.GRAPGQL_TRANSPORT_WS, + SubscriptionProtocol.ApolloWebSocket => SubscriptionProtocols.GRAPHQL_WS, + _ => throw new InvalidOperationException($"{graphClient.SubscriptionProtocol} is unknown") + }; + } + + private string GetSubscribeCommand() + { + return graphClient.SubscriptionProtocol switch + { + SubscriptionProtocol.GraphQLWebSocket => SubscribeCommands.SUBSCRIBE, + SubscriptionProtocol.ApolloWebSocket => SubscribeCommands.START, + _ => throw new InvalidOperationException($"{graphClient.SubscriptionProtocol} is unknown") + }; + } + + private string GetCompleteCommand() + { + return graphClient.SubscriptionProtocol == SubscriptionProtocol.ApolloWebSocket + ? SubscribeCommands.STOP + : WebsocketRequestTypes.COMPLETE; + } + + private void SendRequest(WebsocketRequest request) + { + var json = JsonSerializer.Serialize(request, jsonOptions); + if (!client.Send(json)) + { + throw new IOException("WebSocket message could not be queued for sending."); + } + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/WebsocketRequest.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/WebsocketRequest.cs new file mode 100644 index 00000000..643d76a3 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/WebsocketRequest.cs @@ -0,0 +1,41 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json.Serialization; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Subscriptions; + +public class WebsocketRequest +{ + public WebsocketRequest(string type) + { + Type = type; + } + + [JsonPropertyName("id")] public string Id { get; set; } + + [JsonPropertyName("type")] public string Type { get; set; } + + [JsonPropertyName("payload")] public object Payload { get; set; } +} + +//public class WebsocketRequestPayload +//{ +// [JsonPropertyName("query")] +// public string Query { get; set; } + +// [JsonPropertyName("variables")] +// public Dictionary Variables { get; set; } +//} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/WebsocketRequestTypes.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/WebsocketRequestTypes.cs new file mode 100644 index 00000000..c77f5d77 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/WebsocketRequestTypes.cs @@ -0,0 +1,30 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +namespace Linq2GraphQL.TestClientStandalone.Runtime.Subscriptions +{ + internal class WebsocketRequestTypes + { + internal const string PING = "ping"; + internal const string PONG = "pong"; + internal const string CONNECTION_INIT = "connection_init"; + internal const string CONNECTION_ACK = "connection_ack"; + internal const string ERROR = "error"; + internal const string COMPLETE = "complete"; + internal const string NEXT = "next"; + internal const string DATA = "data"; + internal const string CONNECTION_ERROR = "connection_error"; + } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/WebsocketResponse.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/WebsocketResponse.cs new file mode 100644 index 00000000..871fba7a --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Runtime/Subscriptions/WebsocketResponse.cs @@ -0,0 +1,27 @@ +//--------------------------------------------------------------------- +// +// Copy of the Linq2GraphQL runtime, written by the Linq2GraphQL generator +// in standalone mode. Please don't edit this file - regenerate instead. +// The runtime version is in RuntimeInfo.cs. +// Github:https://github.com/linq2graphql/linq2graphql.client +//--------------------------------------------------------------------- +#nullable disable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json.Serialization; + +namespace Linq2GraphQL.TestClientStandalone.Runtime.Subscriptions; + +public class WebsocketResponse +{ + [JsonPropertyName("id")] public string Id { get; set; } + + [JsonPropertyName("type")] public string Type { get; set; } + + [JsonPropertyName("payload")] public object Payload { get; set; } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Address.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Address.cs new file mode 100644 index 00000000..cae6a429 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Address.cs @@ -0,0 +1,35 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone; + + +public partial class Address : GraphQLTypeBase +{ + [GraphQLMember("name")] + [JsonPropertyName("name")] + public string Name { get; set; } + + [GraphQLMember("street")] + [JsonPropertyName("street")] + public string Street { get; set; } + + [GraphQLMember("postalCode")] + [JsonPropertyName("postalCode")] + public string PostalCode { get; set; } + + [GraphQLMember("createdDate")] + [JsonPropertyName("createdDate")] + public DateOnly CreatedDate { get; set; } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Types/AnimalsConnection.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/AnimalsConnection.cs new file mode 100644 index 00000000..d5dc82ed --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/AnimalsConnection.cs @@ -0,0 +1,55 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone; + + +/// +/// A connection to a list of items. +/// +[global::System.ComponentModel.Description("A connection to a list of items.")] +public partial class AnimalsConnection : GraphQLTypeBase, Linq2GraphQL.TestClientStandalone.Runtime.Common.ICursorPaging +{ + /// + /// Information to aid in pagination. + /// + [global::System.ComponentModel.Description("Information to aid in pagination.")] + [GraphQLMember("pageInfo")] + [JsonPropertyName("pageInfo")] + public Linq2GraphQL.TestClientStandalone.Runtime.Common.PageInfo PageInfo { get; set; } + + /// + /// A list of edges. + /// + [global::System.ComponentModel.Description("A list of edges.")] + [GraphQLMember("edges")] + [JsonPropertyName("edges")] + public List Edges { get; set; } + + /// + /// A flattened list of the nodes. + /// + [global::System.ComponentModel.Description("A flattened list of the nodes.")] + [GraphQLMember("nodes")] + [JsonPropertyName("nodes")] + public List Nodes { get; set; } + + /// + /// Identifies the total count of items in the connection. + /// + [global::System.ComponentModel.Description("Identifies the total count of items in the connection.")] + [GraphQLMember("totalCount")] + [JsonPropertyName("totalCount")] + public int TotalCount { get; set; } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Types/AnimalsEdge.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/AnimalsEdge.cs new file mode 100644 index 00000000..6c88c3a2 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/AnimalsEdge.cs @@ -0,0 +1,39 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone; + + +/// +/// An edge in a connection. +/// +[global::System.ComponentModel.Description("An edge in a connection.")] +public partial class AnimalsEdge : GraphQLTypeBase +{ + /// + /// A cursor for use in pagination. + /// + [global::System.ComponentModel.Description("A cursor for use in pagination.")] + [GraphQLMember("cursor")] + [JsonPropertyName("cursor")] + public string Cursor { get; set; } + + /// + /// The item at the end of the edge. + /// + [global::System.ComponentModel.Description("The item at the end of the edge.")] + [GraphQLMember("node")] + [JsonPropertyName("node")] + public IAnimal Node { get; set; } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Customer.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Customer.cs new file mode 100644 index 00000000..c4e0f651 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Customer.cs @@ -0,0 +1,71 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone; + + +public static class CustomerExtensions +{ + [GraphQLMember("relatedCustomers")] + public static List RelatedCustomers(this Customer customer, [GraphQLArgument("relationType", "Int!")] int relationType) + { + return customer.GetMethodValue>("relatedCustomers", relationType); + } + + [GraphQLMember("relatedCustomer")] + public static Customer RelatedCustomer(this Customer customer, [GraphQLArgument("relationType", "Int!")] int relationType) + { + return customer.GetMethodValue("relatedCustomer", relationType); + } + +} + +public partial class Customer : GraphQLTypeBase +{ + [GraphQLMember("customerId")] + [JsonPropertyName("customerId")] + public Guid CustomerId { get; set; } + + [GraphQLMember("customerName")] + [JsonPropertyName("customerName")] + public string CustomerName { get; set; } + + [GraphQLMember("status")] + [JsonPropertyName("status")] + public CustomerStatus Status { get; set; } + + [GraphQLMember("orders")] + [JsonPropertyName("orders")] + public List Orders { get; set; } + + [GraphQLMember("address")] + [JsonPropertyName("address")] + public Address Address { get; set; } + + [GraphQLMember("created")] + [JsonPropertyName("created")] + public DateTimeOffset? Created { get; set; } + + private LazyProperty> _relatedCustomers = new(); + /// + /// Do not use in Query, only to retrive result + /// + public List RelatedCustomers => _relatedCustomers.Value(() => GetFirstMethodValue>("relatedCustomers")); + + private LazyProperty _relatedCustomer = new(); + /// + /// Do not use in Query, only to retrive result + /// + public Customer RelatedCustomer => _relatedCustomer.Value(() => GetFirstMethodValue("relatedCustomer")); + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Item.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Item.cs new file mode 100644 index 00000000..941a6f1d --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Item.cs @@ -0,0 +1,31 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone; + + +public partial class Item : GraphQLTypeBase +{ + [GraphQLMember("itemId")] + [JsonPropertyName("itemId")] + public string ItemId { get; set; } + + [GraphQLMember("itemName")] + [JsonPropertyName("itemName")] + public string ItemName { get; set; } + + [GraphQLMember("data")] + [JsonPropertyName("data")] + public List Data { get; set; } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Order.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Order.cs new file mode 100644 index 00000000..55f16cd4 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Order.cs @@ -0,0 +1,98 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone; + + +public static class OrderExtensions +{ + [GraphQLMember("myGradeMethod")] + [Obsolete("This method should not be used anymore")] + public static string MyGradeMethod(this Order order, [GraphQLArgument("name", "String!")] string name) + { + return order.GetMethodValue("myGradeMethod", name); + } + + [GraphQLMember("orderHello")] + public static string OrderHello(this Order order, [GraphQLArgument("name", "String!")] string name, [GraphQLArgument("first", "Int!")] int first) + { + return order.GetMethodValue("orderHello", name, first); + } + + [GraphQLMember("orderAddress")] + public static Address OrderAddress(this Order order, [GraphQLArgument("addressType", "AddressType!")] AddressType addressType) + { + return order.GetMethodValue
("orderAddress", addressType); + } + +} + +public partial class Order : GraphQLTypeBase +{ + private LazyProperty _myGradeMethod = new(); + /// + /// Do not use in Query, only to retrive result + /// + [Obsolete("This method should not be used anymore")] + public string MyGradeMethod => _myGradeMethod.Value(() => GetFirstMethodValue("myGradeMethod")); + + private LazyProperty _orderHello = new(); + /// + /// Do not use in Query, only to retrive result + /// + public string OrderHello => _orderHello.Value(() => GetFirstMethodValue("orderHello")); + + private LazyProperty
_orderAddress = new(); + /// + /// Do not use in Query, only to retrive result + /// + public Address OrderAddress => _orderAddress.Value(() => GetFirstMethodValue
("orderAddress")); + + [GraphQLMember("orderId")] + [JsonPropertyName("orderId")] + public Guid OrderId { get; set; } + + [GraphQLMember("customer")] + [JsonPropertyName("customer")] + public Customer Customer { get; set; } + + [GraphQLMember("address")] + [JsonPropertyName("address")] + public Address Address { get; set; } + + [GraphQLMember("orderDate")] + [JsonPropertyName("orderDate")] + public DateTimeOffset OrderDate { get; set; } + + [GraphQLMember("orderDateTime")] + [JsonPropertyName("orderDateTime")] + public DateTimeOffset OrderDateTime { get; set; } + + [GraphQLMember("orderDay")] + [JsonPropertyName("orderDay")] + public DateOnly OrderDay { get; set; } + + [GraphQLMember("lines")] + [JsonPropertyName("lines")] + public List Lines { get; set; } + + [GraphQLMember("entryTime")] + [JsonPropertyName("entryTime")] + public TimeOnly? EntryTime { get; set; } + + [Obsolete("This propery is obsolete and should not be used!")] + [GraphQLMember("grade")] + [JsonPropertyName("grade")] + public string Grade { get; set; } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrderLine.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrderLine.cs new file mode 100644 index 00000000..99c4992e --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrderLine.cs @@ -0,0 +1,39 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone; + + +public partial class OrderLine : GraphQLTypeBase +{ + [GraphQLMember("lineNumber")] + [JsonPropertyName("lineNumber")] + public int LineNumber { get; set; } + + [GraphQLMember("order")] + [JsonPropertyName("order")] + public Order Order { get; set; } + + [GraphQLMember("item")] + [JsonPropertyName("item")] + public Item Item { get; set; } + + [GraphQLMember("price")] + [JsonPropertyName("price")] + public decimal Price { get; set; } + + [GraphQLMember("quantity")] + [JsonPropertyName("quantity")] + public double Quantity { get; set; } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersConnection.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersConnection.cs new file mode 100644 index 00000000..914fa116 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersConnection.cs @@ -0,0 +1,55 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone; + + +/// +/// A connection to a list of items. +/// +[global::System.ComponentModel.Description("A connection to a list of items.")] +public partial class OrdersConnection : GraphQLTypeBase, Linq2GraphQL.TestClientStandalone.Runtime.Common.ICursorPaging +{ + /// + /// Information to aid in pagination. + /// + [global::System.ComponentModel.Description("Information to aid in pagination.")] + [GraphQLMember("pageInfo")] + [JsonPropertyName("pageInfo")] + public Linq2GraphQL.TestClientStandalone.Runtime.Common.PageInfo PageInfo { get; set; } + + /// + /// A list of edges. + /// + [global::System.ComponentModel.Description("A list of edges.")] + [GraphQLMember("edges")] + [JsonPropertyName("edges")] + public List Edges { get; set; } + + /// + /// A flattened list of the nodes. + /// + [global::System.ComponentModel.Description("A flattened list of the nodes.")] + [GraphQLMember("nodes")] + [JsonPropertyName("nodes")] + public List Nodes { get; set; } + + /// + /// Identifies the total count of items in the connection. + /// + [global::System.ComponentModel.Description("Identifies the total count of items in the connection.")] + [GraphQLMember("totalCount")] + [JsonPropertyName("totalCount")] + public int TotalCount { get; set; } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersEdge.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersEdge.cs new file mode 100644 index 00000000..38276cfb --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersEdge.cs @@ -0,0 +1,39 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone; + + +/// +/// An edge in a connection. +/// +[global::System.ComponentModel.Description("An edge in a connection.")] +public partial class OrdersEdge : GraphQLTypeBase +{ + /// + /// A cursor for use in pagination. + /// + [global::System.ComponentModel.Description("A cursor for use in pagination.")] + [GraphQLMember("cursor")] + [JsonPropertyName("cursor")] + public string Cursor { get; set; } + + /// + /// The item at the end of the edge. + /// + [global::System.ComponentModel.Description("The item at the end of the edge.")] + [GraphQLMember("node")] + [JsonPropertyName("node")] + public Order Node { get; set; } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersNoBackwardPaginationConnection.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersNoBackwardPaginationConnection.cs new file mode 100644 index 00000000..d39b9ab4 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersNoBackwardPaginationConnection.cs @@ -0,0 +1,55 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone; + + +/// +/// A connection to a list of items. +/// +[global::System.ComponentModel.Description("A connection to a list of items.")] +public partial class OrdersNoBackwardPaginationConnection : GraphQLTypeBase, Linq2GraphQL.TestClientStandalone.Runtime.Common.ICursorPaging +{ + /// + /// Information to aid in pagination. + /// + [global::System.ComponentModel.Description("Information to aid in pagination.")] + [GraphQLMember("pageInfo")] + [JsonPropertyName("pageInfo")] + public Linq2GraphQL.TestClientStandalone.Runtime.Common.PageInfo PageInfo { get; set; } + + /// + /// A list of edges. + /// + [global::System.ComponentModel.Description("A list of edges.")] + [GraphQLMember("edges")] + [JsonPropertyName("edges")] + public List Edges { get; set; } + + /// + /// A flattened list of the nodes. + /// + [global::System.ComponentModel.Description("A flattened list of the nodes.")] + [GraphQLMember("nodes")] + [JsonPropertyName("nodes")] + public List Nodes { get; set; } + + /// + /// Identifies the total count of items in the connection. + /// + [global::System.ComponentModel.Description("Identifies the total count of items in the connection.")] + [GraphQLMember("totalCount")] + [JsonPropertyName("totalCount")] + public int TotalCount { get; set; } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersNoBackwardPaginationEdge.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersNoBackwardPaginationEdge.cs new file mode 100644 index 00000000..2ba50b75 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersNoBackwardPaginationEdge.cs @@ -0,0 +1,39 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone; + + +/// +/// An edge in a connection. +/// +[global::System.ComponentModel.Description("An edge in a connection.")] +public partial class OrdersNoBackwardPaginationEdge : GraphQLTypeBase +{ + /// + /// A cursor for use in pagination. + /// + [global::System.ComponentModel.Description("A cursor for use in pagination.")] + [GraphQLMember("cursor")] + [JsonPropertyName("cursor")] + public string Cursor { get; set; } + + /// + /// The item at the end of the edge. + /// + [global::System.ComponentModel.Description("The item at the end of the edge.")] + [GraphQLMember("node")] + [JsonPropertyName("node")] + public Order Node { get; set; } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersNoTotalCountConnection.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersNoTotalCountConnection.cs new file mode 100644 index 00000000..d98a1bac --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersNoTotalCountConnection.cs @@ -0,0 +1,47 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone; + + +/// +/// A connection to a list of items. +/// +[global::System.ComponentModel.Description("A connection to a list of items.")] +public partial class OrdersNoTotalCountConnection : GraphQLTypeBase, Linq2GraphQL.TestClientStandalone.Runtime.Common.ICursorPaging +{ + /// + /// Information to aid in pagination. + /// + [global::System.ComponentModel.Description("Information to aid in pagination.")] + [GraphQLMember("pageInfo")] + [JsonPropertyName("pageInfo")] + public Linq2GraphQL.TestClientStandalone.Runtime.Common.PageInfo PageInfo { get; set; } + + /// + /// A list of edges. + /// + [global::System.ComponentModel.Description("A list of edges.")] + [GraphQLMember("edges")] + [JsonPropertyName("edges")] + public List Edges { get; set; } + + /// + /// A flattened list of the nodes. + /// + [global::System.ComponentModel.Description("A flattened list of the nodes.")] + [GraphQLMember("nodes")] + [JsonPropertyName("nodes")] + public List Nodes { get; set; } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersNoTotalCountEdge.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersNoTotalCountEdge.cs new file mode 100644 index 00000000..80a88f92 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersNoTotalCountEdge.cs @@ -0,0 +1,39 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone; + + +/// +/// An edge in a connection. +/// +[global::System.ComponentModel.Description("An edge in a connection.")] +public partial class OrdersNoTotalCountEdge : GraphQLTypeBase +{ + /// + /// A cursor for use in pagination. + /// + [global::System.ComponentModel.Description("A cursor for use in pagination.")] + [GraphQLMember("cursor")] + [JsonPropertyName("cursor")] + public string Cursor { get; set; } + + /// + /// The item at the end of the edge. + /// + [global::System.ComponentModel.Description("The item at the end of the edge.")] + [GraphQLMember("node")] + [JsonPropertyName("node")] + public Order Node { get; set; } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersOffsetPagingConnection.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersOffsetPagingConnection.cs new file mode 100644 index 00000000..527b710b --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersOffsetPagingConnection.cs @@ -0,0 +1,47 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone; + + +/// +/// A connection to a list of items. +/// +[global::System.ComponentModel.Description("A connection to a list of items.")] +public partial class OrdersOffsetPagingConnection : GraphQLTypeBase, Linq2GraphQL.TestClientStandalone.Runtime.Common.ICursorPaging +{ + /// + /// Information to aid in pagination. + /// + [global::System.ComponentModel.Description("Information to aid in pagination.")] + [GraphQLMember("pageInfo")] + [JsonPropertyName("pageInfo")] + public Linq2GraphQL.TestClientStandalone.Runtime.Common.PageInfo PageInfo { get; set; } + + /// + /// A list of edges. + /// + [global::System.ComponentModel.Description("A list of edges.")] + [GraphQLMember("edges")] + [JsonPropertyName("edges")] + public List Edges { get; set; } + + /// + /// A flattened list of the nodes. + /// + [global::System.ComponentModel.Description("A flattened list of the nodes.")] + [GraphQLMember("nodes")] + [JsonPropertyName("nodes")] + public List Nodes { get; set; } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersOffsetPagingEdge.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersOffsetPagingEdge.cs new file mode 100644 index 00000000..b20d5795 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/OrdersOffsetPagingEdge.cs @@ -0,0 +1,39 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone; + + +/// +/// An edge in a connection. +/// +[global::System.ComponentModel.Description("An edge in a connection.")] +public partial class OrdersOffsetPagingEdge : GraphQLTypeBase +{ + /// + /// A cursor for use in pagination. + /// + [global::System.ComponentModel.Description("A cursor for use in pagination.")] + [GraphQLMember("cursor")] + [JsonPropertyName("cursor")] + public string Cursor { get; set; } + + /// + /// The item at the end of the edge. + /// + [global::System.ComponentModel.Description("The item at the end of the edge.")] + [GraphQLMember("node")] + [JsonPropertyName("node")] + public Order Node { get; set; } + +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Pig.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Pig.cs new file mode 100644 index 00000000..8b6f29a7 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Pig.cs @@ -0,0 +1,38 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone; + + +public partial class Pig : GraphQLTypeBase, IAnimal +{ + [GraphQLMember("spices")] + [JsonPropertyName("spices")] + public string Spices { get; set; } + + [GraphQLMember("name")] + [JsonPropertyName("name")] + public string Name { get; set; } + + [GraphQLMember("numberOfLegs")] + [JsonPropertyName("numberOfLegs")] + public int NumberOfLegs { get; set; } + + [GraphQLMember("speed")] + [JsonPropertyName("speed")] + public int Speed { get; set; } + + [GraphQLMember("__typename")] + [JsonPropertyName("__typename")] + public string __TypeName { get; set; } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Spider.cs b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Spider.cs new file mode 100644 index 00000000..9a08069c --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Generated/Types/Spider.cs @@ -0,0 +1,38 @@ +//--------------------------------------------------------------------- +// This code was automatically generated by Linq2GraphQL +// Please don't edit this file +// Github:https://github.com/linq2graphql/linq2graphql.client +// Url: https://linq2graphql.com +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Linq2GraphQL.TestClientStandalone.Runtime; +using Linq2GraphQL.TestClientStandalone.Runtime.Common; + +namespace Linq2GraphQL.TestClientStandalone; + + +public partial class Spider : GraphQLTypeBase, IAnimal +{ + [GraphQLMember("poisonous")] + [JsonPropertyName("poisonous")] + public bool Poisonous { get; set; } + + [GraphQLMember("name")] + [JsonPropertyName("name")] + public string Name { get; set; } + + [GraphQLMember("numberOfLegs")] + [JsonPropertyName("numberOfLegs")] + public int NumberOfLegs { get; set; } + + [GraphQLMember("speed")] + [JsonPropertyName("speed")] + public int Speed { get; set; } + + [GraphQLMember("__typename")] + [JsonPropertyName("__typename")] + public string __TypeName { get; set; } +} diff --git a/test/Linq2GraphQL.TestClientStandalone/Linq2GraphQL.TestClientStandalone.csproj b/test/Linq2GraphQL.TestClientStandalone/Linq2GraphQL.TestClientStandalone.csproj new file mode 100644 index 00000000..2b040d30 --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/Linq2GraphQL.TestClientStandalone.csproj @@ -0,0 +1,28 @@ + + + + + net10.0 + enable + disable + + + + + + + + + + + + + + + + diff --git a/test/Linq2GraphQL.TestClientStandalone/packages.lock.json b/test/Linq2GraphQL.TestClientStandalone/packages.lock.json new file mode 100644 index 00000000..f4321b9e --- /dev/null +++ b/test/Linq2GraphQL.TestClientStandalone/packages.lock.json @@ -0,0 +1,186 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "GraphQL-Parser": { + "type": "Direct", + "requested": "[9.5.1, )", + "resolved": "9.5.1", + "contentHash": "pUYpupTsVyNcKemko8f0iknJa2z/oZsfzwb4crgEFN9/pKf3k35gxVXDDOkD3NVPF1ea6GUV0jgxkjCozFj7Fg==" + }, + "Microsoft.Extensions.AI.Abstractions": { + "type": "Direct", + "requested": "[10.10.0, )", + "resolved": "10.10.0", + "contentHash": "frBHj2ckpevA+KdDLikxv8oxGPrJiyTvaiblYp+vtVEt0q6J73XP5m2SICm86nFK6rl+L1GCKxIy4S5Lb3p2kQ==" + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Direct", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "el1g0mBEbDBGY2bT9mcSfrTWO8QlPdq2nOCnvQugioOFwHV+bVBMeiakoI0dNOdj8d6Hi9K6HY2xzRUWJiDR3w==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.11", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.Http": { + "type": "Direct", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "ujx8RvcKzkxPFBguwgiygbwWVHVK0P7HFlNJ6I0JBRb28tzwe42jixBoA+dGmqG9IHepPVcm04vv2IDnU93ekA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Diagnostics": "10.0.11", + "Microsoft.Extensions.Logging": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11" + } + }, + "Nerdbank.GitVersioning": { + "type": "Direct", + "requested": "[3.8.118, )", + "resolved": "3.8.118", + "contentHash": "cRaG+ICcECG+CzbtQyUV2WftH7yl2B02AjYGGNScXx8TwYavZYwhCewBTiC0qTcsac7m6AzBUYna5xzBWmTGYw==" + }, + "Websocket.Client": { + "type": "Direct", + "requested": "[5.3.0, )", + "resolved": "5.3.0", + "contentHash": "uhdDM+gruCEhHRCKCoyali1HJp0wSS/HBs5X9XZwULNKM2y5ML188TsvcEgWEFOx0NOaHfGNtfoC0cd1p2NOIg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IO.RecyclableMemoryStream": "3.0.0", + "System.Reactive": "6.0.0" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "vUl798SmruTqqlt/xH2gDk3tJlhk6k3HdOXAHirlRfbNKDym4g/kRpUL9S4sl6F6FsOTOMW+ZsDapqlZMOOiEw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "fVi053xdpda9Em7vSkmgVxO/PtgC2m78ekReKWsgcyskqY0U82Bz/MONwxpGzI0hElYKJfw+fupqMVeKW3fSaA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "rFn8RuszZn3qquPVkDytMUlPc2+rXl9MCoygwc1XmAgC5vg5/oXJ8hkOosOrLoBLsqdTy4lFwP6iQdPS9uSYOA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "/a1aJz4m7ylhEDf25ugQChLQoN5XwoGjWw/BoR/ZWWKsO1v4DdJElS1uyngahz4B/eOzjFk1KNTkarRLE5wsIg==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "HT70uGPxMLqqnOzKMcnQtDmeV4r0KHr4qVCLhP7SXil9jMEm8sQXwcybxVVFGXZJ1V44xV0mLqQ54aZbcR2OiQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.11", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.11" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "se7Kx8QpJEt+nf26L4qIVAofGTDr1wbexxsh/Fm3Xc04xUkqUXK06KUS7FLwSQYSjqb7q9n+T7MEcXYBhI1Y5g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "Ljd0Uxoq5XpScD2Bg0nM/r3mwx7Ao5Uq24eo2ARxbGvqJ7Zht6rt2cJtwVRH4Cv+1ZVMdXz6TB43KbpmsxRrvQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "eY1GAKcTfD2maP27J84X9IovT3yjHJ2dVDzPmDg6/XqYvt3jMzJhtfQCLjG9pVsZGAd+8DQ2QrjaDcs2+VQLGw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "syEhXQ/sEaSBFaqzlp9gDGHX/nk6gkQkh1sIUpBO1mlBj3Phu1rmb4ML1uCiyPW9N6Kxfxv3y5FGObC+bV01Qw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.Configuration.Binder": "10.0.11", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "SXcz+kF+4Oo9b1+55zntpJFYfwb1jw66ioxptyNOOTDc8g2FHnBFWjZpsWfCvZIhzr0x+4e2trVTs4OKwQfBtw==" + }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "irv0HuqoH8Ig5i2fO+8dmDNdFdsrO+DoQcedwIlb810qpZHBNQHZLW7C/AHBQDgLLpw2T96vmMAy/aE4Yj55Sg==" + }, + "System.Reactive": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "31kfaW4ZupZzPsI5PVe77VhnvFF55qgma7KZr/E0iFTs6fmdhhG8j0mgEx620iLTey1EynOkEfnyTjtNEpJzGw==" + }, + "Microsoft.Extensions.Configuration": { + "type": "CentralTransitive", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "wlhRqZW8LcJPa+vk2oLAc/REXDItHtkFQdf/QcXYGZbZOO13izcsKY1pCvuFQYwUiZD+hwSZwsKASjqT+BNaVg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "CentralTransitive", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "PSmotV19c7E3lKed++uYo1kSiXFI+uTl37CBSrhq+CfLC3FCHjG7R91+xPnNehQfHS1b0Tzo/CCLPWH3qaEheg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11" + } + }, + "Microsoft.Extensions.Logging": { + "type": "CentralTransitive", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "nUOJwgFkSiLHiVGFpU22pIJtuWYewuSYQ3JVuP/gdK8ASMT807Px+TYQiRWs6uSsOmoyFTaVCwKXTasczV6BpA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11" + } + } + } + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.Tests.Standalone/Linq2GraphQL.Tests.Standalone.csproj b/test/Linq2GraphQL.Tests.Standalone/Linq2GraphQL.Tests.Standalone.csproj new file mode 100644 index 00000000..15967aa7 --- /dev/null +++ b/test/Linq2GraphQL.Tests.Standalone/Linq2GraphQL.Tests.Standalone.csproj @@ -0,0 +1,57 @@ + + + + + net10.0 + enable + enable + + false + true + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + diff --git a/test/Linq2GraphQL.Tests.Standalone/StandaloneNamespaces.cs b/test/Linq2GraphQL.Tests.Standalone/StandaloneNamespaces.cs new file mode 100644 index 00000000..25265a60 --- /dev/null +++ b/test/Linq2GraphQL.Tests.Standalone/StandaloneNamespaces.cs @@ -0,0 +1,35 @@ +// The shared test files import the package namespaces. Nothing here references those packages, so the +// namespaces are declared empty to keep the imports valid, and the standalone copy is imported instead. +global using Linq2GraphQL.TestClientStandalone; +global using Linq2GraphQL.TestClientStandalone.Runtime; +global using Linq2GraphQL.TestClientStandalone.Runtime.Common; +global using Linq2GraphQL.TestClientStandalone.Runtime.Subscriptions; +global using Linq2GraphQL.TestClientStandalone.Runtime.AI; +global using Linq2GraphQL.TestClientStandalone.Runtime.AI.Schema; +global using Linq2GraphQL.TestClientStandalone.Runtime.AI.Tools; + +namespace Linq2GraphQL.Client +{ + namespace Common + { + } + + namespace AI + { + namespace Schema + { + } + + namespace Tools + { + } + } + + namespace Subscriptions + { + } +} + +namespace Linq2GraphQL.TestClient +{ +} diff --git a/test/Linq2GraphQL.Tests.Standalone/packages.lock.json b/test/Linq2GraphQL.Tests.Standalone/packages.lock.json new file mode 100644 index 00000000..50bda511 --- /dev/null +++ b/test/Linq2GraphQL.Tests.Standalone/packages.lock.json @@ -0,0 +1,1082 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "Microsoft.AspNetCore.Mvc.Testing": { + "type": "Direct", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "BJi3rpYETlh0wd14Lg5Is9MWvEf55TAGd7yo+xGHuEZp7ololyvftbQZ1i7ZuITA7DgNPJED6EMhFSWycq7SBg==", + "dependencies": { + "Microsoft.AspNetCore.TestHost": "10.0.11", + "Microsoft.Extensions.DependencyModel": "10.0.11", + "Microsoft.Extensions.Hosting": "10.0.11" + } + }, + "Microsoft.Extensions.AI": { + "type": "Direct", + "requested": "[10.10.0, )", + "resolved": "10.10.0", + "contentHash": "XKvtDUD3KySkvA5UpS+UF67M5rcqiXjAQchTZZ26WnavVKXxwlUTVdK/Ju0tc/25+16AVUdNl8cnVVGX/tx03A==", + "dependencies": { + "Microsoft.Extensions.AI.Abstractions": "10.10.0", + "Microsoft.Extensions.Caching.Abstractions": "10.0.12", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12", + "System.Numerics.Tensors": "10.0.12" + } + }, + "Microsoft.Extensions.TimeProvider.Testing": { + "type": "Direct", + "requested": "[10.10.0, )", + "resolved": "10.10.0", + "contentHash": "8duZdxuO3Oo8tlyQXVq1drXee6GJC9DjUU97Pq3YaIqf92jrAHsWxbbRKwvjQI48Mer3IANwUKF0elqvHeY4Rw==" + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[17.14.1, )", + "resolved": "17.14.1", + "contentHash": "HJKqKOE+vshXra2aEHpi2TlxYX7Z9VFYkr+E5rwEvHC8eIXiyO+K9kNm8vmNom3e2rA56WqxU+/N9NJlLGXsJQ==", + "dependencies": { + "Microsoft.CodeCoverage": "17.14.1", + "Microsoft.TestPlatform.TestHost": "17.14.1" + } + }, + "Moq": { + "type": "Direct", + "requested": "[4.20.72, )", + "resolved": "4.20.72", + "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "Nerdbank.GitVersioning": { + "type": "Direct", + "requested": "[3.8.118, )", + "resolved": "3.8.118", + "contentHash": "cRaG+ICcECG+CzbtQyUV2WftH7yl2B02AjYGGNScXx8TwYavZYwhCewBTiC0qTcsac7m6AzBUYna5xzBWmTGYw==" + }, + "Shouldly": { + "type": "Direct", + "requested": "[4.3.0, )", + "resolved": "4.3.0", + "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", + "dependencies": { + "DiffEngine": "11.3.0", + "EmptyFiles": "4.4.0" + } + }, + "xunit": { + "type": "Direct", + "requested": "[2.9.3, )", + "resolved": "2.9.3", + "contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==", + "dependencies": { + "xunit.analyzers": "1.18.0", + "xunit.assert": "2.9.3", + "xunit.core": "[2.9.3]" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[3.1.5, )", + "resolved": "3.1.5", + "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "dependencies": { + "System.Diagnostics.EventLog": "6.0.0" + } + }, + "ChilliCream.Nitro.App": { + "type": "Transitive", + "resolved": "32.0.2", + "contentHash": "wSD2dFFraVpIsQDiy31k1vkGWYAGdzNVXOhtOIujMt/ADAzfkEbJUsPyF4qoktFspepN4VrAN7DMS8QRqkEhTg==", + "dependencies": { + "Yarp.ReverseProxy": "2.3.0" + } + }, + "DiffEngine": { + "type": "Transitive", + "resolved": "11.3.0", + "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", + "dependencies": { + "EmptyFiles": "4.4.0", + "System.Management": "6.0.1" + } + }, + "EmptyFiles": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" + }, + "GreenDonut": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "nzv5xP1krDEqhq4GekXWdKlvGPb0NM8JF3B0WpEqIS/tVG7U3sBaQuGCMSNJ02kvrtcde+YEPYtcTh7Y0SJ+CA==", + "dependencies": { + "GreenDonut.Abstractions": "16.6.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.ObjectPool": "10.0.0" + } + }, + "GreenDonut.Abstractions": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "egSQAz8BbFmLqOWtX0s/xlaqz/43fHUzdu/9TjpZ3s4F8FIVgO4rBfKNFWt2nsba9tvT1AW2WbAaf+3pY6IN0w==" + }, + "GreenDonut.Data": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "kYYLxAEPJl0/vdWF2i5pNs6pndGbuiz8OFAyUovfsu2Guo3dSomkU91PC0kpOeToXoQyRDdjz3GUKNBFnT9PQw==", + "dependencies": { + "GreenDonut": "16.6.4", + "GreenDonut.Data.Abstractions": "16.6.4" + } + }, + "GreenDonut.Data.Abstractions": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "Ao8i3nm86+vRKlpctKoWvZ4OdjSLPAQmWp3VMFmCuMVF3rbsVR8TxLRcAG/2/B6VdbGQyOO8tJUGSAooncu28g==", + "dependencies": { + "GreenDonut.Abstractions": "16.6.4", + "GreenDonut.Data.Primitives": "16.6.4" + } + }, + "GreenDonut.Data.Primitives": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "p3185VDDm5xV6ZQrkJWkmYaPCVE2BIg6up6R6DrQKnCwIMv3VDzFgNLnVu2eNqoj3iEplBZJBdk1OzvYZO3Xdg==" + }, + "HotChocolate": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "bvEKPvyd0OhZd5usrPxWcGToM860SOlupW+edNobvZiyltlcxnZkfZ7u8Vx8MNtA0OswLyjaL1RI3+JashlE/g==", + "dependencies": { + "HotChocolate.Authorization": "16.6.4", + "HotChocolate.CostAnalysis": "16.6.4", + "HotChocolate.Execution.Projections": "16.6.4", + "HotChocolate.Types": "16.6.4", + "HotChocolate.Types.CursorPagination": "16.6.4", + "HotChocolate.Types.CursorPagination.Extensions": "16.6.4", + "HotChocolate.Types.Mutations": "16.6.4", + "HotChocolate.Types.Queries": "16.6.4", + "HotChocolate.Validation": "16.6.4" + } + }, + "HotChocolate.Abstractions": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "1ELetbKeMEJXK1D/G9kR55XGF4FkaYS8zvLkWIl/3s75BWPTk97vzC/EBgTEC5Q/Wj7zmBDUwdA7a9noezeoFw==", + "dependencies": { + "HotChocolate.Execution.Abstractions": "16.6.4", + "HotChocolate.Language": "16.6.4", + "HotChocolate.Primitives": "16.6.4" + } + }, + "HotChocolate.AspNetCore.Pipeline": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "j3T4SnePMXVbrOyniu9SFvA7jOfHlcVjEeyvEh60UD/xaPIP1+L5WofDpP1088JwwvLlpYCWdyEmXhNoFy8Q+g==", + "dependencies": { + "ChilliCream.Nitro.App": "32.0.2", + "HotChocolate.Caching.Memory": "16.6.4", + "HotChocolate.PersistedOperations.Abstractions": "16.6.4", + "HotChocolate.Transport.Formatters": "16.6.4", + "HotChocolate.Transport.Sockets": "16.6.4" + } + }, + "HotChocolate.Authorization": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "2TIXgq49SYmfC+NMV+poOq83JeScz0ZkEhHSgv91lNDTZINz06DlxcCrlsf2q+ns8fIS3rbnnHJswBzobE+qUA==", + "dependencies": { + "HotChocolate.Types": "16.6.4" + } + }, + "HotChocolate.Caching.Memory": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "oEOWL5+6wVG7HQHil5EydXcCdV296ZSuUO27mZYAxq5wMx9aX9D/ZkUOWU3To1vo75w767tqrr7+OEGWjPxuiA==" + }, + "HotChocolate.CostAnalysis": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "LcrorDRmMdWjRyCOxnYbqIuDB/qdP5oFXBw4gkgNdnjov0C7BGz5z0M074OvgdzsnqQYb7iDWcaPPtZq6zCVxA==", + "dependencies": { + "HotChocolate.Types": "16.6.4" + } + }, + "HotChocolate.Execution.Abstractions": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "pmaAsn6q4C/elx0SVnTGYWZxy3AqY5V7FYf/3CR8fq7i4rMKH/hHe/qfIoAzC6Phbg1tacHlT2YBeOAssQ3GoA==", + "dependencies": { + "HotChocolate.Features": "16.6.4", + "HotChocolate.Language.SyntaxTree": "16.6.4", + "HotChocolate.Language.Web": "16.6.4", + "HotChocolate.Primitives": "16.6.4", + "HotChocolate.Text.Json": "16.6.4", + "HotChocolate.Types.Abstractions": "16.6.4", + "HotChocolate.Utilities.Buffers": "16.6.4", + "HotChocolate.Utilities.Tasks": "16.6.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0" + } + }, + "HotChocolate.Execution.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "OP16Y71IuSuBhGuTNo3gujdBF5HP1WmZYtNV6bUpCv/r4r7dG8rKQYxd2nlnPfTeiFQsvZyqGNKQLxATwxOiCQ==", + "dependencies": { + "HotChocolate.Abstractions": "16.6.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0" + } + }, + "HotChocolate.Execution.Operation.Abstractions": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "yfXI/ps/VYFyVtj3Z29ImoSLxGfn5OTRXmhkpQtKhQLTiFA8DbxU8/swXRx8bTJL8YS3AOAdPn5qiZBD6rz93Q==", + "dependencies": { + "HotChocolate.Features": "16.6.4", + "HotChocolate.Types.Abstractions": "16.6.4" + } + }, + "HotChocolate.Execution.Pipeline": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "ZaJ48M13jKTsVphND7rK+JvWkxM3oDmYLL/JDNj0zjMqMH77PQSLVALK/OOKSI0HxhDbKWdVSvUMKg7MoXrJ5A==", + "dependencies": { + "HotChocolate.Execution.Abstractions": "16.6.4", + "HotChocolate.Validation": "16.6.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0" + } + }, + "HotChocolate.Execution.Projections": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "9cqrsEdbY8JfKgLgRGvM3rBfrC7fvb6rl+kORlXVMOvxDTbL75vrvmKyTHC1HmlJdQ56wTUq33Y6ycEJdoCwNw==", + "dependencies": { + "GreenDonut.Data": "16.6.4", + "GreenDonut.Data.Abstractions": "16.6.4", + "GreenDonut.Data.Primitives": "16.6.4", + "HotChocolate.Caching.Memory": "16.6.4", + "HotChocolate.Execution.Configuration.Abstractions": "16.6.4", + "HotChocolate.Types": "16.6.4" + } + }, + "HotChocolate.Features": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "0SGN2IDkopXsaxaN0qT+Ww5d/Y+yV4ZvG+xCKU0CvWlTnrq6JrpgeZcJF594EwJnXB4Zja3Wn1piKFEh0ewPDw==" + }, + "HotChocolate.Fusion.Language": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "euXggAJYrOhpd0OpDXLZOQ49AocNkIYUCVEoU6403EDTN46PnaDz2ltGnhcYk8bI1qMq0nm+ZbcmREcdRJfonQ==" + }, + "HotChocolate.Fusion.Utilities": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "HDz/NdyIDNCfUx2JvInVbsr4F3f9a8jyXlnM5mbGV0caZv8Hr1ozyY9NOyltZtkomdk0TKXVN8YaxBWnfwMBLA==", + "dependencies": { + "HotChocolate.Execution.Abstractions": "16.6.4", + "HotChocolate.Fusion.Language": "16.6.4", + "HotChocolate.Language": "16.6.4", + "HotChocolate.Types.Abstractions": "16.6.4" + } + }, + "HotChocolate.Language": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "oLEo6gvXVcKN/sDOSKxB0pFYBId1OSdKB+yxVRzg88M3q8N2uq89Q0P7fnpwUuiobay511gvXXihYaBxNt3xVA==", + "dependencies": { + "HotChocolate.Language.SyntaxTree": "16.6.4", + "HotChocolate.Language.Utf8": "16.6.4", + "HotChocolate.Language.Visitors": "16.6.4", + "HotChocolate.Language.Web": "16.6.4" + } + }, + "HotChocolate.Language.SyntaxTree": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "BDsJMdv6LjYEfGVQiAVhSf50xbAIrigoZUzjUoncbXGXzrfAJaVsGkZs6mR1z0UcTWrlSNoFRosxbtY07iPnCg==", + "dependencies": { + "HotChocolate.Utilities.Buffers": "16.6.4", + "Microsoft.Extensions.ObjectPool": "10.0.0" + } + }, + "HotChocolate.Language.Utf8": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "Ria0zRrpy4nXVvuczx14Vbe+8jdVPPryUlKjfUVUZCmU9Ntif/XhaFmVYo3HtS+FVkFHSRA1o0DeTXEoDnzX9Q==", + "dependencies": { + "HotChocolate.Language.SyntaxTree": "16.6.4", + "HotChocolate.Language.Utf8.SyntaxTree": "16.6.4", + "HotChocolate.Utilities.Buffers": "16.6.4" + } + }, + "HotChocolate.Language.Utf8.SyntaxTree": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "2nnk9e2GT4N8HlVXvtYYOEJysPUiZMQnjtSnLSltW9YC1VW/E4IGnxxJ2Gt7Mj+4yUSlQ+61DnzOD+UtSF09Kg==", + "dependencies": { + "HotChocolate.Language.SyntaxTree": "16.6.4", + "HotChocolate.Utilities.Buffers": "16.6.4" + } + }, + "HotChocolate.Language.Visitors": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "MWHB8709Ho2bQkIXuzTGKw91L9z675poWJ1lg6BGmdg3/ZnfsXGAv8cdP/cA4UzB1rOtYJTqrh2YP7xywHvwCQ==", + "dependencies": { + "HotChocolate.Language.SyntaxTree": "16.6.4" + } + }, + "HotChocolate.Language.Web": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "7GWlN019RYlbGJ88j9gw34exwD562PsiJ49zs8RALAzJnmC000KgH5vHikUbPVU5eeO8nUunyCEQafdgypY68A==", + "dependencies": { + "HotChocolate.Language.Utf8": "16.6.4", + "HotChocolate.Language.Visitors": "16.6.4" + } + }, + "HotChocolate.PersistedOperations.Abstractions": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "oUpPCiwIfrlk7Cswh+c9J+iRnmNIbRlA7EmGeL/nOR93xSMiCd9zgrkAtBggP4SdyESY9dD628lQZb8AkqXZdw==", + "dependencies": { + "HotChocolate.Execution.Abstractions": "16.6.4" + } + }, + "HotChocolate.PersistedOperations.Pipeline": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "h1VeBRoKQVg5BZnhLHwUHhGApOa8Rk31s64PNG1darAqUnX9v8ZNd/oiv+cQ6CabraQHmbvvvR2DfE/XGsygcA==", + "dependencies": { + "HotChocolate.PersistedOperations.Abstractions": "16.6.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0" + } + }, + "HotChocolate.Primitives": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "u4FkIs8tKYWJ3I0vTEDPXaPhy4zfhFNZNWqef3tRPGpnQe1rg+7FdM+V1bNs6j94ucY8fX5DTNDJy9OGVe4e3g==", + "dependencies": { + "HotChocolate.Language": "16.6.4" + } + }, + "HotChocolate.Subscriptions": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "L9CzlWy1cHZFixMWyZmtbI5pmqlfwHjGBjF0+pVnbJJaYQv2eSc4Q/8kdQjkhdGuhewPtC44OAMki1br9/Azpw==", + "dependencies": { + "HotChocolate.Abstractions": "16.6.4", + "HotChocolate.Execution.Configuration.Abstractions": "16.6.4", + "HotChocolate.Utilities": "16.6.4" + } + }, + "HotChocolate.Subscriptions.InMemory": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "/3Tm/h3jw+nrv5CHtszE3iAnG3n3odP842QrTid7mxrEmaMEqE74RA00wTmFaO/bbepCnmymXLxox31nC30TAQ==", + "dependencies": { + "HotChocolate.Execution.Configuration.Abstractions": "16.6.4", + "HotChocolate.Subscriptions": "16.6.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0" + } + }, + "HotChocolate.Text.Json": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "sBxtzPsvDjsw9V9VKy/NomlPE1ruprKHU5iTuHJUZ2i20Ihvq4ouxff0O7yCD1iA32NCaLNL3zvNJagZNV/nTQ==" + }, + "HotChocolate.Transport.Formatters": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "fl16wIiX4FAbSs2TSUM7yorg3awV2cUDNBMN9LU/mPcEN9yzgNN7FsIKQdl1TiqiXSAz5/ihVtBqLJBliws8uw==", + "dependencies": { + "HotChocolate.Execution.Abstractions": "16.6.4", + "HotChocolate.Utilities.Buffers": "16.6.4", + "HotChocolate.Utilities.Tasks": "16.6.4" + } + }, + "HotChocolate.Transport.Sockets": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "g67fURnanTGuLJwWxRG4E5PJfl+K6TZx8afNc6u+flbbwmTR+7TKEx7b/ZrlzqxCSqASA4H+KN3h4/3nILT+gQ==" + }, + "HotChocolate.Types": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "UEpl+u8WuvQOK1vdlq+2uucLPQpuNXbzt27ij8xcEwOfZAimYC8dGep4MIb1ItdNk2X7PaOegGPQXWoh+oxSiQ==", + "dependencies": { + "GreenDonut": "16.6.4", + "GreenDonut.Data.Primitives": "16.6.4", + "HotChocolate.Abstractions": "16.6.4", + "HotChocolate.Caching.Memory": "16.6.4", + "HotChocolate.Execution.Abstractions": "16.6.4", + "HotChocolate.Execution.Configuration.Abstractions": "16.6.4", + "HotChocolate.Execution.Operation.Abstractions": "16.6.4", + "HotChocolate.Execution.Pipeline": "16.6.4", + "HotChocolate.Features": "16.6.4", + "HotChocolate.Fusion.Language": "16.6.4", + "HotChocolate.Fusion.Utilities": "16.6.4", + "HotChocolate.PersistedOperations.Abstractions": "16.6.4", + "HotChocolate.PersistedOperations.Pipeline": "16.6.4", + "HotChocolate.Text.Json": "16.6.4", + "HotChocolate.Transport.Formatters": "16.6.4", + "HotChocolate.Types.Abstractions": "16.6.4", + "HotChocolate.Utilities": "16.6.4", + "HotChocolate.Utilities.Base36": "16.6.4", + "HotChocolate.Utilities.DependencyInjection": "16.6.4", + "Microsoft.Extensions.DependencyInjection": "10.0.0", + "Microsoft.Extensions.ObjectPool": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0" + } + }, + "HotChocolate.Types.Abstractions": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "KrftIDiSImZmhQv+Johy6cGOzIKizZS4TbsJwzlSINrUtj/L8G7RVvFaUTl/iYXYIdganPopITsSCu+fs3UIVA==", + "dependencies": { + "HotChocolate.Features": "16.6.4", + "HotChocolate.Primitives": "16.6.4" + } + }, + "HotChocolate.Types.CursorPagination": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "hNoVzFKkZGzTofL0nPtMeqFpv6+zy2jqY9Ef3vnJuzSwCjD08g4SJNt947ViksDoqMUVPtH3xjZMiOS2vN9Z6w==", + "dependencies": { + "HotChocolate.Execution.Configuration.Abstractions": "16.6.4", + "HotChocolate.Types": "16.6.4" + } + }, + "HotChocolate.Types.CursorPagination.Extensions": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "uaWfK7+/WpYF/zOoeLF+QutSVHP3hQEW+MKHRc76Fg4tuhDxSw6c7rW3wEbOys7W1kAgnKfeVWtYkQCj4ifBBg==", + "dependencies": { + "GreenDonut.Data": "16.6.4", + "HotChocolate.Types.CursorPagination": "16.6.4" + } + }, + "HotChocolate.Types.Errors": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "S9KNljhZe94L/OQse+VsVwwIUunoyjBv4IvTfjCPy/6IWvc96q6Pkw5wC9C42X32PrCPqTsXL6vsc7G8mQLh3A==", + "dependencies": { + "HotChocolate.Types": "16.6.4" + } + }, + "HotChocolate.Types.Mutations": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "l5umZs2JyB3wq8giyucXLKmKkaFEZxidzhaiZ2DsxXq969v0OlgZiGxdU5puMUJ1XNrWtQUocnURgo8tqOg/pg==", + "dependencies": { + "HotChocolate.Types": "16.6.4", + "HotChocolate.Types.Errors": "16.6.4" + } + }, + "HotChocolate.Types.Queries": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "WiMtlmgToK8/GLFR5zdUT9YdSmpU2kkDTd7l+56r1TB934+2Qp2hzl0zd6THCPx4Hd9BocwMjrUggSCgnDuDng==", + "dependencies": { + "HotChocolate.Types": "16.6.4", + "HotChocolate.Types.Errors": "16.6.4" + } + }, + "HotChocolate.Types.Scalars.Upload": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "GWkhJth8tpx3zF8xXu7oxXB+aeTGclwDZR8/kA0CI/efsIRbAj5k13HxTN7WG9/b7/SdV+iJer6pRS1J1fWOYQ==", + "dependencies": { + "HotChocolate.Types": "16.6.4" + } + }, + "HotChocolate.Utilities": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "8kg+EoA7eS0uIOR/06CzKcEaZprWAxecwUkHFS2cOys7uRkXaxjgVfzwz1wARkAXEHhaMhFEQvVjHghQGQodYA==", + "dependencies": { + "HotChocolate.Utilities.Buffers": "16.6.4", + "HotChocolate.Utilities.Tasks": "16.6.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0" + } + }, + "HotChocolate.Utilities.Base36": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "i0zQoDkaSXR07627r1l7SQ0psF9zOa7CUsmR7vrSmwhBItYe2AVR6z4M58JDo1MoHR5Nt6Mp8Y0dgrOL6OM6zg==" + }, + "HotChocolate.Utilities.Buffers": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "oeadDwUfn3oF97oNwF3QD3gEoOPOxl/sKwFGKj9BXNwgQnm9ho4DRFZrY0v4Gk78sGEsSxzfK8YlUz7pl5aAXA==" + }, + "HotChocolate.Utilities.DependencyInjection": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "1mw7EmE6mF/Ui+LGW1cyVPm0Rvko+szyU5q3AsH5NmiT+9iEEReLm3h716nF9f4RsYGnuNdXuUt1+eE6tfs/AQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.0" + } + }, + "HotChocolate.Utilities.Tasks": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "/zsN3ARuQe39QWMEMlaQCV4Td4kSa4Ppzh979YeXATB7S039QyDyExgtzsPw2gu0WamuH+hqPsGdo3hSRqqUIQ==" + }, + "HotChocolate.Validation": { + "type": "Transitive", + "resolved": "16.6.4", + "contentHash": "H104wO+jzcY8OhZE9QEKWs5NGtC4Kj04oJOsgJ+do3KI2go9fKNtrHKfJLFfuR6HaREn6SDqswYViiR4BGItSg==", + "dependencies": { + "HotChocolate.Execution.Abstractions": "16.6.4", + "HotChocolate.Types.Abstractions": "16.6.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0" + } + }, + "Microsoft.AspNetCore.TestHost": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "IVGNbZVxLuL80y7cmXYfhIFoTGneHkp4D7xXarYe6V/wjo8/2qiSLUwWax1/h2eHtJ3Y5cuqmtpOyWMnXIsUMA==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.14.1", + "contentHash": "pmTrhfFIoplzFVbhVwUquT+77CbGH+h4/3mBpdmIlYtBi9nAB+kKI6dN3A/nV4DFi3wLLx/BlHIPK+MkbQ6Tpg==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "sI3A6MUAyZwccLnwq0cXuRmC3WBtXieimBWafXSfhRJ4jnM2uGVXrqImDDVyW27u4vVaTOGlJxNJd8d1FGvCNQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "fVi053xdpda9Em7vSkmgVxO/PtgC2m78ekReKWsgcyskqY0U82Bz/MONwxpGzI0hElYKJfw+fupqMVeKW3fSaA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "rFn8RuszZn3qquPVkDytMUlPc2+rXl9MCoygwc1XmAgC5vg5/oXJ8hkOosOrLoBLsqdTy4lFwP6iQdPS9uSYOA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "1KHr/1L56llwQ/yI0tAisEA31UpPsn8aasjASIwELOaN4JIUcbjuQBMdFOIzfNBBeULoUa0XfBe5QDtRRUY+fg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "9/qymSh7hVDMGTGwrLz8MRp5zRyXy9adGDOs4HwRdnLil3oZGYuWeZjbmHgCQ9BL1qBroVfgUK3U/nb61617Cw==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "PJPtFYsZ+r+uz9qqXWUTEyKeJ1EiBGIJtqavkg9ZXijjGSFAk4Fgi5sqIxj+uAyLZwEKgexDUQXhWhvU6l3+og==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "HT70uGPxMLqqnOzKMcnQtDmeV4r0KHr4qVCLhP7SXil9jMEm8sQXwcybxVVFGXZJ1V44xV0mLqQ54aZbcR2OiQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.11", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.11" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "se7Kx8QpJEt+nf26L4qIVAofGTDr1wbexxsh/Fm3Xc04xUkqUXK06KUS7FLwSQYSjqb7q9n+T7MEcXYBhI1Y5g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "JOjac6SQQgZmdmB8WGEw61/7siqMZoWJMkmq2p1goJGxqI59lO6oB4bOl0jNsbaPBdYy5Mlkb+6U7T4+CjnD8Q==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "Tq/UqMaczePv9yWwSsJZRgKtgA46djVR5xHj/lZBCueQ3ag8f9v5mu0EdhrNx7tXxNk+Y9OurG2oKuSKINjr0A==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.11", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "2i6rtW/B5rCnWCnhdmWWEmaM9O0HD0zsPY9eRqa++y4tclI3Uw8zvGbBvhY/LjAdtf8gUHhUPcAWj3DRlWMXmQ==" + }, + "Microsoft.Extensions.Hosting": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "eIDa/Rl+93aj17gMlFsJJx+LhBvb3CP0Mu1PeVYkDp2Y3S4Jock8UynfGQEcx7lrlq+gKW+ECQJHbro/LTPDEQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.Configuration.Binder": "10.0.11", + "Microsoft.Extensions.Configuration.CommandLine": "10.0.11", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.11", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.11", + "Microsoft.Extensions.Configuration.Json": "10.0.11", + "Microsoft.Extensions.Configuration.UserSecrets": "10.0.11", + "Microsoft.Extensions.DependencyInjection": "10.0.11", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Diagnostics": "10.0.11", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.11", + "Microsoft.Extensions.FileProviders.Physical": "10.0.11", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.11", + "Microsoft.Extensions.Logging": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11", + "Microsoft.Extensions.Logging.Configuration": "10.0.11", + "Microsoft.Extensions.Logging.Console": "10.0.11", + "Microsoft.Extensions.Logging.Debug": "10.0.11", + "Microsoft.Extensions.Logging.EventLog": "10.0.11", + "Microsoft.Extensions.Logging.EventSource": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "pwtpF7iF/NNaOBcX+pvMZ7y2+JAVbH5KkNrH9uMZtuVxVJsFTDiWiCR7Tk3HVptsAijaetipUZVRVK2LLq+nvA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.11", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "+24lC4plfbEDNfLAdTV/SWKS7dW+16X4HdydO3R++134kSNTzcbYA4KpR1Hdh6uWisB8Za3AzwyOn+K+NxWIug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "S7LvLeVHKNPaY2NMyxW7c2TBGsLgxoSUBCV5Ev5iN8kgC7EPR2UB7eW7vHsElGMcIUDwRmoxLfvGDynCn3q6EA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.Configuration.Binder": "10.0.11", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Logging": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.11" + } + }, + "Microsoft.Extensions.Logging.Debug": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "wr+j1bjdFXhc8lKTLoq+RbwFM8M+orcMS9xrcqLmDGOxJcXpKizEeE5h6v/GKwCZV02FmhaA7OlNjoq072jZpQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Logging": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11" + } + }, + "Microsoft.Extensions.Logging.EventLog": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "Eck9GpCCpvZ3f6L7IUlN+mPtRVefnf7PsiIG5vi61QawPtLNCEAv2TPD/M3SojcU0PFaef+BxiVGPOShFHtDog==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Logging": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11", + "System.Diagnostics.EventLog": "10.0.11" + } + }, + "Microsoft.Extensions.Logging.EventSource": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "hs6QWECLLohi2VKqUvSGRUvrg7eXR1DqKL95Jrtz3cdD2g2nBA+yJPdRQLZ7SLmnTZWycxfMDK2s0ho+rfst5w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Logging": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "bpeCq0IYmVLACyEUMzFIOQX+zZUElG1t+nu1lSxthe7B+1oNYking7b91305+jNB6iwojp9fqTY9O+Nh7ULQxg==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "eY1GAKcTfD2maP27J84X9IovT3yjHJ2dVDzPmDg6/XqYvt3jMzJhtfQCLjG9pVsZGAd+8DQ2QrjaDcs2+VQLGw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "syEhXQ/sEaSBFaqzlp9gDGHX/nk6gkQkh1sIUpBO1mlBj3Phu1rmb4ML1uCiyPW9N6Kxfxv3y5FGObC+bV01Qw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.Configuration.Binder": "10.0.11", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "dYfCLR52UA+3DL7C4I/pvSaRPkNqxrUAQmbFL2u0zvYKKzqgrFCJl08Df+F1aYc8leu9JvpC9bsURUdpExcBXQ==" + }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "irv0HuqoH8Ig5i2fO+8dmDNdFdsrO+DoQcedwIlb810qpZHBNQHZLW7C/AHBQDgLLpw2T96vmMAy/aE4Yj55Sg==" + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "17.14.1", + "contentHash": "xTP1W6Mi6SWmuxd3a+jj9G9UoC850WGwZUps1Wah9r1ZxgXhdJfj1QqDLJkFjHDCvN42qDL2Ps5KjQYWUU0zcQ==" + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "17.14.1", + "contentHash": "d78LPzGKkJwsJXAQwsbJJ7LE7D1wB+rAyhHHAaODF+RDSQ0NgMjDFkSA1Djw18VrxO76GlKAjRUhl+H8NL8Z+Q==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "17.14.1", + "Newtonsoft.Json": "13.0.3" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "10.0.11", + "contentHash": "QTXEoQBzz00SFWbo7nAg1Ogd4f99lwqcO9uAJ7MYSLEUR28f6As32QktrqG2Fr9cfAfd1GjLyGYspE7Ipj7P6w==" + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ne1843evDugl0md7Fjzy6QjJrzsjh46ZKbhf8GwBXb5f/gw97J4bxMs0NQKifDuThh/f0bZ0e62NPl1jzTuRqA==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "System.Numerics.Tensors": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "17brFrnQHV/XMMazXNqFq4GkvzWy8yzDPo5Gb8TT8zJtMFbUqDycOCyPCp9F+eMc4L75kcCKRZqaeBHMwLCL9Q==" + }, + "System.Reactive": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "31kfaW4ZupZzPsI5PVe77VhnvFF55qgma7KZr/E0iFTs6fmdhhG8j0mgEx620iLTey1EynOkEfnyTjtNEpJzGw==" + }, + "xunit.abstractions": { + "type": "Transitive", + "resolved": "2.0.3", + "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.18.0", + "contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ==" + }, + "xunit.assert": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA==" + }, + "xunit.core": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==", + "dependencies": { + "xunit.extensibility.core": "[2.9.3]", + "xunit.extensibility.execution": "[2.9.3]" + } + }, + "xunit.extensibility.core": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==", + "dependencies": { + "xunit.abstractions": "2.0.3" + } + }, + "xunit.extensibility.execution": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==", + "dependencies": { + "xunit.extensibility.core": "[2.9.3]" + } + }, + "Yarp.ReverseProxy": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "gxtkN3a+9biu9V9Zd5NaTO6VZWXAnS2mhQ0R/VXmSPoTuiQNZsakKikrKpDtKxrL5nUYzbRsHtl40WNq+ZBKKg==", + "dependencies": { + "System.IO.Hashing": "8.0.0" + } + }, + "linq2graphql.testclientstandalone": { + "type": "Project", + "dependencies": { + "GraphQL-Parser": "[9.5.1, )", + "Microsoft.Extensions.AI.Abstractions": "[10.10.0, )", + "Microsoft.Extensions.Caching.Memory": "[10.0.11, )", + "Microsoft.Extensions.Http": "[10.0.11, )", + "Websocket.Client": "[5.3.0, )" + } + }, + "linq2graphql.testserver": { + "type": "Project", + "dependencies": { + "HotChocolate.AspNetCore": "[16.6.4, )", + "HotChocolate.Data": "[16.6.4, )", + "HotChocolate.Types.Analyzers": "[16.6.4, )", + "Linq2GraphQL.TestServer.Shared": "[1.0.0, )" + } + }, + "linq2graphql.testserver.shared": { + "type": "Project", + "dependencies": { + "HotChocolate.AspNetCore": "[16.6.4, )", + "HotChocolate.Data": "[16.6.4, )" + } + }, + "GraphQL-Parser": { + "type": "CentralTransitive", + "requested": "[9.5.1, )", + "resolved": "9.5.1", + "contentHash": "pUYpupTsVyNcKemko8f0iknJa2z/oZsfzwb4crgEFN9/pKf3k35gxVXDDOkD3NVPF1ea6GUV0jgxkjCozFj7Fg==" + }, + "HotChocolate.AspNetCore": { + "type": "CentralTransitive", + "requested": "[16.6.4, )", + "resolved": "16.6.4", + "contentHash": "sxqZ+B2asfW/vAznKNl+xP+sf8eR3h4lRKYrmDtJPTAbJ4Z8+rWW6DNqbFyoU1LAl1teTbRwPaGPxEqYKumA2Q==", + "dependencies": { + "ChilliCream.Nitro.App": "32.0.2", + "HotChocolate": "16.6.4", + "HotChocolate.AspNetCore.Pipeline": "16.6.4", + "HotChocolate.Subscriptions.InMemory": "16.6.4", + "HotChocolate.Transport.Formatters": "16.6.4", + "HotChocolate.Transport.Sockets": "16.6.4", + "HotChocolate.Types.Scalars.Upload": "16.6.4", + "HotChocolate.Utilities.DependencyInjection": "16.6.4" + } + }, + "HotChocolate.Data": { + "type": "CentralTransitive", + "requested": "[16.6.4, )", + "resolved": "16.6.4", + "contentHash": "ruKaYosX4yTOAVeDEQfHFSjuIhmCeQpq3ritskH8i74wn8hKhPRsFA9iFPom3qpiZkCJxsCIPQZumGJGf6xpkQ==", + "dependencies": { + "GreenDonut.Data": "16.6.4", + "GreenDonut.Data.Abstractions": "16.6.4", + "GreenDonut.Data.Primitives": "16.6.4", + "HotChocolate.Execution.Operation.Abstractions": "16.6.4", + "HotChocolate.Execution.Projections": "16.6.4", + "HotChocolate.Types": "16.6.4", + "HotChocolate.Types.CursorPagination.Extensions": "16.6.4" + } + }, + "HotChocolate.Types.Analyzers": { + "type": "CentralTransitive", + "requested": "[16.6.4, )", + "resolved": "16.6.4", + "contentHash": "bigjWuAkEQvxoQCrUzAvowiqS7YrTciape6++hB9YpA2cFH5RQnG+xyTo/MvWCN5jSvnW4izvdae9mhxtfhZZA==" + }, + "Microsoft.Extensions.AI.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.10.0, )", + "resolved": "10.10.0", + "contentHash": "frBHj2ckpevA+KdDLikxv8oxGPrJiyTvaiblYp+vtVEt0q6J73XP5m2SICm86nFK6rl+L1GCKxIy4S5Lb3p2kQ==" + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "CentralTransitive", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "el1g0mBEbDBGY2bT9mcSfrTWO8QlPdq2nOCnvQugioOFwHV+bVBMeiakoI0dNOdj8d6Hi9K6HY2xzRUWJiDR3w==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.11", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "CentralTransitive", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "wlhRqZW8LcJPa+vk2oLAc/REXDItHtkFQdf/QcXYGZbZOO13izcsKY1pCvuFQYwUiZD+hwSZwsKASjqT+BNaVg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "CentralTransitive", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "KICyU3eVi5jvloKm01EXV69L97H/zkhISVtV98cIuzuFOxNx3xTUVcXqvWTz3aq7OvUuDB/MFlPFjmxRaKF7/A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "CentralTransitive", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "mDW7KVFB05M6jiRUyaZiOMWhS31n5HlSZwoYctHAZAucD4sMDJ70IxOmkGDt6RpstchD+keWBjhdzcMpSkWvWQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.11", + "Microsoft.Extensions.FileProviders.Physical": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "CentralTransitive", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "nSPrT8U/cNoB4coqkmnanAMK9PsL7lsjG+LLUKEwHRFwS6E78b8S1wdv/y88EOxBhasWov1rLd7RTHmmsYPOLg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.11", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.11" + } + }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "CentralTransitive", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "BRliLdUowglV8GS+J1G/QsSofCJYYFg3U8QZx0ACRn+a91az/Qnpy+h6PyHS94WgV2TSazX5D/cuuk6wnCJatw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.Configuration.Json": "10.0.11", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.11", + "Microsoft.Extensions.FileProviders.Physical": "10.0.11" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "CentralTransitive", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "PSmotV19c7E3lKed++uYo1kSiXFI+uTl37CBSrhq+CfLC3FCHjG7R91+xPnNehQfHS1b0Tzo/CCLPWH3qaEheg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11" + } + }, + "Microsoft.Extensions.Http": { + "type": "CentralTransitive", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "ujx8RvcKzkxPFBguwgiygbwWVHVK0P7HFlNJ6I0JBRb28tzwe42jixBoA+dGmqG9IHepPVcm04vv2IDnU93ekA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Diagnostics": "10.0.11", + "Microsoft.Extensions.Logging": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11" + } + }, + "Microsoft.Extensions.Logging": { + "type": "CentralTransitive", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "nUOJwgFkSiLHiVGFpU22pIJtuWYewuSYQ3JVuP/gdK8ASMT807Px+TYQiRWs6uSsOmoyFTaVCwKXTasczV6BpA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "CentralTransitive", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "dFc0yDudyD1iIg6z9XT7ofsT3hVO7Y4ylrxGHIVRR0GaZ4CUk4ujOrMoy7wWEdNHZhvJooySg6hZpOxyS8zEVA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Logging": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11", + "Microsoft.Extensions.Logging.Configuration": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11" + } + }, + "System.CodeDom": { + "type": "CentralTransitive", + "requested": "[10.0.11, )", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "Websocket.Client": { + "type": "CentralTransitive", + "requested": "[5.3.0, )", + "resolved": "5.3.0", + "contentHash": "uhdDM+gruCEhHRCKCoyali1HJp0wSS/HBs5X9XZwULNKM2y5ML188TsvcEgWEFOx0NOaHfGNtfoC0cd1p2NOIg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IO.RecyclableMemoryStream": "3.0.0", + "System.Reactive": "6.0.0" + } + } + } + } +} \ No newline at end of file diff --git a/test/Linq2GraphQL.Tests/CoexistenceTests.cs b/test/Linq2GraphQL.Tests/CoexistenceTests.cs new file mode 100644 index 00000000..12170f93 --- /dev/null +++ b/test/Linq2GraphQL.Tests/CoexistenceTests.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Options; +using Shouldly; +using Package = Linq2GraphQL.TestClient; +using Standalone = Linq2GraphQL.TestClientStandalone; + +namespace Linq2GraphQL.Tests; + +/// +/// A standalone client (its own copy of the runtime) and a package-based client in one process, sharing +/// an endpoint and the app's IMemoryCache, as an app that uses clients from several teams would. +/// +public class CoexistenceTests +{ + private static readonly Uri baseAddress = new("https://localhost:7184/graphql/"); + + [Fact] + public async Task StandaloneAndPackageClient_SharingCacheAndEndpoint_BothWork() + { + using var application = new WebApplicationFactory(); + + var package = new Package.SampleClient(application.CreateClient(new() { BaseAddress = baseAddress }), + Options.Create(new Client.GraphClientOptions { UseSafeMode = true }), application.Services); + var standalone = new Standalone.SampleClient(application.CreateClient(new() { BaseAddress = baseAddress }), + Options.Create(new Standalone.Runtime.GraphClientOptions { UseSafeMode = true }), application.Services); + + // Safe mode caches the schema per endpoint; each client must get its own runtime's schema type. + (await package.Query.Customers().Select().ExecuteAsync()).Count.ShouldBe(2); + (await standalone.Query.Customers().Select().ExecuteAsync()).Count.ShouldBe(2); + (await package.Query.Hello().Select().ExecuteAsync()).ShouldBe("Hello, World!"); + (await standalone.Query.Hello().Select().ExecuteAsync()).ShouldBe("Hello, World!"); + } + + [Fact] + public void StandaloneClient_DoesNotReferenceTheRuntimePackages() + { + var references = typeof(Standalone.SampleClient).Assembly.GetReferencedAssemblies().Select(e => e.Name); + + references.ShouldNotContain("Linq2GraphQL.Client"); + references.ShouldNotContain("Linq2GraphQL.Client.Subscriptions"); + } +} diff --git a/test/Linq2GraphQL.Tests/Linq2GraphQL.Tests.csproj b/test/Linq2GraphQL.Tests/Linq2GraphQL.Tests.csproj index 09926f77..589c11fb 100644 --- a/test/Linq2GraphQL.Tests/Linq2GraphQL.Tests.csproj +++ b/test/Linq2GraphQL.Tests/Linq2GraphQL.Tests.csproj @@ -31,6 +31,7 @@ + diff --git a/test/Linq2GraphQL.Tests/StandaloneGeneratorTests.cs b/test/Linq2GraphQL.Tests/StandaloneGeneratorTests.cs new file mode 100644 index 00000000..c0c05efc --- /dev/null +++ b/test/Linq2GraphQL.Tests/StandaloneGeneratorTests.cs @@ -0,0 +1,110 @@ +using Linq2GraphQL.Generator; +using Shouldly; + +namespace Linq2GraphQL.Tests; + +/// +/// Standalone mode writes a copy of the runtime under <namespace>.Runtime next to the client. That the +/// copy compiles and works is covered by Linq2GraphQL.TestClientStandalone and Linq2GraphQL.Tests.Standalone; +/// these tests pin down what the generator emits. +/// +[Collection(GeneratorCollection.Name)] +public class StandaloneGeneratorTests +{ + private const string Sdl = """ + type Query { thing: Thing } + type Subscription { thingAdded: Thing } + type Thing { id: ID! } + """; + + [Fact] + public void Standalone_NothingReferencesTheRuntimePackages() + { + var entries = NewGenerator(standalone: true, includeSubscriptions: true).GenerateFromSdl(Sdl); + + entries.ShouldAllBe(e => !e.Content.Contains("Linq2GraphQL.Client")); + Content(entries, "Client", "TestClient.cs").ShouldContain("using TestNs.Runtime;"); + Content(entries, "Runtime/Client", "GraphClient.cs").ShouldContain("namespace TestNs.Runtime;"); + Content(entries, "Runtime/Subscriptions", "WSClient.cs").ShouldContain("namespace TestNs.Runtime.Subscriptions;"); + } + + [Fact] + public void Standalone_CopiesTheRuntime_WithoutAssemblyAttributes() + { + var entries = NewGenerator(standalone: true, includeSubscriptions: true).GenerateFromSdl(Sdl); + var runtime = entries.Where(e => e.DirectoryName.StartsWith("Runtime")).ToList(); + + runtime.ShouldContain(e => e.DirectoryName == "Runtime/Client/Visitors" && e.FileName == "QueryExpressionVisitor.cs"); + runtime.ShouldNotContain(e => e.FileName == "Assembly.cs"); + runtime.ShouldAllBe(e => !e.Content.Contains("[assembly:")); + runtime.ShouldAllBe(e => e.Content.Contains("")); + } + + [Fact] + public void Standalone_WithoutSubscriptions_LeavesOutTheSubscriptionsRuntime() + { + var entries = NewGenerator(standalone: true, includeSubscriptions: false).GenerateFromSdl(Sdl); + + entries.ShouldNotContain(e => e.DirectoryName.StartsWith("Runtime/Subscriptions")); + Content(entries, "Runtime", "RuntimeInfo.cs").ShouldNotContain("Websocket.Client"); + } + + [Fact] + public void Standalone_InAgentMode_CopiesTheAgentTools() + { + var entries = NewGenerator(standalone: true, includeSubscriptions: false, agent: true).GenerateFromSdl(Sdl); + + Content(entries, "Runtime/AI", "AgentToolsExtensions.cs").ShouldContain("namespace TestNs.Runtime.AI;"); + Content(entries, "Runtime/AI/Schema", "SdlReader.cs").ShouldContain("namespace TestNs.Runtime.AI.Schema;"); + entries.ShouldAllBe(e => !e.Content.Contains("Linq2GraphQL.Client")); + + var info = Content(entries, "Runtime", "RuntimeInfo.cs"); + info.ShouldContain("GraphQL-Parser"); + info.ShouldContain("Microsoft.Extensions.AI.Abstractions"); + } + + [Fact] + public void Standalone_WithoutAgentMode_LeavesOutTheAgentTools() + { + var entries = NewGenerator(standalone: true, includeSubscriptions: true).GenerateFromSdl(Sdl); + + entries.ShouldNotContain(e => e.DirectoryName.StartsWith("Runtime/AI")); + Content(entries, "Runtime", "RuntimeInfo.cs").ShouldNotContain("Microsoft.Extensions.AI"); + } + + [Fact] + public void Standalone_RuntimeInfo_RecordsVersionAndPackages() + { + var info = Content(NewGenerator(standalone: true, includeSubscriptions: true).GenerateFromSdl(Sdl), + "Runtime", "RuntimeInfo.cs"); + + info.ShouldContain("namespace TestNs.Runtime;"); + info.ShouldMatch(@"public const string Version = ""\d+\.\d+\.\d+[^""+]*"";"); + info.ShouldContain("Microsoft.Extensions.Caching.Memory"); + info.ShouldContain("Microsoft.Extensions.Http"); + info.ShouldContain("Websocket.Client"); + } + + [Fact] + public void PackageMode_IsUnchangedByStandalone() + { + var package = NewGenerator(standalone: false, includeSubscriptions: true).GenerateFromSdl(Sdl); + var standalone = NewGenerator(standalone: true, includeSubscriptions: true).GenerateFromSdl(Sdl) + .Where(e => !e.DirectoryName.StartsWith("Runtime")) + .ToList(); + + package.ShouldNotContain(e => e.DirectoryName.StartsWith("Runtime")); + standalone.Select(e => e.FileName).ShouldBe(package.Select(e => e.FileName)); + for (var i = 0; i < package.Count; i++) + { + standalone[i].Content.ShouldBe(package[i].Content.Replace("Linq2GraphQL.Client", "TestNs.Runtime")); + } + } + + private static ClientGenerator NewGenerator(bool standalone, bool includeSubscriptions, bool agent = false) => + new("TestNs", "TestClient", includeSubscriptions, EnumGeneratorStrategy.FailIfMissing, false, false, + agent: agent, standalone: standalone); + + private static string Content(List entries, string directory, string fileName) => + entries.Single(e => e.DirectoryName == directory && e.FileName == fileName).Content; +} diff --git a/test/Linq2GraphQL.Tests/packages.lock.json b/test/Linq2GraphQL.Tests/packages.lock.json index 79a71836..8c334c70 100644 --- a/test/Linq2GraphQL.Tests/packages.lock.json +++ b/test/Linq2GraphQL.Tests/packages.lock.json @@ -926,6 +926,16 @@ "Linq2GraphQL.Client.Subscriptions": "[1.0.0, )" } }, + "linq2graphql.testclientstandalone": { + "type": "Project", + "dependencies": { + "GraphQL-Parser": "[9.5.1, )", + "Microsoft.Extensions.AI.Abstractions": "[10.10.0, )", + "Microsoft.Extensions.Caching.Memory": "[10.0.11, )", + "Microsoft.Extensions.Http": "[10.0.11, )", + "Websocket.Client": "[5.3.0, )" + } + }, "linq2graphql.testserver": { "type": "Project", "dependencies": {