diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate.Language/Extensions/SnapshotExtensions.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate.Language/Extensions/SnapshotExtensions.cs new file mode 100644 index 00000000000..f79b81ada65 --- /dev/null +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate.Language/Extensions/SnapshotExtensions.cs @@ -0,0 +1,16 @@ +using HotChocolate.Language; +using SnapshotValueFormatters = CookieCrumble.HotChocolate.Language.Formatters.SnapshotValueFormatters; + +namespace CookieCrumble.HotChocolate; + +public static class SnapshotExtensions +{ + public static void MatchSnapshot( + this ISyntaxNode? value, + string? postFix = null) + => Snapshot.Match( + value, + postFix, + extension: ".graphql", + formatter: SnapshotValueFormatters.GraphQLSyntaxNode); +} diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate.Language/Formatters/GraphQLSnapshotValueFormatter.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate.Language/Formatters/GraphQLSnapshotValueFormatter.cs index 80686dbfe9e..31e7d0dc9f0 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate.Language/Formatters/GraphQLSnapshotValueFormatter.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate.Language/Formatters/GraphQLSnapshotValueFormatter.cs @@ -9,7 +9,7 @@ internal sealed class GraphQLSyntaxNodeSnapshotValueFormatter : SnapshotValueFor { protected override void Format(IBufferWriter snapshot, ISyntaxNode value) { - var serialized = value.Print().AsSpan(); + var serialized = value.Print(indented: true).AsSpan(); var buffer = ArrayPool.Shared.Rent(serialized.Length); var span = buffer.AsSpan()[..serialized.Length]; var written = 0; diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/CookieCrumbleHotChocolate.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/CookieCrumbleHotChocolate.cs index 0f019587d48..d958cbb59b9 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/CookieCrumbleHotChocolate.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/CookieCrumbleHotChocolate.cs @@ -8,12 +8,12 @@ public sealed class CookieCrumbleHotChocolate : SnapshotModule protected override IEnumerable CreateFormatters() { yield return SnapshotValueFormatters.ExecutionResult; - yield return SnapshotValueFormatters.GraphQL; yield return SnapshotValueFormatters.GraphQLHttp; yield return SnapshotValueFormatters.OperationResult; yield return SnapshotValueFormatters.Schema; yield return SnapshotValueFormatters.SchemaError; yield return SnapshotValueFormatters.Error; + yield return SnapshotValueFormatters.ErrorList; yield return SnapshotValueFormatters.ResultElement; } } diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Extensions/SnapshotExtensions.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Extensions/SnapshotExtensions.cs index a993dfe6dc1..238b1ddb904 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Extensions/SnapshotExtensions.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Extensions/SnapshotExtensions.cs @@ -1,22 +1,12 @@ using CookieCrumble.HotChocolate.Formatters; using HotChocolate; using HotChocolate.Execution; -using HotChocolate.Language; using CoreFormatters = CookieCrumble.Formatters.SnapshotValueFormatters; namespace CookieCrumble.HotChocolate; public static class SnapshotExtensions { - public static void MatchSnapshot( - this ISyntaxNode? value, - string? postFix = null) - => Snapshot.Match( - value, - postFix, - extension: ".graphql", - formatter: SnapshotValueFormatters.GraphQL); - public static void MatchSnapshot( this ISchemaDefinition? value, string? postFix = null) diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ErrorListSnapshotValueFormatter.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ErrorListSnapshotValueFormatter.cs new file mode 100644 index 00000000000..8ce0b3109af --- /dev/null +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ErrorListSnapshotValueFormatter.cs @@ -0,0 +1,30 @@ +using System.Buffers; +using System.Text.Encodings.Web; +using System.Text.Json; +using CookieCrumble.Formatters; +using HotChocolate; +using HotChocolate.Execution; +using HotChocolate.Text.Json; + +namespace CookieCrumble.HotChocolate.Formatters; + +internal sealed class ErrorListSnapshotValueFormatter + : SnapshotValueFormatter> +{ + protected override void Format(IBufferWriter snapshot, IReadOnlyList value) + { + var writerOptions = new JsonWriterOptions + { + Indented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + var serializationOptions = new JsonSerializerOptions + { + WriteIndented = true + }; + + var writer = new JsonWriter(snapshot, writerOptions); + JsonValueFormatter.WriteErrors(writer, value, serializationOptions); + } +} diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ErrorSnapshotValueFormatter.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ErrorSnapshotValueFormatter.cs index e7cb6ecc258..6ebdc255b82 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ErrorSnapshotValueFormatter.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ErrorSnapshotValueFormatter.cs @@ -13,6 +13,6 @@ internal sealed class ErrorSnapshotValueFormatter() protected override void Format(IBufferWriter snapshot, IError value) { var jsonWriter = new JsonWriter(snapshot, new JsonWriterOptions { Indented = true }); - JsonValueFormatter.WriteError(jsonWriter, value, new JsonSerializerOptions { WriteIndented = true }, default); + JsonValueFormatter.WriteError(jsonWriter, value, new JsonSerializerOptions { WriteIndented = true }); } } diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ExecutionResultSnapshotValueFormatter.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ExecutionResultSnapshotValueFormatter.cs index b0b7ce179b6..ae8ff86c9d7 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ExecutionResultSnapshotValueFormatter.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ExecutionResultSnapshotValueFormatter.cs @@ -101,20 +101,25 @@ private static async Task FormatStreamAsync( internal sealed class JsonResultPatcher { - private const string Data = "data"; - private const string Items = "items"; - private const string Incremental = "incremental"; - private const string Path = "path"; + private const string DataProp = "data"; + private const string ItemsProp = "items"; + private const string IncrementalProp = "incremental"; + private const string PendingProp = "pending"; + private const string PathProp = "path"; + private const string SubPathProp = "subPath"; + private const string IdProp = "id"; private JsonObject? _json; + private readonly Dictionary _pendingPaths = new(); public void SetResponse(JsonDocument response) { ArgumentNullException.ThrowIfNull(response); _json = JsonObject.Create(response.RootElement); + ProcessPayload(response.RootElement); } - public void WriteResponse(IBufferWriter snapshot) + public void ApplyPatch(JsonDocument patch) { if (_json is null) { @@ -122,15 +127,10 @@ public void WriteResponse(IBufferWriter snapshot) "You must first set the initial response before you can apply patches."); } - using var writer = new Utf8JsonWriter(snapshot, new JsonWriterOptions { Indented = true }); - - _json.Remove("hasNext"); - - _json.WriteTo(writer); - writer.Flush(); + ProcessPayload(patch.RootElement); } - public void ApplyPatch(JsonDocument patch) + public void WriteResponse(IBufferWriter snapshot) { if (_json is null) { @@ -138,126 +138,101 @@ public void ApplyPatch(JsonDocument patch) "You must first set the initial response before you can apply patches."); } - if (!patch.RootElement.TryGetProperty(Incremental, out var incremental)) - { - throw new ArgumentException("A patch result must contain a property `incremental`."); - } + using var writer = new Utf8JsonWriter(snapshot, new JsonWriterOptions { Indented = true }); - foreach (var element in incremental.EnumerateArray()) - { - if (element.TryGetProperty(Data, out var data)) - { - PatchIncrementalData(element, JsonObject.Create(data)!); - } - else if (element.TryGetProperty(Items, out var items)) - { - PatchIncrementalItems(element, JsonArray.Create(items)!); - } - } - } + _json.Remove("hasNext"); + _json.Remove("pending"); + _json.Remove("incremental"); + _json.Remove("completed"); - private void PatchIncrementalData(JsonElement incremental, JsonObject data) - { - if (incremental.TryGetProperty(Path, out var pathProp)) - { - var (current, last) = SelectNodeToPatch(_json![Data]!, pathProp); - ApplyPatch(current, last, data); - } + _json.WriteTo(writer); + writer.Flush(); } - private void PatchIncrementalItems(JsonElement incremental, JsonArray items) + private void ProcessPayload(JsonElement root) { - if (incremental.TryGetProperty(Path, out var pathProp)) + if (root.TryGetProperty(PendingProp, out var pending)) { - var (current, last) = SelectNodeToPatch(_json![Data]!, pathProp); - var i = last.GetInt32(); - var target = current.AsArray(); - - while (items.Count > 0) + foreach (var entry in pending.EnumerateArray()) { - var item = items[0]; - items.RemoveAt(0); - target.Insert(i++, item); + if (entry.TryGetProperty(IdProp, out var id) + && entry.TryGetProperty(PathProp, out var path)) + { + _pendingPaths[id.GetString()!] = path.Clone(); + } } } - } - private static void ApplyPatch(JsonNode current, JsonElement last, JsonObject patchData) - { - if (last.ValueKind is JsonValueKind.Undefined) + if (root.TryGetProperty(IncrementalProp, out var incremental)) { - foreach (var prop in patchData.ToArray()) + foreach (var element in incremental.EnumerateArray()) { - patchData.Remove(prop.Key); - current[prop.Key] = prop.Value; - } - } - else if (last.ValueKind is JsonValueKind.String) - { - current = current[last.GetString()!]!; + if (!element.TryGetProperty(IdProp, out var idElement)) + { + continue; + } - foreach (var prop in patchData.ToArray()) - { - patchData.Remove(prop.Key); - current[prop.Key] = prop.Value; - } - } - else if (last.ValueKind is JsonValueKind.Number) - { - var index = last.GetInt32(); - var element = current[index]; + var id = idElement.GetString()!; - if (element is null) - { - current[index] = patchData; - } - else - { - foreach (var prop in patchData.ToArray()) + if (!_pendingPaths.TryGetValue(id, out var basePath)) + { + continue; + } + + if (element.TryGetProperty(DataProp, out var data)) + { + PatchData(basePath, element, JsonObject.Create(data)!); + } + else if (element.TryGetProperty(ItemsProp, out var items)) { - patchData.Remove(prop.Key); - element[prop.Key] = prop.Value; + PatchItems(basePath, JsonArray.Create(items)!); } } } - else + } + + private void PatchData(JsonElement basePath, JsonElement incremental, JsonObject data) + { + var current = NavigatePath(_json![DataProp]!, basePath); + + if (incremental.TryGetProperty(SubPathProp, out var subPath)) { - throw new NotSupportedException("Path segment must be int or string."); + current = NavigatePath(current, subPath); + } + + foreach (var prop in data.ToArray()) + { + data.Remove(prop.Key); + current[prop.Key] = prop.Value; } } - private static (JsonNode Node, JsonElement PathSegment) SelectNodeToPatch( - JsonNode root, - JsonElement path) + private void PatchItems(JsonElement basePath, JsonArray items) { - if (path.GetArrayLength() == 0) + var target = NavigatePath(_json![DataProp]!, basePath).AsArray(); + + while (items.Count > 0) { - return (root, default); + var item = items[0]; + items.RemoveAt(0); + target.Add(item); } + } + private static JsonNode NavigatePath(JsonNode root, JsonElement path) + { var current = root; - JsonElement? last = null; - foreach (var element in path.EnumerateArray()) + foreach (var segment in path.EnumerateArray()) { - if (last is not null) + current = segment.ValueKind switch { - current = last.Value.ValueKind switch - { - JsonValueKind.String => current[last.Value.GetString()!]!, - JsonValueKind.Number => current[last.Value.GetInt32()]!, - _ => throw new NotSupportedException("Path segment must be int or string.") - }; - } - - last = element; - } - - if (current is null || last is null) - { - throw new InvalidOperationException("Patch had invalid structure."); + JsonValueKind.String => current[segment.GetString()!]!, + JsonValueKind.Number => current[segment.GetInt32()]!, + _ => throw new NotSupportedException("Path segment must be int or string.") + }; } - return (current, last.Value); + return current; } } diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/GraphQLSnapshotValueFormatter.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/GraphQLSnapshotValueFormatter.cs deleted file mode 100644 index 18f7a3b5ad5..00000000000 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/GraphQLSnapshotValueFormatter.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Buffers; -using CookieCrumble.Formatters; -using HotChocolate.Language; -using HotChocolate.Language.Utilities; - -namespace CookieCrumble.HotChocolate.Formatters; - -internal sealed class GraphQLSnapshotValueFormatter : SnapshotValueFormatter -{ - protected override void Format(IBufferWriter snapshot, ISyntaxNode value) - { - var serialized = value.Print().AsSpan(); - var buffer = ArrayPool.Shared.Rent(serialized.Length); - var span = buffer.AsSpan()[..serialized.Length]; - var written = 0; - - for (var i = 0; i < serialized.Length; i++) - { - if (serialized[i] is not '\r') - { - span[written++] = serialized[i]; - } - } - - span = span[..written]; - snapshot.Append(span); - - ArrayPool.Shared.Return(buffer); - } - - protected override void FormatMarkdown(IBufferWriter snapshot, ISyntaxNode value) - { - snapshot.Append("```graphql"); - snapshot.AppendLine(); - Format(snapshot, value); - snapshot.AppendLine(); - snapshot.Append("```"); - snapshot.AppendLine(); - } -} diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ResultElementSnapshotValueFormatter.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ResultElementSnapshotValueFormatter.cs index 12b7bca0c30..8ae45b6eb8c 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ResultElementSnapshotValueFormatter.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/ResultElementSnapshotValueFormatter.cs @@ -8,5 +8,5 @@ internal sealed class ResultElementSnapshotValueFormatter : SnapshotValueFormatter { protected override void Format(IBufferWriter snapshot, ResultElement element) - => element.WriteTo(snapshot, indented: true); + => element.WriteTo(snapshot, indented: true); } diff --git a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/SnapshotValueFormatters.cs b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/SnapshotValueFormatters.cs index b65a6532e60..d49e5c17482 100644 --- a/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/SnapshotValueFormatters.cs +++ b/src/CookieCrumble/src/CookieCrumble.HotChocolate/Formatters/SnapshotValueFormatters.cs @@ -10,9 +10,6 @@ public static class SnapshotValueFormatters public static ISnapshotValueFormatter ExecutionResult { get; } = new ExecutionResultSnapshotValueFormatter(); - public static ISnapshotValueFormatter GraphQL { get; } = - new GraphQLSnapshotValueFormatter(); - public static ISnapshotValueFormatter GraphQLHttp { get; } = new GraphQLHttpResponseFormatter(); @@ -30,4 +27,7 @@ public static class SnapshotValueFormatters public static ISnapshotValueFormatter ResultElement { get; } = new ResultElementSnapshotValueFormatter(); + + public static ISnapshotValueFormatter ErrorList { get; } = + new ErrorListSnapshotValueFormatter(); } diff --git a/src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/OpenApi/__snapshots__/OpenApiIntegrationTestBase.OpenApi_Includes_Initial_Routes_NET9_0.json b/src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/OpenApi/__snapshots__/OpenApiIntegrationTestBase.OpenApi_Includes_Initial_Routes_NET9_0.json index 759a6540f64..e75a041fec9 100644 --- a/src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/OpenApi/__snapshots__/OpenApiIntegrationTestBase.OpenApi_Includes_Initial_Routes_NET9_0.json +++ b/src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/OpenApi/__snapshots__/OpenApiIntegrationTestBase.OpenApi_Includes_Initial_Routes_NET9_0.json @@ -14,9 +14,9 @@ "schema": { "required": [ "any", + "base64String", "boolean", "byte", - "byteArray", "date", "dateTime", "decimal", @@ -35,6 +35,7 @@ "string", "timeSpan", "unknown", + "uri", "url", "uuid" ], @@ -62,6 +63,10 @@ } ] }, + "base64String": { + "pattern": "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?$", + "type": "string" + }, "boolean": { "type": "boolean" }, @@ -70,10 +75,6 @@ "description": "The Byte scalar type represents an 8-bit signed integer with a minimum value of -128 and a maximum value of 127.", "format": "int32" }, - "byteArray": { - "pattern": "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?$", - "type": "string" - }, "date": { "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string", @@ -211,6 +212,9 @@ "unknown": { "type": "string" }, + "uri": { + "type": "string" + }, "url": { "type": "string" }, @@ -232,9 +236,9 @@ "schema": { "required": [ "any", + "base64String", "boolean", "byte", - "byteArray", "date", "dateTime", "decimal", @@ -281,6 +285,11 @@ ], "nullable": true }, + "base64String": { + "pattern": "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?$", + "type": "string", + "nullable": true + }, "boolean": { "type": "boolean", "nullable": true @@ -291,11 +300,6 @@ "format": "int32", "nullable": true }, - "byteArray": { - "pattern": "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?$", - "type": "string", - "nullable": true - }, "date": { "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string", diff --git a/src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/OpenApi/__snapshots__/OpenApiIntegrationTestBase.OpenApi_Includes_Initial_Routes_NET9_0_Fusion.json b/src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/OpenApi/__snapshots__/OpenApiIntegrationTestBase.OpenApi_Includes_Initial_Routes_NET9_0_Fusion.json index d384caff03e..ef88d786f44 100644 --- a/src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/OpenApi/__snapshots__/OpenApiIntegrationTestBase.OpenApi_Includes_Initial_Routes_NET9_0_Fusion.json +++ b/src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/OpenApi/__snapshots__/OpenApiIntegrationTestBase.OpenApi_Includes_Initial_Routes_NET9_0_Fusion.json @@ -14,9 +14,9 @@ "schema": { "required": [ "any", + "base64String", "boolean", "byte", - "byteArray", "date", "dateTime", "decimal", @@ -35,6 +35,7 @@ "string", "timeSpan", "unknown", + "uri", "url", "uuid" ], @@ -43,6 +44,9 @@ "any": { "type": "string" }, + "base64String": { + "type": "string" + }, "boolean": { "type": "boolean" }, @@ -50,9 +54,6 @@ "type": "string", "description": "The Byte scalar type represents an 8-bit signed integer with a minimum value of -128 and a maximum value of 127." }, - "byteArray": { - "type": "string" - }, "date": { "type": "string", "description": "The `Date` scalar represents an ISO-8601 compliant date type." @@ -159,6 +160,9 @@ "unknown": { "type": "string" }, + "uri": { + "type": "string" + }, "url": { "type": "string" }, @@ -179,9 +183,9 @@ "schema": { "required": [ "any", + "base64String", "boolean", "byte", - "byteArray", "date", "dateTime", "decimal", @@ -209,6 +213,10 @@ "type": "string", "nullable": true }, + "base64String": { + "type": "string", + "nullable": true + }, "boolean": { "type": "boolean", "nullable": true @@ -218,10 +226,6 @@ "description": "The Byte scalar type represents an 8-bit signed integer with a minimum value of -128 and a maximum value of 127.", "nullable": true }, - "byteArray": { - "type": "string", - "nullable": true - }, "date": { "type": "string", "description": "The `Date` scalar represents an ISO-8601 compliant date type.", diff --git a/src/HotChocolate/ApolloFederation/test/Directory.Build.props b/src/HotChocolate/ApolloFederation/test/Directory.Build.props index 7b5e79f43f8..0433866e225 100644 --- a/src/HotChocolate/ApolloFederation/test/Directory.Build.props +++ b/src/HotChocolate/ApolloFederation/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultWebSocketPayloadFormatter.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultWebSocketPayloadFormatter.cs index adf7c5ab3ec..46fb269bca3 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultWebSocketPayloadFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/DefaultWebSocketPayloadFormatter.cs @@ -1,6 +1,6 @@ -using System.Buffers; using System.Text.Json; using HotChocolate.Text.Json; +using HotChocolate.Transport.Formatters; using static HotChocolate.Execution.JsonValueFormatter; namespace HotChocolate.AspNetCore.Formatters; @@ -8,45 +8,94 @@ namespace HotChocolate.AspNetCore.Formatters; /// /// This represents the default implementation for the . /// -public class DefaultWebSocketPayloadFormatter(WebSocketPayloadFormatterOptions options = default) +public sealed class DefaultWebSocketPayloadFormatter(WebSocketPayloadFormatterOptions options = default) : IWebSocketPayloadFormatter { - private readonly JsonWriterOptions _writerOptions = options.Json.CreateWriterOptions(); private readonly JsonSerializerOptions _serializerOptions = options.Json.CreateSerializerOptions(); + private readonly JsonResultFormatter _internalFormatter = new(options.Json); private readonly JsonNullIgnoreCondition _nullIgnoreCondition = options.Json.NullIgnoreCondition; /// - public void Format(OperationResult result, IBufferWriter bufferWriter) + public void Format(OperationResult result, JsonWriter writer) { - var writer = new JsonWriter(bufferWriter, _writerOptions); - WriteValue(writer, result, _serializerOptions, _nullIgnoreCondition); + // Save the writer's current null ignore condition so we can restore it after formatting. + var savedNullIgnoreCondition = writer.NullIgnoreCondition; + + try + { + // Apply the null ignore condition configured for this payload formatter. + writer.NullIgnoreCondition = _nullIgnoreCondition; + _internalFormatter.Format(result, writer); + } + finally + { + // Restore the original null ignore condition. + writer.NullIgnoreCondition = savedNullIgnoreCondition; + } } /// - public void Format(IError error, IBufferWriter bufferWriter) + public void Format(IError error, JsonWriter writer) { - var writer = new JsonWriter(bufferWriter, _writerOptions); - WriteError(writer, error, _serializerOptions, _nullIgnoreCondition); + // Save the writer's current null ignore condition so we can restore it after formatting. + var savedNullIgnoreCondition = writer.NullIgnoreCondition; + + try + { + // Apply the null ignore condition configured for this payload formatter. + writer.NullIgnoreCondition = _nullIgnoreCondition; + WriteError(writer, error, _serializerOptions); + } + finally + { + // Restore the original null ignore condition. + writer.NullIgnoreCondition = savedNullIgnoreCondition; + } } /// - public void Format(IReadOnlyList errors, IBufferWriter bufferWriter) + public void Format(IReadOnlyList errors, JsonWriter writer) { - var writer = new JsonWriter(bufferWriter, _writerOptions); - writer.WriteStartArray(); + // Save the writer's current null ignore condition so we can restore it after formatting. + var savedNullIgnoreCondition = writer.NullIgnoreCondition; - for (var i = 0; i < errors.Count; i++) + try { - WriteError(writer, errors[i], _serializerOptions, _nullIgnoreCondition); - } + // Apply the null ignore condition configured for this payload formatter. + writer.NullIgnoreCondition = _nullIgnoreCondition; - writer.WriteEndArray(); + writer.WriteStartArray(); + + for (var i = 0; i < errors.Count; i++) + { + WriteError(writer, errors[i], _serializerOptions); + } + + writer.WriteEndArray(); + } + finally + { + // Restore the original null ignore condition. + writer.NullIgnoreCondition = savedNullIgnoreCondition; + } } /// - public void Format(IReadOnlyDictionary extensions, IBufferWriter bufferWriter) + public void Format(IReadOnlyDictionary extensions, JsonWriter writer) { - var writer = new JsonWriter(bufferWriter, _writerOptions); - WriteDictionary(writer, extensions, _serializerOptions, _nullIgnoreCondition); + // Save the writer's current null ignore condition so we can restore it after formatting. + var savedNullIgnoreCondition = writer.NullIgnoreCondition; + + try + { + // Apply the null ignore condition configured for this payload formatter. + writer.NullIgnoreCondition = _nullIgnoreCondition; + WriteDictionary(writer, extensions, _serializerOptions); + } + finally + { + // Restore the original null ignore condition. + writer.NullIgnoreCondition = savedNullIgnoreCondition; + } } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IWebSocketPayloadFormatter.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IWebSocketPayloadFormatter.cs index 6f3bb56abf0..e984d141875 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IWebSocketPayloadFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Formatters/IWebSocketPayloadFormatter.cs @@ -1,4 +1,4 @@ -using System.Buffers; +using HotChocolate.Text.Json; namespace HotChocolate.AspNetCore.Formatters; @@ -14,9 +14,9 @@ public interface IWebSocketPayloadFormatter /// The GraphQL operation result. /// /// - /// The buffer writer that is used to write the payload. + /// The JSON writer that is used to write the payload. /// - void Format(OperationResult result, IBufferWriter writer); + void Format(OperationResult result, JsonWriter writer); /// /// Formats the into a WebSocket payload. @@ -25,9 +25,9 @@ public interface IWebSocketPayloadFormatter /// The GraphQL execution error. /// /// - /// The buffer writer that is used to write the error. + /// The JSON writer that is used to write the error. /// - void Format(IError error, IBufferWriter writer); + void Format(IError error, JsonWriter writer); /// /// Formats the into a WebSocket payload. @@ -36,9 +36,9 @@ public interface IWebSocketPayloadFormatter /// The GraphQL execution errors. /// /// - /// The buffer writer that is used to write the errors. + /// The JSON writer that is used to write the errors. /// - void Format(IReadOnlyList errors, IBufferWriter writer); + void Format(IReadOnlyList errors, JsonWriter writer); /// /// Formats the into a WebSocket payload. @@ -47,7 +47,7 @@ public interface IWebSocketPayloadFormatter /// The GraphQL extensions. /// /// - /// The buffer writer that is used to write the extensions. + /// The JSON writer that is used to write the extensions. /// - void Format(IReadOnlyDictionary extensions, IBufferWriter writer); + void Format(IReadOnlyDictionary extensions, JsonWriter writer); } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpMultipartMiddleware.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpMultipartMiddleware.cs index 10fd4b64ea7..386f803870d 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpMultipartMiddleware.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpMultipartMiddleware.cs @@ -102,6 +102,10 @@ protected override async ValueTask ParseRequestsFromBodyAsync( var multipartRequest = ParseMultipartRequest(form); var requests = session.RequestParser.ParseRequest(multipartRequest.Operations); + // we add the file lookup as a feature on the HttpContext and can grab it from + // there and put it on the GraphQL request. + context.Features.Set(multipartRequest.Files); + for (var i = 0; i < requests.Length; i++) { var current = requests[i]; @@ -110,7 +114,15 @@ protected override async ValueTask ParseRequestsFromBodyAsync( if (!multipartRequest.FileMap.Root.TryGetNode(i.ToString(), out var operationRoot)) { - continue; + // Legacy multipart maps do not include an operation index. + if (requests.Length == 1) + { + operationRoot = multipartRequest.FileMap.Root; + } + else + { + continue; + } } if (current.Variables is null) @@ -132,7 +144,7 @@ protected override async ValueTask ParseRequestsFromBodyAsync( current = current with { - Variables = JsonDocument.Parse(bufferWriter.Memory), + Variables = JsonDocument.Parse(bufferWriter.WrittenMemory), VariablesMemoryOwner = bufferWriter }; context.Response.RegisterForDispose(current); @@ -210,7 +222,14 @@ private void RewriteVariables( ref Utf8JsonReader originalVariables, Utf8JsonWriter variables, FileMapTrieNode fileMapRoot) - => RewriteJsonValue(ref originalVariables, variables, fileMapRoot); + { + if (!originalVariables.Read()) + { + throw new JsonException("The variables JSON payload is empty."); + } + + RewriteJsonValue(ref originalVariables, variables, fileMapRoot); + } private static void RewriteJsonValue( ref Utf8JsonReader reader, diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/DefaultHttpRequestInterceptor.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/DefaultHttpRequestInterceptor.cs index 2ef47f2f00b..64afdfcca09 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/DefaultHttpRequestInterceptor.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Interceptors/DefaultHttpRequestInterceptor.cs @@ -19,6 +19,11 @@ public virtual ValueTask OnCreateAsync( { var userState = new UserState(context.User); + if (context.Features.Get() is { } featureLookup) + { + requestBuilder.Features.Set(featureLookup); + } + requestBuilder.Features.Set(userState); requestBuilder.Features.Set(context); requestBuilder.Features.Set(context.User); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs index 76cf382aff4..7648af96dc9 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Parsers/DefaultHttpRequestParser.cs @@ -62,8 +62,11 @@ public async ValueTask ParseRequestAsync( throw new GraphQLRequestException("Request size exceeds maximum allowed size."); } - // We tell the pipe that we've examined everything but consumed nothing yet. - requestBody.AdvanceTo(result.Buffer.Start, result.Buffer.End); + if (!result.IsCompleted && !result.IsCanceled) + { + // We tell the pipe that we've examined everything but consumed nothing yet. + requestBody.AdvanceTo(result.Buffer.Start, result.Buffer.End); + } } while (result is { IsCompleted: false, IsCanceled: false }); @@ -124,8 +127,11 @@ public async ValueTask ParsePersistedOperationRequestAsync( throw new GraphQLRequestException("Request size exceeds maximum allowed size."); } - // We tell the pipe that we've examined everything but consumed nothing yet. - requestBody.AdvanceTo(result.Buffer.Start, result.Buffer.End); + if (!result.IsCompleted && !result.IsCanceled) + { + // We tell the pipe that we've examined everything but consumed nothing yet. + requestBody.AdvanceTo(result.Buffer.Start, result.Buffer.End); + } } while (result is { IsCompleted: false, IsCanceled: false }); @@ -352,9 +358,9 @@ public GraphQLRequest[] ParseRequest(string sourceText) s_utf8.GetBytes(sourceText, span); return Parse(span, _parserOptions, _documentCache, _documentHashProvider); } - catch (OperationIdFormatException) + catch (InvalidGraphQLRequestException ex) { - throw ErrorHelper.InvalidOperationIdFormat(); + throw ErrorHelper.InvalidRequest(ex); } finally { diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs index b2e3ac60836..4950944b520 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/Apollo/ApolloSubscriptionProtocolHandler.cs @@ -249,7 +249,7 @@ public async ValueTask SendResultMessageAsync( jsonWriter.WritePropertyName(MessageProperties.Type); jsonWriter.WriteStringValue(Utf8Messages.Data); jsonWriter.WritePropertyName(Payload); - _formatter.Format(result, arrayWriter); + _formatter.Format(result, jsonWriter); jsonWriter.WriteEndObject(); await session.Connection.SendAsync(arrayWriter.WrittenMemory, cancellationToken); } @@ -268,7 +268,7 @@ public async ValueTask SendErrorMessageAsync( jsonWriter.WritePropertyName(MessageProperties.Type); jsonWriter.WriteStringValue(Utf8Messages.Error); jsonWriter.WritePropertyName(Payload); - _formatter.Format(errors[0], arrayWriter); + _formatter.Format(errors[0], jsonWriter); jsonWriter.WriteEndObject(); await session.Connection.SendAsync(arrayWriter.WrittenMemory, cancellationToken); } @@ -317,7 +317,7 @@ private async ValueTask SendConnectionRejectMessage( } else { - _formatter.Format(extensions, arrayWriter); + _formatter.Format(extensions, jsonWriter); } jsonWriter.WriteEndObject(); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs index 679599aa778..798ff20b98c 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs @@ -13,19 +13,11 @@ namespace HotChocolate.AspNetCore.Subscriptions.Protocols.GraphQLOverWebSocket; -internal sealed class GraphQLOverWebSocketProtocolHandler : IGraphQLOverWebSocketProtocolHandler +internal sealed class GraphQLOverWebSocketProtocolHandler( + ISocketSessionInterceptor interceptor, + IWebSocketPayloadFormatter formatter) + : IGraphQLOverWebSocketProtocolHandler { - private readonly ISocketSessionInterceptor _interceptor; - private readonly IWebSocketPayloadFormatter _formatter; - - public GraphQLOverWebSocketProtocolHandler( - ISocketSessionInterceptor interceptor, - IWebSocketPayloadFormatter formatter) - { - _interceptor = interceptor; - _formatter = formatter; - } - public string Name => GraphQL_Transport_WS; public async ValueTask OnReceiveAsync( @@ -76,7 +68,7 @@ private async ValueTask OnReceiveInternalAsync( : PingMessage.Default; var responsePayload = - await _interceptor.OnPingAsync(session, operationMessageObj, cancellationToken); + await interceptor.OnPingAsync(session, operationMessageObj, cancellationToken); await SendPongMessageAsync(session, responsePayload, cancellationToken); return; @@ -89,7 +81,7 @@ private async ValueTask OnReceiveInternalAsync( ? new PongMessage(payload) : PongMessage.Default; - await _interceptor.OnPongAsync(session, operationMessageObj, cancellationToken); + await interceptor.OnPongAsync(session, operationMessageObj, cancellationToken); return; } @@ -107,7 +99,7 @@ private async ValueTask OnReceiveInternalAsync( : ConnectionInitMessage.Default; var connectionStatus = - await _interceptor.OnConnectAsync( + await interceptor.OnConnectAsync( session, operationMessageObj, cancellationToken); @@ -221,7 +213,7 @@ public async ValueTask SendResultMessageAsync( jsonWriter.WritePropertyName(MessageProperties.Type); jsonWriter.WriteStringValue(Utf8Messages.Next); jsonWriter.WritePropertyName(Payload); - _formatter.Format(result, arrayWriter); + formatter.Format(result, jsonWriter); jsonWriter.WriteEndObject(); await session.Connection.SendAsync(arrayWriter.WrittenMemory, cancellationToken); } @@ -240,7 +232,7 @@ public async ValueTask SendErrorMessageAsync( jsonWriter.WritePropertyName(MessageProperties.Type); jsonWriter.WriteStringValue(Utf8Messages.Error); jsonWriter.WritePropertyName(Payload); - _formatter.Format(errors, arrayWriter); + formatter.Format(errors, jsonWriter); jsonWriter.WriteEndObject(); await session.Connection.SendAsync(arrayWriter.WrittenMemory, cancellationToken); } @@ -251,7 +243,7 @@ public async ValueTask SendCompleteMessageAsync( CancellationToken cancellationToken) { using var writer = new PooledArrayWriter(); - SerializeMessage(writer, _formatter, Utf8Messages.Complete, id: operationSessionId); + SerializeMessage(writer, formatter, Utf8Messages.Complete, id: operationSessionId); await session.Connection.SendAsync(writer.WrittenMemory, cancellationToken); } @@ -267,7 +259,7 @@ public async ValueTask SendPingMessageAsync( else { using var writer = new PooledArrayWriter(); - SerializeMessage(writer, _formatter, Utf8Messages.Ping, payload); + SerializeMessage(writer, formatter, Utf8Messages.Ping, payload); await session.Connection.SendAsync(writer.WrittenMemory, cancellationToken); } } @@ -284,7 +276,7 @@ private async ValueTask SendPongMessageAsync( else { using var writer = new PooledArrayWriter(); - SerializeMessage(writer, _formatter, Utf8Messages.Pong, payload); + SerializeMessage(writer, formatter, Utf8Messages.Pong, payload); await session.Connection.SendAsync(writer.WrittenMemory, cancellationToken); } } @@ -295,7 +287,7 @@ private async ValueTask SendConnectionAcceptMessage( CancellationToken cancellationToken) { using var writer = new PooledArrayWriter(); - SerializeMessage(writer, _formatter, Utf8Messages.ConnectionAccept, payload); + SerializeMessage(writer, formatter, Utf8Messages.ConnectionAccept, payload); await session.Connection.SendAsync(writer.WrittenMemory, cancellationToken); } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/MessageUtilities.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/MessageUtilities.cs index f2c19a3b6f9..23ec241fa08 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/MessageUtilities.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/Protocols/MessageUtilities.cs @@ -33,7 +33,7 @@ public static void SerializeMessage( if (payload is not null) { jsonWriter.WritePropertyName("payload"u8); - formatter.Format(payload, pooledArrayWriter); + formatter.Format(payload, jsonWriter); } jsonWriter.WriteEndObject(); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ErrorHelper.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ErrorHelper.cs index 4497118695b..59474381bff 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ErrorHelper.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ErrorHelper.cs @@ -1,4 +1,5 @@ using HotChocolate.Collections.Immutable; +using HotChocolate.Language; using static HotChocolate.AspNetCore.Properties.AspNetCorePipelineResources; namespace HotChocolate.AspNetCore.Utilities; @@ -14,6 +15,13 @@ public static IError InvalidRequest() .SetCode(ErrorCodes.Server.RequestInvalid) .Build(); + public static GraphQLRequestException InvalidRequest( + InvalidGraphQLRequestException ex) => + new(ErrorBuilder.New() + .SetMessage(ex.Message) + .SetCode(ErrorCodes.Server.RequestInvalid) + .Build()); + public static IError RequestHasNoElements() => ErrorBuilder.New() .SetMessage(ErrorHelper_RequestHasNoElements) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/MiddlewareHelper.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/MiddlewareHelper.cs index d1fb10b3256..fc1c03fa696 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/MiddlewareHelper.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/MiddlewareHelper.cs @@ -132,6 +132,16 @@ await executorSession.RequestParser.ParsePersistedOperationRequestAsync( context.RequestAborted); context.Response.RegisterForDispose(request); } + catch (InvalidGraphQLRequestException ex) + { + // A GraphQL request exception is thrown if the HTTP request body couldn't be + // parsed. In this case, we will return HTTP status code 400 and return a + // GraphQL error result. + IError error = new Error { Message = ex.Message }; + error = executorSession.Handle(error); + executorSession.DiagnosticEvents.ParserErrors(context, [error]); + return new ParseRequestResult([error], HttpStatusCode.BadRequest); + } catch (GraphQLRequestException ex) { // A GraphQL request exception is thrown if the HTTP request body couldn't be diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ThrowHelper.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ThrowHelper.cs index 8ef78aad8eb..c637bf58af7 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ThrowHelper.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/ThrowHelper.cs @@ -42,7 +42,7 @@ public static GraphQLRequestException DefaultHttpRequestParser_MaxRequestSizeExc .SetCode(ErrorCodes.Server.MaxRequestSize) .Build()); - public static GraphQLException HttpMultipartMiddleware_Invalid_Form( + public static GraphQLRequestException HttpMultipartMiddleware_Invalid_Form( Exception ex) => new GraphQLRequestException( ErrorBuilder.New() @@ -52,63 +52,63 @@ public static GraphQLException HttpMultipartMiddleware_Invalid_Form( .SetExtension("underlyingError", ex.Message) .Build()); - public static GraphQLException HttpMultipartMiddleware_No_Operations_Specified() => + public static GraphQLRequestException HttpMultipartMiddleware_No_Operations_Specified() => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_No_Operations_Specified) .SetCode(ErrorCodes.Server.MultiPartNoOperationsSpecified) .Build()); - public static GraphQLException HttpMultipartMiddleware_Fields_Misordered() => + public static GraphQLRequestException HttpMultipartMiddleware_Fields_Misordered() => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_Fields_Misordered) .SetCode(ErrorCodes.Server.MultiPartFieldsMisordered) .Build()); - public static GraphQLException HttpMultipartMiddleware_NoObjectPath(string filename) => + public static GraphQLRequestException HttpMultipartMiddleware_NoObjectPath(string filename) => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_NoObjectPath, filename) .SetCode(ErrorCodes.Server.MultiPartNoObjectPath) .Build()); - public static GraphQLException HttpMultipartMiddleware_FileMissing(string filename) => + public static GraphQLRequestException HttpMultipartMiddleware_FileMissing(string filename) => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_FileMissing, filename) .SetCode(ErrorCodes.Server.MultiPartFileMissing) .Build()); - public static GraphQLException HttpMultipartMiddleware_VariableStructureInvalid() => + public static GraphQLRequestException HttpMultipartMiddleware_VariableStructureInvalid() => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_VariableStructureInvalid) .SetCode(ErrorCodes.Server.MultiPartVariableStructureInvalid) .Build()); - public static GraphQLException HttpMultipartMiddleware_InvalidPath(string path) => + public static GraphQLRequestException HttpMultipartMiddleware_InvalidPath(string path) => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_InvalidPath, path) .SetCode(ErrorCodes.Server.MultiPartInvalidPath) .Build()); - public static GraphQLException HttpMultipartMiddleware_PathMustStartWithVariable() => + public static GraphQLRequestException HttpMultipartMiddleware_PathMustStartWithVariable() => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_PathMustStartWithVariable) .SetCode(ErrorCodes.Server.MultiPartPathMustStartWithVariable) .Build()); - public static GraphQLException HttpMultipartMiddleware_InvalidMapJson() => + public static GraphQLRequestException HttpMultipartMiddleware_InvalidMapJson() => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_InvalidMapJson) .SetCode(ErrorCodes.Server.MultiPartInvalidMapJson) .Build()); - public static GraphQLException HttpMultipartMiddleware_MapNotSpecified() => + public static GraphQLRequestException HttpMultipartMiddleware_MapNotSpecified() => new GraphQLRequestException( ErrorBuilder.New() .SetMessage(ThrowHelper_HttpMultipartMiddleware_MapNotSpecified) diff --git a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs index 49d6da30b07..b993a602350 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatter.cs @@ -26,6 +26,7 @@ public sealed class JsonResultFormatter : IOperationResultFormatter, IExecutionR public JsonResultFormatter(bool indented = false) : this(new JsonResultFormatterOptions { Indented = indented }) { + _nullIgnoreCondition = JsonNullIgnoreCondition.None; } /// @@ -90,50 +91,54 @@ public ValueTask FormatAsync( private void FormatInternal(OperationResult result, IBufferWriter bufferWriter) { - var jsonWriter = new JsonWriter(bufferWriter, _options); + var jsonWriter = new JsonWriter(bufferWriter, _options, _nullIgnoreCondition); + Format(result, jsonWriter); + } + + public void Format(OperationResult result, JsonWriter writer) + { + ArgumentNullException.ThrowIfNull(result); + ArgumentNullException.ThrowIfNull(writer); - jsonWriter.WriteStartObject(); + writer.WriteStartObject(); if (result.RequestIndex.HasValue) { - jsonWriter.WritePropertyName(RequestIndex); - jsonWriter.WriteNumberValue(result.RequestIndex.Value); + writer.WritePropertyName(RequestIndex); + writer.WriteNumberValue(result.RequestIndex.Value); } if (result.VariableIndex.HasValue) { - jsonWriter.WritePropertyName(VariableIndex); - jsonWriter.WriteNumberValue(result.VariableIndex.Value); + writer.WritePropertyName(VariableIndex); + writer.WriteNumberValue(result.VariableIndex.Value); } WriteErrors( - jsonWriter, + writer, result.Errors, - _serializerOptions, - default); + _serializerOptions); if (result.Data.HasValue) { - jsonWriter.WritePropertyName(Data); - result.Data.Value.Formatter.WriteDataTo(jsonWriter); + writer.WritePropertyName(Data); + result.Data.Value.Formatter.WriteDataTo(writer); } WriteExtensions( - jsonWriter, + writer, result.Extensions, - _serializerOptions, - default); + _serializerOptions); if (result.IsIncremental) { WriteIncremental( - jsonWriter, + writer, result, - _serializerOptions, - default); + _serializerOptions); } - jsonWriter.WriteEndObject(); + writer.WriteEndObject(); } private async ValueTask FormatInternalAsync( diff --git a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatterOptions.cs b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatterOptions.cs index f417ecd3e74..157735e8c07 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatterOptions.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonResultFormatterOptions.cs @@ -1,6 +1,6 @@ using System.Text.Encodings.Web; using System.Text.Json; -using HotChocolate.Execution; +using HotChocolate.Text.Json; using static System.Text.Json.JsonSerializerDefaults; using static System.Text.Json.Serialization.JsonIgnoreCondition; diff --git a/src/HotChocolate/AspNetCore/src/Transport.Formatters/MultiPartResultFormatter.cs b/src/HotChocolate/AspNetCore/src/Transport.Formatters/MultiPartResultFormatter.cs index 06d8160d54e..bbfb96fc10f 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Formatters/MultiPartResultFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Formatters/MultiPartResultFormatter.cs @@ -127,6 +127,8 @@ private async ValueTask FormatResponseStreamAsync( while (await enumerator.MoveNextAsync().ConfigureAwait(false)) { + var current = enumerator.Current; + try { if (first || responseStream.Kind is not DeferredResult) @@ -139,9 +141,9 @@ private async ValueTask FormatResponseStreamAsync( MessageHelper.WriteResultHeader(writer); // Next, we write the payload of the part. - MessageHelper.WritePayload(writer, enumerator.Current, _payloadFormatter); + MessageHelper.WritePayload(writer, current, _payloadFormatter); - if (responseStream.Kind is DeferredResult && (enumerator.Current.HasNext ?? false)) + if (responseStream.Kind is DeferredResult && (current.HasNext ?? false)) { // If the result is a deferred result and has a next result, we need to // write a new part so that the client knows that there is more to come. @@ -155,7 +157,7 @@ private async ValueTask FormatResponseStreamAsync( { // The result objects use pooled memory, so we need to ensure that they // return the memory by disposing them. - await enumerator.Current.DisposeAsync().ConfigureAwait(false); + await current.DisposeAsync().ConfigureAwait(false); } } diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs new file mode 100644 index 00000000000..166576cfa6c --- /dev/null +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/DeferOverHttpTests.cs @@ -0,0 +1,516 @@ +using System.Net; +using System.Net.Http.Json; +using HotChocolate.AspNetCore.Formatters; +using HotChocolate.AspNetCore.Tests.Utilities; +using HotChocolate.Types; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.AspNetCore; + +public class DeferOverHttpTests(TestServerFactory serverFactory) : ServerTestBase(serverFactory) +{ + [Theory] + [InlineData(null)] + [InlineData("*/*")] + [InlineData("multipart/mixed")] + [InlineData("multipart/*")] + [InlineData("application/graphql-response+json, multipart/mixed")] + [InlineData("text/event-stream, multipart/mixed")] + public async Task Simple_Defer_Multipart(string? acceptHeader) + { + // arrange + using var server = CreateDeferServer(); + var client = server.CreateClient(); + + // act + using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql"); + request.Content = JsonContent.Create(new + { + query = """ + { + product { + name + ... @defer { + description + } + } + } + """ + }); + + if (acceptHeader is not null) + { + request.Headers.Add("Accept", acceptHeader); + } + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("multipart/mixed", response.Content.Headers.ContentType?.MediaType); + + var content = await response.Content.ReadAsStringAsync(); + + Snapshot + .Create() + .Add(content, "Response") + .MatchInline( + """ + + --- + Content-Type: application/json; charset=utf-8 + + {"data":{"product":{"name":"Abc"}},"pending":[{"id":"2","path":["product"]}],"hasNext":true} + --- + Content-Type: application/json; charset=utf-8 + + {"incremental":[{"id":"2","data":{"description":"Abc desc"}}],"completed":[{"id":"2"}],"hasNext":false} + ----- + + """); + } + + [Theory] + [InlineData("text/event-stream")] + [InlineData("application/graphql-response+json, text/event-stream")] + public async Task Simple_Defer_EventStream(string acceptHeader) + { + // arrange + using var server = CreateDeferServer(); + var client = server.CreateClient(); + + // act + using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql"); + request.Content = JsonContent.Create(new + { + query = """ + { + product { + name + ... @defer { + description + } + } + } + """ + }); + request.Headers.Add("Accept", acceptHeader); + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/event-stream", response.Content.Headers.ContentType?.MediaType); + + var content = await response.Content.ReadAsStringAsync(); + + Snapshot + .Create() + .Add(content, "Response") + .MatchInline( + """ + event: next + data: {"data":{"product":{"name":"Abc"}},"pending":[{"id":"2","path":["product"]}],"hasNext":true} + + event: next + data: {"incremental":[{"id":"2","data":{"description":"Abc desc"}}],"completed":[{"id":"2"}],"hasNext":false} + + event: complete + + + """); + } + + [Theory] + [InlineData(null)] + [InlineData("*/*")] + [InlineData("multipart/mixed")] + [InlineData("multipart/*")] + [InlineData("application/graphql-response+json, multipart/mixed")] + [InlineData("text/event-stream, multipart/mixed")] + public async Task Defer_With_Label_Multipart(string? acceptHeader) + { + // arrange + using var server = CreateDeferServer(); + var client = server.CreateClient(); + + // act + using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql"); + request.Content = JsonContent.Create(new + { + query = """ + { + product { + name + ... @defer(label: "productDescription") { + description + } + } + } + """ + }); + + if (acceptHeader is not null) + { + request.Headers.Add("Accept", acceptHeader); + } + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("multipart/mixed", response.Content.Headers.ContentType?.MediaType); + + var content = await response.Content.ReadAsStringAsync(); + + Snapshot + .Create() + .Add(content, "Response") + .MatchInline( + """ + + --- + Content-Type: application/json; charset=utf-8 + + {"data":{"product":{"name":"Abc"}},"pending":[{"id":"2","path":["product"],"label":"productDescription"}],"hasNext":true} + --- + Content-Type: application/json; charset=utf-8 + + {"incremental":[{"id":"2","data":{"description":"Abc desc"}}],"completed":[{"id":"2"}],"hasNext":false} + ----- + + """); + } + + [Theory] + [InlineData(null)] + [InlineData("*/*")] + [InlineData("application/graphql-response+json, multipart/mixed")] + [InlineData("application/graphql-response+json, text/event-stream, multipart/mixed")] + public async Task Defer_Disabled_By_Variable(string? acceptHeader) + { + // arrange + using var server = CreateDeferServer(); + var client = server.CreateClient(); + + // act + using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql"); + request.Content = JsonContent.Create(new + { + query = + """ + query($shouldDefer: Boolean!) { + product { + name + ... @defer(if: $shouldDefer) { + description + } + } + } + """, + variables = new { shouldDefer = false } + }); + + if (acceptHeader is not null) + { + request.Headers.Add("Accept", acceptHeader); + } + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // When defer is disabled, should get a regular JSON response, not multipart + Assert.Equal("application/graphql-response+json", response.Content.Headers.ContentType?.MediaType); + + var content = await response.Content.ReadAsStringAsync(); + + Snapshot + .Create() + .Add(content, "Response") + .MatchInline( + """ + {"data":{"product":{"name":"Abc","description":"Abc desc"}}} + """); + } + + [Fact] + public async Task Defer_NoStreamableAcceptHeader() + { + // arrange + using var server = CreateDeferServer(); + var client = server.CreateClient(); + + // act + using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql"); + request.Content = JsonContent.Create(new + { + query = """ + { + product { + name + ... @defer { + description + } + } + } + """ + }); + request.Headers.Add("Accept", "application/graphql-response+json"); + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + // assert + // Should reject the request since we have a deferred result but + // the user only accepts non-streaming JSON payload + Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); + Assert.Equal("application/graphql-response+json", response.Content.Headers.ContentType?.MediaType); + + var content = await response.Content.ReadAsStringAsync(); + + Snapshot + .Create() + .Add(content, "Response") + .MatchInline( + """ + {"errors":[{"message":"The specified operation kind is not allowed."}]} + """); + } + + [Theory] + [InlineData(null)] + [InlineData("*/*")] + [InlineData("multipart/mixed")] + [InlineData("multipart/*")] + [InlineData("application/graphql-response+json, multipart/mixed")] + [InlineData("text/event-stream, multipart/mixed")] + public async Task Defer_TypeCondition_Multipart(string? acceptHeader) + { + // arrange + using var server = CreateDeferServer(); + var client = server.CreateClient(); + + // act + using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql"); + request.Content = JsonContent.Create(new + { + query = """ + { + hero { + name + ... on Droid @defer(label: "droid_details") { + primaryFunction + } + } + } + """ + }); + + if (acceptHeader is not null) + { + request.Headers.Add("Accept", acceptHeader); + } + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("multipart/mixed", response.Content.Headers.ContentType?.MediaType); + + var content = await response.Content.ReadAsStringAsync(); + + Snapshot + .Create() + .Add(content, "Response") + .MatchInline( + """ + + --- + Content-Type: application/json; charset=utf-8 + + {"data":{"hero":{"name":"R2-D2"}},"pending":[{"id":"2","path":["hero"],"label":"droid_details"}],"hasNext":true} + --- + Content-Type: application/json; charset=utf-8 + + {"incremental":[{"id":"2","data":{"primaryFunction":"Astromech"}}],"completed":[{"id":"2"}],"hasNext":false} + ----- + + """); + } + + [Theory] + [InlineData(null)] + [InlineData("*/*")] + [InlineData("multipart/mixed")] + [InlineData("multipart/*")] + [InlineData("application/graphql-response+json, multipart/mixed")] + [InlineData("text/event-stream, multipart/mixed")] + public async Task Defer_TypeCondition_If_True(string? acceptHeader) + { + // arrange + using var server = CreateDeferServer(); + var client = server.CreateClient(); + + // act + using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql"); + request.Content = JsonContent.Create(new + { + query = """ + query($if: Boolean!) { + hero { + name + ... on Droid @defer(label: "droid_details", if: $if) { + primaryFunction + } + } + } + """, + variables = new { @if = true } + }); + + if (acceptHeader is not null) + { + request.Headers.Add("Accept", acceptHeader); + } + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("multipart/mixed", response.Content.Headers.ContentType?.MediaType); + + var content = await response.Content.ReadAsStringAsync(); + + Snapshot + .Create() + .Add(content, "Response") + .MatchInline( + """ + + --- + Content-Type: application/json; charset=utf-8 + + {"data":{"hero":{"name":"R2-D2"}},"pending":[{"id":"2","path":["hero"],"label":"droid_details"}],"hasNext":true} + --- + Content-Type: application/json; charset=utf-8 + + {"incremental":[{"id":"2","data":{"primaryFunction":"Astromech"}}],"completed":[{"id":"2"}],"hasNext":false} + ----- + + """); + } + + [Theory] + [InlineData(null)] + [InlineData("*/*")] + [InlineData("application/graphql-response+json, multipart/mixed")] + [InlineData("application/graphql-response+json, text/event-stream, multipart/mixed")] + public async Task Defer_TypeCondition_If_False(string? acceptHeader) + { + // arrange + using var server = CreateDeferServer(); + var client = server.CreateClient(); + + // act + using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql"); + request.Content = JsonContent.Create(new + { + query = """ + query($if: Boolean!) { + hero { + name + ... on Droid @defer(label: "droid_details", if: $if) { + primaryFunction + } + } + } + """, + variables = new { @if = false } + }); + + if (acceptHeader is not null) + { + request.Headers.Add("Accept", acceptHeader); + } + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // When defer is disabled, should get a regular JSON response + Assert.Equal("application/graphql-response+json", response.Content.Headers.ContentType?.MediaType); + + var content = await response.Content.ReadAsStringAsync(); + + Snapshot + .Create() + .Add(content, "Response") + .MatchInline( + """ + {"data":{"hero":{"name":"R2-D2","primaryFunction":"Astromech"}}} + """); + } + + private TestServer CreateDeferServer(HttpTransportVersion serverTransportVersion = HttpTransportVersion.Latest) + { + return ServerFactory.Create( + services => services + .AddRouting() + .AddGraphQLServer() + .AddQueryType() + .AddType() + .AddDefaultBatchDispatcher() + .AddHttpResponseFormatter( + new HttpResponseFormatterOptions + { + HttpTransportVersion = serverTransportVersion + }) + .ModifyOptions(o => + { + o.EnableDefer = true; + o.EnableStream = true; + }), + app => app + .UseRouting() + .UseEndpoints(endpoints => endpoints.MapGraphQL())); + } + + public sealed class Query + { + public Product GetProduct() + => new("Abc"); + + public ICharacter GetHero() + => new Droid { Name = "R2-D2" }; + } + + [InterfaceType("Character")] + public interface ICharacter + { + string Name { get; } + } + + public sealed class Droid : ICharacter + { + public string Name { get; init; } = default!; + + public async Task GetPrimaryFunctionAsync() + { + await Task.Delay(1000); + return "Astromech"; + } + } + + public sealed record Product(string Name) + { + public async Task GetDescriptionAsync() + { + await Task.Delay(1000); + return Name + " desc"; + } + } +} diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs index cd8dd9f78f6..b6889e150e3 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/GraphQLOverHttpSpecTests.cs @@ -292,127 +292,6 @@ public async Task UnsupportedApplicationAcceptHeaderValue() """); } - [Theory] - [InlineData(null)] - [InlineData("*/*")] - [InlineData("multipart/mixed")] - [InlineData("multipart/*")] - [InlineData("application/graphql-response+json, multipart/mixed")] - [InlineData("text/event-stream, multipart/mixed")] - public async Task DeferredQuery_Multipart(string? acceptHeader) - { - // arrange - var server = CreateStarWarsServer(); - var client = server.CreateClient(); - - // act - using var request = new HttpRequestMessage(HttpMethod.Post, s_url); - request.Content = JsonContent.Create(new ClientQueryRequest { Query = "{ ... @defer { __typename } }" }); - AddAcceptHeader(request, acceptHeader); - - using var response = await client.SendAsync(request); - - // assert - Snapshot - .Create() - .Add(response) - .MatchInline( - """ - Headers: - Cache-Control: no-cache - Content-Type: multipart/mixed; boundary="-" - --------------------------> - Status Code: OK - --------------------------> - - --- - Content-Type: application/json; charset=utf-8 - - {"data":{},"hasNext":true} - --- - Content-Type: application/json; charset=utf-8 - - {"incremental":[{"data":{"__typename":"Query"},"path":[]}],"hasNext":false} - ----- - - """); - } - - [Theory] - [InlineData("text/event-stream")] - [InlineData("application/graphql-response+json, text/event-stream")] - public async Task DeferredQuery_EventStream(string acceptHeader) - { - // arrange - var server = CreateStarWarsServer(); - var client = server.CreateClient(); - - // act - using var request = new HttpRequestMessage(HttpMethod.Post, s_url); - request.Content = JsonContent.Create( - new ClientQueryRequest - { - Query = "{ ... @defer { __typename } }" - }); - request.Headers.Add("Accept", acceptHeader); - - using var response = await client.SendAsync(request, ResponseHeadersRead); - - // assert - Snapshot - .Create() - .Add(response) - .MatchInline( - """ - Headers: - Cache-Control: no-cache - Content-Type: text/event-stream; charset=utf-8 - --------------------------> - Status Code: OK - --------------------------> - event: next - data: {"data":{},"hasNext":true} - - event: next - data: {"incremental":[{"data":{"__typename":"Query"},"path":[]}],"hasNext":false} - - event: complete - - - """); - } - - [Fact] - public async Task DeferredQuery_NoStreamableAcceptHeader() - { - // arrange - var server = CreateStarWarsServer(); - var client = server.CreateClient(); - - // act - using var request = new HttpRequestMessage(HttpMethod.Post, s_url); - request.Content = JsonContent.Create(new ClientQueryRequest { Query = "{ ... @defer { __typename } }" }); - request.Headers.Add("Accept", ContentType.GraphQLResponse); - - using var response = await client.SendAsync(request, ResponseHeadersRead); - - // assert - // we are rejecting the request since we have a streamed result and - // the user requests a JSON payload. - Snapshot - .Create() - .Add(response) - .MatchInline( - """ - Headers: - Content-Type: application/graphql-response+json; charset=utf-8 - --------------------------> - Status Code: MethodNotAllowed - --------------------------> - {"errors":[{"message":"The specified operation kind is not allowed."}]} - """); - } - [Fact] public async Task EventStream_Sends_KeepAlive() { diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpMultipartMiddlewareTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpMultipartMiddlewareTests.cs index 3dec27e1b76..ad9963e5d9a 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpMultipartMiddlewareTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpMultipartMiddlewareTests.cs @@ -67,10 +67,10 @@ public async Task IncompleteOperations_Test() // act var form = new MultipartFormDataContent - { - { new StringContent("{}"), "operations" }, - { new StringContent("{}"), "map" } - }; + { + { new StringContent("{}"), "operations" }, + { new StringContent("{}"), "map" } + }; form.Headers.Add(HttpHeaderKeys.Preflight, "1"); diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs index 53c885309ad..77cdf3be1a2 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/HttpPostMiddlewareTests.cs @@ -3,6 +3,7 @@ using HotChocolate.AspNetCore.Instrumentation; using HotChocolate.AspNetCore.Tests.Utilities; using HotChocolate.Execution; +using HotChocolate.Text.Json; using HotChocolate.Transport.Formatters; using HotChocolate.Transport.Http; using Microsoft.AspNetCore.Builder; @@ -261,37 +262,6 @@ query h($id: String!) { result.MatchSnapshot(); } - [Fact] - public async Task SingleRequest_Defer_Results() - { - // arrange - var server = CreateStarWarsServer(); - - // act - var result = - await server.PostRawAsync( - new ClientQueryRequest - { - Query = - """ - { - ... @defer { - wait(m: 300) - } - hero(episode: NEW_HOPE) { - name - ... on Droid @defer(label: "my_id") { - id - } - } - } - """ - }); - - // assert - result.MatchSnapshot(); - } - [Fact] public async Task Single_Diagnostic_Listener_Is_Triggered() { @@ -365,164 +335,6 @@ ... on Droid @defer(label: "my_id") { Assert.True(listenerB.Triggered); } - [Fact] - public async Task Ensure_Multipart_Format_Is_Correct_With_Defer() - { - // arrange - var server = CreateStarWarsServer(); - - // act - var result = - await server.PostHttpAsync( - new ClientQueryRequest - { - Query = - """ - { - ... @defer { - wait(m: 300) - } - hero(episode: NEW_HOPE) { - name - ... on Droid @defer(label: "my_id") { - id - } - } - } - """ - }); - - // assert - new GraphQLHttpResponse(result).MatchInlineSnapshot( - """ - { - "data": { - "hero": { - "name": "R2-D2", - "id": "2001" - }, - "wait": true - } - } - """); - } - - [Fact] - public async Task Ensure_Multipart_Format_Is_Correct_With_Defer_If_Condition_True() - { - // arrange - var server = CreateStarWarsServer(); - - // act - var result = - await server.PostRawAsync( - new ClientQueryRequest - { - Query = - """ - query ($if: Boolean!) { - ... @defer { - wait(m: 300) - } - hero(episode: NEW_HOPE) { - name - ... on Droid @defer(label: "my_id", if: $if) { - id - } - } - } - """, - Variables = new Dictionary { ["if"] = true } - }); - - // assert - result.Content.MatchSnapshot(); - } - - [Fact] - public async Task Ensure_JSON_Format_Is_Correct_With_Defer_If_Condition_False() - { - // arrange - var server = CreateStarWarsServer(); - - // act - var result = - await server.PostRawAsync( - new ClientQueryRequest - { - Query = - """ - query ($if: Boolean!) { - hero(episode: NEW_HOPE) { - name - ... on Droid @defer(label: "my_id", if: $if) { - id - } - } - } - """, - Variables = new Dictionary { ["if"] = false } - }); - - // assert - result.Content.MatchSnapshot(); - } - - [Fact] - public async Task Ensure_Multipart_Format_Is_Correct_With_Stream() - { - // arrange - var server = CreateStarWarsServer(); - - // act - var result = await server.PostHttpAsync( - new ClientQueryRequest - { - Query = - """ - { - ... @defer { - wait(m: 300) - } - hero(episode: NEW_HOPE) { - name - friends(first: 10) { - nodes @stream(initialCount: 1, label: "foo") { - name - } - } - } - } - """ - }); - - // assert - new GraphQLHttpResponse(result).MatchInlineSnapshot( - """ - { - "data": { - "hero": { - "name": "R2-D2", - "friends": { - "nodes": [ - { - "name": "Luke Skywalker" - }, - { - "name": "Han Solo" - }, - { - "name": "Leia Organa" - } - ] - } - }, - "wait": true - } - } - """); - } - [Fact] public async Task SingleRequest_CreateReviewForEpisode_With_ObjectVariable() { diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs index cdc8893a3d6..f7862a2b9fb 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/Apollo/WebSocketProtocolTests.cs @@ -8,6 +8,7 @@ using HotChocolate.AspNetCore.Tests.Utilities.Subscriptions.Apollo; using HotChocolate.Execution; using HotChocolate.Language; +using HotChocolate.Text.Json; using HotChocolate.Transport.Formatters; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/GraphQLOverWebSocket/WebSocketProtocolTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/GraphQLOverWebSocket/WebSocketProtocolTests.cs index 1081dbd3f05..8b431951616 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/GraphQLOverWebSocket/WebSocketProtocolTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/Subscriptions/GraphQLOverWebSocket/WebSocketProtocolTests.cs @@ -8,6 +8,7 @@ using HotChocolate.AspNetCore.Tests.Utilities.Subscriptions.GraphQLOverWebSocket; using HotChocolate.Execution; using HotChocolate.Subscriptions.Diagnostics; +using HotChocolate.Text.Json; using HotChocolate.Transport.Formatters; using HotChocolate.Transport.Sockets.Client; using Microsoft.AspNetCore.Builder; diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL.snap index 9cd5feb1a18..6e306458850 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL.snap @@ -17,7 +17,7 @@ type Droid implements Character { id: ID! name: String! appearsIn: [Episode] - friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") height(unit: Unit): Float primaryFunction: String traits: Any @@ -45,7 +45,7 @@ type Human implements Character { id: ID! name: String! appearsIn: [Episode] - friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") otherHuman: Human height(unit: Unit): Float homePlanet: String diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route.snap index 6ee7c2556d2..f3fa98b3db9 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route.snap @@ -17,7 +17,7 @@ type Droid implements Character { id: ID! name: String! appearsIn: [Episode] - friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") height(unit: Unit): Float primaryFunction: String traits: Any @@ -45,7 +45,7 @@ type Human implements Character { id: ID! name: String! appearsIn: [Episode] - friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") otherHuman: Human height(unit: Unit): Float homePlanet: String diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route_Explicit_Pattern.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route_Explicit_Pattern.snap index 6ee7c2556d2..f3fa98b3db9 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route_Explicit_Pattern.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_SDL_Explicit_Route_Explicit_Pattern.snap @@ -17,7 +17,7 @@ type Droid implements Character { id: ID! name: String! appearsIn: [Episode] - friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") height(unit: Unit): Float primaryFunction: String traits: Any @@ -45,7 +45,7 @@ type Human implements Character { id: ID! name: String! appearsIn: [Episode] - friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") otherHuman: Human height(unit: Unit): Float homePlanet: String diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema.md b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema.md index a178ad384d5..2125a646c56 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema.md +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema.md @@ -28,7 +28,7 @@ type Droid implements Character { id: ID! name: String! appearsIn: [Episode] - friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") height(unit: Unit): Float primaryFunction: String traits: Any @@ -56,7 +56,7 @@ type Human implements Character { id: ID! name: String! appearsIn: [Episode] - friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) + friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") otherHuman: Human height(unit: Unit): Float homePlanet: String diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Slicing_Args_Enabled.md b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Slicing_Args_Enabled.md index 09d0f7a3fcc..892ed24ccbc 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Slicing_Args_Enabled.md +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpGetSchemaMiddlewareTests.Download_GraphQL_Schema_Slicing_Args_Enabled.md @@ -28,7 +28,7 @@ type Droid implements Character { id: ID! name: String! appearsIn: [Episode] - friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ]) + friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ]) @cost(weight: "10") height(unit: Unit): Float primaryFunction: String traits: Any @@ -56,7 +56,7 @@ type Human implements Character { id: ID! name: String! appearsIn: [Episode] - friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ]) + friends("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): FriendsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ]) @cost(weight: "10") otherHuman: Human height(unit: Unit): Float homePlanet: String diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpMultipartMiddlewareTests.IncompleteOperations_Test.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpMultipartMiddlewareTests.IncompleteOperations_Test.snap index b8f3c9524c4..27e77aef431 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpMultipartMiddlewareTests.IncompleteOperations_Test.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpMultipartMiddlewareTests.IncompleteOperations_Test.snap @@ -1,10 +1,13 @@ { "ContentType": "application/graphql-response+json; charset=utf-8", - "StatusCode": "InternalServerError", + "StatusCode": "BadRequest", "Data": null, "Errors": [ { - "message": "Unexpected Execution Error" + "message": "Request must contain either a query or a document id.", + "extensions": { + "code": "HC0009" + } } ], "Extensions": null diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpMultipartMiddlewareTests.MissingObjectPathsForKey_Test.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpMultipartMiddlewareTests.MissingObjectPathsForKey_Test.snap index 792c5172d26..d74bb9e695a 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpMultipartMiddlewareTests.MissingObjectPathsForKey_Test.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpMultipartMiddlewareTests.MissingObjectPathsForKey_Test.snap @@ -4,9 +4,9 @@ "Data": null, "Errors": [ { - "message": "No object paths specified for key '1' in 'map'.", + "message": "File of key '1' is missing.", "extensions": { - "code": "HC0037" + "code": "HC0038" } } ], diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.EmptyRequest.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.EmptyRequest.snap index 82a6fa4506b..d93d472e374 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.EmptyRequest.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.EmptyRequest.snap @@ -4,9 +4,9 @@ "Data": null, "Errors": [ { - "message": "The GraphQL request is empty.", + "message": "Invalid JSON document.", "extensions": { - "code": "HC0009" + "code": "HC0012" } } ], diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_JSON_Format_Is_Correct_With_Defer_If_Condition_False.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_JSON_Format_Is_Correct_With_Defer_If_Condition_False.snap deleted file mode 100644 index a626698e774..00000000000 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_JSON_Format_Is_Correct_With_Defer_If_Condition_False.snap +++ /dev/null @@ -1 +0,0 @@ -{"data":{"hero":{"name":"R2-D2","id":"2001"}}} diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Defer.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Defer.snap deleted file mode 100644 index 0f0c4e7f9ce..00000000000 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Defer.snap +++ /dev/null @@ -1,14 +0,0 @@ - ---- -Content-Type: application/json; charset=utf-8 - -{"data":{"hero":{"name":"R2-D2"}},"hasNext":true} ---- -Content-Type: application/json; charset=utf-8 - -{"incremental":[{"data":{"id":"2001"},"label":"my_id","path":["hero"]}],"hasNext":true} ---- -Content-Type: application/json; charset=utf-8 - -{"incremental":[{"data":{"wait":true},"path":[]}],"hasNext":false} ------ diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Defer_If_Condition_True.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Defer_If_Condition_True.snap deleted file mode 100644 index 0f0c4e7f9ce..00000000000 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Defer_If_Condition_True.snap +++ /dev/null @@ -1,14 +0,0 @@ - ---- -Content-Type: application/json; charset=utf-8 - -{"data":{"hero":{"name":"R2-D2"}},"hasNext":true} ---- -Content-Type: application/json; charset=utf-8 - -{"incremental":[{"data":{"id":"2001"},"label":"my_id","path":["hero"]}],"hasNext":true} ---- -Content-Type: application/json; charset=utf-8 - -{"incremental":[{"data":{"wait":true},"path":[]}],"hasNext":false} ------ diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Stream.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Stream.snap deleted file mode 100644 index fe8822d442a..00000000000 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.Ensure_Multipart_Format_Is_Correct_With_Stream.snap +++ /dev/null @@ -1,14 +0,0 @@ - ---- -Content-Type: application/json; charset=utf-8 - -{"data":{"hero":{"name":"R2-D2","friends":{"nodes":[{"name":"Luke Skywalker"}]}}},"hasNext":true} ---- -Content-Type: application/json; charset=utf-8 - -{"incremental":[{"items":[{"name":"Han Solo"}],"label":"foo","path":["hero","friends","nodes",1]},{"items":[{"name":"Leia Organa"}],"label":"foo","path":["hero","friends","nodes",2]}],"hasNext":true} ---- -Content-Type: application/json; charset=utf-8 - -{"incremental":[{"data":{"wait":true},"path":[]}],"hasNext":false} ------ diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.SingleRequest_Defer_Results.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.SingleRequest_Defer_Results.snap deleted file mode 100644 index fc5c3699096..00000000000 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.SingleRequest_Defer_Results.snap +++ /dev/null @@ -1,5 +0,0 @@ -{ - "ContentType": "multipart/mixed; boundary=\"-\"", - "StatusCode": "OK", - "Content": "\r\n---\r\nContent-Type: application/json; charset=utf-8\r\n\r\n{\"data\":{\"hero\":{\"name\":\"R2-D2\"}},\"hasNext\":true}\r\n---\r\nContent-Type: application/json; charset=utf-8\r\n\r\n{\"incremental\":[{\"data\":{\"id\":\"2001\"},\"label\":\"my_id\",\"path\":[\"hero\"]}],\"hasNext\":true}\r\n---\r\nContent-Type: application/json; charset=utf-8\r\n\r\n{\"incremental\":[{\"data\":{\"wait\":true},\"path\":[]}],\"hasNext\":false}\r\n-----\r\n" -} diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.SingleRequest_Incomplete.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.SingleRequest_Incomplete.snap index bb4c9dbecaf..d93d472e374 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.SingleRequest_Incomplete.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/HttpPostMiddlewareTests.SingleRequest_Incomplete.snap @@ -4,15 +4,9 @@ "Data": null, "Errors": [ { - "message": "Expected a `String`-token, but found a `EndOfFile`-token.", - "locations": [ - { - "line": 1, - "column": 15 - } - ], + "message": "Invalid JSON document.", "extensions": { - "code": "HC0011" + "code": "HC0012" } } ], diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/PersistedOperationMiddlewareTests.ExecutePersistedOperation_HttpPost_Empty_Body_InvalidId.md b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/PersistedOperationMiddlewareTests.ExecutePersistedOperation_HttpPost_Empty_Body_InvalidId.md index 5c779d7f90d..6a65aa830ec 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/PersistedOperationMiddlewareTests.ExecutePersistedOperation_HttpPost_Empty_Body_InvalidId.md +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/__snapshots__/PersistedOperationMiddlewareTests.ExecutePersistedOperation_HttpPost_Empty_Body_InvalidId.md @@ -4,7 +4,7 @@ { "errors": [ { - "message": "The operation id has an invalid format." + "message": "The GraphQL document ID contains invalid characters." } ] } diff --git a/src/HotChocolate/AspNetCore/test/Directory.Build.props b/src/HotChocolate/AspNetCore/test/Directory.Build.props index b62497b326e..3dd14ff76c9 100644 --- a/src/HotChocolate/AspNetCore/test/Directory.Build.props +++ b/src/HotChocolate/AspNetCore/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/Caching/test/Directory.Build.props b/src/HotChocolate/Caching/test/Directory.Build.props index 5b3630fd7db..24fb7f78e32 100644 --- a/src/HotChocolate/Caching/test/Directory.Build.props +++ b/src/HotChocolate/Caching/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/ExecutionTask.cs b/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/ExecutionTask.cs index 90838533a80..b9fb48eda9e 100644 --- a/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/ExecutionTask.cs +++ b/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/ExecutionTask.cs @@ -16,6 +16,9 @@ public abstract class ExecutionTask : IExecutionTask /// public uint Id { get; set; } + /// + public abstract int BranchId { get; } + /// /// Gets the execution engine task context. /// @@ -42,6 +45,9 @@ public abstract class ExecutionTask : IExecutionTask /// public bool IsRegistered { get; set; } + /// + public abstract bool IsDeferred { get; } + /// public void BeginExecute(CancellationToken cancellationToken) { @@ -78,9 +84,13 @@ private async Task ExecuteInternalAsync(CancellationToken cancellationToken) Context.ReportError(this, ex); } } + finally + { + Status = _completionStatus; + Context.Completed(this); - Status = _completionStatus; - Context.Completed(this); + await OnAfterCompletedAsync(cancellationToken).ConfigureAwait(false); + } } /// @@ -91,6 +101,16 @@ private async Task ExecuteInternalAsync(CancellationToken cancellationToken) /// protected abstract ValueTask ExecuteAsync(CancellationToken cancellationToken); + /// + /// Called after the task has completed, regardless of whether it succeeded or faulted. + /// Override this method to perform post-completion logic such as cleanup or notifications. + /// + /// + /// The cancellation token. + /// + /// A representing the asynchronous operation. + protected virtual ValueTask OnAfterCompletedAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask; + /// /// Completes the task as faulted. /// diff --git a/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/IExecutionTask.cs b/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/IExecutionTask.cs index 20014f2d15f..db81778d5ae 100644 --- a/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/IExecutionTask.cs +++ b/src/HotChocolate/Core/src/Abstractions/Execution/Tasks/IExecutionTask.cs @@ -10,6 +10,11 @@ public interface IExecutionTask /// uint Id { get; set; } + /// + /// Gets the execution branch id. + /// + int BranchId { get; } + /// /// Defines the kind of task. /// The task kind is used to apply the correct execution strategy. @@ -44,6 +49,11 @@ public interface IExecutionTask /// bool IsSerial { get; set; } + /// + /// Gets a value indicating whether this task is deprioritized. + /// + bool IsDeferred { get; } + /// /// Specifies if the task was fully registered with the scheduler. /// diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/CompletedResult.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/CompletedResult.cs index a036a399afc..75e3c092e5b 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/CompletedResult.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/CompletedResult.cs @@ -9,12 +9,12 @@ namespace HotChocolate.Execution; /// Field errors that caused the incremental delivery to fail due to error bubbling above the incremental result's path. /// When present, indicates the delivery has failed. /// -public sealed record CompletedResult(uint Id, IReadOnlyList? Errors = null) +public sealed record CompletedResult(int Id, IReadOnlyList? Errors = null) { /// /// Gets the request unique pending data identifier that matches a prior pending result. /// - public uint Id { get; init; } = Id; + public int Id { get; init; } = Id; /// /// Gets field errors that caused the incremental delivery to fail due to error bubbling diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IIncrementalObjectResult.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IIncrementalObjectResult.cs index 8e04e2dc878..3163c879495 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IIncrementalObjectResult.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IIncrementalObjectResult.cs @@ -1,19 +1,60 @@ +using System.Collections.Immutable; + namespace HotChocolate.Execution; /// /// Represents an incremental result that delivers additional fields for a @defer directive. /// -public interface IIncrementalObjectResult : IIncrementalResult +public sealed class IncrementalObjectResult : IIncrementalResult { + /// + /// Initializes a new instance of . + /// + /// + /// The unique identifier that correlates this result with its pending entry. + /// + /// + /// The GraphQL errors that occurred while resolving the deferred fragment. + /// + /// + /// The sub-path to concatenate with the pending result's path, or null + /// if the path is the same as the pending result's path. + /// + /// + /// The additional response fields to merge into the deferred fragment location. + /// + public IncrementalObjectResult( + int id, + ImmutableList? errors = null, + Path? subPath = null, + OperationResultData? data = null) + { + Id = id; + Errors = errors ?? []; + SubPath = subPath; + Data = data; + } + + /// + /// Gets the unique identifier that correlates this incremental result with + /// its corresponding pending entry. + /// + public int Id { get; } + + /// + /// Gets the GraphQL errors that occurred while resolving the deferred fragment. + /// + public ImmutableList Errors { get; } + /// /// Gets the sub-path that is concatenated with the pending result's path to determine /// the final path for this incremental data. When null, the path is the same /// as the pending result's path. /// - Path? SubPath { get; } + public Path? SubPath { get; } /// /// Gets the additional response fields to merge into the deferred fragment location. /// - object? Data { get; } + public OperationResultData? Data { get; } } diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IIncrementalResult.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IIncrementalResult.cs index 52861205a19..c31c72bad6f 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IIncrementalResult.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IIncrementalResult.cs @@ -1,3 +1,5 @@ +using System.Collections.Immutable; + namespace HotChocolate.Execution; /// @@ -8,11 +10,11 @@ public interface IIncrementalResult /// /// Gets the request unique pending data identifier that matches a prior pending result. /// - uint Id { get; } + int Id { get; } /// /// Gets field errors that occurred during execution of this incremental result. /// Only includes errors that did not bubble above the incremental result's path. /// - IReadOnlyList? Errors { get; } + ImmutableList Errors { get; } } diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IResultDataJsonFormatter.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IResultDataJsonFormatter.cs index bc15e8ac0bf..52751fc494c 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IResultDataJsonFormatter.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/IResultDataJsonFormatter.cs @@ -18,11 +18,7 @@ public interface IResultDataJsonFormatter /// The serializer options. /// If options are set to null .Web will be used. /// - /// - /// The null ignore condition. - /// void WriteTo( JsonWriter writer, - JsonSerializerOptions? options = null, - JsonNullIgnoreCondition nullIgnoreCondition = JsonNullIgnoreCondition.None); + JsonSerializerOptions? options = null); } diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs index b88efa7b486..51137738718 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonValueFormatter.cs @@ -8,12 +8,10 @@ namespace HotChocolate.Execution; public static class JsonValueFormatter { - // TODO : are the options still needed? public static void WriteValue( JsonWriter writer, object? value, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { if (value is null) { @@ -24,11 +22,11 @@ public static void WriteValue( switch (value) { case JsonDocument doc: - WriteJsonElement(doc.RootElement, writer, options, nullIgnoreCondition); + WriteJsonElement(doc.RootElement, writer, options); break; case JsonElement element: - WriteJsonElement(element, writer, options, nullIgnoreCondition); + WriteJsonElement(element, writer, options); break; case RawJsonValue rawJsonValue: @@ -36,19 +34,19 @@ public static void WriteValue( break; case Dictionary dict: - WriteDictionary(writer, dict, options, nullIgnoreCondition); + WriteDictionary(writer, dict, options); break; case IReadOnlyDictionary dict: - WriteDictionary(writer, dict, options, nullIgnoreCondition); + WriteDictionary(writer, dict, options); break; case IList list: - WriteList(writer, list, options, nullIgnoreCondition); + WriteList(writer, list, options); break; case IError error: - WriteError(writer, error, options, nullIgnoreCondition); + WriteError(writer, error, options); break; case string s: @@ -108,7 +106,7 @@ public static void WriteValue( break; case IResultDataJsonFormatter formatter: - formatter.WriteTo(writer, options, nullIgnoreCondition); + formatter.WriteTo(writer, options); break; default: @@ -120,8 +118,7 @@ public static void WriteValue( private static void WriteJsonElement( JsonElement element, JsonWriter writer, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { switch (element.ValueKind) { @@ -129,14 +126,8 @@ private static void WriteJsonElement( writer.WriteStartObject(); foreach (var property in element.EnumerateObject()) { - if (property.Value.ValueKind is JsonValueKind.Null - && (nullIgnoreCondition & JsonNullIgnoreCondition.Fields) == JsonNullIgnoreCondition.Fields) - { - continue; - } - writer.WritePropertyName(property.Name); - WriteValue(writer, property.Value, options, nullIgnoreCondition); + WriteValue(writer, property.Value, options); } writer.WriteEndObject(); break; @@ -145,13 +136,7 @@ private static void WriteJsonElement( writer.WriteStartArray(); foreach (var item in element.EnumerateArray()) { - if (item.ValueKind is JsonValueKind.Null - && (nullIgnoreCondition & JsonNullIgnoreCondition.Lists) == JsonNullIgnoreCondition.Lists) - { - continue; - } - - WriteValue(writer, item, options, nullIgnoreCondition); + WriteValue(writer, item, options); } writer.WriteEndArray(); break; @@ -190,21 +175,14 @@ private static void WriteJsonElement( public static void WriteDictionary( JsonWriter writer, IReadOnlyDictionary dict, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { writer.WriteStartObject(); foreach (var item in dict) { - if (item.Value is null - && (nullIgnoreCondition & JsonNullIgnoreCondition.Fields) == JsonNullIgnoreCondition.Fields) - { - continue; - } - writer.WritePropertyName(item.Key); - WriteValue(writer, item.Value, options, nullIgnoreCondition); + WriteValue(writer, item.Value, options); } writer.WriteEndObject(); @@ -213,22 +191,13 @@ public static void WriteDictionary( private static void WriteList( JsonWriter writer, IList list, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { writer.WriteStartArray(); for (var i = 0; i < list.Count; i++) { - var element = list[i]; - - if (element is null - && (nullIgnoreCondition & JsonNullIgnoreCondition.Lists) == JsonNullIgnoreCondition.Lists) - { - continue; - } - - WriteValue(writer, element, options, nullIgnoreCondition); + WriteValue(writer, list[i], options); } writer.WriteEndArray(); @@ -237,8 +206,7 @@ private static void WriteList( public static void WriteErrors( JsonWriter writer, IReadOnlyList errors, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { if (errors is { Count: > 0 }) { @@ -246,9 +214,12 @@ public static void WriteErrors( writer.WriteStartArray(); - for (var i = 0; i < errors.Count; i++) + // We sort errors by path to ensure a stable output: + // - Errors without paths (null) come first + // - Then errors sorted by path + foreach (var error in errors.OrderBy(e => e.Path, PathComparer.Instance)) { - WriteError(writer, errors[i], options, nullIgnoreCondition); + WriteError(writer, error, options); } writer.WriteEndArray(); @@ -258,8 +229,7 @@ public static void WriteErrors( public static void WriteError( JsonWriter writer, IError error, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { writer.WriteStartObject(); @@ -268,7 +238,7 @@ public static void WriteError( WriteLocations(writer, error.Locations); WritePath(writer, error.Path); - WriteExtensions(writer, error.Extensions, options, nullIgnoreCondition); + WriteExtensions(writer, error.Extensions, options); writer.WriteEndObject(); } @@ -276,21 +246,19 @@ public static void WriteError( public static void WriteExtensions( JsonWriter writer, IReadOnlyDictionary? dict, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { if (dict is { Count: > 0 }) { writer.WritePropertyName(Extensions); - WriteDictionary(writer, dict, options, nullIgnoreCondition); + WriteDictionary(writer, dict, options); } } public static void WriteIncremental( JsonWriter writer, OperationResult result, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { if (result.Pending is { Count: > 0 } pending) { @@ -314,7 +282,7 @@ public static void WriteIncremental( for (var i = 0; i < incremental.Count; i++) { - WriteIncrementalItem(writer, incremental[i], options, nullIgnoreCondition); + WriteIncrementalItem(writer, incremental[i], options); } writer.WriteEndArray(); @@ -328,7 +296,7 @@ public static void WriteIncremental( for (var i = 0; i < completed.Count; i++) { - WriteIncrementalCompletedItem(writer, completed[i]); + WriteIncrementalCompletedItem(writer, completed[i], options); } writer.WriteEndArray(); @@ -346,7 +314,7 @@ private static void WriteIncrementalPendingItem(JsonWriter writer, PendingResult writer.WriteStartObject(); writer.WritePropertyName(Id); - writer.WriteNumberValue(item.Id); + writer.WriteStringValue(item.Id.ToString()); writer.WritePropertyName(ResultFieldNames.Path); WritePathValue(writer, item.Path); @@ -363,28 +331,38 @@ private static void WriteIncrementalPendingItem(JsonWriter writer, PendingResult private static void WriteIncrementalItem( JsonWriter writer, IIncrementalResult item, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) + JsonSerializerOptions options) { writer.WriteStartObject(); writer.WritePropertyName(Id); - writer.WriteNumberValue(item.Id); + writer.WriteStringValue(item.Id.ToString()); if (item.Errors is { Count: > 0 }) { - WriteErrors(writer, item.Errors, options, nullIgnoreCondition); + WriteErrors(writer, item.Errors, options); } - if (item is IIncrementalObjectResult objectResult) + if (item is IncrementalObjectResult objectResult) { + if (objectResult.SubPath is not null) + { + writer.WritePropertyName(SubPath); + WritePathValue(writer, objectResult.SubPath); + } + writer.WritePropertyName(Data); - // TODO: Write actual data - writer.WriteStartObject(); - writer.WriteEndObject(); + if (objectResult.Data.HasValue) + { + objectResult.Data.Value.Formatter.WriteDataTo(writer); + } + else + { + writer.WriteNullValue(); + } } - else if (item is IIncrementalListResult listResult) + else if (item is IIncrementalListResult) { writer.WritePropertyName(Items); @@ -400,12 +378,20 @@ private static void WriteIncrementalItem( writer.WriteEndObject(); } - private static void WriteIncrementalCompletedItem(JsonWriter writer, CompletedResult item) + private static void WriteIncrementalCompletedItem( + JsonWriter writer, + CompletedResult item, + JsonSerializerOptions options) { writer.WriteStartObject(); writer.WritePropertyName(Id); - writer.WriteNumberValue(item.Id); + writer.WriteStringValue(item.Id.ToString()); + + if (item.Errors is { Count: > 0 }) + { + WriteErrors(writer, item.Errors, options); + } writer.WriteEndObject(); } @@ -418,9 +404,10 @@ private static void WriteLocations(JsonWriter writer, IReadOnlyList? l writer.WriteStartArray(); - for (var i = 0; i < locations.Count; i++) + // We sort locations to ensure a stable output. + foreach (var location in locations.Order()) { - WriteLocation(writer, locations[i]); + WriteLocation(writer, location); } writer.WriteEndArray(); @@ -474,3 +461,29 @@ private static void WritePathValue(JsonWriter writer, Path path) writer.WriteEndArray(); } } + +file sealed class PathComparer : IComparer +{ + public static readonly PathComparer Instance = new(); + + public int Compare(Path? x, Path? y) + { + // Null paths should come first + if (x is null && y is null) + { + return 0; + } + + if (x is null) + { + return -1; + } + + if (y is null) + { + return 1; + } + + return x.CompareTo(y); + } +} diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/OperationResult.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/OperationResult.cs index 9b07a882801..62392d91d4c 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/OperationResult.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/OperationResult.cs @@ -143,7 +143,7 @@ public Path? Path /// /// Gets the data that is being delivered in this operation result. /// - public OperationResultData? Data { get; } + public OperationResultData? Data { get; internal set; } /// /// Gets the GraphQL errors that occurred during execution. @@ -153,7 +153,10 @@ public ImmutableList Errors get => _errors; set { - if (!Data.HasValue && Errors is null or { Count: 0 } && Extensions is null or { Count: 0 }) + if (!Data.HasValue + && Errors is null or { Count: 0 } + && Extensions is null or { Count: 0 } + && Features.Get() is null) { throw new ArgumentException("Either data, errors or extensions must be provided."); } @@ -173,7 +176,10 @@ public ImmutableList Errors get => _extensions; set { - if (!Data.HasValue && Errors is null or { Count: 0 } && Extensions is null or { Count: 0 }) + if (!Data.HasValue + && Errors is null or { Count: 0 } + && Extensions is null or { Count: 0 } + && Features.Get() is null) { throw new ArgumentException("Either data, errors or extensions must be provided."); } diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/PendingResult.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/PendingResult.cs index 38c0fab5469..acff8018762 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/PendingResult.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/PendingResult.cs @@ -10,12 +10,12 @@ namespace HotChocolate.Execution; /// For @defer: indicates where the deferred fragment fields will be added. /// /// The label from the @defer or @stream directive's label argument, if present. -public sealed record PendingResult(uint Id, Path Path, string? Label = null) +public sealed record PendingResult(int Id, Path Path, string? Label = null) { /// /// Gets the request unique pending data identifier. /// - public uint Id { get; init; } = Id; + public int Id { get; init; } = Id; /// /// Gets the path in the response where the incremental data will be delivered. diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/ResultFieldNames.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/ResultFieldNames.cs index 79dad9f0905..c591e813f68 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/ResultFieldNames.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/ResultFieldNames.cs @@ -75,6 +75,11 @@ public static class ResultFieldNames /// public static ReadOnlySpan Label => "label"u8; + /// + /// Gets the subPath field name. + /// + public static ReadOnlySpan SubPath => "subPath"u8; + /// /// Gets the hasNext field name /// diff --git a/src/HotChocolate/Core/src/Execution.Operation.Abstractions/IOperation.cs b/src/HotChocolate/Core/src/Execution.Operation.Abstractions/IOperation.cs index 103016201e7..0e3db10c7e6 100644 --- a/src/HotChocolate/Core/src/Execution.Operation.Abstractions/IOperation.cs +++ b/src/HotChocolate/Core/src/Execution.Operation.Abstractions/IOperation.cs @@ -44,6 +44,16 @@ public interface IOperation : IFeatureProvider /// ISelectionSet RootSelectionSet { get; } + /// + /// Gets a value indicating whether this operation contains incremental delivery directives + /// such as @defer or @stream. + /// + /// + /// true if the operation contains @defer or @stream directives; + /// otherwise, false. + /// + bool HasIncrementalParts { get; } + /// /// Gets the selection set for the specified and /// . diff --git a/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelection.cs b/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelection.cs index 5de00d5bd0b..72869a0858c 100644 --- a/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelection.cs +++ b/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelection.cs @@ -119,9 +119,34 @@ public interface ISelection /// due to @skip or @include directive evaluation. /// /// - /// This method uses efficient bitwise operations to determine inclusion - /// based on the pre-computed flags. For non-conditional selections, - /// this always returns true. + /// For non-conditional selections, this always returns true. /// bool IsIncluded(ulong includeFlags); + + /// + /// Determines whether this selection is deferred based on the @defer directive flags. + /// + /// + /// The defer condition flags representing which @defer directives are active + /// for the current request, computed from the runtime variable values of the + /// if arguments on @defer directives. + /// + /// + /// true if this selection should be deferred and delivered incrementally + /// in a subsequent payload; otherwise, false if it should be included + /// in the initial response. + /// + /// + /// + /// For selections without any @defer directive, this always returns false. + /// + /// + /// The @defer directive (as specified in the GraphQL Incremental Delivery + /// specification) allows clients to mark fragment spreads or inline fragments + /// as lower priority for the initial response. The server may then choose + /// to deliver those fragments in subsequent payloads, reducing time-to-first-byte + /// for the initial result. + /// + /// + bool IsDeferred(ulong deferFlags); } diff --git a/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelectionSet.cs b/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelectionSet.cs index a64c48f961a..f77c192bf47 100644 --- a/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelectionSet.cs +++ b/src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelectionSet.cs @@ -24,6 +24,16 @@ public interface ISelectionSet /// bool IsConditional { get; } + /// + /// Gets a value indicating whether this selection set contains any selections + /// that may be deferred based on @defer directives. + /// + /// + /// true if one or more selections in this set can be deferred; + /// otherwise, false. + /// + bool HasIncrementalParts { get; } + /// /// Gets the type that declares this selection set. /// diff --git a/src/HotChocolate/Core/src/Types.Scalars.Upload/UploadType.cs b/src/HotChocolate/Core/src/Types.Scalars.Upload/UploadType.cs index 58d6bcf5696..4f52c5bbedc 100644 --- a/src/HotChocolate/Core/src/Types.Scalars.Upload/UploadType.cs +++ b/src/HotChocolate/Core/src/Types.Scalars.Upload/UploadType.cs @@ -10,7 +10,7 @@ namespace HotChocolate.Types; /// /// The GraphQL Upload scalar. /// -public sealed class UploadType : ScalarType +public sealed class UploadType : ScalarType { /// /// Initializes a new instance of the class. @@ -21,14 +21,20 @@ public UploadType() : base("Upload", BindingBehavior.Implicit) Description = UploadResources.UploadType_Description; } - /// - /// This operation is not supported. Upload scalars cannot be used in GraphQL literals. - /// - /// The GraphQL literal (not used). - /// Never returns; always throws. - /// Always thrown as literal input is not supported. - protected override IFile OnCoerceInputLiteral(StringValueNode valueLiteral) - => throw new NotSupportedException(); + public override ScalarSerializationType SerializationType => ScalarSerializationType.String; + + /// + public override object CoerceInputLiteral(IValueNode valueLiteral) + { + if (valueLiteral is not UploadValueNode uploadValue) + { + throw new LeafCoercionException( + $"Cannot coerce the literal of type `{valueLiteral.Kind}` to a file.", + this); + } + + return uploadValue.File; + } /// /// Coerces a JSON string value containing a file reference into an instance. @@ -46,8 +52,15 @@ protected override IFile OnCoerceInputLiteral(StringValueNode valueLiteral) /// /// Thrown when the file reference cannot be found in the file lookup service. /// - protected override IFile OnCoerceInputValue(JsonElement inputValue, IFeatureProvider context) + public override object CoerceInputValue(JsonElement inputValue, IFeatureProvider context) { + if (inputValue.ValueKind is not JsonValueKind.String) + { + throw new LeafCoercionException( + $"Cannot coerce the json value of kind `{inputValue.ValueKind}` to a file.", + this); + } + var fileLookup = context.Features.Get(); var fileName = inputValue.GetString()!; @@ -69,7 +82,7 @@ protected override IFile OnCoerceInputValue(JsonElement inputValue, IFeatureProv /// The runtime value (not used). /// The result element (not used). /// Always thrown as output coercion is not supported. - protected override void OnCoerceOutputValue(IFile runtimeValue, ResultElement resultValue) + public override void OnCoerceOutputValue(IFile runtimeValue, ResultElement resultValue) => throw new NotSupportedException(); /// @@ -78,6 +91,13 @@ protected override void OnCoerceOutputValue(IFile runtimeValue, ResultElement re /// The runtime value (not used). /// Never returns; always throws. /// Always thrown as value to literal conversion is not supported. - protected override StringValueNode OnValueToLiteral(IFile runtimeValue) + public override IValueNode OnValueToLiteral(IFile runtimeValue) => throw new NotSupportedException(); + + /// + public override IValueNode InputValueToLiteral(JsonElement inputValue, IFeatureProvider context) + { + var file = (IFile)CoerceInputValue(inputValue, context); + return new UploadValueNode(inputValue.GetString()!, file); + } } diff --git a/src/HotChocolate/Core/src/Types.Scalars.Upload/UploadValueNode.cs b/src/HotChocolate/Core/src/Types.Scalars.Upload/UploadValueNode.cs new file mode 100644 index 00000000000..c6b2c68dcdc --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Scalars.Upload/UploadValueNode.cs @@ -0,0 +1,62 @@ +using HotChocolate.Language; + +namespace HotChocolate.Types; + +/// +/// Represents an upload value node in the GraphQL abstract syntax tree (AST). +/// This value node is used to represent file uploads in GraphQL operations, +/// containing both a key for identification and the actual file data. +/// +public sealed class UploadValueNode : IValueNode +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// The unique key identifying this upload in the multipart request. + /// + /// + /// The file data associated with this upload. + /// + /// + /// is null or empty. + /// + /// + /// is null. + /// + public UploadValueNode(string key, IFile file) + { + ArgumentException.ThrowIfNullOrEmpty(key); + ArgumentNullException.ThrowIfNull(file); + + Key = key; + File = file; + } + + /// + /// Gets the unique key identifying this upload in the multipart request. + /// + public string Key { get; } + + /// + /// Gets the file data associated with this upload. + /// + public IFile File { get; } + + object? IValueNode.Value => Key; + + /// + public SyntaxKind Kind => SyntaxKind.StringValue; + + /// + public Language.Location? Location => null; + + /// + public IEnumerable GetNodes() => []; + + /// + public override string ToString() => ToString(true); + + /// + public string ToString(bool indented) => $"\"{Key}\""; +} diff --git a/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorBuilderExtensions.TransactionScope.cs b/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorBuilderExtensions.TransactionScope.cs deleted file mode 100644 index 230f155b4d3..00000000000 --- a/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorBuilderExtensions.TransactionScope.cs +++ /dev/null @@ -1,116 +0,0 @@ -using HotChocolate.Execution.Configuration; -using HotChocolate.Execution.Processing; -using Microsoft.Extensions.DependencyInjection.Extensions; - -// ReSharper disable once CheckNamespace -namespace Microsoft.Extensions.DependencyInjection; - -public static partial class RequestExecutorBuilderExtensions -{ - /// - /// Adds a custom transaction scope handler to the schema. - /// - /// - /// The request executor builder. - /// - /// - /// The concrete type of the transaction scope handler. - /// - /// - /// The request executor builder. - /// - /// - /// The is null. - /// - /// - /// The will be activated with the of the schema services. - /// If your needs to access application services you need to - /// make the services available in the schema services via . - /// - public static IRequestExecutorBuilder AddTransactionScopeHandler( - this IRequestExecutorBuilder builder) - where T : class, ITransactionScopeHandler - { - ArgumentNullException.ThrowIfNull(builder); - - // we host the transaction scope in the global DI. - builder.Services.TryAddSingleton(); - - return ConfigureSchemaServices( - builder, - static services => - { - services.RemoveAll(); - services.AddSingleton(); - }); - } - - /// - /// Adds a custom transaction scope handler to the schema. - /// - /// - /// The request executor builder. - /// - /// - /// A factory to create the transaction scope. - /// - /// - /// The request executor builder. - /// - /// - /// - /// The passed to the - /// is for the schema services. If you need to access application services - /// you need to either make the services available in the schema services - /// via or use - /// - /// to access the application services from within the schema service provider. - /// - public static IRequestExecutorBuilder AddTransactionScopeHandler( - this IRequestExecutorBuilder builder, - Func factory) - { - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(factory); - - return ConfigureSchemaServices( - builder, - services => - { - services.RemoveAll(); - services.AddSingleton(factory); - }); - } - - /// - /// Adds the which uses - /// for mutation transactions. - /// - /// - /// The request executor builder. - /// - /// - /// The request executor builder. - /// - /// - /// The is null. - /// - public static IRequestExecutorBuilder AddDefaultTransactionScopeHandler( - this IRequestExecutorBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); - - return AddTransactionScopeHandler(builder); - } - - internal static IRequestExecutorBuilder TryAddNoOpTransactionScopeHandler( - this IRequestExecutorBuilder builder) - { - builder.Services.TryAddSingleton(); - - return ConfigureSchemaServices( - builder, - static services => - services.TryAddSingleton()); - } -} diff --git a/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs index 3e8fd18f4e5..ede52a9b255 100644 --- a/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs +++ b/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs @@ -138,7 +138,6 @@ private static DefaultRequestExecutorBuilder CreateBuilder( { var builder = new DefaultRequestExecutorBuilder(services, schemaName); - builder.TryAddNoOpTransactionScopeHandler(); builder.TryAddTypeInterceptor(); builder.TryAddTypeInterceptor(); diff --git a/src/HotChocolate/Core/src/Types/Execution/NeedsFormatting.cs b/src/HotChocolate/Core/src/Types/Execution/NeedsFormatting.cs index 59d5f97cd61..bc2692cbbcb 100644 --- a/src/HotChocolate/Core/src/Types/Execution/NeedsFormatting.cs +++ b/src/HotChocolate/Core/src/Types/Execution/NeedsFormatting.cs @@ -26,19 +26,14 @@ internal abstract class NeedsFormatting : IResultDataJsonFormatter /// /// The JSON serializer options. /// - /// - /// The null ignore condition. - /// public abstract void FormatValue( JsonWriter writer, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition); + JsonSerializerOptions options); void IResultDataJsonFormatter.WriteTo( JsonWriter writer, - JsonSerializerOptions? options, - JsonNullIgnoreCondition nullIgnoreCondition) - => FormatValue(writer, options ?? JsonSerializerOptionDefaults.GraphQL, nullIgnoreCondition); + JsonSerializerOptions? options) + => FormatValue(writer, options ?? JsonSerializerOptionDefaults.GraphQL); public static JsonNeedsFormatting Create(TValue value) { @@ -80,14 +75,10 @@ internal sealed class JsonNeedsFormatting(JsonDocument value) : NeedsFormatting /// /// The JSON serializer options. /// - /// - /// The null ignore condition. - /// public override void FormatValue( JsonWriter writer, - JsonSerializerOptions options, - JsonNullIgnoreCondition nullIgnoreCondition) - => JsonValueFormatter.WriteValue(writer, Value, options, nullIgnoreCondition); + JsonSerializerOptions options) + => JsonValueFormatter.WriteValue(writer, Value, options); /// /// Returns the string representation of the inner value. diff --git a/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs b/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs index da39cc93d89..6f7306d8cf5 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Pipeline/OperationExecutionMiddleware.cs @@ -17,7 +17,6 @@ internal sealed class OperationExecutionMiddleware private readonly IFactory _contextFactory; private readonly QueryExecutor _queryExecutor; private readonly SubscriptionExecutor _subscriptionExecutor; - private readonly ITransactionScopeHandler _transactionScopeHandler; private readonly IExecutionDiagnosticEvents _diagnosticEvents; private object? _cachedQuery; private object? _cachedMutation; @@ -27,20 +26,17 @@ private OperationExecutionMiddleware( IFactory contextFactory, QueryExecutor queryExecutor, SubscriptionExecutor subscriptionExecutor, - ITransactionScopeHandler transactionScopeHandler, IExecutionDiagnosticEvents diagnosticEvents) { ArgumentNullException.ThrowIfNull(next); ArgumentNullException.ThrowIfNull(contextFactory); ArgumentNullException.ThrowIfNull(queryExecutor); ArgumentNullException.ThrowIfNull(subscriptionExecutor); - ArgumentNullException.ThrowIfNull(transactionScopeHandler); _next = next; _contextFactory = contextFactory; _queryExecutor = queryExecutor; _subscriptionExecutor = subscriptionExecutor; - _transactionScopeHandler = transactionScopeHandler; _diagnosticEvents = diagnosticEvents; } @@ -69,9 +65,21 @@ public async ValueTask InvokeAsync( using (_diagnosticEvents.ExecuteOperation(context)) { - if (context.VariableValues.Length is 0 or 1) + if (operation.Definition.Operation is OperationType.Subscription) { - await ExecuteOperationRequestAsync(context, batchDispatcher, operation).ConfigureAwait(false); + context.Result = await _subscriptionExecutor + .ExecuteAsync(context, () => GetQueryRootValue(context)) + .ConfigureAwait(false); + } + else if (context.VariableValues.Length is 0 or 1) + { + context.Result = + await ExecuteQueryOrMutationAsync( + context, + batchDispatcher, + operation, + context.VariableValues[0]) + .ConfigureAwait(false); } else { @@ -87,59 +95,16 @@ public async ValueTask InvokeAsync( } } - private async Task ExecuteOperationRequestAsync( - RequestContext context, - IBatchDispatcher batchDispatcher, - Operation operation) - { - if (operation.Definition.Operation is OperationType.Subscription) - { - context.Result = await _subscriptionExecutor - .ExecuteAsync(context, () => GetQueryRootValue(context)) - .ConfigureAwait(false); - } - else - { - context.Result = - await ExecuteQueryOrMutationAsync( - context, - batchDispatcher, - operation, - context.VariableValues[0]) - .ConfigureAwait(false); - } - } - private async Task ExecuteVariableBatchRequestAsync( RequestContext context, IBatchDispatcher batchDispatcher, Operation operation) - { - if (operation.Definition.Operation is OperationType.Query) - { - await ExecuteVariableBatchRequestOptimizedAsync(context, batchDispatcher, operation); - return; - } - - var variableSet = context.VariableValues; - var tasks = new Task[variableSet.Length]; - - for (var i = 0; i < variableSet.Length; i++) - { - tasks[i] = ExecuteQueryOrMutationNoStreamAsync(context, batchDispatcher, operation, variableSet[i], i); - } - - var results = await Task.WhenAll(tasks).ConfigureAwait(false); - context.Result = new OperationResultBatch([.. results]); - } - - private async Task ExecuteVariableBatchRequestOptimizedAsync( - RequestContext context, - IBatchDispatcher batchDispatcher, - Operation operation) { var variableSets = context.VariableValues; - var query = GetQueryRootValue(context); + var queryRoot = GetQueryRootValue(context); + var rootValue = operation.Definition.Operation is OperationType.Mutation + ? GetMutationRootValue(context) + : queryRoot; var operationContextBuffer = ArrayPool.Shared.Rent(variableSets.Length); var resultBuffer = ArrayPool.Shared.Rent(variableSets.Length); @@ -149,7 +114,8 @@ private async Task ExecuteVariableBatchRequestOptimizedAsync( context, batchDispatcher, operation, - query, + rootValue, + queryRoot, operationContextBuffer.AsSpan(0, variableSets.Length), variableSets[variableIndex], variableIndex, @@ -159,8 +125,9 @@ private async Task ExecuteVariableBatchRequestOptimizedAsync( try { await _queryExecutor.ExecuteBatchAsync( - operationContextBuffer.AsMemory(0, variableSets.Length), - resultBuffer.AsMemory(0, variableSets.Length)); + operationContextBuffer, + resultBuffer, + variableSets.Length); context.Result = new OperationResultBatch([.. resultBuffer.AsSpan(0, variableSets.Length)]); } @@ -182,7 +149,8 @@ static void Initialize( RequestContext context, IBatchDispatcher batchDispatcher, Operation operation, - object? query, + object? rootValue, + object? queryRoot, Span operationContexts, IVariableValueCollection variables, int variableIndex, @@ -197,8 +165,8 @@ static void Initialize( batchDispatcher, operation, variables, - query, - () => query, + rootValue, + () => queryRoot, variableIndex); operationContexts[variableIndex] = operationContextOwner; @@ -253,25 +221,27 @@ private async Task ExecuteQueryOrMutationAsync( try { - var result = - await ExecuteQueryOrMutationAsync( - context, - batchDispatcher, - operation, - operationContext, - variables) - .ConfigureAwait(false); - - // TODO : DEFER - // if (operationContext.DeferredScheduler.HasResults) - // { - // var results = operationContext.DeferredScheduler.CreateResultStream(result); - // var responseStream = new ResponseStream(() => results, ExecutionResultKind.DeferredResult); - // responseStream.RegisterForCleanup(result); - // responseStream.RegisterForCleanup(operationContextOwner); - // operationContextOwner = null; - // return responseStream; - // } + var queryRoot = GetQueryRootValue(context); + var rootValue = operation.Definition.Operation is OperationType.Mutation + ? GetMutationRootValue(context) + : queryRoot; + + operationContext.Initialize( + context, + context.RequestServices, + batchDispatcher, + operation, + variables, + rootValue, + () => queryRoot); + + var result = await _queryExecutor.ExecuteAsync(operationContext).ConfigureAwait(false); + + if (result.IsStreamResult()) + { + result.RegisterForCleanup(operationContextOwner); + operationContextOwner = null; + } return result; } @@ -290,109 +260,37 @@ await ExecuteQueryOrMutationAsync( } } - private async Task ExecuteQueryOrMutationNoStreamAsync( - RequestContext context, - IBatchDispatcher batchDispatcher, - Operation operation, - IVariableValueCollection variables, - int variableIndex) + private object? GetQueryRootValue(RequestContext context) { - var operationContextOwner = _contextFactory.Create(); - var operationContext = operationContextOwner.OperationContext; + var queryType = context.Schema.QueryType; - try + if (queryType is null) { - return await ExecuteQueryOrMutationAsync( - context, - batchDispatcher, - operation, - operationContext, - variables, - variableIndex) - .ConfigureAwait(false); + return null; } - catch (OperationCanceledException) - { - // if an operation is canceled we will abandon the rented operation context - // to ensure that the abandoned tasks do not leak into new operations. - operationContextOwner = null; - // we rethrow so that another middleware can deal with the cancellation. - throw; - } - finally - { - operationContextOwner?.Dispose(); - } + return RootValueResolver.Resolve( + context, + context.RequestServices, + Unsafe.As(queryType), + ref _cachedQuery); } - private async Task ExecuteQueryOrMutationAsync( - RequestContext context, - IBatchDispatcher batchDispatcher, - Operation operation, - OperationContext operationContext, - IVariableValueCollection variables, - int variableIndex = -1) + private object? GetMutationRootValue(RequestContext context) { - if (operation.Definition.Operation is OperationType.Query) - { - var query = GetQueryRootValue(context); - - operationContext.Initialize( - context, - context.RequestServices, - batchDispatcher, - operation, - variables, - query, - () => query, - variableIndex); + var mutationType = context.Schema.MutationType; - return await _queryExecutor.ExecuteAsync(operationContext).ConfigureAwait(false); - } - - if (operation.Definition.Operation is OperationType.Mutation) + if (mutationType is null) { - using var transactionScope = _transactionScopeHandler.Create(context); - - var mutation = GetMutationRootValue(context); - - operationContext.Initialize( - context, - context.RequestServices, - batchDispatcher, - operation, - variables, - mutation, - () => GetQueryRootValue(context), - variableIndex); - - var result = await _queryExecutor.ExecuteAsync(operationContext).ConfigureAwait(false); - - // we capture the result here so that we can capture it in the transaction scope. - context.Result = result; - - // we complete the transaction scope and are done. - transactionScope.Complete(); - return result; + return null; } - throw new InvalidOperationException(); - } - - private object? GetQueryRootValue(RequestContext context) - => RootValueResolver.Resolve( - context, - context.RequestServices, - Unsafe.As(context.Schema.QueryType), - ref _cachedQuery); - - private object? GetMutationRootValue(RequestContext context) - => RootValueResolver.Resolve( + return RootValueResolver.Resolve( context, context.RequestServices, - Unsafe.As(context.Schema.MutationType)!, + Unsafe.As(ref mutationType), ref _cachedMutation); + } private static bool IsOperationAllowed(Operation operation, IOperationRequest request) { @@ -409,11 +307,10 @@ private static bool IsOperationAllowed(Operation operation, IOperationRequest re _ => true }; - // TODO : DEFER - // if (allowed && operation.HasIncrementalParts) - // { - // return allowed && (request.Flags & AllowStreams) == AllowStreams; - // } + if (allowed && operation.HasIncrementalParts) + { + return (request.Flags & AllowStreams) == AllowStreams; + } return allowed; } @@ -424,9 +321,7 @@ private static bool IsRequestTypeAllowed( { if (variables is { Count: > 1 }) { - // TODO : DEFER return operation.Definition.Operation is not OperationType.Subscription; - // && !operation.HasIncrementalParts; } return true; @@ -439,15 +334,12 @@ public static RequestMiddlewareConfiguration Create() var contextFactory = factoryContext.Services.GetRequiredService>(); var queryExecutor = factoryContext.SchemaServices.GetRequiredService(); var subscriptionExecutor = factoryContext.SchemaServices.GetRequiredService(); - var transactionScopeHandler = - factoryContext.SchemaServices.GetRequiredService(); var diagnosticEvents = factoryContext.SchemaServices.GetRequiredService(); var middleware = new OperationExecutionMiddleware( next, contextFactory, queryExecutor, subscriptionExecutor, - transactionScopeHandler, diagnosticEvents); return async context => diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/BranchTracker.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/BranchTracker.cs new file mode 100644 index 00000000000..b5d543a7dee --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/BranchTracker.cs @@ -0,0 +1,13 @@ +namespace HotChocolate.Execution.Processing; + +internal sealed class BranchTracker +{ + private int _nextId; + + public const int SystemBranchId = -1; + + public int CreateNewBranchId() + => Interlocked.Increment(ref _nextId); + + public void Reset() => _nextId = 0; +} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DefaultTransactionScope.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DefaultTransactionScope.cs deleted file mode 100644 index a9d6aed608e..00000000000 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/DefaultTransactionScope.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Transactions; - -namespace HotChocolate.Execution.Processing; - -/// -/// Represents the default mutation transaction scope implementation. -/// -public class DefaultTransactionScope : ITransactionScope -{ - /// - /// Initializes a new instance of . - /// - /// - /// The GraphQL request context. - /// - /// - /// The mutation transaction scope. - /// - public DefaultTransactionScope(RequestContext context, TransactionScope transaction) - { - ArgumentNullException.ThrowIfNull(context); - ArgumentNullException.ThrowIfNull(transaction); - - Context = context; - Transaction = transaction; - } - - /// - /// Gets GraphQL request context. - /// - protected RequestContext Context { get; } - - /// - /// Gets the mutation transaction scope. - /// - protected TransactionScope Transaction { get; } - - /// - /// Completes a transaction (commits or discards the changes). - /// - public void Complete() - { - if (Context.Result is OperationResult { Data: not null, Errors: null or { Count: 0 } }) - { - Transaction.Complete(); - } - } - - /// - /// Performs application-defined tasks associated with freeing, - /// releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - Transaction.Dispose(); - } -} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DefaultTransactionScopeHandler.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DefaultTransactionScopeHandler.cs deleted file mode 100644 index 01355a09534..00000000000 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/DefaultTransactionScopeHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Transactions; - -namespace HotChocolate.Execution.Processing; - -/// -/// Represents the default mutation transaction scope handler implementation. -/// -public class DefaultTransactionScopeHandler : ITransactionScopeHandler -{ - /// - /// Creates a new transaction scope for the current - /// request represented by the . - /// - /// - /// The GraphQL request context. - /// - /// - /// Returns a new . - /// - public virtual ITransactionScope Create(RequestContext context) - { - return new DefaultTransactionScope( - context, - new TransactionScope( - TransactionScopeOption.Required, - new TransactionOptions - { - IsolationLevel = IsolationLevel.ReadCommitted - }, - TransactionScopeAsyncFlowOption.Enabled)); - } -} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferCondition.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferCondition.cs new file mode 100644 index 00000000000..3458d6cf4ce --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferCondition.cs @@ -0,0 +1,104 @@ +using System.Diagnostics.CodeAnalysis; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Execution.Processing; + +internal readonly struct DeferCondition(string? ifVariableName) : IEquatable +{ + public string? IfVariableName => ifVariableName; + + public bool IsDeferred(IVariableValueCollection variableValues) + { + if (ifVariableName is not null) + { + if (!variableValues.TryGetValue(ifVariableName, out var value)) + { + throw new InvalidOperationException($"The variable {ifVariableName} has an invalid value."); + } + + if (!value.Value) + { + return false; + } + } + + return true; + } + + public bool Equals(DeferCondition other) + => string.Equals(ifVariableName, other.IfVariableName, StringComparison.Ordinal); + + public override bool Equals([NotNullWhen(true)] object? obj) + => obj is DeferCondition other && Equals(other); + + public override int GetHashCode() + => HashCode.Combine(ifVariableName); + + public static bool TryCreate(InlineFragmentNode inlineFragment, out DeferCondition deferCondition) + => TryCreate(inlineFragment.Directives, out deferCondition); + + public static bool TryCreate(FragmentSpreadNode fragmentSpread, out DeferCondition deferCondition) + => TryCreate(fragmentSpread.Directives, out deferCondition); + + private static bool TryCreate(IReadOnlyList directives, out DeferCondition deferCondition) + { + if (directives.Count == 0) + { + deferCondition = default; + return false; + } + + for (var i = 0; i < directives.Count; i++) + { + var directive = directives[i]; + + if (!directive.Name.Value.Equals(DirectiveNames.Defer.Name, StringComparison.Ordinal)) + { + continue; + } + + // @defer with no arguments is unconditionally deferred. + if (directive.Arguments.Count == 0) + { + deferCondition = new DeferCondition(null); + return true; + } + + for (var j = 0; j < directive.Arguments.Count; j++) + { + var argument = directive.Arguments[j]; + + if (!argument.Name.Value.Equals(DirectiveNames.Defer.Arguments.If, StringComparison.Ordinal)) + { + continue; + } + + switch (argument.Value) + { + // @defer(if: $variable) - conditionally deferred at runtime. + case VariableNode variable: + deferCondition = new DeferCondition(variable.Name.Value); + return true; + + // @defer(if: true) - unconditionally deferred. + case BooleanValueNode { Value: true }: + deferCondition = new DeferCondition(null); + return true; + + // @defer(if: false) - statically not deferred, no condition needed. + case BooleanValueNode { Value: false }: + deferCondition = default; + return false; + } + } + + // @defer directive found but no `if` argument matched - unconditionally deferred. + deferCondition = new DeferCondition(null); + return true; + } + + deferCondition = default; + return false; + } +} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferConditionCollection.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferConditionCollection.cs new file mode 100644 index 00000000000..4d1a17d9655 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferConditionCollection.cs @@ -0,0 +1,50 @@ +using System.Collections; + +namespace HotChocolate.Execution.Processing; + +internal sealed class DeferConditionCollection : ICollection +{ + private readonly OrderedDictionary _dictionary = []; + + public DeferCondition this[int index] + => _dictionary.GetAt(index).Key; + + public int Count => _dictionary.Count; + + public bool IsReadOnly => false; + + public bool Add(DeferCondition item) + { + if (_dictionary.Count == 64) + { + throw new InvalidOperationException( + "The maximum number of defer conditions has been reached."); + } + + return _dictionary.TryAdd(item, _dictionary.Count); + } + + void ICollection.Add(DeferCondition item) + => Add(item); + + public bool Remove(DeferCondition item) + => throw new InvalidOperationException("This is an add only collection."); + + void ICollection.Clear() + => throw new InvalidOperationException("This is an add only collection."); + + public bool Contains(DeferCondition item) + => _dictionary.ContainsKey(item); + + public int IndexOf(DeferCondition item) + => _dictionary.GetValueOrDefault(item, -1); + + public void CopyTo(DeferCondition[] array, int arrayIndex) + => _dictionary.Keys.CopyTo(array, arrayIndex); + + public IEnumerator GetEnumerator() + => _dictionary.Keys.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); +} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.Pooling.cs new file mode 100644 index 00000000000..a8a7c45e778 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.Pooling.cs @@ -0,0 +1,58 @@ +using System.Diagnostics; + +namespace HotChocolate.Execution.Processing; + +internal sealed partial class DeferExecutionCoordinator +{ +#if DEBUG + private bool _isInitialized; +#endif + + /// + /// Initializes the coordinator for a new execution cycle. + /// Must be called before any other operations when leased from a pool. + /// + public void Initialize(BranchTracker branchTracker, int mainBranchId) + { + Debug.Assert(branchTracker is not null); + Debug.Assert(mainBranchId > 0); + + _branchTracker = branchTracker; + _mainBranchId = mainBranchId; + +#if DEBUG + _isInitialized = true; +#endif + } + + /// + /// Resets the coordinator to its initial state so it can be reused. + /// + public void Reset() + { + _branchIdLookup.Clear(); + _branchLookup.Clear(); + _mainBranchChildren?.Clear(); + _completed.Clear(); + _delivered.Clear(); + _results.Clear(); + _branchTracker = null!; + _pendingBuilder = null; + _incrementalBuilder = null; + _completedBuilder = null; + _processQueue = null; + _hasBranches = false; + _isComplete = false; + _mainBranchId = 0; + _pendingBranches = 0; + +#if DEBUG + _isInitialized = false; +#endif + + if (_results.Capacity > 64) + { + _results.Capacity = 64; + } + } +} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs new file mode 100644 index 00000000000..ec36cf2dbec --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferExecutionCoordinator.cs @@ -0,0 +1,307 @@ +using System.Collections.Immutable; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using HotChocolate.Fetching; + +namespace HotChocolate.Execution.Processing; + +internal sealed partial class DeferExecutionCoordinator +{ + private readonly object _sync = new(); + private readonly Dictionary _branchIdLookup = []; + private readonly Dictionary _branchLookup = []; + private HashSet? _mainBranchChildren; + private readonly Dictionary _completed = []; + private readonly HashSet _delivered = []; + private readonly List _results = []; + private readonly AsyncAutoResetEvent _signal = new(); + private BranchTracker _branchTracker = null!; + private int _mainBranchId; + private ImmutableList.Builder? _pendingBuilder; + private ImmutableList.Builder? _incrementalBuilder; + private ImmutableList.Builder? _completedBuilder; + private Queue? _processQueue; + private volatile bool _hasBranches; + private volatile bool _isComplete; + private int _pendingBranches; + +#pragma warning disable IDE0052 // Remove unread private members + private static int s_nextId; + private readonly int _id; +#pragma warning restore IDE0052 // Remove unread private members + + public DeferExecutionCoordinator() + { + _id = Interlocked.Increment(ref s_nextId); + } + + /// + /// Gets whether any deferred execution branches have been registered. + /// + public bool HasBranches => _hasBranches; + + /// + /// Registers a new deferred execution branch for the specified + /// and , returning a unique branch identifier. + /// If the branch was already registered, the existing identifier is returned. + /// + public int Branch(int currentBranchId, Path path, DeferUsage deferUsage) + { + Debug.Assert(_isInitialized); + + var key = new DeferredBranchKey(path, deferUsage, currentBranchId); + + lock (_sync) + { + if (!_branchIdLookup.TryGetValue(key, out var newBranchId)) + { + newBranchId = _branchTracker.CreateNewBranchId(); + GetChildrenUnsafe(currentBranchId).Add(newBranchId); + _branchLookup.Add(newBranchId, new DeferredBranch(path, deferUsage, currentBranchId)); + _branchIdLookup.Add(key, newBranchId); + _hasBranches = true; + _pendingBranches++; + } + + return newBranchId; + } + } + + /// + /// Enqueues the initial (non-deferred) result for delivery. + /// Any already-completed child branches are folded in as incremental data. + /// + public void EnqueueResult(OperationResult result) + { + Debug.Assert(_isInitialized); + + lock (_sync) + { + ComposeAndDeliverUnsafe(_mainBranchId, result); + } + } + + /// + /// Enqueues a deferred result for the specified branch. + /// If the parent branch has already been delivered, the result is composed + /// and delivered immediately; otherwise it is stored until the parent is delivered. + /// + public void EnqueueResult(OperationResult result, int branchId) + { + Debug.Assert(_isInitialized); + + lock (_sync) + { + _completed[branchId] = result; + + if (IsParentDeliveredUnsafe(branchId) + && _completed.Remove(branchId, out var readyResult)) + { + ComposeAndDeliverUnsafe(branchId, readyResult); + } + } + } + + /// + /// Returns an async stream of composed operation results in delivery order. + /// The stream completes automatically when all branches have been delivered. + /// + public async IAsyncEnumerable ReadResultsAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + Debug.Assert(_isInitialized); + + List? snapshot = null; + await using var registration = cancellationToken.Register(_signal.Set); + + while (!cancellationToken.IsCancellationRequested) + { + await _signal; + + cancellationToken.ThrowIfCancellationRequested(); + + lock (_sync) + { + snapshot ??= []; + snapshot.Clear(); + snapshot.AddRange(_results); + _results.Clear(); + } + + foreach (var result in snapshot) + { + yield return result; + } + + if (_isComplete) + { + yield break; + } + } + } + + private void ComposeAndDeliverUnsafe(int branchId, OperationResult result) + { + var children = GetChildrenUnsafe(branchId); + + if (children.Count > 0) + { + var pendingBuilder = _pendingBuilder ??= ImmutableList.CreateBuilder(); + var incrementalBuilder = _incrementalBuilder ??= ImmutableList.CreateBuilder(); + var completedBuilder = _completedBuilder ??= ImmutableList.CreateBuilder(); + var processQueue = _processQueue ??= new Queue(); + + pendingBuilder.Clear(); + incrementalBuilder.Clear(); + completedBuilder.Clear(); + processQueue.Clear(); + + foreach (var childId in children) + { + var child = _branchLookup[childId]; + + pendingBuilder.Add( + new PendingResult( + childId, + child.Path, + child.Group.Label)); + + if (_completed.Remove(childId, out var childResult)) + { + result.RegisterForCleanup(childResult); + AddCompletedBranch(childId, childResult, incrementalBuilder, completedBuilder); + _delivered.Add(childId); + _pendingBranches--; + processQueue.Enqueue(childId); + } + } + + while (processQueue.TryDequeue(out var parentId)) + { + foreach (var grandchildId in GetChildrenUnsafe(parentId)) + { + var branch = _branchLookup[grandchildId]; + + pendingBuilder.Add( + new PendingResult( + grandchildId, + branch.Path, + branch.Group.Label)); + + if (_completed.Remove(grandchildId, out var gcResult)) + { + result.RegisterForCleanup(gcResult); + AddCompletedBranch(grandchildId, gcResult, incrementalBuilder, completedBuilder); + _delivered.Add(grandchildId); + _pendingBranches--; + processQueue.Enqueue(grandchildId); + } + } + } + + result.Pending = pendingBuilder.ToImmutable(); + result.Incremental = incrementalBuilder.ToImmutable(); + result.Completed = completedBuilder.ToImmutable(); + } + + // For deferred branches (not main branch), transform the result's data into an incremental result. + // Per spec: only the initial payload has root "data"; subsequent payloads use "incremental" array. + if (branchId != _mainBranchId) + { + var incrementalBuilder = _incrementalBuilder ??= ImmutableList.CreateBuilder(); + var completedBuilder = _completedBuilder ??= ImmutableList.CreateBuilder(); + + if (children.Count == 0) + { + incrementalBuilder.Clear(); + completedBuilder.Clear(); + } + + AddCompletedBranch(branchId, result, incrementalBuilder, completedBuilder); + + result.Incremental = incrementalBuilder.ToImmutable(); + result.Completed = completedBuilder.ToImmutable(); + result.Data = null; + result.Errors = []; + } + + _delivered.Add(branchId); + + if (branchId != _mainBranchId) + { + _pendingBranches--; + } + + var isComplete = _delivered.Contains(_mainBranchId) && _pendingBranches == 0; + result.HasNext = !isComplete; + + _results.Add(result); + _isComplete = isComplete; + _signal.Set(); + } + + /// + /// Determines whether the parent of the specified branch has already + /// delivered its result to the response stream. + /// + private bool IsParentDeliveredUnsafe(int branchId) + => _branchLookup.TryGetValue(branchId, out var branch) + && _delivered.Contains(branch.ParentBranchId); + + private static void AddCompletedBranch( + int branchId, + OperationResult branchResult, + ImmutableList.Builder incrementalBuilder, + ImmutableList.Builder completedBuilder) + { + if (branchResult.Data.HasValue && !branchResult.Data.Value.IsValueNull) + { + // data is valid (possibly with contained errors) — deliver incremental data + incrementalBuilder.Add( + new IncrementalObjectResult( + branchId, + branchResult.Errors, + subPath: null, + branchResult.Data)); + completedBuilder.Add(new CompletedResult(branchId)); + } + else + { + // errors bubbled above the incremental result's path — no data to deliver + completedBuilder.Add(new CompletedResult(branchId, branchResult.Errors)); + } + } + + /// + /// Gets the child branches for the specified branch. + /// For the main branch, uses the dedicated field; for deferred branches, + /// uses the children set stored in the branch lookup. + /// + private HashSet GetChildrenUnsafe(int branchId) + { + if (branchId == _mainBranchId) + { + return _mainBranchChildren ??= []; + } + + ref var branch = ref CollectionsMarshal.GetValueRefOrNullRef(_branchLookup, branchId); + + if (Unsafe.IsNullRef(ref branch)) + { + return []; + } + + return branch.Children ??= []; + } + + private readonly record struct DeferredBranchKey(Path Path, DeferUsage Group, int ParentBranchId); + + private struct DeferredBranch(Path path, DeferUsage group, int parentBranchId) + { + public Path Path { get; } = path; + public DeferUsage Group { get; } = group; + public int ParentBranchId { get; } = parentBranchId; + public HashSet? Children { get; set; } + } +} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/DeferUsage.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferUsage.cs new file mode 100644 index 00000000000..b29e99e557a --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/DeferUsage.cs @@ -0,0 +1,23 @@ +namespace HotChocolate.Execution.Processing; + +/// +/// Represents a usage of the @defer directive encountered during operation compilation. +/// Forms a parent chain to model nested defer scopes. +/// +/// +/// The optional label from @defer(label: "..."), used to identify the deferred +/// payload in the incremental delivery response. +/// +/// +/// The parent defer usage when this @defer is nested inside another deferred fragment, +/// or null if this is a top-level defer. +/// +/// +/// The index into the for the if condition +/// associated with this defer directive. This index maps to a bit position in the +/// runtime defer flags bitmask. +/// +public sealed record DeferUsage( + string? Label, + DeferUsage? Parent, + byte DeferConditionIndex); diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/FieldSelectionNode.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/FieldSelectionNode.cs index f5964f13f64..a4f31e1b859 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/FieldSelectionNode.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/FieldSelectionNode.cs @@ -3,7 +3,7 @@ namespace HotChocolate.Execution.Processing; /// -/// Represents a field selection node with its path include flags. +/// Represents a field selection node with its path include flags and defer usage. /// /// /// The syntax node that represents the field selection. @@ -11,4 +11,11 @@ namespace HotChocolate.Execution.Processing; /// /// The flags that must be all set for this selection to be included. /// -public sealed record FieldSelectionNode(FieldNode Node, ulong PathIncludeFlags); +/// +/// The defer usage context this field was collected under, or null if the field +/// is not inside a deferred fragment. +/// +public sealed record FieldSelectionNode( + FieldNode Node, + ulong PathIncludeFlags, + DeferUsage? DeferUsage = null); diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/ITransactionScope.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/ITransactionScope.cs deleted file mode 100644 index cc8690c559f..00000000000 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/ITransactionScope.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace HotChocolate.Execution.Processing; - -/// -/// Represents a mutation transaction scope. -/// -public interface ITransactionScope : IDisposable -{ - /// - /// Completes a transaction (commits or discards the changes). - /// - void Complete(); -} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/ITransactionScopeHandler.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/ITransactionScopeHandler.cs deleted file mode 100644 index 85b87cd9435..00000000000 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/ITransactionScopeHandler.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace HotChocolate.Execution.Processing; - -/// -/// Allows to make mutation execution transactional. -/// -public interface ITransactionScopeHandler -{ - /// - /// Creates a new transaction scope for the current - /// request represented by the . - /// - /// - /// The GraphQL request context. - /// - /// - /// Returns a new . - /// - ITransactionScope Create(RequestContext context); -} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/IncludeConditionCollection.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/IncludeConditionCollection.cs index f9cbbdd292f..0ab3f5e8527 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/IncludeConditionCollection.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/IncludeConditionCollection.cs @@ -2,7 +2,7 @@ namespace HotChocolate.Execution.Processing; -internal class IncludeConditionCollection : ICollection +internal sealed class IncludeConditionCollection : ICollection { private readonly OrderedDictionary _dictionary = []; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Global.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Global.cs index 4cf9057205f..51acff2efb4 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Global.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Global.cs @@ -27,6 +27,8 @@ public IServiceProvider Services public Operation Operation => _operationContext.Operation; + public DeferUsage? DeferUsage { get; private set; } + public IOperationResultBuilder OperationResult => _operationResultBuilder; public IDictionary ContextData => _operationContext.ContextData; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pooling.cs index 49bcf648d8f..afe58a02392 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pooling.cs @@ -22,20 +22,20 @@ public void Initialize( Selection selection, ResultElement resultValue, OperationContext operationContext, - IImmutableDictionary scopedContextData, - Path? path) + DeferUsage? deferUsage, + IImmutableDictionary scopedContextData) { _operationContext = operationContext; _operationResultBuilder.Context = _operationContext; _services = operationContext.Services; _selection = selection; - _path = path; ResultValue = resultValue; _parent = parent; _parser = operationContext.InputParser; ScopedContextData = scopedContextData; LocalContextData = s_emptyLocalContextData; Arguments = _selection.Arguments; + DeferUsage = deferUsage; RequestAborted = _operationContext.RequestAborted; } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pure.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pure.cs index a87acc09772..baab85de8fc 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pure.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/MiddlewareContext.Pure.cs @@ -120,16 +120,30 @@ public bool HasErrors => parentContext.ContextData; public T Parent() - => _parent switch + { + if (_parent is T casted) { - T casted => casted, - null => default!, - _ => throw ResolverContext_CannotCastParent( - Selection.Field.Coordinate, - Path, - typeof(T), - _parent.GetType()) - }; + return casted; + } + + if (_parent is null) + { + return default!; + } + + _typeConverter ??= parentContext._operationContext.Converter; + + if (_typeConverter.TryConvert(_parent, out casted)) + { + return casted; + } + + throw ResolverContext_CannotCastParent( + Selection.Field.Coordinate, + Path, + typeof(T), + _parent.GetType()); + } public T ArgumentValue(string name) { @@ -235,9 +249,7 @@ private T CoerceArgumentValue(ArgumentValue argument) return default!; } - _typeConverter ??= - parentContext.Services.GetService() ?? - DefaultTypeConverter.Default; + _typeConverter ??= parentContext._operationContext.Converter; if (value is T castedValue || _typeConverter.TryConvert(value, out castedValue, out var conversionException)) diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/NoOpTransactionScope.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/NoOpTransactionScope.cs deleted file mode 100644 index 8d0a783ca05..00000000000 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/NoOpTransactionScope.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace HotChocolate.Execution.Processing; - -/// -/// This transaction scope represents a non transactional mutation transaction scope. -/// -internal sealed class NoOpTransactionScope : ITransactionScope -{ - public void Complete() - { - } - - public void Dispose() - { - } -} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/NoOpTransactionScopeHandler.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/NoOpTransactionScopeHandler.cs deleted file mode 100644 index 5080c0bc943..00000000000 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/NoOpTransactionScopeHandler.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace HotChocolate.Execution.Processing; - -/// -/// This transaction scope handler represents creates -/// a non transactional mutation transaction scope. -/// -internal sealed class NoOpTransactionScopeHandler : ITransactionScopeHandler -{ - private readonly NoOpTransactionScope _noOpTransaction = new(); - - public ITransactionScope Create(RequestContext context) => _noOpTransaction; -} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Operation.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Operation.cs index 75b017cfd62..e638f9a283e 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Operation.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Operation.cs @@ -18,6 +18,7 @@ public sealed class Operation : IOperation private readonly ConcurrentDictionary<(int, string), SelectionSet> _selectionSets = []; private readonly OperationCompiler _compiler; private readonly IncludeConditionCollection _includeConditions; + private readonly DeferConditionCollection _deferConditions; private readonly OperationFeatureCollection _features; private object[] _elementsById; private int _lastId; @@ -32,9 +33,11 @@ internal Operation( SelectionSet rootSelectionSet, OperationCompiler compiler, IncludeConditionCollection includeConditions, + DeferConditionCollection deferConditions, OperationFeatureCollection features, int lastId, - object[] elementsById) + object[] elementsById, + bool hasIncrementalParts) { ArgumentException.ThrowIfNullOrWhiteSpace(id); ArgumentException.ThrowIfNullOrWhiteSpace(hash); @@ -45,6 +48,7 @@ internal Operation( ArgumentNullException.ThrowIfNull(rootSelectionSet); ArgumentNullException.ThrowIfNull(compiler); ArgumentNullException.ThrowIfNull(includeConditions); + ArgumentNullException.ThrowIfNull(deferConditions); ArgumentNullException.ThrowIfNull(elementsById); Id = id; @@ -56,9 +60,11 @@ internal Operation( RootSelectionSet = rootSelectionSet; _compiler = compiler; _includeConditions = includeConditions; + _deferConditions = deferConditions; _lastId = lastId; _elementsById = elementsById; _features = features; + HasIncrementalParts = hasIncrementalParts; } /// @@ -118,6 +124,9 @@ ISelectionSet IOperation.RootSelectionSet IFeatureCollection IFeatureProvider.Features => Features; + /// + public bool HasIncrementalParts { get; } + /// /// Gets the selection set for the specified /// if the selections named return type is an object type. @@ -183,6 +192,7 @@ public SelectionSet GetSelectionSet(Selection selection, IObjectTypeDefinition t selection, objectType, _includeConditions, + _deferConditions, ref _elementsById, ref _lastId); _selectionSets.TryAdd(key, selectionSet); @@ -247,13 +257,33 @@ public ulong CreateIncludeFlags(IVariableValueCollection variables) { if (includeCondition.IsIncluded(variables)) { - includeFlags |= 1ul << index++; + includeFlags |= 1ul << index; } + + index++; } return includeFlags; } + public ulong CreateDeferFlags(IVariableValueCollection variables) + { + var index = 0; + var deferFlags = 0ul; + + foreach (var deferCondition in _deferConditions) + { + if (deferCondition.IsDeferred(variables)) + { + deferFlags |= 1ul << index; + } + + index++; + } + + return deferFlags; + } + internal Selection GetSelectionById(int id) => Unsafe.As(Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_elementsById), id)); diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationCompiler.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationCompiler.cs index 1209e28d5c0..7e8fbef5906 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationCompiler.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationCompiler.cs @@ -43,7 +43,7 @@ public static Operation Compile( DocumentNode document, Schema schema, IFeatureProvider? context = null) - => Compile(id, id, null, document, schema); + => Compile(id, id, null, document, schema, context); public static Operation Compile( string id, @@ -51,7 +51,7 @@ public static Operation Compile( DocumentNode document, Schema schema, IFeatureProvider? context = null) - => Compile(id, id, operationName, document, schema); + => Compile(id, id, operationName, document, schema, context); public static Operation Compile( string id, @@ -79,11 +79,14 @@ public Operation Compile( ArgumentNullException.ThrowIfNull(document); // Before we can plan an operation, we must de-fragmentize it and remove static include conditions. - document = _documentRewriter.RewriteDocument(document, operationName); + var result = _documentRewriter.RewriteDocument(document, operationName); + document = result.Document; var operationDefinition = document.GetOperation(operationName); var includeConditions = new IncludeConditionCollection(); + var deferConditions = new DeferConditionCollection(); IncludeConditionVisitor.Instance.Visit(operationDefinition, includeConditions); + DeferConditionVisitor.Instance.Visit(operationDefinition, deferConditions); var fields = _fieldsPool.Get(); var compilationContext = new CompilationContext(s_objectArrayPool.Rent(128)); @@ -99,7 +102,9 @@ public Operation Compile( operationDefinition.SelectionSet.Selections, rootType, fields, - includeConditions); + includeConditions, + deferConditions, + parentDeferUsage: null); var selectionSet = BuildSelectionSet( SelectionPath.Root, @@ -121,9 +126,11 @@ public Operation Compile( selectionSet, compiler: this, includeConditions, + deferConditions, compilationContext.Features, lastId, - compilationContext.ElementsById); + compilationContext.ElementsById, + hasIncrementalParts: result.HasIncrementalParts); selectionSet.Complete(operation); @@ -149,6 +156,7 @@ internal SelectionSet CompileSelectionSet( Selection selection, ObjectType objectType, IncludeConditionCollection includeConditions, + DeferConditionCollection deferConditions, ref object[] elementsById, ref int lastId) { @@ -168,7 +176,9 @@ internal SelectionSet CompileSelectionSet( first.Node.SelectionSet!.Selections, objectType, fields, - includeConditions); + includeConditions, + deferConditions, + parentDeferUsage: first.DeferUsage); if (nodes.Length > 1) { @@ -181,7 +191,9 @@ internal SelectionSet CompileSelectionSet( node.Node.SelectionSet!.Selections, objectType, fields, - includeConditions); + includeConditions, + deferConditions, + parentDeferUsage: nodes[i].DeferUsage); } } @@ -203,7 +215,9 @@ private void CollectFields( IReadOnlyList selections, IObjectTypeDefinition typeContext, OrderedDictionary> fields, - IncludeConditionCollection includeConditions) + IncludeConditionCollection includeConditions, + DeferConditionCollection deferConditions, + DeferUsage? parentDeferUsage) { for (var i = 0; i < selections.Count; i++) { @@ -226,7 +240,7 @@ private void CollectFields( pathIncludeFlags |= 1ul << index; } - nodes.Add(new FieldSelectionNode(fieldNode, pathIncludeFlags)); + nodes.Add(new FieldSelectionNode(fieldNode, pathIncludeFlags, parentDeferUsage)); } else if (selection is InlineFragmentNode inlineFragmentNode && DoesTypeApply(inlineFragmentNode.TypeCondition, typeContext)) @@ -239,12 +253,24 @@ private void CollectFields( pathIncludeFlags |= 1ul << index; } + var newDeferUsage = parentDeferUsage; + + if (DeferCondition.TryCreate(inlineFragmentNode, out var deferCondition)) + { + deferConditions.Add(deferCondition); + var deferIndex = deferConditions.IndexOf(deferCondition); + var label = GetDeferLabel(inlineFragmentNode); + newDeferUsage = new DeferUsage(label, parentDeferUsage, (byte)deferIndex); + } + CollectFields( pathIncludeFlags, inlineFragmentNode.SelectionSet.Selections, typeContext, fields, - includeConditions); + includeConditions, + deferConditions, + newDeferUsage); } } } @@ -260,16 +286,20 @@ private SelectionSet BuildSelectionSet( var i = 0; var selections = new Selection[fieldMap.Count]; var isConditional = false; + var hasDeferredSelections = false; var includeFlags = new List(); + var deferUsages = new List(); var selectionSetId = ++lastId; var alwaysIncluded = false; foreach (var (responseName, nodes) in fieldMap) { includeFlags.Clear(); + deferUsages.Clear(); var first = nodes[0]; var isInternal = IsInternal(first.Node); + var hasNonDeferredNode = first.DeferUsage is null; if (first.PathIncludeFlags == 0) { @@ -280,6 +310,11 @@ private SelectionSet BuildSelectionSet( includeFlags.Add(first.PathIncludeFlags); } + if (first.DeferUsage is not null) + { + deferUsages.Add(first.DeferUsage); + } + if (nodes.Count > 1) { for (var j = 1; j < nodes.Count; j++) @@ -305,6 +340,15 @@ private SelectionSet BuildSelectionSet( includeFlags.Add(next.PathIncludeFlags); } + if (next.DeferUsage is null) + { + hasNonDeferredNode = true; + } + else if (!hasNonDeferredNode) + { + deferUsages.Add(next.DeferUsage); + } + if (isInternal) { isInternal = IsInternal(next.Node); @@ -317,6 +361,39 @@ private SelectionSet BuildSelectionSet( CollapseIncludeFlags(includeFlags); } + // If any field node is not inside a deferred fragment, the selection + // is not deferred — it must be included in the initial response. + DeferUsage[]? finalDeferUsage = null; + ulong deferMask = 0; + + if (!hasNonDeferredNode && deferUsages.Count > 0) + { + // Remove child defer usages when their parent is also in the set. + // A field should be delivered with the outermost (earliest) defer + // that contains it. + for (var j = deferUsages.Count - 1; j >= 0; j--) + { + var parent = deferUsages[j].Parent; + while (parent is not null) + { + if (deferUsages.Contains(parent)) + { + deferUsages.RemoveAt(j); + break; + } + + parent = parent.Parent; + } + } + + finalDeferUsage = deferUsages.ToArray(); + foreach (var usage in deferUsages) + { + deferMask |= 1ul << usage.DeferConditionIndex; + } + hasDeferredSelections = true; + } + if (!typeContext.Fields.TryGetField(first.Node.Name.Value, out var field)) { throw ThrowHelper.FieldDoesNotExistOnType(first.Node, typeContext.Name); @@ -336,10 +413,12 @@ private SelectionSet BuildSelectionSet( field, nodes.ToArray(), includeFlags.Count > 0 ? includeFlags.ToArray() : [], - isInternal, - arguments, - fieldDelegate, - pureFieldDelegate); + deferUsage: finalDeferUsage, + deferMask: deferMask, + isInternal: isInternal, + arguments: arguments, + resolverPipeline: fieldDelegate, + pureResolver: pureFieldDelegate); if (optimizers.Length > 0) { @@ -360,7 +439,7 @@ private SelectionSet BuildSelectionSet( // if there are no optimizers registered for this selection we exit early. if (optimizers.Length == 0) { - return new SelectionSet(selectionSetId, path, typeContext, selections, isConditional); + return new SelectionSet(selectionSetId, path, typeContext, selections, isConditional, hasDeferredSelections); } var current = ImmutableCollectionsMarshal.AsImmutableArray(selections); @@ -385,7 +464,7 @@ private SelectionSet BuildSelectionSet( // This mean we can simply construct the SelectionSet. if (current == rewritten) { - return new SelectionSet(selectionSetId, path, typeContext, selections, isConditional); + return new SelectionSet(selectionSetId, path, typeContext, selections, isConditional, hasDeferredSelections); } if (current.Length < rewritten.Length) @@ -405,7 +484,7 @@ private SelectionSet BuildSelectionSet( } selections = ImmutableCollectionsMarshal.AsArray(rewritten)!; - return new SelectionSet(selectionSetId, path, typeContext, selections, isConditional); + return new SelectionSet(selectionSetId, path, typeContext, selections, isConditional, hasDeferredSelections); } private static void CollapseIncludeFlags(List includeFlags) @@ -521,6 +600,34 @@ private static bool IsInternal(FieldNode fieldNode) return false; } + private static string? GetDeferLabel(InlineFragmentNode node) + { + for (var i = 0; i < node.Directives.Count; i++) + { + var directive = node.Directives[i]; + + if (!directive.Name.Value.Equals(DirectiveNames.Defer.Name, StringComparison.Ordinal)) + { + continue; + } + + for (var j = 0; j < directive.Arguments.Count; j++) + { + var arg = directive.Arguments[j]; + + if (arg.Name.Value.Equals(DirectiveNames.Defer.Arguments.Label, StringComparison.Ordinal) + && arg.Value is StringValueNode labelValue) + { + return labelValue.Value; + } + } + + return null; + } + + return null; + } + private class IncludeConditionVisitor : SyntaxWalker { public static readonly IncludeConditionVisitor Instance = new(); @@ -550,6 +657,23 @@ protected override ISyntaxVisitorAction Enter( } } + private class DeferConditionVisitor : SyntaxWalker + { + public static readonly DeferConditionVisitor Instance = new(); + + protected override ISyntaxVisitorAction Enter( + InlineFragmentNode node, + DeferConditionCollection context) + { + if (DeferCondition.TryCreate(node, out var condition)) + { + context.Add(condition); + } + + return base.Enter(node, context); + } + } + private class CompilationContext { private object[] _elementsById; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs index 87c7c41ec72..47d5b3aa89c 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Execution.cs @@ -16,9 +16,23 @@ public WorkScheduler Scheduler AssertInitialized(); return _currentWorkScheduler; } - internal set + } + + public DeferExecutionCoordinator DeferExecutionCoordinator + { + get + { + AssertInitialized(); + return _currentDeferExecutionCoordinator; + } + } + + public int ExecutionBranchId + { + get { - _currentWorkScheduler = value; + AssertInitialized(); + return _branchId; } } @@ -38,7 +52,8 @@ public ResolverTask CreateResolverTask( Selection selection, ResultElement resultValue, IImmutableDictionary scopedContextData, - Path? path = null) + int? executionBranchId = null, + DeferUsage? deferUsage = null) { AssertInitialized(); @@ -50,8 +65,33 @@ public ResolverTask CreateResolverTask( resultValue, this, scopedContextData, - path); + executionBranchId ?? _branchId, + deferUsage); return resolverTask; } + + public DeferTask CreateDeferTask( + SelectionSet selectionSet, + Path selectionPath, + object? parent, + IImmutableDictionary scopedContextData, + int executionBranchId, + DeferUsage deferUsage) + { + AssertInitialized(); + + var deferTask = new DeferTask(); + + deferTask.Initialize( + this, + parent, + scopedContextData, + selectionSet, + selectionPath, + executionBranchId, + deferUsage); + + return deferTask; + } } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Operation.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Operation.cs index ab9451f9a43..59fcdf97e96 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Operation.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Operation.cs @@ -46,6 +46,11 @@ public IVariableValueCollection Variables /// public ulong IncludeFlags { get; private set; } + /// + /// Gets the include flags for the current request. + /// + public ulong DeferFlags { get; private set; } + /// /// Gets the value representing the instance of the /// diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs index 4c2d9605d8a..c7fd8d6df80 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationContext.Pooling.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Runtime.CompilerServices; using HotChocolate.Execution.DependencyInjection; using HotChocolate.Execution.Instrumentation; @@ -14,8 +15,12 @@ namespace HotChocolate.Execution.Processing; internal sealed partial class OperationContext { private readonly IFactory _resolverTaskFactory; + private readonly BranchTracker _branchTracker = new(); private readonly WorkScheduler _workScheduler; + private readonly DeferExecutionCoordinator _deferExecutionCoordinator = new(); private WorkScheduler _currentWorkScheduler; + private BranchTracker _currentBranchTracker; + private DeferExecutionCoordinator _currentDeferExecutionCoordinator; private readonly AggregateServiceScopeInitializer _serviceScopeInitializer; private RequestContext _requestContext = null!; private Schema _schema = null!; @@ -30,6 +35,7 @@ internal sealed partial class OperationContext private Func _resolveQueryRootValue = null!; private IBatchDispatcher _batchDispatcher = null!; private InputParser _inputParser = null!; + private int _branchId; private int _variableIndex; private object? _rootValue; private bool _isInitialized; @@ -42,6 +48,8 @@ public OperationContext( _resolverTaskFactory = resolverTaskFactory; _workScheduler = new WorkScheduler(this); _currentWorkScheduler = _workScheduler; + _currentBranchTracker = _branchTracker; + _currentDeferExecutionCoordinator = _deferExecutionCoordinator; _serviceScopeInitializer = serviceScopeInitializer; Converter = typeConverter; } @@ -75,18 +83,30 @@ public void Initialize( _resolveQueryRootValue = resolveQueryRootValue; _batchDispatcher = batchDispatcher; _variableIndex = variableIndex; - _isInitialized = true; IncludeFlags = operation.CreateIncludeFlags(variables); + DeferFlags = operation.CreateDeferFlags(variables); Result.Data = new ResultDocument(operation, IncludeFlags); Result.RequestIndex = _requestContext.RequestIndex; Result.VariableIndex = variableIndex; - _workScheduler.Initialize(batchDispatcher); + _currentBranchTracker = _branchTracker; _currentWorkScheduler = _workScheduler; + _currentDeferExecutionCoordinator = _deferExecutionCoordinator; + _isInitialized = true; + + // once the operation context is marked as initialized we can initialize sub components. + _branchId = _currentBranchTracker.CreateNewBranchId(); + _workScheduler.Initialize(_requestContext, batchDispatcher); + _deferExecutionCoordinator.Initialize(_currentBranchTracker, _branchId); } - public void InitializeFrom(OperationContext context) + public void InitializeDeferContext( + OperationContext context, + SelectionSet selectionSet, + Path selectionPath, + int executionBranchId, + DeferUsage deferUsage) { _requestContext = context._requestContext; _schema = context._schema; @@ -102,23 +122,46 @@ public void InitializeFrom(OperationContext context) _rootValue = context._rootValue; _resolveQueryRootValue = context._resolveQueryRootValue; _batchDispatcher = context._batchDispatcher; + _currentBranchTracker = context._currentBranchTracker; + _currentWorkScheduler = context._currentWorkScheduler; + _currentDeferExecutionCoordinator = context._currentDeferExecutionCoordinator; + _branchId = executionBranchId; _isInitialized = true; - IncludeFlags = _operation.CreateIncludeFlags(_variables); - Result.Data = new ResultDocument(_operation, IncludeFlags); + IncludeFlags = context.IncludeFlags; + DeferFlags = context.DeferFlags; + Result.Data = new ResultDocument( + context.Operation, + selectionSet, + selectionPath, + context.IncludeFlags, + context.DeferFlags, + deferUsage); Result.RequestIndex = _requestContext.RequestIndex; Result.VariableIndex = context._variableIndex; + } - _workScheduler.Initialize(_batchDispatcher); - _currentWorkScheduler = _workScheduler; + public void InitializeWorkSchedulerFrom(OperationContext context) + { + Debug.Assert(_isInitialized); + + _currentBranchTracker = context._currentBranchTracker; + _currentWorkScheduler = context._currentWorkScheduler; + _branchId = _currentBranchTracker.CreateNewBranchId(); + _deferExecutionCoordinator.Initialize(_currentBranchTracker, _branchId); } public void Clean() { if (_isInitialized) { - _currentWorkScheduler = _workScheduler; + _branchTracker.Reset(); _workScheduler.Clear(); + _deferExecutionCoordinator.Reset(); + + _currentBranchTracker = _branchTracker; + _currentWorkScheduler = _workScheduler; + _requestContext = null!; _schema = null!; _errorHandler = null!; @@ -131,6 +174,7 @@ public void Clean() _rootValue = null; _resolveQueryRootValue = null!; _batchDispatcher = null!; + _branchId = int.MinValue; _isInitialized = false; Result.Reset(); } @@ -141,6 +185,7 @@ public void ResetScheduler() if (_isInitialized) { _currentWorkScheduler = _workScheduler; + _currentBranchTracker = _branchTracker; } } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationPrinter.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationPrinter.cs index 427236654ff..c318832320f 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/OperationPrinter.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/OperationPrinter.cs @@ -114,33 +114,51 @@ private static FieldNode CreateFieldSelection( private static DirectiveNode CreateExecutionInfo(Selection selection) { - var arguments = new ArgumentNode[selection.IsInternal ? 4 : 3]; - arguments[0] = new ArgumentNode("id", new IntValueNode(selection.Id)); - arguments[1] = new ArgumentNode("kind", new EnumValueNode(selection.Strategy.ToString().ToUpperInvariant())); + var argumentCount = 3; + + if (selection.IsInternal) + { + argumentCount++; + } + + if (selection.HasDeferUsage) + { + argumentCount++; + } + + var index = 0; + var arguments = new ArgumentNode[argumentCount]; + arguments[index++] = new ArgumentNode("id", new IntValueNode(selection.Id)); + arguments[index++] = new ArgumentNode("kind", new EnumValueNode(selection.Strategy.ToString().ToUpperInvariant())); if (selection.IsList) { if (selection.IsLeaf) { - arguments[2] = new ArgumentNode("type", new EnumValueNode("LEAF_LIST")); + arguments[index++] = new ArgumentNode("type", new EnumValueNode("LEAF_LIST")); } else { - arguments[2] = new ArgumentNode("type", new EnumValueNode("COMPOSITE_LIST")); + arguments[index++] = new ArgumentNode("type", new EnumValueNode("COMPOSITE_LIST")); } } else if (selection.Type.IsCompositeType()) { - arguments[2] = new ArgumentNode("type", new EnumValueNode("COMPOSITE")); + arguments[index++] = new ArgumentNode("type", new EnumValueNode("COMPOSITE")); } else if (selection.IsLeaf) { - arguments[2] = new ArgumentNode("type", new EnumValueNode("LEAF")); + arguments[index++] = new ArgumentNode("type", new EnumValueNode("LEAF")); } if (selection.IsInternal) { - arguments[3] = new ArgumentNode("internal", BooleanValueNode.True); + arguments[index++] = new ArgumentNode("internal", BooleanValueNode.True); + } + + if (selection.HasDeferUsage) + { + arguments[index++] = new ArgumentNode("isDeferred", BooleanValueNode.True); } return new DirectiveNode("__execute", arguments); diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs index 6fce5589c74..ae1cc54474f 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/QueryExecutor.cs @@ -1,83 +1,187 @@ using System.Collections.Immutable; +using System.Diagnostics; using static HotChocolate.Execution.Processing.Tasks.ResolverTaskFactory; namespace HotChocolate.Execution.Processing; internal sealed class QueryExecutor { - public Task ExecuteAsync( + public Task ExecuteAsync( OperationContext operationContext) => ExecuteAsync(operationContext, ImmutableDictionary.Empty); - public Task ExecuteAsync( + public Task ExecuteAsync( OperationContext operationContext, IImmutableDictionary scopedContext) { - ArgumentNullException.ThrowIfNull(operationContext); - ArgumentNullException.ThrowIfNull(scopedContext); + if (operationContext.Operation.HasIncrementalParts) + { + return ExecuteIncrementalAsync(operationContext, scopedContext); + } + + return ExecuteNoIncrementalAsync(operationContext, scopedContext); + } + + private static async Task ExecuteIncrementalAsync( + OperationContext operationContext, + IImmutableDictionary scopedContext) + { + EnqueueRootResolverTasks( + operationContext, + operationContext.RootValue, + operationContext.Result.Data.Data, + scopedContext); + + var branchId = operationContext.ExecutionBranchId; + var scheduler = operationContext.Scheduler; + var coordinator = operationContext.DeferExecutionCoordinator; + + var execution = scheduler.ExecuteAsync1(); + await scheduler.WaitForCompletionAsync(branchId).ConfigureAwait(false); + var initialResult = operationContext.BuildResult(); - return ExecuteInternalAsync(operationContext, scopedContext); + if (!coordinator.HasBranches) + { + if (!execution.IsCompletedSuccessfully) + { + await execution.ConfigureAwait(false); + } + + return initialResult; + } + + coordinator.EnqueueResult(initialResult); + return new ResponseStream(CreateStream, ExecutionResultKind.DeferredResult); + + async IAsyncEnumerable CreateStream() + { + var requestAborted = operationContext.RequestAborted; + await foreach (var result in coordinator.ReadResultsAsync(requestAborted)) + { + yield return result; + } + + await execution.ConfigureAwait(false); + } } - private static async Task ExecuteInternalAsync( + private static async Task ExecuteNoIncrementalAsync( OperationContext operationContext, IImmutableDictionary scopedContext) { - EnqueueResolverTasks( + EnqueueRootResolverTasks( operationContext, operationContext.RootValue, operationContext.Result.Data.Data, - scopedContext, - Path.Root); + scopedContext); - await operationContext.Scheduler.ExecuteAsync().ConfigureAwait(false); + await operationContext.Scheduler.ExecuteAsync1().ConfigureAwait(false); return operationContext.BuildResult(); } - public async Task ExecuteBatchAsync( - ReadOnlyMemory operationContexts, - Memory results) + public Task ExecuteBatchAsync( + OperationContextOwner[] operationContexts, + IExecutionResult[] results, + int length) { - var scopedContext = ImmutableDictionary.Empty; + Debug.Assert(length > 0); + Debug.Assert(length <= operationContexts.Length); + Debug.Assert(length <= results.Length); + + if (operationContexts[0].OperationContext.Operation.HasIncrementalParts) + { + return ExecuteBatchIncrementalAsync(operationContexts, results, length); + } + + return ExecuteBatchNoIncrementalAsync(operationContexts, results, length); + } + private async Task ExecuteBatchNoIncrementalAsync( + OperationContextOwner[] operationContexts, + IExecutionResult[] results, + int length) + { // when using batching we will use the same scheduler // to execute more efficiently with DataLoader. - var scheduler = operationContexts.Span[0].OperationContext.Scheduler; + var parentContext = operationContexts[0].OperationContext; - FillSchedulerWithWork(scheduler, operationContexts.Span, scopedContext); + FillSchedulerWithWork(parentContext, operationContexts, length); - await scheduler.ExecuteAsync().ConfigureAwait(false); + await parentContext.Scheduler.ExecuteAsync1().ConfigureAwait(false); - BuildResults(operationContexts.Span, results.Span); + for (var i = 0; i < length; ++i) + { + results[i] = operationContexts[i].OperationContext.BuildResult(); + } } - private static void FillSchedulerWithWork( - WorkScheduler scheduler, - ReadOnlySpan operationContexts, - ImmutableDictionary scopedContext) + private async Task ExecuteBatchIncrementalAsync( + OperationContextOwner[] operationContexts, + IExecutionResult[] results, + int length) { - foreach (var contextOwner in operationContexts) + // when using batching we will use the same scheduler + // to execute more efficiently with DataLoader. + var parentContext = operationContexts[0].OperationContext; + var scheduler = parentContext.Scheduler; + + FillSchedulerWithWork(parentContext, operationContexts, length); + + var execution = parentContext.Scheduler.ExecuteAsync1(); + + for (var i = 0; i < length; ++i) { - var context = contextOwner.OperationContext; - context.Scheduler = scheduler; + if (i == 0) + { + var branchId = parentContext.ExecutionBranchId; + await scheduler.WaitForCompletionAsync(branchId).ConfigureAwait(false); + parentContext.DeferExecutionCoordinator.EnqueueResult(parentContext.BuildResult()); + results[i] = new ResponseStream(CreateStreamAndComplete, ExecutionResultKind.DeferredResult); + } + else + { + var context = operationContexts[i].OperationContext; + var branchId = context.ExecutionBranchId; + await scheduler.WaitForCompletionAsync(branchId).ConfigureAwait(false); + context.DeferExecutionCoordinator.EnqueueResult(context.BuildResult()); + results[i] = new ResponseStream(CreateStream, ExecutionResultKind.DeferredResult); + } + } - EnqueueResolverTasks( - context, - context.RootValue, - context.Result.Data.Data, - scopedContext, - Path.Root); + async IAsyncEnumerable CreateStreamAndComplete() + { + var requestAborted = parentContext.RequestAborted; + await foreach (var result in parentContext.DeferExecutionCoordinator.ReadResultsAsync(requestAborted)) + { + yield return result; + } + + await execution.ConfigureAwait(false); + } + + IAsyncEnumerable CreateStream() + { + var requestAborted = parentContext.RequestAborted; + return parentContext.DeferExecutionCoordinator.ReadResultsAsync(requestAborted); } } - private static void BuildResults( - ReadOnlySpan operationContexts, - Span results) + private static void FillSchedulerWithWork( + OperationContext parentContext, + OperationContextOwner[] operationContexts, + int length) { - for (var i = 0; i < operationContexts.Length; ++i) + for (var i = 0; i < length; i++) { - results[i] = operationContexts[i].OperationContext.BuildResult(); + var context = operationContexts[i].OperationContext; + context.InitializeWorkSchedulerFrom(parentContext); + + EnqueueRootResolverTasks( + context, + context.RootValue, + context.Result.Data.Data, + ImmutableDictionary.Empty); } } } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs index 7aacaaff091..66565f554de 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Selection.cs @@ -15,6 +15,8 @@ public sealed class Selection : ISelection, IFeatureProvider private readonly FieldSelectionNode[] _syntaxNodes; private readonly ulong[] _includeFlags; private readonly byte[] _utf8ResponseName; + private readonly DeferUsage[] _deferUsage; + private readonly ulong _deferMask; private Flags _flags; private SelectionSet? _declaringSelectionSet; @@ -24,6 +26,8 @@ internal Selection( ObjectField field, FieldSelectionNode[] syntaxNodes, ulong[] includeFlags, + DeferUsage[]? deferUsage = null, + ulong deferMask = 0, bool isInternal = false, ArgumentMap? arguments = null, FieldDelegate? resolverPipeline = null, @@ -50,6 +54,8 @@ internal Selection( hasPureResolver: pureResolver is not null); _syntaxNodes = syntaxNodes; _includeFlags = includeFlags; + _deferUsage = deferUsage ?? []; + _deferMask = deferMask; _flags = isInternal ? Flags.Internal : Flags.None; if (field.Type.NamedType().IsLeafType()) @@ -73,6 +79,8 @@ private Selection( IType type, FieldSelectionNode[] syntaxNodes, ulong[] includeFlags, + DeferUsage[] deferUsage, + ulong deferMask, Flags flags, ArgumentMap? arguments, SelectionExecutionStrategy strategy, @@ -89,6 +97,8 @@ private Selection( Strategy = strategy; _syntaxNodes = syntaxNodes; _includeFlags = includeFlags; + _deferUsage = deferUsage; + _deferMask = deferMask; _flags = flags; _utf8ResponseName = utf8ResponseName; } @@ -266,6 +276,148 @@ public bool IsIncluded(ulong includeFlags) return false; } + /// + /// Gets a value indicating whether this selection has any defer usage. + /// + internal bool HasDeferUsage => _deferUsage.Length > 0; + + /// + public bool IsDeferred(ulong deferFlags) + => _deferMask != 0 && (_deferMask & deferFlags) == _deferMask; + + /// + /// Determines whether this selection is deferred relative to a parent defer usage. + /// + /// + /// The defer condition flags representing which @defer directives are active + /// for the current request, computed from the runtime variable values of the + /// if arguments on @defer directives. + /// + /// + /// The defer usage of the parent context, or null if the parent is not deferred. + /// When provided, this selection is only considered deferred if its primary defer usage + /// matches the given parent, ensuring that the selection is delivered in the correct + /// incremental payload. + /// + /// + /// true if this selection is deferred and belongs to the specified parent + /// defer context; otherwise, false. + /// + public bool IsDeferred(ulong deferFlags, DeferUsage? parentDeferUsage) + { + if (_deferMask != 0 && (_deferMask & deferFlags) == _deferMask) + { + if (parentDeferUsage is null) + { + return true; + } + + // If the primary defer usage matches the parent's defer context, + // this selection is already being delivered in that context + // and does not need to be deferred separately. + if (ReferenceEquals(GetPrimaryDeferUsage(deferFlags), parentDeferUsage)) + { + return false; + } + + return true; + } + + return false; + } + + /// + /// Gets the primary defer usage for this selection given the active defer flags. + /// The primary defer usage determines which execution branch the selection belongs to. + /// If multiple defer usages are active and one is a parent of another, the parent takes precedence. + /// + /// The active defer flags. + /// + /// The primary defer usage, or null if the selection is not deferred or has no active defer usages. + /// + public DeferUsage? GetPrimaryDeferUsage(ulong deferFlags) + { + if (_deferUsage.Length == 0) + { + return null; + } + + // Fast path for single defer usage (most common case). + if (_deferUsage.Length == 1) + { + var usage = _deferUsage[0]; + + // Walk up the parent chain to find the nearest active defer. + // A defer directive is inactive when its condition evaluates to false at runtime + // (e.g. @defer(if: $var) with $var = false). When inactive, the fragment + // is not deferred and its content folds into the parent scope — but the + // parent scope may itself be deferred. + while (usage is not null) + { + if ((deferFlags & (1UL << usage.DeferConditionIndex)) != 0) + { + return usage; + } + + usage = usage.Parent; + } + + // No active defer in the chain — field is not deferred. + return null; + } + + // Multiple defer usages: the field was collected from multiple deferred + // fragments. Resolve each to its nearest active ancestor, then find the + // outermost (primary) among them. + DeferUsage? primary = null; + + for (var i = 0; i < _deferUsage.Length; i++) + { + // Walk up the parent chain to find the nearest active defer. + var effective = _deferUsage[i]; + + while (effective is not null) + { + if ((deferFlags & (1UL << effective.DeferConditionIndex)) != 0) + { + break; + } + + effective = effective.Parent; + } + + if (effective is null) + { + // This occurrence has no active defer in its chain — + // the field appears non-deferred and belongs in the initial response. + return null; + } + + if (primary is null || primary == effective) + { + primary = effective; + continue; + } + + // Two different active defers. Keep the outermost: check if + // effective is an ancestor of primary. + var ancestor = primary.Parent; + + while (ancestor is not null) + { + if (ancestor == effective) + { + primary = effective; + break; + } + + ancestor = ancestor.Parent; + } + } + + return primary; + } + public Selection WithField(ObjectField field) { ArgumentNullException.ThrowIfNull(field); @@ -278,6 +430,8 @@ public Selection WithField(ObjectField field) field.Type, _syntaxNodes, _includeFlags, + _deferUsage, + _deferMask, _flags, Arguments, Strategy, @@ -301,6 +455,8 @@ public Selection WithType(IType type) type, _syntaxNodes, _includeFlags, + _deferUsage, + _deferMask, _flags, Arguments, Strategy, diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/SelectionSet.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/SelectionSet.cs index d545b1b4805..5f1f0db2702 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/SelectionSet.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/SelectionSet.cs @@ -23,7 +23,8 @@ internal SelectionSet( SelectionPath path, IObjectTypeDefinition type, Selection[] selections, - bool isConditional) + bool isConditional, + bool hasDeferredSelections = false) { ArgumentNullException.ThrowIfNull(selections); @@ -31,6 +32,12 @@ internal SelectionSet( Path = path; Type = type; _flags = isConditional ? Flags.Conditional : Flags.None; + + if (hasDeferredSelections) + { + _flags |= Flags.HasDeferredSelections; + } + _selections = selections; _responseNameLookup = _selections.ToFrozenDictionary(t => t.ResponseName); _utf8ResponseNameLookup = SelectionLookup.Create(this); @@ -51,6 +58,9 @@ internal SelectionSet( /// public bool IsConditional => (_flags & Flags.Conditional) == Flags.Conditional; + /// + public bool HasIncrementalParts => (_flags & Flags.HasDeferredSelections) == Flags.HasDeferredSelections; + /// /// Gets the type context of this selection set. /// @@ -122,7 +132,8 @@ private enum Flags { None = 0, Conditional = 1, - Sealed = 2 + Sealed = 2, + HasDeferredSelections = 4 } public override string ToString() diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/SubscriptionExecutor.Subscription.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/SubscriptionExecutor.Subscription.cs index 1eda1d3c4a4..266a52807b0 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/SubscriptionExecutor.Subscription.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/SubscriptionExecutor.Subscription.cs @@ -189,7 +189,8 @@ private async Task OnEvent(object payload) .ExecuteAsync(operationContext, scopedContextData) .ConfigureAwait(false); - return result; + // todo : we still need to think about defer in subscriptions + return result.ExpectOperationResult(); } catch (OperationCanceledException ex) { @@ -256,8 +257,8 @@ private async ValueTask SubscribeAsync() rootSelection, resultMap, operationContext, - _scopedContextData, - null); + deferUsage: null, + _scopedContextData); // it is important that we correctly coerce the arguments before // invoking subscribe. diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs new file mode 100644 index 00000000000..2a3223c6e54 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/DeferTask.cs @@ -0,0 +1,109 @@ +using System.Buffers; +using System.Collections.Immutable; +using HotChocolate.Execution.DependencyInjection; + +namespace HotChocolate.Execution.Processing.Tasks; + +internal sealed class DeferTask : ExecutionTask +{ + private static readonly ArrayPool s_pool = ArrayPool.Shared; + private OperationContextOwner _deferContextOwner = null!; + private object? _parent; + private IImmutableDictionary _scopedContext = null!; + private int _executionBranchId; + private DeferUsage _deferUsage = null!; + + // the defer tasks runs in the system branch as it's just an orchestration task. + public override int BranchId => BranchTracker.SystemBranchId; + + public override bool IsDeferred => true; + + protected override IExecutionTaskContext Context => _deferContextOwner.OperationContext; + + protected override async ValueTask ExecuteAsync(CancellationToken cancellationToken) + { + var deferContext = _deferContextOwner.OperationContext; + var data = deferContext.Result.Data.Data; + var bufferedTasks = s_pool.Rent(data.GetPropertyCount()); + var i = 0; + + try + { + foreach (var field in data.EnumerateObject()) + { + bufferedTasks[i++] = + deferContext.CreateResolverTask( + _parent, + field.AssertSelection(), + field.Value, + _scopedContext, + _executionBranchId, + _deferUsage); + } + + // we register our deferred tasks for execution ... + deferContext.Scheduler.Register(bufferedTasks.AsSpan(0, i)); + } + finally + { + if (i > 0) + { + bufferedTasks.AsSpan(0, i).Clear(); + } + + s_pool.Return(bufferedTasks); + } + + // ... and then wait for the scheduler to complete the deferred tasks. + await deferContext.Scheduler.WaitForCompletionAsync(_executionBranchId); + + // once the execution branch has completed we enqueue the completed + // result with the defer coordinator so it can be delivered. + deferContext.DeferExecutionCoordinator.EnqueueResult(deferContext.BuildResult(), _executionBranchId); + } + + protected override ValueTask OnAfterCompletedAsync(CancellationToken cancellationToken) + { + // TODO : we need to give the context back here and not rest once we have a pool. + Reset(); + return ValueTask.CompletedTask; + } + + public void Initialize( + OperationContext parentContext, + object? parent, + IImmutableDictionary scopedContext, + SelectionSet selectionSet, + Path selectionPath, + int executionBranchId, + DeferUsage deferUsage) + { + var contextFactory = parentContext.Services.GetRequiredService>(); + _deferContextOwner = contextFactory.Create(); + + // we first need to initialize the rented context for this defer operation. + _deferContextOwner.OperationContext.InitializeDeferContext( + parentContext, + selectionSet, + selectionPath, + executionBranchId, + deferUsage); + + _parent = parent; + _scopedContext = scopedContext; + _executionBranchId = executionBranchId; + _deferUsage = deferUsage; + } + + public new void Reset() + { + _deferContextOwner.Dispose(); + _deferContextOwner = null!; + _parent = null!; + _scopedContext = null!; + _executionBranchId = 0; + _deferUsage = null!; + + base.Reset(); + } +} diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ExecutionTaskPool.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ExecutionTaskPool.cs index e9d83df99b8..55a0c1a73ce 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ExecutionTaskPool.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ExecutionTaskPool.cs @@ -5,7 +5,7 @@ namespace HotChocolate.Execution.Processing.Tasks; /// -/// A pool of objects. Buffers a set of objects to ensure fast, thread safe object pooling +/// A pool of objects. Buffers a set of objects to ensure fast, thread safe object pooling /// internal sealed class ExecutionTaskPool : ObjectPool where T : class, IExecutionTask @@ -22,8 +22,8 @@ public ExecutionTaskPool(TPolicy policy, int maximumRetained = 256) } /// - /// Gets an object from the buffer if one is available, otherwise get a new buffer - /// from the pool one. + /// Gets an object from the buffer if one is available, otherwise get a new buffer + /// from the pool one. /// /// A . public override T Get() @@ -49,8 +49,8 @@ public override T Get() } /// - /// Return an object from the buffer if one is available. If the buffer is full - /// return the buffer to the pool + /// Return an object from the buffer if one is available. If the buffer is full + /// return the buffer to the pool /// public override void Return(T obj) { diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.CompleteValue.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.CompleteValue.cs index 7e3bb2192ae..74771cf956c 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.CompleteValue.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.CompleteValue.cs @@ -19,7 +19,7 @@ private void CompleteValue(bool success, CancellationToken cancellationToken) // we will only try to complete the resolver value if there are no known errors. if (success) { - var completionContext = new ValueCompletionContext(_operationContext, _context, _taskBuffer); + var completionContext = new ValueCompletionContext(_operationContext, _context, _taskBuffer, BranchId); Complete(completionContext, _selection, resultValue, result); } } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.Pooling.cs index 50e086e5c8c..34d3347da15 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.Pooling.cs @@ -14,12 +14,15 @@ public void Initialize( ResultElement resultValue, OperationContext operationContext, IImmutableDictionary scopedContextData, - Path? path) + int executionBranchId, + DeferUsage? deferUsage) { _operationContext = operationContext; _selection = selection; - _context.Initialize(parent, selection, resultValue, operationContext, scopedContextData, path); + _context.Initialize(parent, selection, resultValue, operationContext, deferUsage, scopedContextData); IsSerial = selection.Strategy is SelectionExecutionStrategy.Serial; + BranchId = executionBranchId; + DeferUsage = deferUsage; } /// @@ -34,6 +37,8 @@ internal bool Reset() _context.Clean(); Status = ExecutionTaskStatus.WaitingToRun; IsSerial = false; + BranchId = int.MinValue; + DeferUsage = null; IsRegistered = false; Next = null; Previous = null; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs index 07244b911ab..d95750915f4 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs @@ -8,8 +8,9 @@ namespace HotChocolate.Execution.Processing.Tasks; internal sealed partial class ResolverTask(ObjectPool objectPool) : IExecutionTask { private readonly MiddlewareContext _context = new(); - private readonly List _taskBuffer = []; - private readonly Dictionary _args = new(StringComparer.Ordinal); + private readonly List _taskBuffer = []; + private readonly Dictionary _args = + new Dictionary(StringComparer.Ordinal); private OperationContext _operationContext = null!; private Selection _selection = null!; private ExecutionTaskStatus _completionStatus = ExecutionTaskStatus.Completed; @@ -19,6 +20,20 @@ internal sealed partial class ResolverTask(ObjectPool objectPool) /// public uint Id { get; set; } + /// + /// Gets the execution branch identifier this task belongs to. + /// Used by the defer coordinator to track which deferred execution branch + /// this task contributes results to. + /// + public int BranchId { get; private set; } + + /// + /// Gets the primary defer usage that caused this execution branch to be created. + /// Used to determine whether child tasks should create new branches when their + /// primary defer usage differs from this one. + /// + internal DeferUsage? DeferUsage { get; private set; } + /// /// Gets access to the resolver context for this task. /// @@ -62,6 +77,9 @@ public ExecutionTaskKind Kind /// public bool IsRegistered { get; set; } + /// + public bool IsDeferred => DeferUsage is not null; + /// public void BeginExecute(CancellationToken cancellationToken) { diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs index 8c056fc6e17..3e803b14bcb 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTaskFactory.cs @@ -1,6 +1,6 @@ using System.Collections.Immutable; +using System.Buffers; using System.Diagnostics; -using System.Runtime.InteropServices; using HotChocolate.Text.Json; using HotChocolate.Types; using static HotChocolate.Execution.Processing.ValueCompletion; @@ -9,114 +9,129 @@ namespace HotChocolate.Execution.Processing.Tasks; internal static class ResolverTaskFactory { - private static List? s_pooled = []; + private static readonly ArrayPool s_pool = ArrayPool.Shared; - static ResolverTaskFactory() { } - - public static void EnqueueResolverTasks( + public static void EnqueueRootResolverTasks( OperationContext operationContext, object? parent, ResultElement resultValue, - IImmutableDictionary scopedContext, - Path path) + IImmutableDictionary scopedContext) { var selectionSet = resultValue.AssertSelectionSet(); - var selections = selectionSet.Selections; - var scheduler = operationContext.Scheduler; - var bufferedTasks = Interlocked.Exchange(ref s_pooled, null) ?? []; - Debug.Assert(bufferedTasks.Count == 0, "The buffer must be clean."); + var bufferedTasks = s_pool.Rent(resultValue.GetPropertyCount()); + var mainBranchId = operationContext.ExecutionBranchId; + var data = resultValue.EnumerateObject(); + var i = 0; try { - // we are iterating reverse so that in the case of a mutation the first - // synchronous root selection is executed first, since the work scheduler - // is using two stacks one for parallel work and one for synchronous work. - // the scheduler tries to schedule new work first. - // coincidentally we can use that to schedule a mutation so that we honor the spec - // guarantees while executing efficient. - var fieldValues = selections.Length == 1 - ? resultValue.EnumerateObject() - : resultValue.EnumerateObject().Reverse(); - foreach (var field in fieldValues) + if (selectionSet.HasIncrementalParts) { - bufferedTasks.Add( - operationContext.CreateResolverTask( - parent, - field.AssertSelection(), - field.Value, - scopedContext)); - } + var coordinator = operationContext.DeferExecutionCoordinator; + var deferFlags = operationContext.DeferFlags; + var branches = ImmutableDictionary.Empty; + DeferUsage? lastDeferUsage = null; - if (bufferedTasks.Count == 0) - { - // in the case all root fields are skipped we execute a dummy task in order - // to not have extra logic for this case. - scheduler.Register(new NoOpExecutionTask(operationContext)); + foreach (var field in data) + { + var selection = field.AssertSelection(); + + if (selection.IsDeferred(deferFlags)) + { + // if IsDeferred is true then GetPrimaryDeferUsage will be guaranteed + // to return a defer usage for the same deferFlags + var deferUsage = selection.GetPrimaryDeferUsage(deferFlags); + Debug.Assert(deferUsage is not null); + + field.Value.MarkAsDeferred(); + + if (lastDeferUsage == deferUsage) + { + continue; + } + + if (!branches.TryGetValue(deferUsage, out var branchId)) + { + branchId = coordinator.Branch(mainBranchId, Path.Root, deferUsage); + branches = branches.Add(deferUsage, branchId); + } + + lastDeferUsage = deferUsage; + continue; + } + + bufferedTasks[i++] = + operationContext.CreateResolverTask( + parent, + selection, + field.Value, + scopedContext); + } + + if (i == 0 && branches.IsEmpty) + { + // in the case all root fields are skipped we execute a dummy task in order + // to not have extra logic for this case. + scheduler.Register(new NoOpExecutionTask(operationContext)); + } + else + { + if (i > 0) + { + scheduler.Register(bufferedTasks.AsSpan(0, i)); + } + + if (!branches.IsEmpty) + { + foreach (var (deferUsage, branchId) in branches) + { + scheduler.Register( + operationContext.CreateDeferTask( + selectionSet, + Path.Root, + parent, + scopedContext, + branchId, + deferUsage)); + } + } + } } else { - scheduler.Register(CollectionsMarshal.AsSpan(bufferedTasks)); + foreach (var field in data) + { + bufferedTasks[i++] = + operationContext.CreateResolverTask( + parent, + field.AssertSelection(), + field.Value, + scopedContext); + } + + if (i == 0) + { + // in the case all root fields are skipped we execute a dummy task in order + // to not have extra logic for this case. + scheduler.Register(new NoOpExecutionTask(operationContext)); + } + else + { + scheduler.Register(bufferedTasks.AsSpan(0, i)); + } } } finally { - bufferedTasks.Clear(); - Interlocked.Exchange(ref s_pooled!, bufferedTasks); - } - } - - // TODO : remove ? defer? - /* - public static ResolverTask EnqueueElementTasks( - OperationContext operationContext, - Selection selection, - object? parent, - Path path, - int index, - IAsyncEnumerator value, - IImmutableDictionary scopedContext) - { - var parentResult = operationContext.Result.RentObject(1); - var bufferedTasks = Interlocked.Exchange(ref s_pooled, null) ?? []; - Debug.Assert(bufferedTasks.Count == 0, "The buffer must be clean."); - - var resolverTask = - operationContext.CreateResolverTask( - selection, - parent, - parentResult, - 0, - scopedContext, - path.Append(index)); - - try - { - CompleteInline( - operationContext, - resolverTask.Context, - selection, - selection.Type.ElementType(), - 0, - parentResult, - value.Current, - bufferedTasks); - - // if we have child tasks we need to register them. - if (bufferedTasks.Count > 0) + if (i > 0) { - operationContext.Scheduler.Register(CollectionsMarshal.AsSpan(bufferedTasks)); + bufferedTasks.AsSpan(0, i).Clear(); } - } - finally - { - bufferedTasks.Clear(); - Interlocked.Exchange(ref s_pooled, bufferedTasks); - } - return resolverTask; + s_pool.Return(bufferedTasks); + } } - */ public static void EnqueueOrInlineResolverTasks( ValueCompletionContext context, @@ -129,30 +144,99 @@ public static void EnqueueOrInlineResolverTasks( Debug.Assert(resultValue.Type?.NamedType()?.IsAssignableFrom(selectionSetType) ?? false); var operationContext = context.OperationContext; + var parentDeferUsage = context.ResolverContext.DeferUsage; resultValue.SetObjectValue(selectionSet); - foreach (var field in resultValue.EnumerateObject()) + if (selectionSet.HasIncrementalParts) { - var selection = field.AssertSelection(); + var coordinator = operationContext.DeferExecutionCoordinator; + var deferFlags = operationContext.DeferFlags; + var branches = ImmutableDictionary.Empty; + DeferUsage? lastDeferUsage = null; + Path? currentPath = null; + + var parentBranchId = context.ParentBranchId; - if (selection.Strategy is SelectionExecutionStrategy.Pure) + foreach (var field in resultValue.EnumerateObject()) { - ResolveAndCompleteInline( - context, - selection, - selectionSetType, - field.Value, - parent); + var selection = field.AssertSelection(); + + if (selection.IsDeferred(deferFlags, parentDeferUsage)) + { + // if IsDeferred is true then GetPrimaryDeferUsage will be guaranteed + // to return a defer usage for the same deferFlags + var deferUsage = selection.GetPrimaryDeferUsage(deferFlags); + Debug.Assert(deferUsage is not null); + + field.Value.MarkAsDeferred(); + + if (lastDeferUsage == deferUsage) + { + continue; + } + + if (!branches.TryGetValue(deferUsage, out var branchId)) + { + currentPath ??= resultValue.Path; + branchId = coordinator.Branch(parentBranchId, currentPath, deferUsage); + branches = branches.Add(deferUsage, branchId); + context.Tasks.Add( + operationContext.CreateDeferTask( + selectionSet, + currentPath, + parent, + context.ResolverContext.ScopedContextData, + branchId, + deferUsage)); + } + + lastDeferUsage = deferUsage; + } + else if (selection.Strategy is SelectionExecutionStrategy.Pure) + { + ResolveAndCompleteInline( + context, + selection, + selectionSetType, + field.Value, + parent); + } + else + { + context.Tasks.Add( + operationContext.CreateResolverTask( + parent, + selection, + field.Value, + context.ResolverContext.ScopedContextData)); + } } - else + } + else + { + foreach (var field in resultValue.EnumerateObject()) { - context.Tasks.Add( - operationContext.CreateResolverTask( - parent, + var selection = field.AssertSelection(); + + if (selection.Strategy is SelectionExecutionStrategy.Pure) + { + ResolveAndCompleteInline( + context, selection, + selectionSetType, field.Value, - context.ResolverContext.ScopedContextData)); + parent); + } + else + { + context.Tasks.Add( + operationContext.CreateResolverTask( + parent, + selection, + field.Value, + context.ResolverContext.ScopedContextData)); + } } } } @@ -226,6 +310,10 @@ private static void ResolveAndCompleteInline( private sealed class NoOpExecutionTask(OperationContext context) : ExecutionTask { + public override int BranchId => context.ExecutionBranchId; + + public override bool IsDeferred => false; + protected override IExecutionTaskContext Context { get; } = context; protected override ValueTask ExecuteAsync(CancellationToken cancellationToken) diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletion.List.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletion.List.cs index 69935791071..66633045b89 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletion.List.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletion.List.cs @@ -146,6 +146,6 @@ internal static void PropagateNullValues(ResultElement result) } result.Invalidate(); - } while (result.Parent is { IsInvalidated: false }); + } while (result.Parent is { ValueKind: not JsonValueKind.Undefined, IsInvalidated: false }); } } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletionContext.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletionContext.cs index 7c9adcbfa68..423cded9c98 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletionContext.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/ValueCompletionContext.cs @@ -1,5 +1,3 @@ -using HotChocolate.Execution.Processing.Tasks; - namespace HotChocolate.Execution.Processing; internal readonly ref struct ValueCompletionContext @@ -7,16 +5,20 @@ internal readonly ref struct ValueCompletionContext public ValueCompletionContext( OperationContext operationContext, MiddlewareContext resolverContext, - List tasks) + List tasks, + int parentBranchId) { OperationContext = operationContext; ResolverContext = resolverContext; Tasks = tasks; + ParentBranchId = parentBranchId; } public OperationContext OperationContext { get; } public MiddlewareContext ResolverContext { get; } - public List Tasks { get; } + public List Tasks { get; } + + public int ParentBranchId { get; } } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/VariableCoercionHelper.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/VariableCoercionHelper.cs index bc1920bd6ce..a463ceb3686 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/VariableCoercionHelper.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/VariableCoercionHelper.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Text.Json; using HotChocolate.Features; using HotChocolate.Language; @@ -35,6 +36,28 @@ public void CoerceVariableValues( nameof(variableValues)); } + try + { + CoerceVariableValuesInternal(schema, variableDefinitions, variableValues, coercedValues, context); + } + finally + { + var memoryBuilder = context.Features.Get(); + if (memoryBuilder is not null) + { + memoryBuilder.Seal(); + context.Features.Set(null); + } + } + } + + private void CoerceVariableValuesInternal( + ISchemaDefinition schema, + IReadOnlyList variableDefinitions, + JsonElement variableValues, + Dictionary coercedValues, + IFeatureProvider context) + { var hasVariables = variableValues.ValueKind is JsonValueKind.Object; for (var i = 0; i < variableDefinitions.Count; i++) @@ -93,12 +116,11 @@ private VariableValue CoerceVariableValue( IFeatureProvider context) { var root = Path.Root.Append(variableDefinition.Variable.Name.Value); - var valueParser = new JsonValueParser(); try { var runtimeValue = _inputParser.ParseInputValue(inputValue, variableType, context, path: root); - var valueLiteral = CoerceInputLiteral(inputValue, variableType, ref valueParser, depth: 0); + var valueLiteral = CoerceInputLiteral(inputValue, variableType, context, depth: 0); return new VariableValue( variableDefinition.Variable.Name.Value, @@ -108,19 +130,12 @@ private VariableValue CoerceVariableValue( } catch (GraphQLException) { - valueParser._memory?.Abandon(); throw; } catch (Exception ex) { - valueParser._memory?.Abandon(); throw ThrowHelper.VariableValueInvalidType(variableDefinition, ex); } - finally - { - valueParser._memory?.Seal(); - valueParser._memory = null; - } } private static IInputType AssertInputType( @@ -138,7 +153,7 @@ private static IInputType AssertInputType( private IValueNode CoerceInputLiteral( JsonElement inputValue, IInputType type, - ref JsonValueParser valueParser, + IFeatureProvider context, int depth) { if (depth > 64) @@ -154,7 +169,7 @@ private IValueNode CoerceInputLiteral( switch (type.Kind) { case TypeKind.Scalar: - return valueParser.Parse(inputValue, depth); + return Unsafe.As(type).InputValueToLiteral(inputValue, context); case TypeKind.Enum: if (inputValue.ValueKind is not JsonValueKind.String) @@ -191,7 +206,7 @@ private IValueNode CoerceInputLiteral( } else { - var value = CoerceInputLiteral(property.Value, field.Type, ref valueParser, depth + 1); + var value = CoerceInputLiteral(property.Value, field.Type, context, depth + 1); fields.Add(new ObjectFieldNode(field.Name, value)); } } @@ -223,13 +238,13 @@ private IValueNode CoerceInputLiteral( foreach (var item in inputValue.EnumerateArray()) { - items.Add(CoerceInputLiteral(item, elementType, ref valueParser, elementDepth)); + items.Add(CoerceInputLiteral(item, elementType, context, elementDepth)); } return new ListValueNode(items); case TypeKind.NonNull: - return CoerceInputLiteral(inputValue, type.InnerType().EnsureInputType(), ref valueParser, depth); + return CoerceInputLiteral(inputValue, type.InnerType().EnsureInputType(), context, depth); default: throw new NotSupportedException(); diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkQueue.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkQueue.cs index 1a19ba60bde..194bbe97d38 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkQueue.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkQueue.cs @@ -4,10 +4,11 @@ namespace HotChocolate.Execution.Processing; internal sealed class WorkQueue { - private readonly Stack _stack = new(); + private readonly Stack _immediateStack = new(); + private readonly Stack _deferredStack = new(); private int _running; - public bool IsEmpty => _stack.Count == 0; + public bool IsEmpty => _immediateStack.Count == 0 && _deferredStack.Count == 0; public bool HasRunningTasks => _running > 0; @@ -25,7 +26,8 @@ public bool Complete() public bool TryTake([MaybeNullWhen(false)] out IExecutionTask executionTask) { - if (_stack.TryPop(out executionTask)) + if (_immediateStack.TryPop(out executionTask) + || _deferredStack.TryPop(out executionTask)) { Interlocked.Increment(ref _running); return true; @@ -38,12 +40,20 @@ public void Push(IExecutionTask executionTask) { ArgumentNullException.ThrowIfNull(executionTask); - _stack.Push(executionTask); + if (executionTask.IsDeferred) + { + _deferredStack.Push(executionTask); + } + else + { + _immediateStack.Push(executionTask); + } } public void Clear() { - _stack.Clear(); + _immediateStack.Clear(); + _deferredStack.Clear(); _running = 0; } } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Execute.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Execute.cs index d0319f43e83..1a2a89ade5f 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Execute.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Execute.cs @@ -10,7 +10,7 @@ internal sealed partial class WorkScheduler : IObserver /// /// Execute the work. /// - public async Task ExecuteAsync() + public async Task ExecuteAsync1() { AssertNotPooled(); @@ -24,6 +24,19 @@ public async Task ExecuteAsync() } } + /// + /// Waits for all tasks in the specified execution branch to complete. + /// Returns immediately if the branch has already completed or was never registered. + /// + public ValueTask WaitForCompletionAsync(int executionBranchId) + { + AssertNotPooled(); + + return _activeBranches.TryGetValue(executionBranchId, out var branch) + ? branch.WaitForCompletionAsync(operationContext.RequestAborted) + : ValueTask.CompletedTask; + } + private async Task ExecuteInternalAsync(IExecutionTask?[] buffer) { RESTART: diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Pooling.cs index 8304359ae77..df826dc5f93 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Pooling.cs @@ -27,8 +27,9 @@ internal sealed partial class WorkScheduler(OperationContext operationContext) private bool _isCompleted; private bool _isInitialized; - public void Initialize(IBatchDispatcher batchDispatcher) + public void Initialize(RequestContext requestContext, IBatchDispatcher batchDispatcher) { + _requestContext = requestContext; _batchDispatcher = batchDispatcher; _batchDispatcherSession = _batchDispatcher.Subscribe(this); @@ -42,22 +43,16 @@ public void Initialize(IBatchDispatcher batchDispatcher) _isInitialized = true; } - public void Reset() - { - var batchDispatcher = _batchDispatcher; - Clear(); - Initialize(batchDispatcher); - } - public void Clear() { _work.Clear(); _serial.Clear(); _completed.Clear(); + _activeBranches.Clear(); _signal.Reset(); _result = null!; - _batchDispatcherSession.Dispose(); + _batchDispatcherSession?.Dispose(); _batchDispatcherSession = null!; _batchDispatcher = null!; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs index 6ba38989d96..574e44a2046 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs @@ -1,5 +1,3 @@ -using HotChocolate.Execution.Processing.Tasks; - namespace HotChocolate.Execution.Processing; /// @@ -7,6 +5,8 @@ namespace HotChocolate.Execution.Processing; /// internal sealed partial class WorkScheduler { + private readonly Dictionary _activeBranches = []; + /// /// Defines if the execution is completed. /// @@ -38,6 +38,7 @@ public void Register(IExecutionTask task) lock (_sync) { work.Push(task); + RegisterBranchTaskUnsafe(task.BranchId); } _signal.Set(); @@ -46,14 +47,15 @@ public void Register(IExecutionTask task) /// /// Registers work with the task backlog. /// - public void Register(ReadOnlySpan tasks) + public void Register(ReadOnlySpan tasks) { AssertNotPooled(); lock (_sync) { - foreach (var task in tasks) + for (var i = tasks.Length - 1; i >= 0; i--) { + var task = tasks[i]; task.Id = Interlocked.Increment(ref _nextId); task.IsRegistered = true; @@ -65,6 +67,8 @@ public void Register(ReadOnlySpan tasks) { _work.Push(task); } + + RegisterBranchTaskUnsafe(task.BranchId); } } @@ -80,18 +84,73 @@ public void Complete(IExecutionTask task) if (task.IsRegistered) { - // complete is thread-safe var work = task.IsSerial ? _serial : _work; + CompleteBranchTask(task.BranchId); + + // complete is thread-safe if (work.Complete()) { _completed.TryAdd(task.Id, true); + _signal.Set(); + } + } + } - lock (_sync) - { - _signal.Set(); - } + private void RegisterBranchTaskUnsafe(int branchId) + { + if (branchId == BranchTracker.SystemBranchId) + { + return; + } + + if (!_activeBranches.TryGetValue(branchId, out var branch)) + { + branch = new Branch(branchId); + _activeBranches.Add(branchId, branch); + } + + branch.RegisterTask(); + } + + private void CompleteBranchTask(int branchId) + { + if (branchId == BranchTracker.SystemBranchId) + { + return; + } + + lock (_sync) + { + if (_activeBranches.TryGetValue(branchId, out var branch) + && branch.CompleteTask()) + { + _activeBranches.Remove(branchId); + branch.Complete(); } } } + + private sealed class Branch(int id) + { + private readonly AsyncManualResetEvent _signal = new(); + private int _runningTasks; + + public int Id { get; } = id; + + public int RunningTasks => _runningTasks; + + public void RegisterTask() => _runningTasks++; + + public bool CompleteTask() => --_runningTasks == 0; + + public void Complete() => _signal.Set(); + + public async ValueTask WaitForCompletionAsync(CancellationToken cancellationToken) + { + await using var registration = cancellationToken.Register(_signal.Set); + await _signal; + cancellationToken.ThrowIfCancellationRequested(); + } + } } diff --git a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj index 33ab8f4d448..40c34203c94 100644 --- a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj +++ b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj @@ -45,7 +45,6 @@ - diff --git a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.DbRow.cs b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.DbRow.cs index 87eaa13f8c9..f6044c35b1a 100644 --- a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.DbRow.cs +++ b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.DbRow.cs @@ -24,7 +24,7 @@ internal readonly struct DbRow // 27 bits ParentRow + 5 reserved bits private readonly int _parentRow; - // 15 bits OperationReferenceId + 8 bits Flags + 9 reserved bits + // 15 bits OperationReferenceId + 9 bits Flags + 8 reserved bits private readonly int _opRefIdAndFlags; public DbRow( @@ -125,9 +125,9 @@ public OperationReferenceType OperationReferenceType /// Element metadata flags. /// /// - /// 8 bits = 256 combinations + /// 9 bits = 512 combinations /// - public ElementFlags Flags => (ElementFlags)((_opRefIdAndFlags >> 15) & 0xFF); + public ElementFlags Flags => (ElementFlags)((_opRefIdAndFlags >> 15) & 0x1FF); /// /// True for primitive JSON values (strings, numbers, booleans, null). @@ -143,7 +143,7 @@ internal enum OperationReferenceType : byte } [Flags] - internal enum ElementFlags : byte + internal enum ElementFlags : short { None = 0, IsRoot = 1, @@ -153,6 +153,7 @@ internal enum ElementFlags : byte IsExcluded = 16, IsNullable = 32, IsInvalidated = 64, - IsEncoded = 128 + IsEncoded = 128, + IsDeferred = 256 } } diff --git a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.MetaDb.cs b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.MetaDb.cs index af81d3cc647..9a53952b925 100644 --- a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.MetaDb.cs +++ b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.MetaDb.cs @@ -244,19 +244,19 @@ internal ElementFlags GetFlags(Cursor cursor) var span = _chunks[cursor.Chunk].AsSpan(cursor.ByteOffset + 16); var value = MemoryMarshal.Read(span); - return (ElementFlags)((value >> 15) & 0xFF); + return (ElementFlags)((value >> 15) & 0x1FF); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void SetFlags(Cursor cursor, ElementFlags flags) { AssertValidCursor(cursor); - Debug.Assert((byte)flags <= 255, "Flags value exceeds 8-bit limit"); + Debug.Assert((short)flags <= 511, "Flags value exceeds 9-bit limit"); var fieldSpan = _chunks[cursor.Chunk].AsSpan(cursor.ByteOffset + 16); var currentValue = MemoryMarshal.Read(fieldSpan); - var clearedValue = currentValue & unchecked((int)0xFF807FFF); // ~(0xFF << 15) + var clearedValue = currentValue & unchecked((int)0xFF007FFF); // ~(0x1FF << 15) var newValue = clearedValue | ((int)flags << 15); MemoryMarshal.Write(fieldSpan, newValue); diff --git a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.WriteTo.cs b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.WriteTo.cs index 6c97757b57e..da024baeee1 100644 --- a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.WriteTo.cs +++ b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.WriteTo.cs @@ -112,8 +112,9 @@ private void WriteObject(Cursor start, DbRow startRow) var row = document._metaDb.Get(current); Debug.Assert(row.TokenType is ElementTokenType.PropertyName); - if ((ElementFlags.IsInternal & row.Flags) == ElementFlags.IsInternal - || (ElementFlags.IsExcluded & row.Flags) == ElementFlags.IsExcluded) + var flags = row.Flags; + + if ((flags & (ElementFlags.IsInternal | ElementFlags.IsExcluded | ElementFlags.IsDeferred)) != 0) { // skip name+value current += 2; diff --git a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs index b54c9ae4178..49e16eb06a9 100644 --- a/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs +++ b/src/HotChocolate/Core/src/Types/Text/Json/ResultDocument.cs @@ -15,6 +15,7 @@ public sealed partial class ResultDocument : IDisposable private static readonly Encoding s_utf8Encoding = Encoding.UTF8; private readonly Operation _operation; private readonly ulong _includeFlags; + private readonly Path _rootPath = Path.Root; internal MetaDb _metaDb; private int _nextDataIndex; private int _rentedDataSize; @@ -37,6 +38,26 @@ public ResultDocument(Operation operation, ulong includeFlags) Data = CreateObject(Cursor.Zero, operation.RootSelectionSet); } + public ResultDocument( + Operation operation, + SelectionSet selectionSet, + Path path, + ulong includeFlags, + ulong deferFlags, + DeferUsage deferUsage) + { + ArgumentNullException.ThrowIfNull(operation); + ArgumentNullException.ThrowIfNull(selectionSet); + ArgumentNullException.ThrowIfNull(deferUsage); + + _metaDb = MetaDb.CreateForEstimatedRows(Cursor.RowsPerChunk); + _operation = operation; + _includeFlags = includeFlags; + _rootPath = path; + + Data = CreateObject(Cursor.Zero, selectionSet, includeFlags, deferFlags, deferUsage); + } + public ResultElement Data { get; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -128,14 +149,14 @@ internal Path CreatePath(Cursor current) // Stop at root via IsRoot flag. if ((_metaDb.GetFlags(current) & ElementFlags.IsRoot) == ElementFlags.IsRoot) { - return Path.Root; + return _rootPath; } Span chain = stackalloc Cursor[64]; var c = current; var written = 0; - do + while (true) { chain[written++] = c; @@ -151,9 +172,9 @@ internal Path CreatePath(Cursor current) { throw new InvalidOperationException("The path is to deep."); } - } while (true); + } - var path = Path.Root; + var path = _rootPath; var parentTokenType = ElementTokenType.StartObject; chain = chain[..written]; @@ -353,7 +374,6 @@ private void WriteRawValueTo(Utf8JsonWriter writer, DbRow row) writer.WriteRawValue(ReadRawValue(row), skipInputValidation: true); return; - // TODO : We need to handle any types. default: throw new NotSupportedException(); } @@ -428,19 +448,34 @@ private ReadOnlySpan ReadRawValue(DbRow row) [MethodImpl(MethodImplOptions.AggressiveInlining)] private ReadOnlySpan ReadLocalData(int location, int size) { - var chunkIndex = location / JsonMemory.BufferSize; - var offset = location % JsonMemory.BufferSize; + var startChunkIndex = location / JsonMemory.BufferSize; + var offsetInStartChunk = location % JsonMemory.BufferSize; // Fast path: data fits in a single chunk - if (offset + size <= JsonMemory.BufferSize) + if (offsetInStartChunk + size <= JsonMemory.BufferSize) { - return _data[chunkIndex].AsSpan(offset, size); + return _data[startChunkIndex].AsSpan(offsetInStartChunk, size); } - // Data spans chunk boundaries - this should be rare for typical JSON values - throw new NotSupportedException( - "Reading data that spans chunk boundaries as a span is not supported. " - + "Use WriteLocalDataTo for writing to an IBufferWriter instead."); + Span buffer = new byte[size]; + var bytesRead = 0; + var currentLocation = location; + + while (bytesRead < size) + { + var chunkIndex = currentLocation / JsonMemory.BufferSize; + var offsetInChunk = currentLocation % JsonMemory.BufferSize; + var chunk = _data[chunkIndex]; + + var bytesToCopyFromThisChunk = Math.Min(size - bytesRead, JsonMemory.BufferSize - offsetInChunk); + var chunkSpan = chunk.AsSpan(offsetInChunk, bytesToCopyFromThisChunk); + + chunkSpan.CopyTo(buffer[bytesRead..]); + bytesRead += bytesToCopyFromThisChunk; + currentLocation += bytesToCopyFromThisChunk; + } + + return buffer; } internal ResultElement CreateObject(Cursor parent, SelectionSet selectionSet) @@ -462,6 +497,35 @@ internal ResultElement CreateObject(Cursor parent, SelectionSet selectionSet) } } + private ResultElement CreateObject( + Cursor parent, + SelectionSet selectionSet, + ulong includeFlags, + ulong deferFlags, + DeferUsage deferUsage) + { + lock (_dataChunkLock) + { + var startObjectCursor = WriteStartObject(parent, selectionSet.Id); + + var selectionCount = 0; + foreach (var selection in selectionSet.Selections) + { + if (selection.IsIncluded(includeFlags) + && selection.IsDeferred(deferFlags) + && selection.GetPrimaryDeferUsage(deferFlags) == deferUsage) + { + WriteEmptyProperty(startObjectCursor, selection); + selectionCount++; + } + } + + WriteEndObject(startObjectCursor, selectionCount); + + return new ResultElement(this, startObjectCursor); + } + } + internal ResultElement CreateObject(Cursor parent, int propertyCount) { lock (_dataChunkLock) @@ -574,6 +638,19 @@ internal void AssignNullValue(ResultElement target) parentRow: _metaDb.GetParent(target.Cursor)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void MarkAsDeferred(ResultElement target) + { + // Selection metadata and write filters are tracked on the property row. + var propertyCursor = target.Cursor.AddRows(-1); + var elementTokenType = _metaDb.GetElementTokenType(propertyCursor, resolveReferences: false); + + CheckExpectedType(ElementTokenType.PropertyName, elementTokenType); + + var flags = _metaDb.GetFlags(propertyCursor); + _metaDb.SetFlags(propertyCursor, flags | ElementFlags.IsDeferred); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private int ClaimDataSpace(int size) { diff --git a/src/HotChocolate/Core/src/Types/Text/Json/ResultElement.cs b/src/HotChocolate/Core/src/Types/Text/Json/ResultElement.cs index eff92f7166c..a15560dd6c3 100644 --- a/src/HotChocolate/Core/src/Types/Text/Json/ResultElement.cs +++ b/src/HotChocolate/Core/src/Types/Text/Json/ResultElement.cs @@ -1143,6 +1143,13 @@ public void SetNumberValue(decimal value) _parent.AssignNumberValue(this, buffer[..bytesWritten]); } + internal void MarkAsDeferred() + { + CheckValidInstance(); + + _parent.MarkAsDeferred(this); + } + /// /// Writes this element as JSON to the specified buffer writer. /// diff --git a/src/HotChocolate/Core/src/Types/Types/Introspection/__InputValue.cs b/src/HotChocolate/Core/src/Types/Types/Introspection/__InputValue.cs index 6535b364296..d1d11b0e10c 100644 --- a/src/HotChocolate/Core/src/Types/Types/Introspection/__InputValue.cs +++ b/src/HotChocolate/Core/src/Types/Types/Introspection/__InputValue.cs @@ -76,7 +76,7 @@ public static object IsDeprecated(IResolverContext context) public static object? DefaultValue(IResolverContext context) { var field = context.Parent(); - return field.DefaultValue.IsNull() ? null : field.DefaultValue!.Print(); + return field.DefaultValue.IsNull() ? null : field.DefaultValue!.ToString(indented: false); } public static object AppliedDirectives(IResolverContext context) diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlagsHelper.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlagsHelper.cs index 9bce97065b6..e340bf0cc7d 100644 --- a/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlagsHelper.cs +++ b/src/HotChocolate/Core/src/Types/Types/Pagination/ConnectionFlagsHelper.cs @@ -22,7 +22,7 @@ public static ConnectionFlags GetConnectionFlags(IResolverContext context) private static ConnectionFlags CreateConnectionFlags(IResolverContext context) { - if (context.Selection.Field.Flags.HasFlag(CoreFieldFlags.Connection)) + if (!context.Selection.Field.Flags.HasFlag(CoreFieldFlags.Connection)) { return ConnectionFlags.None; } diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs index 43e768d69cd..9c034024c51 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/ScalarType.cs @@ -5,6 +5,7 @@ using HotChocolate.Text.Json; using HotChocolate.Types.Descriptors.Configurations; using HotChocolate.Utilities; +using static HotChocolate.Utilities.ThrowHelper; using static HotChocolate.Serialization.SchemaDebugFormatter; namespace HotChocolate.Types; @@ -121,6 +122,8 @@ public bool IsAssignableFrom(ITypeDefinition type) /// public virtual bool IsValueCompatible(IValueNode valueLiteral) { + ArgumentNullException.ThrowIfNull(valueLiteral); + if ((SerializationType & ScalarSerializationType.String) == ScalarSerializationType.String && valueLiteral is { Kind: SyntaxKind.StringValue }) { @@ -163,6 +166,11 @@ public virtual bool IsValueCompatible(IValueNode valueLiteral) /// public virtual bool IsValueCompatible(JsonElement inputValue) { + if (inputValue.ValueKind is JsonValueKind.Undefined) + { + throw new ArgumentException("Undefined JSON value kind.", nameof(inputValue)); + } + if ((SerializationType & ScalarSerializationType.String) == ScalarSerializationType.String && inputValue.ValueKind == JsonValueKind.String) { @@ -182,7 +190,7 @@ public virtual bool IsValueCompatible(JsonElement inputValue) } if ((SerializationType & ScalarSerializationType.Boolean) == ScalarSerializationType.Boolean - && inputValue.ValueKind == JsonValueKind.True) + && (inputValue.ValueKind == JsonValueKind.True || inputValue.ValueKind == JsonValueKind.False)) { return true; } @@ -214,6 +222,67 @@ public virtual bool IsValueCompatible(JsonElement inputValue) /// public abstract IValueNode ValueToLiteral(object runtimeValue); + /// + /// Converts a JSON input value into a GraphQL literal (AST value node). + /// + /// + /// The JSON input value to convert. + /// + /// + /// Provides access to the coercion context, including features like memory builders + /// for efficient JSON parsing. + /// + /// + /// Returns a GraphQL literal representation (AST value node) of the input value. + /// + /// + /// Unable to convert the given into a literal. + /// + public virtual IValueNode InputValueToLiteral(JsonElement inputValue, IFeatureProvider context) + { + if (!IsValueCompatible(inputValue)) + { + throw CreateInputValueToLiteralError(inputValue, context); + } + + // We try to get a memory builder from the context and assign it to our JsonValueParser + // which rewrites the json into a GraphQL value node. + // The memory builder allows us to store the actual values as UTF-8 string. + var utf8MemoryBuilder = context.Features.Get(); + var builderExistedBeforeParsing = utf8MemoryBuilder is not null; + + var parser = new JsonValueParser(doNotSeal: true) { _memory = utf8MemoryBuilder }; + var literal = parser.Parse(inputValue); + + // If no builder existed so far but we now created one by rewriting the JSON value, + // then we store the JSON builder on the context so that it can be picked up and reused by other values + // in the current coercion of input values. + if (!builderExistedBeforeParsing && utf8MemoryBuilder is not null) + { + context.Features.Set(utf8MemoryBuilder); + } + + return literal; + } + + /// + /// Creates the exception to throw when + /// encounters an incompatible input value. + /// + /// + /// The incompatible input value. + /// + /// + /// The coercion context. + /// + /// + /// A describing the coercion failure. + /// + protected virtual LeafCoercionException CreateInputValueToLiteralError( + JsonElement inputValue, + IFeatureProvider context) + => Scalar_Cannot_ConvertValueToLiteral(this, inputValue); + /// /// Returns a string that represents the current . /// diff --git a/src/HotChocolate/Core/src/Types/Utilities/DictionaryToJsonDocumentConverter.cs b/src/HotChocolate/Core/src/Types/Utilities/DictionaryToJsonDocumentConverter.cs index 546d806ebbe..e3171b67db0 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/DictionaryToJsonDocumentConverter.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/DictionaryToJsonDocumentConverter.cs @@ -20,7 +20,7 @@ public static JsonElement FromDictionary(IReadOnlyDictionary di { using var buffer = new PooledArrayWriter(); var writer = new JsonWriter(buffer, s_options); - JsonValueFormatter.WriteDictionary(writer, dictionary, s_jsonSerializerOptions, JsonNullIgnoreCondition.None); + JsonValueFormatter.WriteDictionary(writer, dictionary, s_jsonSerializerOptions); var jsonReader = new Utf8JsonReader(buffer.WrittenSpan); return JsonElement.ParseValue(ref jsonReader); @@ -47,7 +47,7 @@ public static JsonElement FromList(IReadOnlyList list) { using var buffer = new PooledArrayWriter(); var writer = new JsonWriter(buffer, s_options); - JsonValueFormatter.WriteValue(writer, list, s_jsonSerializerOptions, JsonNullIgnoreCondition.None); + JsonValueFormatter.WriteValue(writer, list, s_jsonSerializerOptions); var jsonReader = new Utf8JsonReader(buffer.WrittenSpan); return JsonElement.ParseValue(ref jsonReader); diff --git a/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs b/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs index 3590225cadc..c66cc02803e 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs @@ -672,6 +672,20 @@ public static LeafCoercionException Scalar_Cannot_ConvertValueToLiteral( scalarType); } + public static LeafCoercionException Scalar_Cannot_ConvertInputValueToLiteral( + ITypeDefinition scalarType, + JsonElement inputValue) + { + return new LeafCoercionException( + ErrorBuilder.New() + .SetMessage( + TypeResources.Scalar_Cannot_ConvertValueToLiteral, + scalarType.Name, + inputValue.ValueKind) + .Build(), + scalarType); + } + public static LeafCoercionException Scalar_Cannot_CoerceOutputValue( ITypeDefinition scalarType, object runtimeValue) diff --git a/src/HotChocolate/Core/src/Validation/ErrorHelper.cs b/src/HotChocolate/Core/src/Validation/ErrorHelper.cs index 216814019e0..08a31c05f2b 100644 --- a/src/HotChocolate/Core/src/Validation/ErrorHelper.cs +++ b/src/HotChocolate/Core/src/Validation/ErrorHelper.cs @@ -19,7 +19,7 @@ public static IError DeferAndStreamNotAllowedOnMutationOrSubscriptionRoot( => ErrorBuilder.New() .SetMessage(Resources.ErrorHelper_DeferAndStreamNotAllowedOnMutationOrSubscriptionRoot) .AddLocation(selection) - .SpecifiedBy("sec-Defer-And-Stream-Directives-Are-Used-On-Valid-Root-Field") + .SpecifiedBy("sec-Defer-And-Stream-Directives-Are-Used-On-Valid-Root-Field", rfc: 1110) .Build(); extension(DocumentValidatorContext context) @@ -183,7 +183,7 @@ public IError ArgumentValueIsNotCompatible( .AddLocation(value) .SetPath(context.CreateErrorPath()) .SetExtension("argument", node.Name.Value) - .SetExtension("argumentValue", value.ToString()) + .SetExtension("argumentValue", value.ToString(indented: false)) .SetExtension("locationType", locationType.FullTypeName()) .SpecifiedBy("sec-Values-of-Correct-Type") .Build(); diff --git a/src/HotChocolate/Core/test/Directory.Build.props b/src/HotChocolate/Core/test/Directory.Build.props index 6ae29fd74ba..103bc78ee36 100644 --- a/src/HotChocolate/Core/test/Directory.Build.props +++ b/src/HotChocolate/Core/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/Core/test/Execution.Tests/Errors/__snapshots__/ErrorHandlerTests.FilterOnlyNullRefExceptions.json b/src/HotChocolate/Core/test/Execution.Tests/Errors/__snapshots__/ErrorHandlerTests.FilterOnlyNullRefExceptions.json index 4cfef53f3ea..d9767e95a5e 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Errors/__snapshots__/ErrorHandlerTests.FilterOnlyNullRefExceptions.json +++ b/src/HotChocolate/Core/test/Execution.Tests/Errors/__snapshots__/ErrorHandlerTests.FilterOnlyNullRefExceptions.json @@ -1,11 +1,5 @@ { "errors": [ - { - "message": "Unexpected Execution Error", - "path": [ - "foo" - ] - }, { "message": "Unexpected Execution Error", "path": [ @@ -14,6 +8,12 @@ "extensions": { "code": "NullRef" } + }, + { + "message": "Unexpected Execution Error", + "path": [ + "foo" + ] } ], "data": { diff --git a/src/HotChocolate/Core/test/Execution.Tests/Integration/StarWarsCodeFirst/StarWarsCodeFirstTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Integration/StarWarsCodeFirst/StarWarsCodeFirstTests.cs index a31263101d3..07b64a243d6 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Integration/StarWarsCodeFirst/StarWarsCodeFirstTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Integration/StarWarsCodeFirst/StarWarsCodeFirstTests.cs @@ -11,33 +11,45 @@ public class StarWarsCodeFirstTests(ITestOutputHelper output) public async Task Schema() { // arrange + var snapshot = Snapshot.Create(); var executor = await CreateExecutorAsync(); // act var schema = executor.Schema.ToString(); + snapshot.Add(schema); // assert - schema.MatchSnapshot(); + snapshot.Match(); } [Fact] public async Task GetHeroName() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { hero { name } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgFieldExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { hero { @@ -50,14 +62,20 @@ await ExpectValid( } } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgFieldArgumentExample1() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { human(id: "1000") { @@ -65,14 +83,20 @@ await ExpectValid( height } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgFieldArgumentExample2() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { human(id: "1000") { @@ -80,14 +104,20 @@ await ExpectValid( height(unit: FOOT) } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgAliasExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { empireHero: hero(episode: EMPIRE) { @@ -97,14 +127,20 @@ await ExpectValid( name } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgFragmentExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { leftComparison: hero(episode: EMPIRE) { @@ -124,14 +160,20 @@ fragment comparisonFields on Character { } } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgOperationNameExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query HeroNameAndFriends { hero { @@ -143,14 +185,20 @@ query HeroNameAndFriends { } } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgVariableExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query HeroNameAndFriends($episode: Episode) { hero(episode: $episode) { @@ -168,14 +216,20 @@ query HeroNameAndFriends($episode: Episode) { { "episode": "JEDI" } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgVariableWithDefaultValueExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query HeroNameAndFriends($episode: Episode = JEDI) { hero(episode: $episode) { @@ -187,14 +241,20 @@ query HeroNameAndFriends($episode: Episode = JEDI) { } } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgDirectiveIncludeExample1() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query Hero($episode: Episode, $withFriends: Boolean!) { hero(episode: $episode) { @@ -213,14 +273,20 @@ friends @include(if: $withFriends) { "episode": "JEDI", "withFriends": false } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgDirectiveIncludeExample2() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query Hero($episode: Episode, $withFriends: Boolean!) { hero(episode: $episode) { @@ -239,14 +305,20 @@ friends @include(if: $withFriends) { "episode": "JEDI", "withFriends": true } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgDirectiveSkipExample1() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query Hero($episode: Episode, $withFriends: Boolean!) { hero(episode: $episode) { @@ -265,14 +337,20 @@ friends @skip(if: $withFriends) { "episode": "JEDI", "withFriends": false } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgDirectiveSkipExample2() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query Hero($episode: Episode, $withFriends: Boolean!) { hero(episode: $episode) { @@ -291,14 +369,20 @@ friends @skip(if: $withFriends) { "episode": "JEDI", "withFriends": true } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgDirectiveSkipExample1WithPlainClrVarTypes() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query Hero($episode: Episode, $withFriends: Boolean!) { hero(episode: $episode) { @@ -317,14 +401,20 @@ friends @skip(if: $withFriends) { "episode": "JEDI", "withFriends": false } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgMutationExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { createReview(episode: $ep, review: $review) { @@ -342,14 +432,20 @@ mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { "commentary": "This is a great movie!" } } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgMutationIgnoreAdditionalInputFieldsExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { createReview(episode: $ep, review: $review) { @@ -374,14 +470,20 @@ mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { "commentary": "This is a great movie!" } } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgTwoMutationsExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ mutation CreateReviewForEpisode($ep: Episode!, $ep2: Episode!, $review: ReviewInput!) { createReview(episode: $ep, review: $review) { @@ -404,14 +506,20 @@ mutation CreateReviewForEpisode($ep: Episode!, $ep2: Episode!, $review: ReviewIn "commentary": "This is a great movie!" } } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgMutationExample_With_ValueVariables() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ mutation CreateReviewForEpisode( $ep: Episode! @@ -432,14 +540,20 @@ mutation CreateReviewForEpisode( "stars": 5, "commentary": "This is a great movie!" } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgInlineFragmentExample1() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query HeroForEpisode($ep: Episode!) { hero(episode: $ep) { @@ -458,14 +572,20 @@ ... on Human { { "ep": "JEDI" } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgInlineFragmentExample2() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query HeroForEpisode($ep: Episode!) { hero(episode: $ep) { @@ -484,14 +604,20 @@ ... on Human { { "ep": "EMPIRE" } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task GraphQLOrgMetaFieldAndUnionExample() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { search(text: "an") { @@ -510,14 +636,20 @@ ... on Starship { } } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task NonNullListVariableValues() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query op($ep: [Episode!]!) { heroes(episodes: $ep) { @@ -530,14 +662,20 @@ query op($ep: [Episode!]!) { { "ep": ["EMPIRE"] } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task ConditionalInlineFragment() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { heroes(episodes: [EMPIRE]) { @@ -547,14 +685,20 @@ ... @include(if: true) { } } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task NonNullEnumsSerializeCorrectlyFromVariables() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query getHero($episode: Episode!) { hero(episode: $episode) { @@ -567,28 +711,40 @@ query getHero($episode: Episode!) { { "episode": "NEW_HOPE" } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task EnumValueIsCoercedToListValue() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ { heroes(episodes: EMPIRE) { name } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task TypeNameFieldIsCorrectlyExecutedOnInterfaces() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query foo { hero(episode: NEW_HOPE) { @@ -618,14 +774,20 @@ ... on Droid { } } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task Execute_ListWithNullValues_ResultContainsNullElement() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query { human(id: "1001") { @@ -636,14 +798,17 @@ await ExpectValid( } } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task SubscribeToReview() { // arrange + var snapshot = Snapshot.Create(); var executor = await CreateExecutorAsync(output: output); // act @@ -659,7 +824,6 @@ public async Task SubscribeToReview() var results = subscriptionResult.ReadResultsAsync(); - // assert await executor.ExecuteAsync( """ mutation { @@ -682,13 +846,17 @@ await executor.ExecuteAsync( } } - eventResult?.MatchSnapshot(); + snapshot.Add(eventResult); + + // assert + snapshot.Match(); } [Fact] public async Task SubscribeToReview_WithInlineFragment() { // arrange + var snapshot = Snapshot.Create(); var executor = await CreateExecutorAsync(output: output); // act @@ -704,7 +872,6 @@ ... on Review { } """); - // assert await executor.ExecuteAsync( """ mutation { @@ -727,13 +894,17 @@ await executor.ExecuteAsync( } } - eventResult?.MatchSnapshot(); + snapshot.Add(eventResult); + + // assert + snapshot.Match(); } [Fact] public async Task SubscribeToReview_FragmentDefinition() { // arrange + var snapshot = Snapshot.Create(); var executor = await CreateExecutorAsync(output: output); // act @@ -751,7 +922,6 @@ fragment SomeFrag on Review { } """); - // assert await executor.ExecuteAsync( """ mutation { @@ -774,13 +944,17 @@ await executor.ExecuteAsync( } } - eventResult?.MatchSnapshot(); + snapshot.Add(eventResult); + + // assert + snapshot.Match(); } [Fact] public async Task SubscribeToReview_With_Variables() { // arrange + var snapshot = Snapshot.Create(); var executor = await CreateExecutorAsync(); // act @@ -804,7 +978,6 @@ public async Task SubscribeToReview_With_Variables() """) .Build()); - // assert await executor.ExecuteAsync( """ mutation { @@ -827,7 +1000,10 @@ await executor.ExecuteAsync( } } - eventResult?.MatchSnapshot(); + snapshot.Add(eventResult); + + // assert + snapshot.Match(); } /// @@ -1106,7 +1282,11 @@ await ExpectValid( [Theory] public async Task Include_With_Literal(string ifValue) { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(postFix: ifValue); + + // act + snapshot.Add(await ExpectValid( $$""" { human(id: "1000") { @@ -1114,8 +1294,10 @@ name @include(if: {{ifValue}}) height } } - """) - .MatchSnapshotAsync(postFix: ifValue); + """)); + + // assert + snapshot.Match(); } [InlineData(true)] @@ -1123,7 +1305,11 @@ name @include(if: {{ifValue}}) [Theory] public async Task Include_With_Variable(bool ifValue) { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(postFix: ifValue.ToString()); + + // act + snapshot.Add(await ExpectValid( """ query ($if: Boolean!) { human(id: "1000") { @@ -1137,8 +1323,10 @@ name @include(if: $if) { "if": {{ifValue.ToString().ToLowerInvariant()}} } - """)) - .MatchSnapshotAsync(postFix: ifValue); + """))); + + // assert + snapshot.Match(); } [InlineData("true")] @@ -1146,7 +1334,11 @@ name @include(if: $if) [Theory] public async Task Skip_With_Literal(string ifValue) { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(postFix: ifValue); + + // act + snapshot.Add(await ExpectValid( $$""" { human(id: "1000") { @@ -1154,8 +1346,10 @@ name @skip(if: {{ifValue}}) height } } - """) - .MatchSnapshotAsync(postFix: ifValue); + """)); + + // assert + snapshot.Match(); } [InlineData(true)] @@ -1163,7 +1357,11 @@ name @skip(if: {{ifValue}}) [Theory] public async Task Skip_With_Variable(bool ifValue) { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(postFix: ifValue.ToString()); + + // act + snapshot.Add(await ExpectValid( """ query ($if: Boolean!) { human(id: "1000") { @@ -1177,14 +1375,20 @@ name @skip(if: $if) { "if": {{ifValue.ToString().ToLowerInvariant()}} } - """)) - .MatchSnapshotAsync(postFix: ifValue); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task SkipAll() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query ($if: Boolean!) { human(id: "1000") @skip(if: $if) { @@ -1198,14 +1402,20 @@ await ExpectValid( { "if": true } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task SkipAll_Default_False() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query ($if: Boolean! = false) { human(id: "1000") @skip(if: $if) { @@ -1213,14 +1423,20 @@ await ExpectValid( height } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task SkipAll_Default_True() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query ($if: Boolean! = true) { human(id: "1000") @skip(if: $if) { @@ -1228,14 +1444,20 @@ await ExpectValid( height } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task SkipAllSecondLevelFields() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query ($if: Boolean!) { human(id: "1000") { @@ -1248,14 +1470,20 @@ name @skip(if: $if) { "if": true } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } [Fact] public async Task Ensure_Type_Introspection_Returns_Null_If_Type_Not_Found() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query { a: __type(name: "Foo") { @@ -1265,42 +1493,76 @@ await ExpectValid( name } } - """) - .MatchSnapshotAsync(); + """)); + + // assert + snapshot.Match(); } [Fact] public async Task Ensure_Benchmark_Query_GetHeroQuery() { + // arrange + var snapshot = Snapshot.Create(); var query = FileResource.Open("GetHeroQuery.graphql"); - await ExpectValid(query).MatchSnapshotAsync(); + + // act + snapshot.Add(await ExpectValid(query)); + + // assert + snapshot.Match(); } [Fact] public async Task Ensure_Benchmark_Query_GetHeroWithFriendsQuery() { + // arrange + var snapshot = Snapshot.Create(); var query = FileResource.Open("GetHeroWithFriendsQuery.graphql"); - await ExpectValid(query).MatchSnapshotAsync(); + + // act + snapshot.Add(await ExpectValid(query)); + + // assert + snapshot.Match(); } [Fact] public async Task Ensure_Benchmark_Query_GetTwoHeroesWithFriendsQuery() { + // arrange + var snapshot = Snapshot.Create(); var query = FileResource.Open("GetTwoHeroesWithFriendsQuery.graphql"); - await ExpectValid(query).MatchSnapshotAsync(); + + // act + snapshot.Add(await ExpectValid(query)); + + // assert + snapshot.Match(); } [Fact] public async Task Ensure_Benchmark_Query_LargeQuery() { + // arrange + var snapshot = Snapshot.Create(); var query = FileResource.Open("LargeQuery.graphql"); - await ExpectValid(query).MatchSnapshotAsync(); + + // act + snapshot.Add(await ExpectValid(query)); + + // assert + snapshot.Match(); } [Fact] public async Task NestedFragmentsWithNestedObjectFieldsAndSkip() { - await ExpectValid( + // arrange + var snapshot = Snapshot.Create(); + + // act + snapshot.Add(await ExpectValid( """ query ($if: Boolean!) { human(id: "1000") { @@ -1349,7 +1611,9 @@ fragment Human3 on Human { { "if": true } - """)) - .MatchSnapshotAsync(); + """))); + + // assert + snapshot.Match(); } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/TypeConverterTests.ExceptionPropagation.cs b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/TypeConverterTests.ExceptionPropagation.cs index 19a9252ca15..fe9a607ea91 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/TypeConverterTests.ExceptionPropagation.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/TypeConverterTests.ExceptionPropagation.cs @@ -76,7 +76,10 @@ public async Task Exception_IsAvailableInErrorFilter(TestCase testCase) .AddGraphQLServer() .AddQueryType() .AddDirectiveType() - .AddTypeConverter(x => x == "ok" ? new BrokenType(1) : throw new CustomIdSerializationException("Boom")) + .AddTypeConverter( + x => x == "ok" + ? new BrokenType(1) + : throw new CustomIdSerializationException("Boom")) .BindRuntimeType() .ModifyRequestOptions(x => x.IncludeExceptionDetails = true) .AddErrorFilter(x => @@ -116,9 +119,13 @@ public async Task Exception_IsAvailableInErrorFilter_Mutation(TestCase testCase) Exception? caughtException = null; var executor = await new ServiceCollection() .AddGraphQLServer() + .AddQueryType() .AddMutationType() .AddDirectiveType() - .AddTypeConverter(x => x == "ok" ? new BrokenType(1) : throw new CustomIdSerializationException("Boom")) + .AddTypeConverter( + x => x == "ok" + ? new BrokenType(1) + : throw new CustomIdSerializationException("Boom")) .BindRuntimeType() .ModifyRequestOptions(x => x.IncludeExceptionDetails = true) .ModifyOptions(x => x.StrictValidation = false) @@ -161,6 +168,7 @@ public async Task Exception_IsAvailableInErrorFilter_Mutation_WithMutationConven Exception? caughtException = null; var executor = await new ServiceCollection() .AddGraphQLServer() + .AddQueryType() .AddMutationType() .AddMutationConventions() .AddDirectiveType() @@ -175,6 +183,8 @@ public async Task Exception_IsAvailableInErrorFilter_Mutation_WithMutationConven }) .BuildRequestExecutorAsync(); + var s = executor.Schema.ToString(); + // act var requestBuilder = OperationRequestBuilder .New() @@ -280,11 +290,10 @@ public class SomeQuery public string? FieldWithListOfScalarsInput(List arg) => null; public string? FieldWithObjectWithListOfScalarsInput(ObjectWithListOfIds arg) => null; public string? FieldWithNestedObjectInput(NestedObject arg) => null; - // ReSharper disable once MemberHidesStaticFromOuterClass public string? FieldWithListOfObjectsInput(ListOfObjectsInput arg) => null; public string? FieldWithNonNullScalarInput([GraphQLNonNullType] BrokenType arg) => null; public string? Echo(string arg) => null; - public NestedObject? NestedObjectOutput => null; + public NestedObject? NestedObjectOutput => new NestedObject(new ObjectWithId(new BrokenType(1))); } public class SomeQueryConventionFriendlyQueryType @@ -307,7 +316,7 @@ public class SomeQueryConventionFriendlyQueryType [Error] public ObjectWithId? Echo(string arg) => null; [Error] - public NestedObject? NestedObjectOutput => null; + public NestedObject? NestedObjectOutput => new NestedObject(new ObjectWithId(new BrokenType(1))); } public class CustomIdSerializationException(string message) : Exception(message) diff --git a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_Mutation_NestedDirectiveInput.snap b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_Mutation_NestedDirectiveInput.snap index 9e2d4ccb7fc..45759d08ac1 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_Mutation_NestedDirectiveInput.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_Mutation_NestedDirectiveInput.snap @@ -8,19 +8,23 @@ "column": 54 } ], + "extensions": { + "code": "HC0001", + "coordinate": "boom.arg" + } + }, + { + "message": "Cannot return null for non-nullable field.", "path": [ "nestedObjectOutput", - "inner", - "id" + "inner" ], "extensions": { - "code": "HC0001", - "coordinate": "boom.arg", - "exception": { - "message": "Boom", - "stackTrace": "Test" - } + "code": "HC0018" } } - ] + ], + "data": { + "nestedObjectOutput": null + } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_Mutation_WithMutationConventions_NestedDirectiveInput.snap b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_Mutation_WithMutationConventions_NestedDirectiveInput.snap index eb4e0bb639b..8b7229c6d2a 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_Mutation_WithMutationConventions_NestedDirectiveInput.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_Mutation_WithMutationConventions_NestedDirectiveInput.snap @@ -8,20 +8,26 @@ "column": 70 } ], + "extensions": { + "code": "HC0001", + "coordinate": "boom.arg" + } + }, + { + "message": "Cannot return null for non-nullable field.", "path": [ "nestedObjectOutput", "nestedObject", - "inner", - "id" + "inner" ], "extensions": { - "code": "HC0001", - "coordinate": "boom.arg", - "exception": { - "message": "Boom", - "stackTrace": "Test" - } + "code": "HC0018" } } - ] + ], + "data": { + "nestedObjectOutput": { + "nestedObject": null + } + } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_NestedDirectiveInput.snap b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_NestedDirectiveInput.snap index 22ca9203d52..88ff7f8df06 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_NestedDirectiveInput.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_NestedDirectiveInput.snap @@ -8,19 +8,23 @@ "column": 46 } ], + "extensions": { + "code": "HC0001", + "coordinate": "boom.arg" + } + }, + { + "message": "Cannot return null for non-nullable field.", "path": [ "nestedObjectOutput", - "inner", - "id" + "inner" ], "extensions": { - "code": "HC0001", - "coordinate": "boom.arg", - "exception": { - "message": "Boom", - "stackTrace": "Test" - } + "code": "HC0018" } } - ] + ], + "data": { + "nestedObjectOutput": null + } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_WithQueryConventions_NestedDirectiveInput.snap b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_WithQueryConventions_NestedDirectiveInput.snap index e83aca75a15..1eeced3e4d7 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_WithQueryConventions_NestedDirectiveInput.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/__snapshots__/TypeConverterTests.Exception_IsAvailableInErrorFilter_WithQueryConventions_NestedDirectiveInput.snap @@ -8,19 +8,21 @@ "column": 68 } ], + "extensions": { + "code": "HC0001", + "coordinate": "boom.arg" + } + }, + { + "message": "Cannot return null for non-nullable field.", "path": [ "nestedObjectOutput", - "inner", - "id" + "inner" ], "extensions": { - "code": "HC0001", - "coordinate": "boom.arg", - "exception": { - "message": "Boom", - "stackTrace": "Test" - } + "code": "HC0018" } } - ] + ], + "data": null } diff --git a/src/HotChocolate/Core/test/Execution.Tests/MiddlewareContextTests.cs b/src/HotChocolate/Core/test/Execution.Tests/MiddlewareContextTests.cs index 79fe1136c93..bb438f6ae4f 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/MiddlewareContextTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/MiddlewareContextTests.cs @@ -334,11 +334,9 @@ public async Task SetResultContextData_Delegate_IntValue_When_Deferred() continue; } - // TODO : FIX THIS TEST - throw new InvalidOperationException(); - // Assert.NotNull(queryResult.Incremental?[0].ContextData); - // Assert.True(queryResult.Incremental[0].ContextData!.TryGetValue("abc", out var value)); - // Assert.Equal(2, value); + Assert.NotNull(queryResult.ContextData); + Assert.True(queryResult.ContextData.TryGetValue("abc", out var value)); + Assert.Equal(2, value); } } @@ -450,7 +448,8 @@ public async Task SetResultExtensionData_With_ObjectValue() """); } - [Fact] + // TODO : FIX BEFORE V16 RELEASE + [Fact(Skip = "We need to research how we deal with extensions")] public async Task SetResultExtensionData_With_ObjectValue_WhenDeferred() { using var cts = new CancellationTokenSource(5000); diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/OperationCompilerTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Processing/OperationCompilerTests.cs index 67dda972039..9bf5bf5f017 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Processing/OperationCompilerTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/OperationCompilerTests.cs @@ -658,6 +658,547 @@ fragment Foo on Droid { MatchSnapshot(document, operation); } + // ------------------------------------------------------------------ + // Defer deduplication tests + // Based on graphql-spec PR 1110 (incremental delivery) deduplication + // semantics and the graphql-js reference implementation tests. + // ------------------------------------------------------------------ + + [Fact] + public void Defer_Inline_Fragment_Deduplication_Non_Deferred_Wins() + { + // arrange + // A field that appears both inside and outside @defer should NOT + // be marked as deferred. The non-deferred usage wins per spec + // GetFilteredDeferUsageSet: if any fieldDetails has no deferUsage, + // the filtered set is cleared. + var schema = SchemaBuilder.New() + .AddStarWarsTypes() + .Create(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero(episode: EMPIRE) { + name + ... @defer { + name + id + } + } + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public void Defer_Fragment_Spread_Deferred_And_Non_Deferred() + { + // arrange + // Same named fragment used both with @defer and without. + // Non-deferred usage wins regardless of order. Per spec, + // if a fragment spread is visited without @defer first, + // the name is added to visitedFragments and the deferred + // spread is skipped. If deferred is first, the non-deferred + // revisit overrides. + var schema = SchemaBuilder.New() + .AddStarWarsTypes() + .Create(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero(episode: EMPIRE) { + ...CharFields @defer(label: "DeferCharFields") + ...CharFields + } + } + + fragment CharFields on Character { + name + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public void Defer_Fragment_Spread_Non_Deferred_Then_Deferred() + { + // arrange + // Same fragment spread used non-deferred first, then deferred. + // Non-deferred usage wins per spec. + var schema = SchemaBuilder.New() + .AddStarWarsTypes() + .Create(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero(episode: EMPIRE) { + ...CharFields + ...CharFields @defer(label: "DeferCharFields") + } + } + + fragment CharFields on Character { + name + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public void Defer_Nested_Inline_Fragments() + { + // arrange + // Two levels of nested @defer. Both fields should be deferred. + // Per spec, nested defers create a parent chain of DeferUsages. + var schema = SchemaBuilder.New() + .AddStarWarsTypes() + .Create(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero(episode: EMPIRE) { + ... @defer { + name + ... @defer { + id + } + } + } + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public void Defer_Nested_Field_Overlap_Parent_And_Child() + { + // arrange + // Field "name" appears in both parent and child @defer. + // Per spec GetFilteredDeferUsageSet, when a deferUsage's + // ancestor is also in the set, the child is removed. + // The field is still deferred (delivered with the parent defer). + var schema = SchemaBuilder.New() + .AddStarWarsTypes() + .Create(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero(episode: EMPIRE) { + ... @defer { + name + ... @defer { + name + id + } + } + } + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public void Defer_Multiple_Nested_Same_Fragment() + { + // arrange + // Multiple nested defers referencing the same fragment. + // Demonstrates deduplication across multiple defer levels. + // Per graphql-js test: "Can deduplicate multiple defers on + // the same object" + var schema = SchemaBuilder.New() + .AddStarWarsTypes() + .Create(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero(episode: EMPIRE) { + ... @defer { + ...CharFields + ... @defer { + ...CharFields + ... @defer { + ...CharFields + } + } + } + } + } + + fragment CharFields on Character { + name + id + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public void Defer_If_False_Not_Deferred() + { + // arrange + // @defer(if: false) should not produce deferred selections. + // Per spec, when if argument is false, the defer directive + // is ignored and no DeferUsage is created. + var schema = SchemaBuilder.New() + .AddStarWarsTypes() + .Create(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero(episode: EMPIRE) { + ... @defer(if: false) { + name + id + } + } + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public async Task Defer_Different_Branches_Overlapping_Fields() + { + // arrange + // Fields present in both the initial payload and a deferred + // fragment. Only fields unique to the defer should be deferred. + // Mirrors graphql-js test: "Deduplicates fields present in the + // initial payload" + var schema = + await new ServiceCollection() + .AddGraphQLServer() + .AddDocumentFromString( + """ + type Query { + foo: Foo + } + + type Foo { + bar: Bar + baz: String + } + + type Bar { + a: String + b: String + } + """) + .UseField(next => next) + .BuildSchemaAsync(); + + var document = Utf8GraphQLParser.Parse( + """ + { + foo { + bar { + a + } + ... @defer { + bar { + b + } + baz + } + } + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public async Task Defer_Different_Branches_Non_Overlapping_Levels() + { + // arrange + // Two defers at different tree levels with overlapping field + // paths. Mirrors graphql-js test: "Deduplicate fields with + // deferred fragments in different branches at multiple + // non-overlapping levels" + var schema = + await new ServiceCollection() + .AddGraphQLServer() + .AddDocumentFromString( + """ + type Query { + a: A + g: G + } + + type A { + b: B + } + + type B { + c: C + e: E + } + + type C { + d: String + } + + type E { + f: String + } + + type G { + h: String + } + """) + .UseField(next => next) + .BuildSchemaAsync(); + + var document = Utf8GraphQLParser.Parse( + """ + { + a { + b { + c { + d + } + ... @defer { + e { + f + } + } + } + } + ... @defer { + a { + b { + e { + f + } + } + } + g { + h + } + } + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public async Task Defer_Nested_With_Parent_Field_Deduplication() + { + // arrange + // When a field appears in a parent @defer and also in a nested + // child @defer, the field should only be delivered with the + // parent defer. Mirrors graphql-js test: "Deduplicates fields + // present in a parent defer payload" + var schema = + await new ServiceCollection() + .AddGraphQLServer() + .AddDocumentFromString( + """ + type Query { + hero: Hero + } + + type Hero { + nestedObject: NestedObject + } + + type NestedObject { + deeperObject: DeeperObject + } + + type DeeperObject { + foo: String + bar: String + } + """) + .UseField(next => next) + .BuildSchemaAsync(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero { + ... @defer { + nestedObject { + deeperObject { + foo + ... @defer { + foo + bar + } + } + } + } + } + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + + [Fact] + public async Task Defer_Multiple_Levels_Field_Deduplication() + { + // arrange + // Deduplication across three levels: initial has foo, first + // defer adds bar, second defer adds baz, third defer adds bak. + // Mirrors graphql-js test: "Deduplicates fields with deferred + // fragments at multiple levels" + var schema = + await new ServiceCollection() + .AddGraphQLServer() + .AddDocumentFromString( + """ + type Query { + hero: Hero + } + + type Hero { + nestedObject: NestedObject + } + + type NestedObject { + deeperObject: DeeperObject + } + + type DeeperObject { + foo: String + bar: String + baz: String + bak: String + } + """) + .UseField(next => next) + .BuildSchemaAsync(); + + var document = Utf8GraphQLParser.Parse( + """ + { + hero { + nestedObject { + deeperObject { + foo + } + } + ... @defer { + nestedObject { + deeperObject { + foo + bar + } + ... @defer { + deeperObject { + foo + bar + baz + ... @defer { + foo + bar + baz + bak + } + } + } + } + } + } + } + """); + + // act + var operation = OperationCompiler.Compile( + "opid", + document, + schema); + + // assert + MatchSnapshot(document, operation); + } + [Fact] public void Reuse_Selection() { diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs index f6bc46247b0..8ab6a019d5b 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/VariableCoercionHelperTests.cs @@ -5,7 +5,6 @@ using HotChocolate.StarWars.Models; using HotChocolate.StarWars.Types; using HotChocolate.Types; -using Moq; namespace HotChocolate.Execution.Processing; @@ -26,12 +25,12 @@ public void VariableCoercionHelper_Schema_Is_Null() }; var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act void Action() => helper.CoerceVariableValues( - null!, variableDefinitions, default, coercedValues, featureProvider.Object); + null!, variableDefinitions, default, coercedValues, featureProvider); // assert Assert.Throws(Action); @@ -43,12 +42,12 @@ public void VariableCoercionHelper_VariableDefinitions_Is_Null() // arrange var schema = SchemaBuilder.New().AddStarWarsTypes().Create(); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act void Action() - => helper.CoerceVariableValues(schema, null!, default, coercedValues, featureProvider.Object); + => helper.CoerceVariableValues(schema, null!, default, coercedValues, featureProvider); // assert Assert.Throws(Action); @@ -71,12 +70,12 @@ public void VariableCoercionHelper_CoercedValues_Is_Null() Array.Empty()) }; - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act void Action() => helper.CoerceVariableValues( - schema, variableDefinitions, default, null!, featureProvider.Object); + schema, variableDefinitions, default, null!, featureProvider); // assert Assert.Throws(Action); @@ -100,12 +99,12 @@ public void Coerce_Nullable_String_Variable_With_Default_Where_Value_Is_Not_Prov }; var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, default, coercedValues, featureProvider.Object); + schema, variableDefinitions, default, coercedValues, featureProvider); // assert Assert.Collection(coercedValues, @@ -136,12 +135,12 @@ public void Coerce_Nullable_String_Variable_Where_Value_Is_Not_Provided() }; var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, default, coercedValues, featureProvider.Object); + schema, variableDefinitions, default, coercedValues, featureProvider); // assert Assert.Empty(coercedValues); @@ -166,12 +165,12 @@ public void Coerce_Nullable_String_Variable_With_Default_Where_Value_Is_Provided var variableValues = JsonDocument.Parse("""{"abc": "xyz"}"""); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert Assert.Collection(coercedValues, @@ -203,12 +202,12 @@ public void Coerce_Nullable_String_Variable_With_Default_Where_Plain_Value_Is_Pr var variableValues = JsonDocument.Parse("""{"abc": "xyz"}"""); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert Assert.Collection(coercedValues, @@ -240,12 +239,12 @@ public void Coerce_Nullable_String_Variable_With_Default_Where_Null_Is_Provided( var variableValues = JsonDocument.Parse("""{"abc": null}"""); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert Assert.Collection(coercedValues, @@ -277,12 +276,16 @@ public void Coerce_Nullable_ReviewInput_Variable_With_Object() var variableValues = JsonDocument.Parse("""{"abc": {"stars": 5}}"""); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, + variableDefinitions, + variableValues.RootElement, + coercedValues, + featureProvider); // assert Assert.Collection(coercedValues, @@ -314,12 +317,12 @@ public void Error_When_Value_Is_Null_On_Non_Null_Variable() var variableValues = JsonDocument.Parse("""{"abc": null}"""); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act void Action() => helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert Assert.Throws(Action) @@ -327,13 +330,15 @@ void Action() => helper.CoerceVariableValues( .ToList() .MatchInlineSnapshot( """ - [ + "errors": [ { - "Message": "Cannot accept null for non-nullable input.", - "Code": null, - "Path": null, - "Locations": null, - "Extensions": null + "message": "Cannot accept null for non-nullable input.", + "extensions": { + "code": "HC0018", + "inputPath": [ + "abc" + ] + } } ] """); @@ -358,12 +363,12 @@ public void Error_When_Value_Type_Does_Not_Match_Variable_Type() var variableValues = JsonDocument.Parse("""{"abc": 1}"""); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act void Action() => helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert Assert.Throws(Action) @@ -371,15 +376,12 @@ void Action() => helper.CoerceVariableValues( .ToList() .MatchInlineSnapshot( """ - [ + "errors": [ { - "Message": "The value `1` is not compatible with the type `String`.", - "Code": null, - "Path": null, - "Locations": null, - "Extensions": { - "variable": "abc" - } + "message": "String cannot coerce the given value JSON element of type `Number` to a runtime value.", + "path": [ + "abc" + ] } ] """); @@ -403,23 +405,24 @@ public void Variable_Type_Is_Not_An_Input_Type() var variableValues = JsonDocument.Parse("""{"abc": 1}"""); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act void Action() => helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert Assert.Throws(Action).Errors.MatchInlineSnapshot( """ - [ + "errors": [ { - "Message": "Variable `abc` has an invalid type `Human`.", - "Code": null, - "Path": null, - "Locations": null, - "Extensions": null + "message": "Variable `abc` is not an input type.", + "extensions": { + "code": "HC0017", + "variable": "abc", + "type": "Human" + } } ] """); @@ -443,12 +446,12 @@ public void Error_When_Input_Field_Has_Different_Properties_Than_Defined() var variableValues = JsonDocument.Parse("""{"abc": {"abc": "def"}}"""); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act void Action() => helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert Assert.Throws(Action) @@ -456,16 +459,15 @@ void Action() => helper.CoerceVariableValues( .ToList() .MatchInlineSnapshot( """ - [ + "errors": [ { - "Message": "`stars` is a required field of `ReviewInput`.", - "Code": null, - "Path": null, - "Locations": null, - "Extensions": { - "field": "stars", - "type": "ReviewInput", - "variable": "abc" + "message": "The required input field `stars` is missing.", + "extensions": { + "inputPath": [ + "abc", + "stars" + ], + "field": "ReviewInput.stars" } } ] @@ -515,12 +517,12 @@ enum TestEnum { """); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert var entry = Assert.Single(coercedValues); @@ -581,12 +583,12 @@ enum TestEnum { """); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert var entry = Assert.Single(coercedValues); @@ -645,12 +647,12 @@ enum TestEnum { """); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert var entry = Assert.Single(coercedValues); @@ -705,12 +707,12 @@ enum TestEnum { """); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert var entry = Assert.Single(coercedValues); @@ -765,12 +767,12 @@ enum TestEnum { """); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); // act helper.CoerceVariableValues( - schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider.Object); + schema, variableDefinitions, variableValues.RootElement, coercedValues, featureProvider); // assert var entry = Assert.Single(coercedValues); @@ -832,7 +834,7 @@ enum TestEnum { """); var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); @@ -842,7 +844,7 @@ enum TestEnum { variableDefinitions, variableValues.RootElement, coercedValues, - featureProvider.Object); + featureProvider); // assert var entry = Assert.Single(coercedValues); @@ -877,7 +879,7 @@ public void Variable_Is_Nullable_And_Not_Set() }; var coercedValues = new Dictionary(); - var featureProvider = new Mock(); + var featureProvider = new MockFeatureProvider(); var helper = new VariableCoercionHelper(new()); @@ -887,9 +889,14 @@ public void Variable_Is_Nullable_And_Not_Set() variableDefinitions, default, coercedValues, - featureProvider.Object); + featureProvider); // assert Assert.Empty(coercedValues); } + + private class MockFeatureProvider : IFeatureProvider + { + public IFeatureCollection Features { get; } = new FeatureCollection(); + } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/WorkQueueTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Processing/WorkQueueTests.cs index 7be41b109fe..eef3426fe9c 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Processing/WorkQueueTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/WorkQueueTests.cs @@ -123,7 +123,187 @@ public void New() Assert.True(queue.IsEmpty); } - public class MockExecutionTask : IExecutionTask + [Fact] + public void Enqueue_Deferred_Task() + { + // arrange + var queue = new WorkQueue(); + var task = new MockExecutionTask(isDeferred: true); + + // act + queue.Push(task); + + // assert + Assert.False(queue.HasRunningTasks); + Assert.False(queue.IsEmpty); + } + + [Fact] + public void Immediate_Tasks_Take_Priority_Over_Deferred() + { + // arrange + var queue = new WorkQueue(); + var deferredTask = new MockExecutionTask(isDeferred: true); + var immediateTask = new MockExecutionTask(isDeferred: false); + + // Push deferred task first + queue.Push(deferredTask); + // Then push immediate task + queue.Push(immediateTask); + + // act + queue.TryTake(out var firstTask); + queue.TryTake(out var secondTask); + + // assert + Assert.Same(immediateTask, firstTask); + Assert.Same(deferredTask, secondTask); + } + + [Fact] + public void Mixed_Immediate_And_Deferred_Tasks() + { + // arrange + var queue = new WorkQueue(); + var immediate1 = new MockExecutionTask(isDeferred: false); + var deferred1 = new MockExecutionTask(isDeferred: true); + var immediate2 = new MockExecutionTask(isDeferred: false); + var deferred2 = new MockExecutionTask(isDeferred: true); + + // act + queue.Push(immediate1); + queue.Push(deferred1); + queue.Push(immediate2); + queue.Push(deferred2); + + // assert - all immediate tasks should be taken before deferred + Assert.True(queue.TryTake(out var task1)); + Assert.Same(immediate2, task1); // LIFO for immediate stack + + Assert.True(queue.TryTake(out var task2)); + Assert.Same(immediate1, task2); + + Assert.True(queue.TryTake(out var task3)); + Assert.Same(deferred2, task3); // LIFO for deferred stack + + Assert.True(queue.TryTake(out var task4)); + Assert.Same(deferred1, task4); + + Assert.True(queue.IsEmpty); + } + + [Fact] + public void TryTake_Empty_Queue_Returns_False() + { + // arrange + var queue = new WorkQueue(); + + // act + var success = queue.TryTake(out var task); + + // assert + Assert.False(success); + Assert.Null(task); + Assert.False(queue.HasRunningTasks); + } + + [Fact] + public void Complete_Returns_True_When_All_Complete() + { + // arrange + var queue = new WorkQueue(); + var task = new MockExecutionTask(); + queue.Push(task); + queue.TryTake(out _); + + // act + var allComplete = queue.Complete(); + + // assert + Assert.True(allComplete); + Assert.False(queue.HasRunningTasks); + } + + [Fact] + public void Complete_Returns_False_When_More_Tasks_Running() + { + // arrange + var queue = new WorkQueue(); + var task1 = new MockExecutionTask(); + var task2 = new MockExecutionTask(); + queue.Push(task1); + queue.Push(task2); + queue.TryTake(out _); + queue.TryTake(out _); + + // act + var allComplete = queue.Complete(); + + // assert + Assert.False(allComplete); + Assert.True(queue.HasRunningTasks); + } + + [Fact] + public void Complete_Without_Take_Throws() + { + // arrange + var queue = new WorkQueue(); + + // act & assert + Assert.Throws(() => queue.Complete()); + } + + [Fact] + public void Clear_Resets_Running_Counter() + { + // arrange + var queue = new WorkQueue(); + var task = new MockExecutionTask(); + queue.Push(task); + queue.TryTake(out _); + + // act + queue.Clear(); + + // assert + Assert.False(queue.HasRunningTasks); + Assert.True(queue.IsEmpty); + } + + [Fact] + public void Push_Null_Throws_ArgumentNullException() + { + // arrange + var queue = new WorkQueue(); + + // act & assert + Assert.Throws(() => queue.Push(null!)); + } + + [Fact] + public void Deferred_Tasks_Only() + { + // arrange + var queue = new WorkQueue(); + var deferred1 = new MockExecutionTask(isDeferred: true); + var deferred2 = new MockExecutionTask(isDeferred: true); + + // act + queue.Push(deferred1); + queue.Push(deferred2); + + // assert - LIFO order + Assert.True(queue.TryTake(out var task1)); + Assert.Same(deferred2, task1); + + Assert.True(queue.TryTake(out var task2)); + Assert.Same(deferred1, task2); + + Assert.True(queue.IsEmpty); + } + + public class MockExecutionTask(bool isDeferred = false) : IExecutionTask { public uint Id { get; set; } public ExecutionTaskKind Kind { get; } @@ -134,6 +314,10 @@ public class MockExecutionTask : IExecutionTask public bool IsSerial { get; set; } public bool IsRegistered { get; set; } + public int BranchId => throw new NotImplementedException(); + + public bool IsDeferred { get; } = isDeferred; + public void BeginExecute(CancellationToken cancellationToken) { throw new NotImplementedException(); diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Crypto_List_Test.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Crypto_List_Test.snap index 80ebb631801..c02363aecf7 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Crypto_List_Test.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Crypto_List_Test.snap @@ -149,42 +149,42 @@ query DashboardContainerQuery { } } } - gainers: assets(first: 5, where: { price: { change24Hour: { gt: 0 } } }, order: { price: { change24Hour: DESC } }) @__execute(id: 4, kind: DEFAULT, type: COMPOSITE) { + gainers: assets(first: 5, where: { price: { change24Hour: { gt: 0 } } }, order: { price: { change24Hour: DESC } }) @__execute(id: 4, kind: DEFAULT, type: COMPOSITE, isDeferred: true) { ... on AssetsConnection { - nodes @__execute(id: 40, kind: DEFAULT, type: COMPOSITE_LIST) { + nodes @__execute(id: 40, kind: DEFAULT, type: COMPOSITE_LIST, isDeferred: true) { ... on Asset { - id @__execute(id: 42, kind: DEFAULT, type: LEAF) - symbol @__execute(id: 43, kind: DEFAULT, type: LEAF) - name @__execute(id: 44, kind: DEFAULT, type: LEAF) - imageUrl @__execute(id: 45, kind: DEFAULT, type: LEAF) - isInWatchlist @__execute(id: 46, kind: DEFAULT, type: LEAF) - price @__execute(id: 47, kind: DEFAULT, type: COMPOSITE) { + id @__execute(id: 42, kind: DEFAULT, type: LEAF, isDeferred: true) + symbol @__execute(id: 43, kind: DEFAULT, type: LEAF, isDeferred: true) + name @__execute(id: 44, kind: DEFAULT, type: LEAF, isDeferred: true) + imageUrl @__execute(id: 45, kind: DEFAULT, type: LEAF, isDeferred: true) + isInWatchlist @__execute(id: 46, kind: DEFAULT, type: LEAF, isDeferred: true) + price @__execute(id: 47, kind: DEFAULT, type: COMPOSITE, isDeferred: true) { ... on AssetPrice { - currency @__execute(id: 49, kind: DEFAULT, type: LEAF) - lastPrice @__execute(id: 50, kind: DEFAULT, type: LEAF) - change24Hour @__execute(id: 51, kind: DEFAULT, type: LEAF) - id @__execute(id: 52, kind: DEFAULT, type: LEAF) + currency @__execute(id: 49, kind: DEFAULT, type: LEAF, isDeferred: true) + lastPrice @__execute(id: 50, kind: DEFAULT, type: LEAF, isDeferred: true) + change24Hour @__execute(id: 51, kind: DEFAULT, type: LEAF, isDeferred: true) + id @__execute(id: 52, kind: DEFAULT, type: LEAF, isDeferred: true) } } } } } } - losers: assets(first: 5, where: { price: { change24Hour: { lt: 0 } } }, order: { price: { change24Hour: ASC } }) @__execute(id: 5, kind: DEFAULT, type: COMPOSITE) { + losers: assets(first: 5, where: { price: { change24Hour: { lt: 0 } } }, order: { price: { change24Hour: ASC } }) @__execute(id: 5, kind: DEFAULT, type: COMPOSITE, isDeferred: true) { ... on AssetsConnection { - nodes @__execute(id: 54, kind: DEFAULT, type: COMPOSITE_LIST) { + nodes @__execute(id: 54, kind: DEFAULT, type: COMPOSITE_LIST, isDeferred: true) { ... on Asset { - id @__execute(id: 56, kind: DEFAULT, type: LEAF) - symbol @__execute(id: 57, kind: DEFAULT, type: LEAF) - name @__execute(id: 58, kind: DEFAULT, type: LEAF) - imageUrl @__execute(id: 59, kind: DEFAULT, type: LEAF) - isInWatchlist @__execute(id: 60, kind: DEFAULT, type: LEAF) - price @__execute(id: 61, kind: DEFAULT, type: COMPOSITE) { + id @__execute(id: 56, kind: DEFAULT, type: LEAF, isDeferred: true) + symbol @__execute(id: 57, kind: DEFAULT, type: LEAF, isDeferred: true) + name @__execute(id: 58, kind: DEFAULT, type: LEAF, isDeferred: true) + imageUrl @__execute(id: 59, kind: DEFAULT, type: LEAF, isDeferred: true) + isInWatchlist @__execute(id: 60, kind: DEFAULT, type: LEAF, isDeferred: true) + price @__execute(id: 61, kind: DEFAULT, type: COMPOSITE, isDeferred: true) { ... on AssetPrice { - currency @__execute(id: 63, kind: DEFAULT, type: LEAF) - lastPrice @__execute(id: 64, kind: DEFAULT, type: LEAF) - change24Hour @__execute(id: 65, kind: DEFAULT, type: LEAF) - id @__execute(id: 66, kind: DEFAULT, type: LEAF) + currency @__execute(id: 63, kind: DEFAULT, type: LEAF, isDeferred: true) + lastPrice @__execute(id: 64, kind: DEFAULT, type: LEAF, isDeferred: true) + change24Hour @__execute(id: 65, kind: DEFAULT, type: LEAF, isDeferred: true) + id @__execute(id: 66, kind: DEFAULT, type: LEAF, isDeferred: true) } } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Different_Branches_Non_Overlapping_Levels.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Different_Branches_Non_Overlapping_Levels.snap new file mode 100644 index 00000000000..f739813dc34 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Different_Branches_Non_Overlapping_Levels.snap @@ -0,0 +1,56 @@ +{ + a { + b { + c { + d + } + ... @defer { + e { + f + } + } + } + } + ... @defer { + a { + b { + e { + f + } + } + } + g { + h + } + } +} + +--------------------------------------------------------- + +{ + ... on Query { + a @__execute(id: 2, kind: DEFAULT, type: COMPOSITE) { + ... on A { + b @__execute(id: 5, kind: DEFAULT, type: COMPOSITE) { + ... on B { + c @__execute(id: 7, kind: DEFAULT, type: COMPOSITE) { + ... on C { + d @__execute(id: 10, kind: DEFAULT, type: LEAF) + } + } + e @__execute(id: 8, kind: DEFAULT, type: COMPOSITE, isDeferred: true) { + ... on E { + f @__execute(id: 12, kind: DEFAULT, type: LEAF, isDeferred: true) + } + } + } + } + } + } + g @__execute(id: 3, kind: DEFAULT, type: COMPOSITE, isDeferred: true) { + ... on G { + h @__execute(id: 14, kind: DEFAULT, type: LEAF, isDeferred: true) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Different_Branches_Overlapping_Fields.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Different_Branches_Overlapping_Fields.snap new file mode 100644 index 00000000000..539c8571e4c --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Different_Branches_Overlapping_Fields.snap @@ -0,0 +1,31 @@ +{ + foo { + bar { + a + } + ... @defer { + bar { + b + } + baz + } + } +} + +--------------------------------------------------------- + +{ + ... on Query { + foo @__execute(id: 2, kind: DEFAULT, type: COMPOSITE) { + ... on Foo { + bar @__execute(id: 4, kind: DEFAULT, type: COMPOSITE) { + ... on Bar { + a @__execute(id: 7, kind: DEFAULT, type: LEAF) + b @__execute(id: 8, kind: DEFAULT, type: LEAF, isDeferred: true) + } + } + baz @__execute(id: 5, kind: DEFAULT, type: LEAF, isDeferred: true) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread.snap index 266464b86b5..0f170598cdc 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread.snap @@ -19,7 +19,7 @@ fragment Foo on Droid { } ... on Droid { name @__execute(id: 6, kind: PURE, type: LEAF) - id @__execute(id: 7, kind: PURE, type: LEAF) + id @__execute(id: 7, kind: PURE, type: LEAF, isDeferred: true) } } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread_Deferred_And_Non_Deferred.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread_Deferred_And_Non_Deferred.snap new file mode 100644 index 00000000000..a0d1cc72cd5 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread_Deferred_And_Non_Deferred.snap @@ -0,0 +1,25 @@ +{ + hero(episode: EMPIRE) { + ... CharFields @defer(label: "DeferCharFields") + ... CharFields + } +} + +fragment CharFields on Character { + name +} + +--------------------------------------------------------- + +{ + ... on Query { + hero(episode: EMPIRE) @__execute(id: 2, kind: PURE, type: COMPOSITE) { + ... on Human { + name @__execute(id: 4, kind: PURE, type: LEAF) + } + ... on Droid { + name @__execute(id: 6, kind: PURE, type: LEAF) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread_Non_Deferred_Then_Deferred.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread_Non_Deferred_Then_Deferred.snap new file mode 100644 index 00000000000..8e3eeba6e7a --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Fragment_Spread_Non_Deferred_Then_Deferred.snap @@ -0,0 +1,25 @@ +{ + hero(episode: EMPIRE) { + ... CharFields + ... CharFields @defer(label: "DeferCharFields") + } +} + +fragment CharFields on Character { + name +} + +--------------------------------------------------------- + +{ + ... on Query { + hero(episode: EMPIRE) @__execute(id: 2, kind: PURE, type: COMPOSITE) { + ... on Human { + name @__execute(id: 4, kind: PURE, type: LEAF) + } + ... on Droid { + name @__execute(id: 6, kind: PURE, type: LEAF) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_If_False_Not_Deferred.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_If_False_Not_Deferred.snap new file mode 100644 index 00000000000..4be263ff043 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_If_False_Not_Deferred.snap @@ -0,0 +1,25 @@ +{ + hero(episode: EMPIRE) { + ... @defer(if: false) { + name + id + } + } +} + +--------------------------------------------------------- + +{ + ... on Query { + hero(episode: EMPIRE) @__execute(id: 2, kind: PURE, type: COMPOSITE) { + ... on Human { + name @__execute(id: 4, kind: PURE, type: LEAF) + id @__execute(id: 5, kind: PURE, type: LEAF) + } + ... on Droid { + name @__execute(id: 7, kind: PURE, type: LEAF) + id @__execute(id: 8, kind: PURE, type: LEAF) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Inline_Fragment.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Inline_Fragment.snap index 5a04edfda5d..720a4913373 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Inline_Fragment.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Inline_Fragment.snap @@ -14,11 +14,11 @@ hero(episode: EMPIRE) @__execute(id: 2, kind: PURE, type: COMPOSITE) { ... on Human { name @__execute(id: 4, kind: PURE, type: LEAF) - id @__execute(id: 5, kind: PURE, type: LEAF) + id @__execute(id: 5, kind: PURE, type: LEAF, isDeferred: true) } ... on Droid { name @__execute(id: 7, kind: PURE, type: LEAF) - id @__execute(id: 8, kind: PURE, type: LEAF) + id @__execute(id: 8, kind: PURE, type: LEAF, isDeferred: true) } } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Inline_Fragment_Deduplication_Non_Deferred_Wins.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Inline_Fragment_Deduplication_Non_Deferred_Wins.snap new file mode 100644 index 00000000000..1878071767a --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Inline_Fragment_Deduplication_Non_Deferred_Wins.snap @@ -0,0 +1,26 @@ +{ + hero(episode: EMPIRE) { + name + ... @defer { + name + id + } + } +} + +--------------------------------------------------------- + +{ + ... on Query { + hero(episode: EMPIRE) @__execute(id: 2, kind: PURE, type: COMPOSITE) { + ... on Human { + name @__execute(id: 4, kind: PURE, type: LEAF) + id @__execute(id: 5, kind: PURE, type: LEAF, isDeferred: true) + } + ... on Droid { + name @__execute(id: 7, kind: PURE, type: LEAF) + id @__execute(id: 8, kind: PURE, type: LEAF, isDeferred: true) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Multiple_Levels_Field_Deduplication.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Multiple_Levels_Field_Deduplication.snap new file mode 100644 index 00000000000..639b466ae65 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Multiple_Levels_Field_Deduplication.snap @@ -0,0 +1,53 @@ +{ + hero { + nestedObject { + deeperObject { + foo + } + } + ... @defer { + nestedObject { + deeperObject { + foo + bar + } + ... @defer { + deeperObject { + foo + bar + baz + ... @defer { + foo + bar + baz + bak + } + } + } + } + } + } +} + +--------------------------------------------------------- + +{ + ... on Query { + hero @__execute(id: 2, kind: DEFAULT, type: COMPOSITE) { + ... on Hero { + nestedObject @__execute(id: 4, kind: DEFAULT, type: COMPOSITE) { + ... on NestedObject { + deeperObject @__execute(id: 6, kind: DEFAULT, type: COMPOSITE) { + ... on DeeperObject { + foo @__execute(id: 8, kind: DEFAULT, type: LEAF) + bar @__execute(id: 9, kind: DEFAULT, type: LEAF, isDeferred: true) + baz @__execute(id: 10, kind: DEFAULT, type: LEAF, isDeferred: true) + bak @__execute(id: 11, kind: DEFAULT, type: LEAF, isDeferred: true) + } + } + } + } + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Multiple_Nested_Same_Fragment.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Multiple_Nested_Same_Fragment.snap new file mode 100644 index 00000000000..ed6e98d752b --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Multiple_Nested_Same_Fragment.snap @@ -0,0 +1,35 @@ +{ + hero(episode: EMPIRE) { + ... @defer { + ... CharFields + ... @defer { + ... CharFields + ... @defer { + ... CharFields + } + } + } + } +} + +fragment CharFields on Character { + name + id +} + +--------------------------------------------------------- + +{ + ... on Query { + hero(episode: EMPIRE) @__execute(id: 2, kind: PURE, type: COMPOSITE) { + ... on Human { + name @__execute(id: 4, kind: PURE, type: LEAF, isDeferred: true) + id @__execute(id: 5, kind: PURE, type: LEAF, isDeferred: true) + } + ... on Droid { + name @__execute(id: 7, kind: PURE, type: LEAF, isDeferred: true) + id @__execute(id: 8, kind: PURE, type: LEAF, isDeferred: true) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_Field_Overlap_Parent_And_Child.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_Field_Overlap_Parent_And_Child.snap new file mode 100644 index 00000000000..ddb19f2faa8 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_Field_Overlap_Parent_And_Child.snap @@ -0,0 +1,28 @@ +{ + hero(episode: EMPIRE) { + ... @defer { + name + ... @defer { + name + id + } + } + } +} + +--------------------------------------------------------- + +{ + ... on Query { + hero(episode: EMPIRE) @__execute(id: 2, kind: PURE, type: COMPOSITE) { + ... on Human { + name @__execute(id: 4, kind: PURE, type: LEAF, isDeferred: true) + id @__execute(id: 5, kind: PURE, type: LEAF, isDeferred: true) + } + ... on Droid { + name @__execute(id: 7, kind: PURE, type: LEAF, isDeferred: true) + id @__execute(id: 8, kind: PURE, type: LEAF, isDeferred: true) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_Inline_Fragments.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_Inline_Fragments.snap new file mode 100644 index 00000000000..bbe25fd79f4 --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_Inline_Fragments.snap @@ -0,0 +1,27 @@ +{ + hero(episode: EMPIRE) { + ... @defer { + name + ... @defer { + id + } + } + } +} + +--------------------------------------------------------- + +{ + ... on Query { + hero(episode: EMPIRE) @__execute(id: 2, kind: PURE, type: COMPOSITE) { + ... on Human { + name @__execute(id: 4, kind: PURE, type: LEAF, isDeferred: true) + id @__execute(id: 5, kind: PURE, type: LEAF, isDeferred: true) + } + ... on Droid { + name @__execute(id: 7, kind: PURE, type: LEAF, isDeferred: true) + id @__execute(id: 8, kind: PURE, type: LEAF, isDeferred: true) + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_With_Parent_Field_Deduplication.snap b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_With_Parent_Field_Deduplication.snap new file mode 100644 index 00000000000..694472fbc5d --- /dev/null +++ b/src/HotChocolate/Core/test/Execution.Tests/Processing/__snapshots__/OperationCompilerTests.Defer_Nested_With_Parent_Field_Deduplication.snap @@ -0,0 +1,36 @@ +{ + hero { + ... @defer { + nestedObject { + deeperObject { + foo + ... @defer { + foo + bar + } + } + } + } + } +} + +--------------------------------------------------------- + +{ + ... on Query { + hero @__execute(id: 2, kind: DEFAULT, type: COMPOSITE) { + ... on Hero { + nestedObject @__execute(id: 4, kind: DEFAULT, type: COMPOSITE, isDeferred: true) { + ... on NestedObject { + deeperObject @__execute(id: 6, kind: DEFAULT, type: COMPOSITE, isDeferred: true) { + ... on DeeperObject { + foo @__execute(id: 8, kind: DEFAULT, type: LEAF, isDeferred: true) + bar @__execute(id: 9, kind: DEFAULT, type: LEAF, isDeferred: true) + } + } + } + } + } + } + } +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/SchemaFirstTests.cs b/src/HotChocolate/Core/test/Execution.Tests/SchemaFirstTests.cs index 2b4c581ec39..966fe84b147 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/SchemaFirstTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/SchemaFirstTests.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using System.Text.Json; namespace HotChocolate.Execution; @@ -276,9 +277,8 @@ public Task ChangeChannelParametersAsync( ChangeChannelParameterInput input, CancellationToken _) { - var message = Assert.IsType( - Assert.IsType>( - input.ParameterChangeInfo[0].Value)["a"]); + var value = Assert.IsType(input.ParameterChangeInfo[0].Value); + var message = Assert.IsType(value.GetProperty("a").GetString()); return Task.FromResult(new ChangeChannelParameterPayload { Message = message }); } diff --git a/src/HotChocolate/Core/test/Execution.Tests/Serialization/MultiPartResponseStreamSerializerTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Serialization/MultiPartResponseStreamSerializerTests.cs index f2029a486fe..77a2157a922 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Serialization/MultiPartResponseStreamSerializerTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Serialization/MultiPartResponseStreamSerializerTests.cs @@ -23,6 +23,7 @@ public async Task Serialize_Response_Stream() o.EnableDefer = true; o.EnableStream = true; }) + .ModifyRequestOptions(o => o.IncludeExceptionDetails = true) .ExecuteRequestAsync( """ { diff --git a/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap b/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap index f913502ff09..31c7de9c494 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap +++ b/src/HotChocolate/Core/test/Execution.Tests/Serialization/__snapshots__/MultiPartResponseStreamSerializerTests.Serialize_Response_Stream.snap @@ -1,10 +1,10 @@ - ---- -Content-Type: application/json; charset=utf-8 - -{"data":{"hero":{"id":"2001"}},"hasNext":true} ---- -Content-Type: application/json; charset=utf-8 - -{"incremental":[{"data":{"friends":{"nodes":[{"id":"1000","name":"Luke Skywalker"},{"id":"1002","name":"Han Solo"},{"id":"1003","name":"Leia Organa"}]}},"label":"friends","path":["hero"]}],"hasNext":false} ------ + +--- +Content-Type: application/json; charset=utf-8 + +{"data":{"hero":{"id":"2001"}},"pending":[{"id":"2","path":["hero"],"label":"friends"}],"hasNext":true} +--- +Content-Type: application/json; charset=utf-8 + +{"incremental":[{"id":"2","data":{"friends":{"nodes":[{"id":"1000","name":"Luke Skywalker"},{"id":"1002","name":"Han Solo"},{"id":"1003","name":"Leia Organa"}]}}}],"completed":[{"id":"2"}],"hasNext":false} +----- diff --git a/src/HotChocolate/Core/test/Execution.Tests/SourceObjectConversionTests.cs b/src/HotChocolate/Core/test/Execution.Tests/SourceObjectConversionTests.cs index 95e90106dbc..3210aa17025 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/SourceObjectConversionTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/SourceObjectConversionTests.cs @@ -28,9 +28,8 @@ public async Task ConvertSourceObject() var result = await executor.ExecuteAsync("{ foo { qux } }"); // assert - Assert.True( - Assert.IsType(result).Errors is null, - "There should be no errors."); + var operationResult = Assert.IsType(result); + Assert.True(operationResult.Errors.IsEmpty, "There should be no errors."); Assert.True( conversionTriggered, "The custom converter should have been hit."); diff --git a/src/HotChocolate/Core/test/Execution.Tests/TransactionScopeHandlerTests.cs b/src/HotChocolate/Core/test/Execution.Tests/TransactionScopeHandlerTests.cs deleted file mode 100644 index b08e86b3b1f..00000000000 --- a/src/HotChocolate/Core/test/Execution.Tests/TransactionScopeHandlerTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -using HotChocolate.Execution.Processing; -using HotChocolate.Tests; -using Microsoft.Extensions.DependencyInjection; - -namespace HotChocolate.Execution; - -public class TransactionScopeHandlerTests -{ - [Fact] - public async Task Custom_Transaction_Is_Correctly_Completed_and_Disposed() - { - var completed = false; - var disposed = false; - - void Complete() => completed = true; - void Dispose() => disposed = true; - - await new ServiceCollection() - .AddGraphQL() - .AddQueryType() - .AddMutationType() - .ModifyRequestOptions(o => o.ExecutionTimeout = TimeSpan.FromMilliseconds(100)) - .AddTransactionScopeHandler(_ => new MockTransactionScopeHandler(Complete, Dispose)) - .ExecuteRequestAsync("mutation { doNothing }"); - - Assert.True(completed, "transaction must be completed"); - Assert.True(disposed, "transaction must be disposed"); - } - - [Fact] - public async Task Custom_Transaction_Is_Detects_Error_and_Disposes() - { - var completed = false; - var disposed = false; - - void Complete() => completed = true; - void Dispose() => disposed = true; - - await new ServiceCollection() - .AddGraphQL() - .AddQueryType() - .AddMutationType() - .AddTransactionScopeHandler(_ => new MockTransactionScopeHandler(Complete, Dispose)) - .ExecuteRequestAsync("mutation { doError }"); - - Assert.False(completed, "transaction was not completed due to error"); - Assert.True(disposed, "transaction must be disposed"); - } - - [Fact] - public async Task DefaultTransactionScopeHandler_Creates_SystemTransactionScope() - { - await new ServiceCollection() - .AddGraphQL() - .AddQueryType() - .AddMutationType() - .AddDefaultTransactionScopeHandler() - .ExecuteRequestAsync("mutation { foundTransactionScope }") - .MatchSnapshotAsync(); - } - - [Fact] - public async Task By_Default_There_Is_No_TransactionScope() - { - await new ServiceCollection() - .AddGraphQL() - .AddQueryType() - .AddMutationType() - .ExecuteRequestAsync("mutation { foundTransactionScope }") - .MatchSnapshotAsync(); - } - - public class Query - { - public string DoNothing() => "Hello"; - } - - public class Mutation - { - public string DoNothing() => "Hello"; - - public string DoError() => throw new GraphQLException("I am broken!"); - - public bool FoundTransactionScope() => - System.Transactions.Transaction.Current is not null; - } - - public class MockTransactionScopeHandler(Action complete, Action dispose) : ITransactionScopeHandler - { - public ITransactionScope Create(RequestContext context) - => new MockTransactionScope(complete, dispose, context); - } - - public class MockTransactionScope(Action complete, Action dispose, RequestContext context) : ITransactionScope - { - public void Complete() - { - if (context.Result is OperationResult { Data: not null, Errors: null or { Count: 0 } }) - { - complete(); - } - } - - public void Dispose() => dispose(); - } -} diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TransactionScopeHandlerTests.By_Default_There_Is_No_TransactionScope.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TransactionScopeHandlerTests.By_Default_There_Is_No_TransactionScope.snap deleted file mode 100644 index 441f405f1e7..00000000000 --- a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TransactionScopeHandlerTests.By_Default_There_Is_No_TransactionScope.snap +++ /dev/null @@ -1,5 +0,0 @@ -{ - "data": { - "foundTransactionScope": false - } -} diff --git a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TransactionScopeHandlerTests.DefaultTransactionScopeHandler_Creates_SystemTransactionScope.snap b/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TransactionScopeHandlerTests.DefaultTransactionScopeHandler_Creates_SystemTransactionScope.snap deleted file mode 100644 index 7d862e77e4e..00000000000 --- a/src/HotChocolate/Core/test/Execution.Tests/__snapshots__/TransactionScopeHandlerTests.DefaultTransactionScopeHandler_Creates_SystemTransactionScope.snap +++ /dev/null @@ -1,5 +0,0 @@ -{ - "data": { - "foundTransactionScope": true - } -} diff --git a/src/HotChocolate/Core/test/StarWars/Resolvers/SharedResolvers.cs b/src/HotChocolate/Core/test/StarWars/Resolvers/SharedResolvers.cs index 2e4091fa3c2..473b02916e1 100644 --- a/src/HotChocolate/Core/test/StarWars/Resolvers/SharedResolvers.cs +++ b/src/HotChocolate/Core/test/StarWars/Resolvers/SharedResolvers.cs @@ -5,7 +5,16 @@ namespace HotChocolate.StarWars.Resolvers; public class SharedResolvers { - public IEnumerable GetCharacter( + public async Task> GetCharacters( + [Parent] ICharacter character, + [Service] CharacterRepository repository) + { + await Task.Delay(250); + + return GetCharactersInternal(character, repository); + } + + private IEnumerable GetCharactersInternal( [Parent] ICharacter character, [Service] CharacterRepository repository) { diff --git a/src/HotChocolate/Core/test/StarWars/Types/CharacterType.cs b/src/HotChocolate/Core/test/StarWars/Types/CharacterType.cs index cecfe575c55..5d31be234da 100644 --- a/src/HotChocolate/Core/test/StarWars/Types/CharacterType.cs +++ b/src/HotChocolate/Core/test/StarWars/Types/CharacterType.cs @@ -15,8 +15,14 @@ protected override void Configure(IInterfaceTypeDescriptor descripto descriptor.Field(f => f.Name) .Type>(); - descriptor.Field(f => f.Friends) - .UsePaging(); + descriptor + .Field(f => f.Friends) + .UsePaging() + .Resolve(async ctx => + { + await Task.Delay(250); + return ctx.Parent().Friends; + }); descriptor.Field(f => f.AppearsIn) .Type>(); diff --git a/src/HotChocolate/Core/test/StarWars/Types/DroidType.cs b/src/HotChocolate/Core/test/StarWars/Types/DroidType.cs index 77a9307eea9..1857021c61f 100644 --- a/src/HotChocolate/Core/test/StarWars/Types/DroidType.cs +++ b/src/HotChocolate/Core/test/StarWars/Types/DroidType.cs @@ -20,7 +20,7 @@ protected override void Configure(IObjectTypeDescriptor descriptor) descriptor.Field(t => t.AppearsIn) .Type>(); - descriptor.Field(r => r.GetCharacter(null!, null!)) + descriptor.Field(r => r.GetCharacters(null!, null!)) .UsePaging() .Name("friends"); diff --git a/src/HotChocolate/Core/test/StarWars/Types/HumanType.cs b/src/HotChocolate/Core/test/StarWars/Types/HumanType.cs index 45ff5909165..bc883ee38f1 100644 --- a/src/HotChocolate/Core/test/StarWars/Types/HumanType.cs +++ b/src/HotChocolate/Core/test/StarWars/Types/HumanType.cs @@ -15,7 +15,7 @@ protected override void Configure(IObjectTypeDescriptor descriptor) descriptor.Field(t => t.AppearsIn).Type>(); descriptor - .Field(r => r.GetCharacter(null!, null!)) + .Field(r => r.GetCharacters(null!, null!)) .UsePaging() .Name("friends") .Parallel(); diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/__snapshots__/IdDescriptorTests.Id_Honors_CustomTypeNaming_Throws_On_InvalidInputs_InvalidArgs.snap b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/__snapshots__/IdDescriptorTests.Id_Honors_CustomTypeNaming_Throws_On_InvalidInputs_InvalidArgs.snap index 696c3baa90b..2d87122cbb5 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Relay/__snapshots__/IdDescriptorTests.Id_Honors_CustomTypeNaming_Throws_On_InvalidInputs_InvalidArgs.snap +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Relay/__snapshots__/IdDescriptorTests.Id_Honors_CustomTypeNaming_Throws_On_InvalidInputs_InvalidArgs.snap @@ -1,9 +1,9 @@ { "errors": [ { - "message": "The node id type name `FooFoo` does not match the expected type name `RenamedUser`.", + "message": "The node id type name `FooFooFluentSingle` does not match the expected type name `FooFooFluent`.", "path": [ - "validUserIdInput" + "validFluentFooIdInput" ] }, { @@ -13,15 +13,15 @@ ] }, { - "message": "The node id type name `FooFooFluentSingle` does not match the expected type name `FooFooFluent`.", + "message": "The node id type name `RenamedUser` does not match the expected type name `FooFooFluentSingle`.", "path": [ - "validFluentFooIdInput" + "validSingleTypeFluentFooIdInput" ] }, { - "message": "The node id type name `RenamedUser` does not match the expected type name `FooFooFluentSingle`.", + "message": "The node id type name `FooFoo` does not match the expected type name `RenamedUser`.", "path": [ - "validSingleTypeFluentFooIdInput" + "validUserIdInput" ] } ], diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/Base64StringTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/Base64StringTypeTests.cs index 2c789c6c16b..43e51873704 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/Base64StringTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/Base64StringTypeTests.cs @@ -52,10 +52,10 @@ public void IsValueCompatible_Null_ReturnsFalse() var type = new Base64StringType(); // act - var result = type.IsValueCompatible(null!); + void Error() => type.IsValueCompatible(null!); // assert - Assert.False(result); + Assert.Throws(Error); } [Fact] diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/ByteArrayTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/ByteArrayTypeTests.cs index ec9b34624aa..2e74f9c7010 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/ByteArrayTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/ByteArrayTypeTests.cs @@ -54,10 +54,10 @@ public void IsValueCompatible_Null_ReturnsFalse() var type = new ByteArrayType(); // act - var result = type.IsValueCompatible(null!); + void Error() => type.IsValueCompatible(null!); // assert - Assert.False(result); + Assert.Throws(Error); } [Fact] diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UriTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UriTypeTests.cs index 74ac81f3111..dced1c2561a 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UriTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UriTypeTests.cs @@ -245,10 +245,10 @@ public void IsValueCompatible_Null_ReturnsFalse() var type = new UriType(); // act - var compatible = type.IsValueCompatible(null!); + void Error() => type.IsValueCompatible(null!); // assert - Assert.False(compatible); + Assert.Throws(Error); } [Fact] diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UrlTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UrlTypeTests.cs index b0d093c6524..3ec78c1ae9a 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UrlTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UrlTypeTests.cs @@ -303,10 +303,10 @@ public void IsValueCompatible_Null_ReturnsFalse() var type = new UrlType(); // act - var compatible = type.IsValueCompatible(null!); + void Error() => type.IsValueCompatible(null!); // assert - Assert.False(compatible); + Assert.Throws(Error); } [Fact] diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UuidTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UuidTypeTests.cs index 0e572e35536..93557968bc7 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UuidTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/UuidTypeTests.cs @@ -67,10 +67,10 @@ public void IsValueCompatible_Null() var type = new UuidType(); // act - var isCompatible = type.IsValueCompatible(null!); + void Error() => type.IsValueCompatible(null!); // assert - Assert.False(isCompatible); + Assert.Throws(Error); } [Fact] diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputObjectTypeTests.Input_Infer_Default_Values.snap b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputObjectTypeTests.Input_Infer_Default_Values.snap index 68787826546..bd380c9f2a0 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputObjectTypeTests.Input_Infer_Default_Values.snap +++ b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputObjectTypeTests.Input_Infer_Default_Values.snap @@ -1,4 +1,4 @@ -schema { +schema { query: Query } @@ -14,7 +14,13 @@ input InputWithDefaultInput { withStringDefault: String = "abc" withNullDefault: String enum: FooEnum! = BAR - complexInput: [[ComplexInput!]!]! = [ [ { foo: 1 } ] ] + complexInput: [[ComplexInput!]!]! = [ + [ + { + foo: 1 + } + ] + ] withoutDefault: String } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.OneOf_A_and_B_Are_Set.snap b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.OneOf_A_and_B_Are_Set.snap index 11ad4beb419..e97a024ab58 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.OneOf_A_and_B_Are_Set.snap +++ b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.OneOf_A_and_B_Are_Set.snap @@ -1,22 +1,11 @@ -[ +"errors": [ { - "Message": "More than one field of the OneOf Input Object `OneOfInput` is set. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", - "Code": "HC0055", - "Path": null, - "Locations": null, - "Extensions": { + "message": "More than one field of the OneOf Input Object `OneOfInput` is set. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", + "extensions": { "code": "HC0055", - "inputPath": { - "Name": "root", - "Parent": { - "Parent": null, - "Length": 0, - "IsRoot": true - }, - "Length": 1, - "IsRoot": false - } - }, - "Exception": null + "inputPath": [ + "root" + ] + } } ] diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.OneOf_A_is_Null_and_B_has_Value.snap b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.OneOf_A_is_Null_and_B_has_Value.snap index 11ad4beb419..e97a024ab58 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.OneOf_A_is_Null_and_B_has_Value.snap +++ b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.OneOf_A_is_Null_and_B_has_Value.snap @@ -1,22 +1,11 @@ -[ +"errors": [ { - "Message": "More than one field of the OneOf Input Object `OneOfInput` is set. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", - "Code": "HC0055", - "Path": null, - "Locations": null, - "Extensions": { + "message": "More than one field of the OneOf Input Object `OneOfInput` is set. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", + "extensions": { "code": "HC0055", - "inputPath": { - "Name": "root", - "Parent": { - "Parent": null, - "Length": 0, - "IsRoot": true - }, - "Length": 1, - "IsRoot": false - } - }, - "Exception": null + "inputPath": [ + "root" + ] + } } ] diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/ObjectTypeTests.ObjectType_FieldDefaultValue_SerializesCorrectly.snap b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/ObjectTypeTests.ObjectType_FieldDefaultValue_SerializesCorrectly.snap index 39512c2e9d2..a1970befda4 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/ObjectTypeTests.ObjectType_FieldDefaultValue_SerializesCorrectly.snap +++ b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/ObjectTypeTests.ObjectType_FieldDefaultValue_SerializesCorrectly.snap @@ -3,7 +3,9 @@ schema { } type Bar { - _123(_456: FooInput = { description: "hello" }): String + _123(_456: FooInput = { + description: "hello" + }): String } input FooInput { diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Apply_SemanticNonNull_To_SchemaFirst.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Apply_SemanticNonNull_To_SchemaFirst.snap index 08e902a8348..6f5c17b0637 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Apply_SemanticNonNull_To_SchemaFirst.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Apply_SemanticNonNull_To_SchemaFirst.snap @@ -24,4 +24,6 @@ type Query { innerNonNullObjectNestedArray: [[Foo]] @semanticNonNull(levels: [ 0, 2 ]) } -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_CodeFirst.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_CodeFirst.snap index 08e902a8348..6f5c17b0637 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_CodeFirst.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_CodeFirst.snap @@ -24,4 +24,6 @@ type Query { innerNonNullObjectNestedArray: [[Foo]] @semanticNonNull(levels: [ 0, 2 ]) } -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst.snap index 08e902a8348..6f5c17b0637 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst.snap @@ -24,4 +24,6 @@ type Query { innerNonNullObjectNestedArray: [[Foo]] @semanticNonNull(levels: [ 0, 2 ]) } -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst_With_GraphQLType_As_String.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst_With_GraphQLType_As_String.snap index 08e902a8348..6f5c17b0637 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst_With_GraphQLType_As_String.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst_With_GraphQLType_As_String.snap @@ -24,4 +24,6 @@ type Query { innerNonNullObjectNestedArray: [[Foo]] @semanticNonNull(levels: [ 0, 2 ]) } -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst_With_GraphQLType_As_Type.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst_With_GraphQLType_As_Type.snap index 08e902a8348..6f5c17b0637 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst_With_GraphQLType_As_Type.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Derive_SemanticNonNull_From_ImplementationFirst_With_GraphQLType_As_Type.snap @@ -24,4 +24,6 @@ type Query { innerNonNullObjectNestedArray: [[Foo]] @semanticNonNull(levels: [ 0, 2 ]) } -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Interface_With_Id_Field.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Interface_With_Id_Field.snap index ce542824ecb..e570f5dc042 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Interface_With_Id_Field.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Interface_With_Id_Field.snap @@ -32,7 +32,9 @@ a stable key. """ directive @lookup on FIELD_DEFINITION -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION """ By default, only a single source schema is allowed to contribute diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.MutationConventions.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.MutationConventions.snap index 2601f50bd61..bd47c6c8033 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.MutationConventions.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.MutationConventions.snap @@ -22,4 +22,6 @@ type MyError implements Error { union DoSomethingError = MyError -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_With_Id_Field.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_With_Id_Field.snap index 01d6a381ac1..a5c210d73ae 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_With_Id_Field.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Object_With_Id_Field.snap @@ -10,4 +10,6 @@ type Query { myNode: MyType @semanticNonNull } -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Pagination.snap b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Pagination.snap index 146dc8ea148..e663d5e4be4 100644 --- a/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Pagination.snap +++ b/src/HotChocolate/Core/test/Types.Tests/__snapshots__/SemanticNonNullTests.Pagination.snap @@ -53,4 +53,6 @@ type QueryWithPagination { offsetPagination(skip: Int take: Int): OffsetPaginationCollectionSegment } -directive @semanticNonNull(levels: [Int!] = [ 0 ]) on FIELD_DEFINITION +directive @semanticNonNull(levels: [Int!] = [ + 0 +]) on FIELD_DEFINITION diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTargetDefinedRuleTests.UndefinedFragment.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTargetDefinedRuleTests.UndefinedFragment.snap index 36a5533a7ed..67ec0396494 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTargetDefinedRuleTests.UndefinedFragment.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTargetDefinedRuleTests.UndefinedFragment.snap @@ -1,27 +1,18 @@ -[ +"errors": [ { - "Message": "The specified fragment `undefinedFragment` does not exist.", - "Code": null, - "Path": { - "Name": "dog", - "Parent": { - "Parent": null, - "Length": 0, - "IsRoot": true - }, - "Length": 1, - "IsRoot": false - }, - "Locations": [ + "message": "The specified fragment `undefinedFragment` does not exist.", + "locations": [ { - "Line": 3, - "Column": 9 + "line": 3, + "column": 9 } ], - "Extensions": { + "path": [ + "dog" + ], + "extensions": { "fragment": "undefinedFragment", "specifiedBy": "https://spec.graphql.org/September2025/#sec-Fragment-Spread-Target-Defined" - }, - "Exception": null + } } ] diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTypeExistenceRuleTests.NotExistingTypeOnInlineFragment.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTypeExistenceRuleTests.NotExistingTypeOnInlineFragment.snap index b2ed37b87a5..67189828640 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTypeExistenceRuleTests.NotExistingTypeOnInlineFragment.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTypeExistenceRuleTests.NotExistingTypeOnInlineFragment.snap @@ -1,27 +1,18 @@ -[ +"errors": [ { - "Message": "Unknown type `NotInSchema`.", - "Code": null, - "Path": { - "Name": "dog", - "Parent": { - "Parent": null, - "Length": 0, - "IsRoot": true - }, - "Length": 1, - "IsRoot": false - }, - "Locations": [ + "message": "Unknown type `NotInSchema`.", + "locations": [ { - "Line": 8, - "Column": 5 + "line": 8, + "column": 5 } ], - "Extensions": { + "path": [ + "dog" + ], + "extensions": { "typeCondition": "NotInSchema", "specifiedBy": "https://spec.graphql.org/September2025/#sec-Fragment-Spread-Type-Existence" - }, - "Exception": null + } } ] diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTypeExistenceRuleTests.NotOnExistingTypeOnFragment.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTypeExistenceRuleTests.NotOnExistingTypeOnFragment.snap index 6d553eefef4..8224f40ea91 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTypeExistenceRuleTests.NotOnExistingTypeOnFragment.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentSpreadTypeExistenceRuleTests.NotOnExistingTypeOnFragment.snap @@ -1,28 +1,19 @@ -[ +"errors": [ { - "Message": "Unknown type `NotInSchema`.", - "Code": null, - "Path": { - "Name": "dog", - "Parent": { - "Parent": null, - "Length": 0, - "IsRoot": true - }, - "Length": 1, - "IsRoot": false - }, - "Locations": [ + "message": "Unknown type `NotInSchema`.", + "locations": [ { - "Line": 7, - "Column": 1 + "line": 7, + "column": 1 } ], - "Extensions": { + "path": [ + "dog" + ], + "extensions": { "typeCondition": "NotInSchema", "fragment": "notOnExistingType", "specifiedBy": "https://spec.graphql.org/September2025/#sec-Fragment-Spread-Type-Existence" - }, - "Exception": null + } } ] diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentsMustBeUsedRuleTests.UnusedFragment.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentsMustBeUsedRuleTests.UnusedFragment.snap index a87ed8a4a73..8181f035de9 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentsMustBeUsedRuleTests.UnusedFragment.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/FragmentsMustBeUsedRuleTests.UnusedFragment.snap @@ -1,18 +1,15 @@ -[ +"errors": [ { - "Message": "The specified fragment `nameFragment` is not used within the current document.", - "Code": null, - "Path": null, - "Locations": [ + "message": "The specified fragment `nameFragment` is not used within the current document.", + "locations": [ { - "Line": 1, - "Column": 1 + "line": 1, + "column": 1 } ], - "Extensions": { + "extensions": { "fragment": "nameFragment", "specifiedBy": "https://spec.graphql.org/September2025/#sec-Fragments-Must-Be-Used" - }, - "Exception": null + } } ] diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field.snap index 63403a4069e..f7adec06a8c 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field.snap @@ -1,27 +1,15 @@ -[ +"errors": [ { - "Message": "Introspection is not allowed for the current request.", - "Code": "HC0046", - "Path": null, - "Locations": [ + "message": "Introspection is not allowed for the current request.", + "locations": [ { - "Line": 2, - "Column": 5 + "line": 2, + "column": 5 } ], - "Extensions": { + "extensions": { "code": "HC0046", - "field": { - "Kind": "Name", - "Location": { - "Start": 6, - "End": 16, - "Line": 2, - "Column": 5 - }, - "Value": "__schema" - } - }, - "Exception": null + "field": "__schema" + } } ] diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field_Custom_Message.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field_Custom_Message.snap index 873fa5e2ad9..de955b8d46d 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field_Custom_Message.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Schema_Field_Custom_Message.snap @@ -1,27 +1,15 @@ -[ +"errors": [ { - "Message": "Baz", - "Code": "HC0046", - "Path": null, - "Locations": [ + "message": "Baz", + "locations": [ { - "Line": 2, - "Column": 5 + "line": 2, + "column": 5 } ], - "Extensions": { + "extensions": { "code": "HC0046", - "field": { - "Kind": "Name", - "Location": { - "Start": 6, - "End": 16, - "Line": 2, - "Column": 5 - }, - "Value": "__schema" - } - }, - "Exception": null + "field": "__schema" + } } ] diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Type_Field.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Type_Field.snap index e5e677122a1..bf57814ffa5 100644 --- a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Type_Field.snap +++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/IntrospectionRuleTests.IntrospectionNotAllowed_Type_Field.snap @@ -1,27 +1,15 @@ -[ +"errors": [ { - "Message": "Introspection is not allowed for the current request.", - "Code": "HC0046", - "Path": null, - "Locations": [ + "message": "Introspection is not allowed for the current request.", + "locations": [ { - "Line": 2, - "Column": 5 + "line": 2, + "column": 5 } ], - "Extensions": { + "extensions": { "code": "HC0046", - "field": { - "Kind": "Name", - "Location": { - "Start": 6, - "End": 13, - "Line": 2, - "Column": 5 - }, - "Value": "__type" - } - }, - "Exception": null + "field": "__type" + } } ] diff --git a/src/HotChocolate/CostAnalysis/test/Directory.Build.props b/src/HotChocolate/CostAnalysis/test/Directory.Build.props index 33277d1d22c..bf9ede89e6c 100644 --- a/src/HotChocolate/CostAnalysis/test/Directory.Build.props +++ b/src/HotChocolate/CostAnalysis/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryablePagingProjectionOptimizer.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryablePagingProjectionOptimizer.cs index 4026a494ce0..10422d51f6c 100644 --- a/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryablePagingProjectionOptimizer.cs +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryablePagingProjectionOptimizer.cs @@ -60,9 +60,7 @@ private Selection CreateCombinedSelection( Array.Empty(), new SelectionSetNode(selections)); - var nodesPipeline = - selection.ResolverPipeline ?? - context.CompileResolverPipeline(nodesField, combinedField); + var nodesPipeline = context.CompileResolverPipeline(nodesField, combinedField); return new Selection( context.NewSelectionId(), diff --git a/src/HotChocolate/Data/src/Data/Projections/ProjectionOptimizer.cs b/src/HotChocolate/Data/src/Data/Projections/ProjectionOptimizer.cs index 6e6d1fe04bd..65adaa59dd1 100644 --- a/src/HotChocolate/Data/src/Data/Projections/ProjectionOptimizer.cs +++ b/src/HotChocolate/Data/src/Data/Projections/ProjectionOptimizer.cs @@ -14,10 +14,11 @@ public void OptimizeSelectionSet(SelectionSetOptimizerContext context) selectionToProcess.ExceptWith(processedSelections); foreach (var responseName in selectionToProcess) { + var selection = context.GetSelection(responseName); var rewrittenSelection = provider.RewriteSelection( context, - context.GetSelection(responseName)); + selection); context.ReplaceSelection(rewrittenSelection); diff --git a/src/HotChocolate/Data/test/Data.Filters.Tests/__snapshots__/QueryableFilteringExtensionsTests.Extension_Should_BeMissingMiddleware.snap b/src/HotChocolate/Data/test/Data.Filters.Tests/__snapshots__/QueryableFilteringExtensionsTests.Extension_Should_BeMissingMiddleware.snap index 7ccd948a1b6..df55ead1466 100644 --- a/src/HotChocolate/Data/test/Data.Filters.Tests/__snapshots__/QueryableFilteringExtensionsTests.Extension_Should_BeMissingMiddleware.snap +++ b/src/HotChocolate/Data/test/Data.Filters.Tests/__snapshots__/QueryableFilteringExtensionsTests.Extension_Should_BeMissingMiddleware.snap @@ -4,12 +4,6 @@ Result: "errors": [ { "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 1, - "column": 3 - } - ], "path": [ "missingMiddleware" ], diff --git a/src/HotChocolate/Data/test/Data.Filters.Tests/__snapshots__/QueryableFilteringExtensionsTests.Extension_Should_BeTypeMismatch.snap b/src/HotChocolate/Data/test/Data.Filters.Tests/__snapshots__/QueryableFilteringExtensionsTests.Extension_Should_BeTypeMismatch.snap index 5c508686550..0223cd82ecc 100644 --- a/src/HotChocolate/Data/test/Data.Filters.Tests/__snapshots__/QueryableFilteringExtensionsTests.Extension_Should_BeTypeMismatch.snap +++ b/src/HotChocolate/Data/test/Data.Filters.Tests/__snapshots__/QueryableFilteringExtensionsTests.Extension_Should_BeTypeMismatch.snap @@ -4,12 +4,6 @@ Result: "errors": [ { "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 1, - "column": 3 - } - ], "path": [ "typeMismatch" ], diff --git a/src/HotChocolate/Data/test/Data.NodaTime.Tests/IntegrationTests.cs b/src/HotChocolate/Data/test/Data.NodaTime.Tests/IntegrationTests.cs index 470ed682943..dd3ec400456 100644 --- a/src/HotChocolate/Data/test/Data.NodaTime.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Data/test/Data.NodaTime.Tests/IntegrationTests.cs @@ -61,20 +61,22 @@ public async Task NodaTime_Paging_Filtering_And_Sorting() """)); // assert - result.ExpectOperationResult().Data.MatchInlineSnapshot( + result.ExpectOperationResult().MatchInlineSnapshot( """ { - "books": { + "data": { + "books": { "nodes": [ - { - "title": "Book2", - "publishedDate": "2008-01-17" - }, - { - "title": "Book1", - "publishedDate": "2008-01-16" - } + { + "title": "Book2", + "publishedDate": "2008-01-17" + }, + { + "title": "Book1", + "publishedDate": "2008-01-16" + } ] + } } } """); diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_Executable.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_Executable.snap index 194b934c7c5..36905a80618 100644 --- a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_Executable.snap +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_Executable.snap @@ -4,12 +4,6 @@ Result: "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 2, - "column": 25 - } - ], "path": [ "rootExecutable" ] diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionExtensionsTests.Extension_Should_BeMissingMiddleware.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionExtensionsTests.Extension_Should_BeMissingMiddleware.snap index 97df417ce81..679941092f9 100644 --- a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionExtensionsTests.Extension_Should_BeMissingMiddleware.snap +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionExtensionsTests.Extension_Should_BeMissingMiddleware.snap @@ -4,12 +4,6 @@ Result: "errors": [ { "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 1, - "column": 3 - } - ], "path": [ "missingMiddleware" ], diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionExtensionsTests.Extension_Should_BeTypeMismatch.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionExtensionsTests.Extension_Should_BeTypeMismatch.snap index 9dd61ef1e9b..33277723934 100644 --- a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionExtensionsTests.Extension_Should_BeTypeMismatch.snap +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionExtensionsTests.Extension_Should_BeTypeMismatch.snap @@ -4,12 +4,6 @@ Result: "errors": [ { "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 1, - "column": 3 - } - ], "path": [ "typeMismatch" ], diff --git a/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/QueryableSortingExtensionsTests.Extension_Should_BeMissingMiddleware.snap b/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/QueryableSortingExtensionsTests.Extension_Should_BeMissingMiddleware.snap index 1f2adad2388..eaf7a51c63b 100644 --- a/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/QueryableSortingExtensionsTests.Extension_Should_BeMissingMiddleware.snap +++ b/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/QueryableSortingExtensionsTests.Extension_Should_BeMissingMiddleware.snap @@ -4,12 +4,6 @@ Result: "errors": [ { "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 1, - "column": 3 - } - ], "path": [ "missingMiddleware" ], diff --git a/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/QueryableSortingExtensionsTests.Extension_Should_BeTypeMismatch.snap b/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/QueryableSortingExtensionsTests.Extension_Should_BeTypeMismatch.snap index c5d93a07a31..5bf9e0da3fe 100644 --- a/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/QueryableSortingExtensionsTests.Extension_Should_BeTypeMismatch.snap +++ b/src/HotChocolate/Data/test/Data.Sorting.Tests/__snapshots__/QueryableSortingExtensionsTests.Extension_Should_BeTypeMismatch.snap @@ -4,12 +4,6 @@ Result: "errors": [ { "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 1, - "column": 3 - } - ], "path": [ "typeMismatch" ], diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.ExecuteAsync_Should_Fail_When_SingleOrDefaultMoreThanOne.snap b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.ExecuteAsync_Should_Fail_When_SingleOrDefaultMoreThanOne.snap index 8863d34f99a..143a6373df9 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.ExecuteAsync_Should_Fail_When_SingleOrDefaultMoreThanOne.snap +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.ExecuteAsync_Should_Fail_When_SingleOrDefaultMoreThanOne.snap @@ -2,12 +2,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 2, - "column": 5 - } - ], "path": [ "executable" ] diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET10_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET10_0.md index 81a944d2170..1e67951d758 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET10_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET10_0.md @@ -19,7 +19,7 @@ LIMIT @p ## SQL 1 ```sql --- @keys={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @keys={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT s."OwnerId", s1.c, s1."Id", s1."Name", s1.c0, s1."Id0" FROM ( SELECT p."OwnerId" @@ -31,7 +31,7 @@ FROM ( LEFT JOIN ( SELECT s0.c, s0."Id", s0."Name", s0.c0, s0."Id0", s0."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Dog' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Cat' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@keys) @@ -44,7 +44,7 @@ ORDER BY s."OwnerId", s1."OwnerId", s1."Name", s1."Id" ## Expression 1 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 5 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET8_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET8_0.md index 3435e62af9d..6d4d3bf7427 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET8_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET8_0.md @@ -19,7 +19,7 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @__keys_0={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT t."OwnerId", t0.c, t0."Id", t0."Name", t0.c0, t0."Id0" FROM ( SELECT p."OwnerId" @@ -31,7 +31,7 @@ FROM ( LEFT JOIN ( SELECT t1.c, t1."Id", t1."Name", t1.c0, t1."Id0", t1."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Dog' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Cat' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@__keys_0) @@ -44,7 +44,7 @@ ORDER BY t."OwnerId", t0."OwnerId", t0."Name", t0."Id" ## Expression 1 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 5 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET9_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET9_0.md index 2a43662c793..696537b58fb 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET9_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_NET9_0.md @@ -19,7 +19,7 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @__keys_0={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT s."OwnerId", s1.c, s1."Id", s1."Name", s1.c0, s1."Id0" FROM ( SELECT p."OwnerId" @@ -31,7 +31,7 @@ FROM ( LEFT JOIN ( SELECT s0.c, s0."Id", s0."Name", s0.c0, s0."Id0", s0."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Dog' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Cat' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@__keys_0) @@ -44,7 +44,7 @@ ORDER BY s."OwnerId", s1."OwnerId", s1."Name", s1."Id" ## Expression 1 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 5 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET10_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET10_0.md index 71dc2cf4014..ba99b34dd82 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET10_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET10_0.md @@ -19,8 +19,8 @@ LIMIT @p ## SQL 1 ```sql --- @keys={ '6', '5', '4', '3', '2', ... } (DbType = Object) -SELECT s."OwnerId", s1.c, s1."Id", s1."IsPurring", s1."Name", s1.c0, s1."IsBarking", s1."Id0" +-- @keys={ '1', '2', '3', '4', '5', ... } (DbType = Object) +SELECT s."OwnerId", s1.c, s1."Id", s1."IsBarking", s1."Name", s1.c0, s1."IsPurring", s1."Id0" FROM ( SELECT p."OwnerId" FROM "Owners" AS o @@ -29,9 +29,9 @@ FROM ( GROUP BY p."OwnerId" ) AS s LEFT JOIN ( - SELECT s0.c, s0."Id", s0."IsPurring", s0."Name", s0.c0, s0."IsBarking", s0."Id0", s0."OwnerId" + SELECT s0.c, s0."Id", s0."IsBarking", s0."Name", s0.c0, s0."IsPurring", s0."Id0", s0."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."IsPurring", p0."Name", p0."AnimalType" = 'Dog' AS c0, p0."IsBarking", o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."IsBarking", p0."Name", p0."AnimalType" = 'Cat' AS c0, p0."IsPurring", o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@keys) @@ -44,7 +44,7 @@ ORDER BY s."OwnerId", s1."OwnerId", s1."Name", s1."Id" ## Expression 1 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, IsPurring = Convert(root, Cat).IsPurring, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, IsBarking = Convert(root, Dog).IsBarking, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, IsBarking = Convert(root, Dog).IsBarking, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, IsPurring = Convert(root, Cat).IsPurring, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 5 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET8_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET8_0.md index e60974e32b0..245a0524493 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET8_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET8_0.md @@ -19,8 +19,8 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '6', '5', '4', '3', '2', ... } (DbType = Object) -SELECT t."OwnerId", t0.c, t0."Id", t0."IsPurring", t0."Name", t0.c0, t0."IsBarking", t0."Id0" +-- @__keys_0={ '1', '2', '3', '4', '5', ... } (DbType = Object) +SELECT t."OwnerId", t0.c, t0."Id", t0."IsBarking", t0."Name", t0.c0, t0."IsPurring", t0."Id0" FROM ( SELECT p."OwnerId" FROM "Owners" AS o @@ -29,9 +29,9 @@ FROM ( GROUP BY p."OwnerId" ) AS t LEFT JOIN ( - SELECT t1.c, t1."Id", t1."IsPurring", t1."Name", t1.c0, t1."IsBarking", t1."Id0", t1."OwnerId" + SELECT t1.c, t1."Id", t1."IsBarking", t1."Name", t1.c0, t1."IsPurring", t1."Id0", t1."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."IsPurring", p0."Name", p0."AnimalType" = 'Dog' AS c0, p0."IsBarking", o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."IsBarking", p0."Name", p0."AnimalType" = 'Cat' AS c0, p0."IsPurring", o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@__keys_0) @@ -44,7 +44,7 @@ ORDER BY t."OwnerId", t0."OwnerId", t0."Name", t0."Id" ## Expression 1 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, IsPurring = Convert(root, Cat).IsPurring, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, IsBarking = Convert(root, Dog).IsBarking, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, IsBarking = Convert(root, Dog).IsBarking, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, IsPurring = Convert(root, Cat).IsPurring, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 5 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET9_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET9_0.md index d9b611f6aca..8206a9f2316 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET9_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_Fragments_NET9_0.md @@ -19,8 +19,8 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '6', '5', '4', '3', '2', ... } (DbType = Object) -SELECT s."OwnerId", s1.c, s1."Id", s1."IsPurring", s1."Name", s1.c0, s1."IsBarking", s1."Id0" +-- @__keys_0={ '1', '2', '3', '4', '5', ... } (DbType = Object) +SELECT s."OwnerId", s1.c, s1."Id", s1."IsBarking", s1."Name", s1.c0, s1."IsPurring", s1."Id0" FROM ( SELECT p."OwnerId" FROM "Owners" AS o @@ -29,9 +29,9 @@ FROM ( GROUP BY p."OwnerId" ) AS s LEFT JOIN ( - SELECT s0.c, s0."Id", s0."IsPurring", s0."Name", s0.c0, s0."IsBarking", s0."Id0", s0."OwnerId" + SELECT s0.c, s0."Id", s0."IsBarking", s0."Name", s0.c0, s0."IsPurring", s0."Id0", s0."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."IsPurring", p0."Name", p0."AnimalType" = 'Dog' AS c0, p0."IsBarking", o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."IsBarking", p0."Name", p0."AnimalType" = 'Cat' AS c0, p0."IsPurring", o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@__keys_0) @@ -44,7 +44,7 @@ ORDER BY s."OwnerId", s1."OwnerId", s1."Name", s1."Id" ## Expression 1 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, IsPurring = Convert(root, Cat).IsPurring, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, IsBarking = Convert(root, Dog).IsBarking, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, IsBarking = Convert(root, Dog).IsBarking, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, IsPurring = Convert(root, Cat).IsPurring, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 5 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET10_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET10_0.md index 8fe7cdca3a6..6d7576f0086 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET10_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET10_0.md @@ -19,7 +19,7 @@ LIMIT @p ## SQL 1 ```sql --- @keys={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @keys={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT p."OwnerId" AS "Key", count(*)::int AS "Count" FROM "Owners" AS o INNER JOIN "Pets" AS p ON o."Id" = p."OwnerId" @@ -36,7 +36,7 @@ GROUP BY p."OwnerId" ## SQL 2 ```sql --- @keys={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @keys={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT s."OwnerId", s1.c, s1."Id", s1."Name", s1.c0, s1."Id0" FROM ( SELECT p."OwnerId" @@ -48,7 +48,7 @@ FROM ( LEFT JOIN ( SELECT s0.c, s0."Id", s0."Name", s0.c0, s0."Id0", s0."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Dog' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Cat' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@keys) @@ -61,7 +61,7 @@ ORDER BY s."OwnerId", s1."OwnerId", s1."Name", s1."Id" ## Expression 2 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerWithCountDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerWithCountDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 7 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET8_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET8_0.md index 66c568168d1..b446cc1bc3d 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET8_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET8_0.md @@ -19,7 +19,7 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @__keys_0={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT p."OwnerId" AS "Key", count(*)::int AS "Count" FROM "Owners" AS o INNER JOIN "Pets" AS p ON o."Id" = p."OwnerId" @@ -36,7 +36,7 @@ GROUP BY p."OwnerId" ## SQL 2 ```sql --- @__keys_0={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @__keys_0={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT t."OwnerId", t0.c, t0."Id", t0."Name", t0.c0, t0."Id0" FROM ( SELECT p."OwnerId" @@ -48,7 +48,7 @@ FROM ( LEFT JOIN ( SELECT t1.c, t1."Id", t1."Name", t1.c0, t1."Id0", t1."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Dog' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Cat' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@__keys_0) @@ -61,7 +61,7 @@ ORDER BY t."OwnerId", t0."OwnerId", t0."Name", t0."Id" ## Expression 2 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerWithCountDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerWithCountDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 7 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET9_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET9_0.md index b21242bc907..e1c975188e5 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET9_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Owner_Animals_With_TotalCount_NET9_0.md @@ -19,7 +19,7 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @__keys_0={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT p."OwnerId" AS "Key", count(*)::int AS "Count" FROM "Owners" AS o INNER JOIN "Pets" AS p ON o."Id" = p."OwnerId" @@ -36,7 +36,7 @@ GROUP BY p."OwnerId" ## SQL 2 ```sql --- @__keys_0={ '6', '5', '4', '3', '2', ... } (DbType = Object) +-- @__keys_0={ '1', '2', '3', '4', '5', ... } (DbType = Object) SELECT s."OwnerId", s1.c, s1."Id", s1."Name", s1.c0, s1."Id0" FROM ( SELECT p."OwnerId" @@ -48,7 +48,7 @@ FROM ( LEFT JOIN ( SELECT s0.c, s0."Id", s0."Name", s0.c0, s0."Id0", s0."OwnerId" FROM ( - SELECT p0."AnimalType" = 'Cat' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Dog' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row + SELECT p0."AnimalType" = 'Dog' AS c, p0."Id", p0."Name", p0."AnimalType" = 'Cat' AS c0, o0."Id" AS "Id0", p0."OwnerId", ROW_NUMBER() OVER(PARTITION BY p0."OwnerId" ORDER BY p0."Name", p0."Id") AS row FROM "Owners" AS o0 INNER JOIN "Pets" AS p0 ON o0."Id" = p0."OwnerId" WHERE o0."Id" = ANY (@__keys_0) @@ -61,7 +61,7 @@ ORDER BY s."OwnerId", s1."OwnerId", s1."Name", s1."Id" ## Expression 2 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerWithCountDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), null))).Take(11).ToList()}) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].Where(t => value(HotChocolate.Data.InterfaceIntegrationTests+AnimalsByOwnerWithCountDataLoader+<>c__DisplayClass2_0).keys.Contains(t.Id)).SelectMany(t => t.Pets).GroupBy(t => t.OwnerId).Select(g => new Group`2() {Key = g.Key, Items = g.OrderBy(y => y.Name).ThenBy(y => y.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), null))).Take(11).ToList()}) ``` ## Result 7 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Pets.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Pets.md index 68032883f6d..a138ff6b1c6 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Pets.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Pets.md @@ -4,7 +4,7 @@ ```sql -- @__p_0='11' -SELECT p."AnimalType" = 'Cat', p."Id", p."Name", p."AnimalType" = 'Dog' +SELECT p."AnimalType" = 'Dog', p."Id", p."Name", p."AnimalType" = 'Cat' FROM "Pets" AS p ORDER BY p."Name", p."Id" LIMIT @__p_0 @@ -13,7 +13,7 @@ LIMIT @__p_0 ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), null))).Take(11) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), null))).Take(11) ``` ## Result 3 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Pets_NET10_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Pets_NET10_0.md index 75c7b042fc4..36067d96eb1 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Pets_NET10_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/InterfaceIntegrationTests.Query_Pets_NET10_0.md @@ -4,7 +4,7 @@ ```sql -- @p='11' -SELECT p."AnimalType" = 'Cat', p."Id", p."Name", p."AnimalType" = 'Dog' +SELECT p."AnimalType" = 'Dog', p."Id", p."Name", p."AnimalType" = 'Cat' FROM "Pets" AS p ORDER BY p."Name", p."Id" LIMIT @p @@ -13,7 +13,7 @@ LIMIT @p ## Expression 0 ```text -[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Select(root => IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), null))).Take(11) +[Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression].OrderBy(t => t.Name).ThenBy(t => t.Id).Select(root => IIF((root Is Dog), Convert(new Dog() {Id = Convert(root, Dog).Id, Name = Convert(root, Dog).Name}, Animal), IIF((root Is Cat), Convert(new Cat() {Id = Convert(root, Cat).Id, Name = Convert(root, Cat).Name}, Animal), null))).Take(11) ``` ## Result 3 diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET10_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET10_0.md index 2db8db0b122..0ac9a80f9a6 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET10_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET10_0.md @@ -19,7 +19,7 @@ LIMIT @p ## SQL 1 ```sql --- @keys={ '2', '1' } (DbType = Object) +-- @keys={ '1', '2' } (DbType = Object) SELECT p1."BrandId", p3."Id", p3."AvailableStock", p3."BrandId", p3."Description", p3."ImageFileName", p3."MaxStockThreshold", p3."Name", p3."OnReorder", p3."Price", p3."RestockThreshold", p3."TypeId" FROM ( SELECT p."BrandId" diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET8_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET8_0.md index b560f68ce8d..665e255b1cc 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET8_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET8_0.md @@ -19,7 +19,7 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '2', '1' } (DbType = Object) +-- @__keys_0={ '1', '2' } (DbType = Object) SELECT t."BrandId", t0."Id", t0."AvailableStock", t0."BrandId", t0."Description", t0."ImageFileName", t0."MaxStockThreshold", t0."Name", t0."OnReorder", t0."Price", t0."RestockThreshold", t0."TypeId" FROM ( SELECT p."BrandId" diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET9_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET9_0.md index 409cdb5263b..b1c183a796b 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET9_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_NET9_0.md @@ -19,7 +19,7 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '2', '1' } (DbType = Object) +-- @__keys_0={ '1', '2' } (DbType = Object) SELECT p1."BrandId", p3."Id", p3."AvailableStock", p3."BrandId", p3."Description", p3."ImageFileName", p3."MaxStockThreshold", p3."Name", p3."OnReorder", p3."Price", p3."RestockThreshold", p3."TypeId" FROM ( SELECT p."BrandId" diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET10_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET10_0.md index 8332860815f..0054b833968 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET10_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET10_0.md @@ -19,7 +19,7 @@ LIMIT @p ## SQL 1 ```sql --- @keys={ '2', '1' } (DbType = Object) +-- @keys={ '1', '2' } (DbType = Object) SELECT p1."BrandId", p3."Name", p3."BrandId", p3."Id" FROM ( SELECT p."BrandId" diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET8_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET8_0.md index 915df77d43e..72bbe8876c6 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET8_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET8_0.md @@ -19,7 +19,7 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '2', '1' } (DbType = Object) +-- @__keys_0={ '1', '2' } (DbType = Object) SELECT t."BrandId", t0."Name", t0."BrandId", t0."Id" FROM ( SELECT p."BrandId" diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET9_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET9_0.md index 7f482fb572d..c2f46a23fa2 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET9_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/PagingHelperIntegrationTests.Nested_Paging_First_2_With_Projections_NET9_0.md @@ -19,7 +19,7 @@ LIMIT @__p_0 ## SQL 1 ```sql --- @__keys_0={ '2', '1' } (DbType = Object) +-- @__keys_0={ '1', '2' } (DbType = Object) SELECT p1."BrandId", p3."Name", p3."BrandId", p3."Id" FROM ( SELECT p."BrandId" diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/ProjectableDataLoaderTests.Project_Key_To_Collection_Expression_Integration.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/ProjectableDataLoaderTests.Project_Key_To_Collection_Expression_Integration.md index 9cb6d640200..2406cb30ad4 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/ProjectableDataLoaderTests.Project_Key_To_Collection_Expression_Integration.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/ProjectableDataLoaderTests.Project_Key_To_Collection_Expression_Integration.md @@ -3,7 +3,7 @@ ## SQL ```text --- @__keys_0={ '2', '1' } (DbType = Object) +-- @__keys_0={ '1', '2' } (DbType = Object) SELECT b."Id", p."Name", p."Id" FROM "Brands" AS b LEFT JOIN "Products" AS p ON b."Id" = p."BrandId" diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/ProjectableDataLoaderTests.Project_Key_To_Collection_Expression_Integration_NET10_0.md b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/ProjectableDataLoaderTests.Project_Key_To_Collection_Expression_Integration_NET10_0.md index f738f8cf7b0..d0f477bb9db 100644 --- a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/ProjectableDataLoaderTests.Project_Key_To_Collection_Expression_Integration_NET10_0.md +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/ProjectableDataLoaderTests.Project_Key_To_Collection_Expression_Integration_NET10_0.md @@ -3,7 +3,7 @@ ## SQL ```text --- @keys={ '2', '1' } (DbType = Object) +-- @keys={ '1', '2' } (DbType = Object) SELECT b."Id", p."Name", p."Id" FROM "Brands" AS b LEFT JOIN "Products" AS p ON b."Id" = p."BrandId" diff --git a/src/HotChocolate/Data/test/Directory.Build.props b/src/HotChocolate/Data/test/Directory.Build.props index 4516f1b3c88..aba8188c0cf 100644 --- a/src/HotChocolate/Data/test/Directory.Build.props +++ b/src/HotChocolate/Data/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics/ActivityEnricher.cs b/src/HotChocolate/Diagnostics/src/Diagnostics/ActivityEnricher.cs index 6e4c839572a..10e92743932 100644 --- a/src/HotChocolate/Diagnostics/src/Diagnostics/ActivityEnricher.cs +++ b/src/HotChocolate/Diagnostics/src/Diagnostics/ActivityEnricher.cs @@ -238,9 +238,7 @@ protected virtual void EnrichRequestVariables( GraphQLRequest request, ISyntaxNode variables, Activity activity) - { - activity.SetTag("graphql.http.request.variables", variables.Print()); - } + => activity.SetTag("graphql.http.request.variables", variables.Print(indented: false)); protected virtual void EnrichBatchVariables( HttpContext context, @@ -248,9 +246,7 @@ protected virtual void EnrichBatchVariables( ISyntaxNode variables, int index, Activity activity) - { - activity.SetTag($"graphql.http.request[{index}].variables", variables.Print()); - } + => activity.SetTag($"graphql.http.request[{index}].variables", variables.Print(indented: false)); protected virtual void EnrichRequestExtensions( HttpContext context, diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics/InstrumentationOptions.cs b/src/HotChocolate/Diagnostics/src/Diagnostics/InstrumentationOptions.cs index 6a831e02f40..a28fe4fbd41 100644 --- a/src/HotChocolate/Diagnostics/src/Diagnostics/InstrumentationOptions.cs +++ b/src/HotChocolate/Diagnostics/src/Diagnostics/InstrumentationOptions.cs @@ -23,7 +23,7 @@ public sealed class InstrumentationOptions public bool IncludeDocument { get; set; } /// - /// Specifies if DataLoader batch keys shall included into the tracing data. + /// Specifies if DataLoader batch keys shall be included into the tracing data. /// public bool IncludeDataLoaderKeys { get; set; } diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_capture_deferred_response.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_capture_deferred_response.snap index 42ce918b5ff..88a26adcf95 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_capture_deferred_response.snap +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_capture_deferred_response.snap @@ -151,60 +151,51 @@ "Value": "OK" } ], - "event": [], - "activities": [ - { - "OperationName": "ResolveFieldValue", - "DisplayName": "/hero/id", - "Status": "Ok", - "tags": [ - { - "Key": "graphql.selection.name", - "Value": "id" - }, - { - "Key": "graphql.selection.type", - "Value": "ID!" - }, - { - "Key": "graphql.selection.path", - "Value": "/hero/id" - }, - { - "Key": "graphql.selection.hierarchy", - "Value": "/hero/id" - }, - { - "Key": "graphql.selection.field.name", - "Value": "id" - }, - { - "Key": "graphql.selection.field.coordinate", - "Value": "Droid.id" - }, - { - "Key": "graphql.selection.field.declaringType", - "Value": "Droid" - }, - { - "Key": "otel.status_code", - "Value": "OK" - } - ], - "event": [] + "event": [] + }, + { + "OperationName": "ResolveFieldValue", + "DisplayName": "/hero/id", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.selection.name", + "Value": "id" + }, + { + "Key": "graphql.selection.type", + "Value": "ID!" + }, + { + "Key": "graphql.selection.path", + "Value": "/hero/id" + }, + { + "Key": "graphql.selection.hierarchy", + "Value": "/hero/id" + }, + { + "Key": "graphql.selection.field.name", + "Value": "id" + }, + { + "Key": "graphql.selection.field.coordinate", + "Value": "Droid.id" + }, + { + "Key": "graphql.selection.field.declaringType", + "Value": "Droid" + }, + { + "Key": "otel.status_code", + "Value": "OK" } - ] + ], + "event": [] } ] } ] - }, - { - "OperationName": "ExecuteStream", - "DisplayName": "ExecuteStream", - "Status": "Unset", - "tags": [], - "event": [] } ] } diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_ensure_list_path_is_correctly_built.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_ensure_list_path_is_correctly_built.snap index 41a0dfdc2ad..8a5b3455d73 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_ensure_list_path_is_correctly_built.snap +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_ensure_list_path_is_correctly_built.snap @@ -195,7 +195,7 @@ }, { "OperationName": "ResolveFieldValue", - "DisplayName": "/hero/friends/nodes[2]/friends", + "DisplayName": "/hero/friends/nodes[0]/friends", "Status": "Ok", "tags": [ { @@ -208,7 +208,7 @@ }, { "Key": "graphql.selection.path", - "Value": "/hero/friends/nodes[2]/friends" + "Value": "/hero/friends/nodes[0]/friends" }, { "Key": "graphql.selection.hierarchy", @@ -275,7 +275,7 @@ }, { "OperationName": "ResolveFieldValue", - "DisplayName": "/hero/friends/nodes[0]/friends", + "DisplayName": "/hero/friends/nodes[2]/friends", "Status": "Ok", "tags": [ { @@ -288,7 +288,7 @@ }, { "Key": "graphql.selection.path", - "Value": "/hero/friends/nodes[0]/friends" + "Value": "/hero/friends/nodes[2]/friends" }, { "Key": "graphql.selection.hierarchy", diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionComplexTypeDefinition.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionComplexTypeDefinition.cs index 073a86c5cca..405f5c84697 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionComplexTypeDefinition.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionComplexTypeDefinition.cs @@ -175,12 +175,13 @@ public override string ToString() /// Creates a from a /// . /// - public ComplexTypeDefinitionNodeBase ToSyntaxNode() => this switch - { - FusionInterfaceTypeDefinition i => SchemaDebugFormatter.Format(i), - FusionObjectTypeDefinition o => SchemaDebugFormatter.Format(o), - _ => throw new ArgumentOutOfRangeException() - }; + public ComplexTypeDefinitionNodeBase ToSyntaxNode() + => this switch + { + FusionInterfaceTypeDefinition i => SchemaDebugFormatter.Format(i), + FusionObjectTypeDefinition o => SchemaDebugFormatter.Format(o), + _ => throw new ArgumentOutOfRangeException() + }; ISyntaxNode ISyntaxNodeProvider.ToSyntaxNode() => ToSyntaxNode(); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionScalarTypeDefinition.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionScalarTypeDefinition.cs index 2ecb60abaa1..287e23ba318 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionScalarTypeDefinition.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/FusionScalarTypeDefinition.cs @@ -32,6 +32,7 @@ public FusionScalarTypeDefinition( Name = name; Description = description; IsInaccessible = isInaccessible; + IsUpload = name.Equals("Upload"); // these properties are initialized // in the type complete step. @@ -65,6 +66,11 @@ public FusionScalarTypeDefinition( /// public bool IsInaccessible { get; } + /// + /// Specifies if this scalar is the file upload scalar. + /// + public bool IsUpload { get; } + /// /// Gets the directives applied to this scalar type. /// diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs index 8da1f9d4d10..6a88341d878 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs @@ -92,14 +92,16 @@ private GraphQLHttpRequest CreateHttpRequest( return new GraphQLHttpRequest(CreateOperationBatchRequest(operationSourceText, originalRequest)) { Uri = _configuration.BaseAddress, - Accept = _configuration.BatchingAcceptHeaderValues + Accept = _configuration.BatchingAcceptHeaderValues, + EnableFileUploads = originalRequest.RequiresFileUpload }; } return new GraphQLHttpRequest(CreateVariableBatchRequest(operationSourceText, originalRequest)) { Uri = _configuration.BaseAddress, - Accept = _configuration.BatchingAcceptHeaderValues + Accept = _configuration.BatchingAcceptHeaderValues, + EnableFileUploads = originalRequest.RequiresFileUpload }; } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/JsonVariableCoercion.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/JsonVariableCoercion.cs new file mode 100644 index 00000000000..35ef6c4312b --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/JsonVariableCoercion.cs @@ -0,0 +1,541 @@ +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using HotChocolate.Buffers; +using HotChocolate.Execution; +using HotChocolate.Features; +using HotChocolate.Fusion.Types; +using HotChocolate.Language; +using HotChocolate.Transport.Http; +using HotChocolate.Types; + +namespace HotChocolate.Fusion.Execution; + +internal ref struct JsonVariableCoercion +{ + private const int MaxAllowedDepth = 64; + private readonly IFeatureProvider _context; + private readonly ref Utf8MemoryBuilder? _memory; + + public JsonVariableCoercion(IFeatureProvider context, ref Utf8MemoryBuilder? memory) + { + _context = context; + _memory = ref memory; + } + + public bool TryCoerceVariableValue( + string variableName, + IInputType variableType, + JsonElement inputValue, + [NotNullWhen(true)] out VariableValue? variableValue, + [NotNullWhen(false)] out IError? error) + { + if (inputValue.ValueKind is JsonValueKind.Undefined) + { + throw new ArgumentException("Undefined JSON value kind."); + } + + var root = Path.Root.Append(variableName); + + try + { + if (TryParseAndValidate(variableType, inputValue, root, 0, out var valueLiteral, out error)) + { + variableValue = new VariableValue(variableName, variableType, valueLiteral); + return true; + } + + variableValue = null; + return false; + } + catch + { + _memory?.Abandon(); + _memory = null; + throw; + } + } + + private bool TryParseAndValidate( + IInputType type, + JsonElement element, + Path path, + int depth, + [NotNullWhen(true)] out IValueNode? value, + [NotNullWhen(false)] out IError? error) + { + if (depth > MaxAllowedDepth) + { + throw new InvalidOperationException("Max allowed depth reached."); + } + + // Handle NonNull types + if (type.Kind is TypeKind.NonNull) + { + if (element.ValueKind is JsonValueKind.Null) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("The value is not a non-null value.") + .SetExtension("variable", $"{path}") + .Build(); + return false; + } + + type = (IInputType)type.InnerType(); + } + + // Handle null values + if (element.ValueKind is JsonValueKind.Null) + { + value = NullValueNode.Default; + error = null; + return true; + } + + // Handle List types + if (type.Kind is TypeKind.List) + { + if (element.ValueKind is not JsonValueKind.Array) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("The value is not a list value.") + .SetExtension("variable", $"{path}") + .Build(); + return false; + } + + var elementType = (IInputType)type.ListType().ElementType; + var buffer = ArrayPool.Shared.Rent(64); + var count = 0; + + try + { + var index = 0; + foreach (var item in element.EnumerateArray()) + { + if (count == buffer.Length) + { + var temp = buffer; + var tempSpan = temp.AsSpan(); + buffer = ArrayPool.Shared.Rent(count * 2); + tempSpan.CopyTo(buffer); + tempSpan.Clear(); + ArrayPool.Shared.Return(temp); + } + + if (!TryParseAndValidate( + elementType, + item, + path.Append(index), + depth + 1, + out var itemValue, + out error)) + { + value = null; + return false; + } + + buffer[count++] = itemValue; + index++; + } + + value = new ListValueNode(buffer.AsSpan(0, count).ToArray()); + error = null; + return true; + } + finally + { + buffer.AsSpan(0, count).Clear(); + ArrayPool.Shared.Return(buffer); + } + } + + // Handle InputObject types + if (type.Kind is TypeKind.InputObject) + { + return TryParseInputObject(type, element, path, depth, out value, out error); + } + + // Handle Scalar types + if (type is FusionScalarTypeDefinition scalarType) + { + return TryParseScalar(scalarType, element, path, depth, out value, out error); + } + + // Handle Enum types + if (type is FusionEnumTypeDefinition enumType) + { + return TryParseEnum(enumType, element, path, out value, out error); + } + + throw new NotSupportedException( + $"The type `{type.FullTypeName()}` is not a valid input type."); + } + + private bool TryParseInputObject( + IInputType type, + JsonElement element, + Path path, + int depth, + [NotNullWhen(true)] out IValueNode? value, + [NotNullWhen(false)] out IError? error) + { + if (element.ValueKind is not JsonValueKind.Object) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("The value is not an object value.") + .SetExtension("variable", $"{path}") + .Build(); + return false; + } + + var inputObjectType = (FusionInputObjectTypeDefinition)type; + var oneOf = inputObjectType.IsOneOf; + + // Count fields first for OneOf validation + var fieldCount = 0; + foreach (var _ in element.EnumerateObject()) + { + fieldCount++; + } + + if (oneOf && fieldCount is 0) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("The OneOf Input Object `{0}` requires that exactly one field is supplied and that field must not be `null`. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", inputObjectType.Name) + .SetCode(ErrorCodes.Execution.OneOfNoFieldSet) + .SetPath(path) + .Build(); + return false; + } + + if (oneOf && fieldCount > 1) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("More than one field of the OneOf Input Object `{0}` is set. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", inputObjectType.Name) + .SetCode(ErrorCodes.Execution.OneOfMoreThanOneFieldSet) + .SetPath(path) + .Build(); + return false; + } + + var numberOfInputFields = inputObjectType.Fields.Count; + var processedCount = 0; + bool[]? processedBuffer = null; + var processed = depth <= 256 && numberOfInputFields <= 32 + ? stackalloc bool[numberOfInputFields] + : processedBuffer = ArrayPool.Shared.Rent(numberOfInputFields); + + if (processedBuffer is not null) + { + processed.Clear(); + } + + var buffer = ArrayPool.Shared.Rent(64); + var count = 0; + + try + { + foreach (var property in element.EnumerateObject()) + { + if (!inputObjectType.Fields.TryGetField(property.Name, out var fieldDefinition)) + { + value = null; + error = ErrorBuilder.New() + .SetMessage( + "The field `{0}` is not defined on the input object type `{1}`.", + property.Name, + inputObjectType.Name) + .SetExtension("variable", $"{path}") + .Build(); + return false; + } + + if (oneOf && property.Value.ValueKind is JsonValueKind.Null) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("`null` was set to the field `{0}` of the OneOf Input Object `{1}`. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", property.Name, inputObjectType.Name) + .SetCode(ErrorCodes.Execution.OneOfFieldIsNull) + .SetPath(path) + .SetCoordinate(fieldDefinition.Coordinate) + .Build(); + return false; + } + + if (count == buffer.Length) + { + var temp = buffer; + var tempSpan = temp.AsSpan(); + buffer = ArrayPool.Shared.Rent(count * 2); + tempSpan.CopyTo(buffer); + tempSpan.Clear(); + ArrayPool.Shared.Return(temp); + } + + if (!TryParseAndValidate( + fieldDefinition.Type, + property.Value, + path.Append(property.Name), + depth + 1, + out var fieldValue, + out error)) + { + value = null; + return false; + } + + buffer[count++] = new ObjectFieldNode(property.Name, fieldValue); + processed[fieldDefinition.Index] = true; + processedCount++; + } + + // Check for missing required fields + if (!oneOf && processedCount != numberOfInputFields) + { + for (var i = 0; i < numberOfInputFields; i++) + { + if (!processed[i]) + { + var field = inputObjectType.Fields[i]; + + if (field.Type.Kind == TypeKind.NonNull && field.DefaultValue is null) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("The required input field `{0}` is missing.", field.Name) + .SetPath(path.Append(field.Name)) + .SetExtension("field", field.Coordinate.ToString()) + .Build(); + return false; + } + } + } + } + + value = new ObjectValueNode(buffer.AsSpan(0, count).ToArray()); + error = null; + return true; + } + finally + { + buffer.AsSpan(0, count).Clear(); + ArrayPool.Shared.Return(buffer); + + if (processedBuffer is not null) + { + ArrayPool.Shared.Return(processedBuffer); + } + } + } + + private readonly bool TryParseScalar( + FusionScalarTypeDefinition scalarType, + JsonElement element, + Path path, + int depth, + [NotNullWhen(true)] out IValueNode? value, + [NotNullWhen(false)] out IError? error) + { + if (scalarType.IsUpload) + { + if (element.ValueKind is JsonValueKind.String + && element.GetString() is { Length: > 0 } fileKey + && _context.Features.GetRequired().TryGetFile(fileKey, out var file)) + { + value = new FileReferenceNode(file.OpenReadStream, file.Name, file.ContentType); + error = null; + return true; + } + + error = ErrorBuilder.New() + .SetMessage("The value is not a valid file.") + .SetExtension("variable", $"{path}") + .Build(); + value = null; + return false; + } + else + { + value = ParseLiteral(element, depth); + + if (!scalarType.IsValueCompatible(value)) + { + error = ErrorBuilder.New() + .SetMessage( + "The value `{0}` is not a valid value for the scalar type `{1}`.", + value, + scalarType.Name) + .SetExtension("variable", $"{path}") + .Build(); + value = null; + return false; + } + } + + error = null; + return true; + } + + private static bool TryParseEnum( + FusionEnumTypeDefinition enumType, + JsonElement element, + Path path, + [NotNullWhen(true)] out IValueNode? value, + [NotNullWhen(false)] out IError? error) + { + if (element.ValueKind is not JsonValueKind.String) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("The value is not an enum value.") + .SetExtension("variable", $"{path}") + .Build(); + return false; + } + + var enumValue = element.GetString()!; + + if (!enumType.Values.ContainsName(enumValue)) + { + value = null; + error = ErrorBuilder.New() + .SetMessage("The value `{0}` is not a valid value for the enum type `{1}`.", enumValue, enumType.Name) + .SetExtension("variable", $"{path}") + .Build(); + return false; + } + + value = new EnumValueNode(enumValue); + error = null; + return true; + } + + private readonly IValueNode ParseLiteral(JsonElement element, int depth) + { + if (depth > MaxAllowedDepth) + { + throw new InvalidOperationException("Max allowed depth reached."); + } + + switch (element.ValueKind) + { + case JsonValueKind.Null: + return NullValueNode.Default; + + case JsonValueKind.True: + return BooleanValueNode.True; + + case JsonValueKind.False: + return BooleanValueNode.False; + + case JsonValueKind.String: + { + var rawValue = element.GetRawText(); + var utf8Value = System.Text.Encoding.UTF8.GetBytes(rawValue); + var span = utf8Value.AsSpan(); + span = span[1..^1]; // Remove quotes + var segment = WriteValue(span); + return new StringValueNode(null, segment, false); + } + + case JsonValueKind.Number: + { + var rawValue = element.GetRawText(); + var utf8Value = System.Text.Encoding.UTF8.GetBytes(rawValue); + var span = utf8Value.AsSpan(); + var segment = WriteValue(span); + + if (span.IndexOfAny((byte)'e', (byte)'E') > -1) + { + return new FloatValueNode(segment, FloatFormat.Exponential); + } + + if (span.IndexOf((byte)'.') > -1) + { + return new FloatValueNode(segment, FloatFormat.FixedPoint); + } + + return new IntValueNode(segment); + } + + case JsonValueKind.Array: + { + var buffer = ArrayPool.Shared.Rent(64); + var count = 0; + + try + { + foreach (var item in element.EnumerateArray()) + { + if (count == buffer.Length) + { + var temp = buffer; + var tempSpan = temp.AsSpan(); + buffer = ArrayPool.Shared.Rent(count * 2); + tempSpan.CopyTo(buffer); + tempSpan.Clear(); + ArrayPool.Shared.Return(temp); + } + + buffer[count++] = ParseLiteral(item, depth + 1); + } + + return new ListValueNode(buffer.AsSpan(0, count).ToArray()); + } + finally + { + buffer.AsSpan(0, count).Clear(); + ArrayPool.Shared.Return(buffer); + } + } + + case JsonValueKind.Object: + { + var buffer = ArrayPool.Shared.Rent(64); + var count = 0; + + try + { + foreach (var item in element.EnumerateObject()) + { + if (count == buffer.Length) + { + var temp = buffer; + var tempSpan = temp.AsSpan(); + buffer = ArrayPool.Shared.Rent(count * 2); + tempSpan.CopyTo(buffer); + tempSpan.Clear(); + ArrayPool.Shared.Return(temp); + } + + buffer[count++] = new ObjectFieldNode( + item.Name, + ParseLiteral(item.Value, depth + 1)); + } + + return new ObjectValueNode(buffer.AsSpan(0, count).ToArray()); + } + finally + { + buffer.AsSpan(0, count).Clear(); + ArrayPool.Shared.Return(buffer); + } + } + + default: + throw new InvalidOperationException($"Unexpected JSON value kind: {element.ValueKind}"); + } + } + + private readonly ReadOnlyMemorySegment WriteValue(ReadOnlySpan value) + { + _memory ??= new Utf8MemoryBuilder(); + return _memory.Write(value); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Operation.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Operation.cs index 75e0f95e67b..17f70bfee08 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Operation.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Operation.cs @@ -105,6 +105,8 @@ ISelectionSet IOperation.RootSelectionSet /// public IFeatureCollection Features => _features; + public bool HasIncrementalParts => throw new NotImplementedException(); + /// /// Gets the selection set for the specified /// if the selections named return type is an object type. diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs index dcdca53e219..7eb3b4f51d0 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Selection.cs @@ -158,6 +158,11 @@ internal void Seal(SelectionSet selectionSet) DeclaringSelectionSet = selectionSet; } + public bool IsDeferred(ulong deferFlags) + { + throw new NotImplementedException(); + } + [Flags] private enum Flags { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs index 2ab53071b58..4b4dca60b11 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs @@ -61,6 +61,8 @@ public SelectionSet(int id, IObjectTypeDefinition type, Selection[] selections, /// public ReadOnlySpan Selections => _selections; + public bool HasIncrementalParts => throw new NotImplementedException(); + IEnumerable ISelectionSet.GetSelections() => _selections; /// diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Pipeline/OperationVariableCoercionMiddleware.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Pipeline/OperationVariableCoercionMiddleware.cs index ce81dcf1492..3fd76df4ef0 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Pipeline/OperationVariableCoercionMiddleware.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Pipeline/OperationVariableCoercionMiddleware.cs @@ -59,6 +59,7 @@ private static bool TryCoerceVariables( using (diagnosticEvents.CoerceVariables(context)) { if (VariableCoercionHelper.TryCoerceVariableValues( + context, context.Schema, variableDefinitions, operationRequest.VariableValues?.Document.RootElement ?? default, @@ -85,6 +86,7 @@ private static bool TryCoerceVariables( foreach (var variableValuesInput in variableValuesSetInput.EnumerateArray()) { if (VariableCoercionHelper.TryCoerceVariableValues( + context, context.Schema, variableDefinitions, variableValuesInput, diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/VariableCoercionHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/VariableCoercionHelper.cs index 6260f0d7b1f..ef0fc3dde9c 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/VariableCoercionHelper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/VariableCoercionHelper.cs @@ -1,22 +1,16 @@ -using System.Buffers; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using HotChocolate.Execution; -using HotChocolate.Fusion.Types; +using HotChocolate.Features; using HotChocolate.Language; using HotChocolate.Types; namespace HotChocolate.Fusion.Execution; -// TODO : File Upload Rewrite -// return new FileReferenceNode( -// fileValueNode.Value.OpenReadStream, -// fileValueNode.Value.Name, -// fileValueNode.Value.ContentType); - internal static class VariableCoercionHelper { public static bool TryCoerceVariableValues( + IFeatureProvider context, ISchemaDefinition schema, IReadOnlyList variableDefinitions, JsonElement variableValues, @@ -33,6 +27,7 @@ public static bool TryCoerceVariableValues( nameof(variableValues)); } + Utf8MemoryBuilder? memory = null; var hasVariables = variableValues.ValueKind is JsonValueKind.Object; coercedVariableValues = []; error = null; @@ -75,9 +70,11 @@ public static bool TryCoerceVariableValues( else { if (TryCoerceVariableValue( + context, variableDefinition, variableType, propertyValue, + ref memory, out var variableValue, out error)) { @@ -91,272 +88,26 @@ public static bool TryCoerceVariableValues( } } + memory?.Seal(); return true; } private static bool TryCoerceVariableValue( + IFeatureProvider context, VariableDefinitionNode variableDefinition, IInputType variableType, JsonElement value, + ref Utf8MemoryBuilder? memory, [NotNullWhen(true)] out VariableValue? variableValue, [NotNullWhen(false)] out IError? error) { - var root = Path.Root.Append(variableDefinition.Variable.Name.Value); - var parser = new JsonValueParser(); - var valueLiteral = parser.Parse(value); - - if (!ValidateValue( - variableType, - valueLiteral, - root, - 0, - out error)) - { - variableValue = null; - return false; - } - - variableValue = new VariableValue( + var coercion = new JsonVariableCoercion(context, ref memory); + return coercion.TryCoerceVariableValue( variableDefinition.Variable.Name.Value, variableType, - valueLiteral); - return true; - } - - private static bool ValidateValue( - IInputType type, - IValueNode value, - Path path, - int stack, - [NotNullWhen(false)] out IError? error) - { - if (type.Kind is TypeKind.NonNull) - { - if (value.Kind is SyntaxKind.NullValue) - { - error = ErrorBuilder.New() - .SetMessage("The value `{0}` is not a non-null value.", value) - .SetExtension("variable", $"{path}") - .Build(); - return false; - } - - type = (IInputType)type.InnerType(); - } - - if (value.Kind is SyntaxKind.NullValue) - { - error = null; - return true; - } - - if (type.Kind is TypeKind.List) - { - if (value is not ListValueNode listValue) - { - error = ErrorBuilder.New() - .SetMessage("The value `{0}` is not a list value.", value) - .SetExtension("variable", $"{path}") - .Build(); - return false; - } - - var elementType = (IInputType)type.ListType().ElementType; - - for (var i = 0; i < listValue.Items.Count; i++) - { - if (!ValidateValue(elementType, listValue.Items[i], path.Append(i), stack, out error)) - { - return false; - } - } - - error = null; - return true; - } - - if (type.Kind is TypeKind.InputObject) - { - if (value is not ObjectValueNode objectValue) - { - error = ErrorBuilder.New() - .SetMessage("The value `{0}` is not an object value.", value) - .SetExtension("variable", $"{path}") - .Build(); - return false; - } - - var inputObjectType = (FusionInputObjectTypeDefinition)type; - - var oneOf = inputObjectType.IsOneOf; - - if (oneOf && objectValue.Fields.Count is 0) - { - // TODO : resources - error = ErrorBuilder.New() - .SetMessage("The OneOf Input Object `{0}` requires that exactly one field is supplied and that field must not be `null`. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", inputObjectType.Name) - .SetCode(ErrorCodes.Execution.OneOfNoFieldSet) - .SetPath(path) - .Build(); - return false; - } - - if (oneOf && objectValue.Fields.Count > 1) - { - // TODO : resources - error = ErrorBuilder.New() - .SetMessage("More than one field of the OneOf Input Object `{0}` is set. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", inputObjectType.Name) - .SetCode(ErrorCodes.Execution.OneOfMoreThanOneFieldSet) - .SetPath(path) - .Build(); - return false; - } - - var numberOfInputFields = inputObjectType.Fields.Count; - - var processedCount = 0; - bool[]? processedBuffer = null; - var processed = stack <= 256 && numberOfInputFields <= 32 - ? stackalloc bool[numberOfInputFields] - : processedBuffer = ArrayPool.Shared.Rent(numberOfInputFields); - - if (processedBuffer is not null) - { - processed.Clear(); - } - - if (processedBuffer is null) - { - stack += numberOfInputFields; - } - - try - { - for (var i = 0; i < objectValue.Fields.Count; i++) - { - var field = objectValue.Fields[i]; - if (!inputObjectType.Fields.TryGetField(field.Name.Value, out var fieldDefinition)) - { - // TODO : resources - error = ErrorBuilder.New() - .SetMessage( - "The field `{0}` is not defined on the input object type `{1}`.", - field.Name.Value, - inputObjectType.Name) - .SetExtension("variable", $"{path}") - .Build(); - return false; - } - - if (oneOf && field.Value.Kind is SyntaxKind.NullValue) - { - // TODO : resources - error = ErrorBuilder.New() - .SetMessage("`null` was set to the field `{0}`of the OneOf Input Object `{1}`. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", field.Name, inputObjectType.Name) - .SetCode(ErrorCodes.Execution.OneOfFieldIsNull) - .SetPath(path) - .SetCoordinate(fieldDefinition.Coordinate) - .Build(); - return false; - } - - if (!ValidateValue( - fieldDefinition.Type, - field.Value, - path.Append(field.Name.Value), - stack, - out error)) - { - return false; - } - - processed[fieldDefinition.Index] = true; - processedCount++; - } - - // If not all fields of the input object type were specified, - // we have to check if any of the ones left out are non-null - // and do not have a default value, and if so, raise an error. - if (!oneOf && processedCount != numberOfInputFields) - { - for (var i = 0; i < numberOfInputFields; i++) - { - if (!processed[i]) - { - var field = inputObjectType.Fields[i]; - - if (field.Type.Kind == TypeKind.NonNull && field.DefaultValue is null) - { - error = ErrorBuilder.New() - .SetMessage("The required input field `{0}` is missing.", field.Name) - .SetPath(path.Append(field.Name)) - .SetExtension("field", field.Coordinate.ToString()) - .Build(); - return false; - } - } - } - } - - error = null; - return true; - } - finally - { - if (processedBuffer is not null) - { - ArrayPool.Shared.Return(processedBuffer); - } - } - } - - if (type is IScalarTypeDefinition scalarType) - { - if (!scalarType.IsValueCompatible(value)) - { - // TODO : resources - error = ErrorBuilder.New() - .SetMessage( - "The value `{0}` is not a valid value for the scalar type `{1}`.", - value, - scalarType.Name) - .SetExtension("variable", $"{path}") - .Build(); - return false; - } - - error = null; - return true; - } - - if (type is FusionEnumTypeDefinition enumType) - { - if (value is not (StringValueNode or EnumValueNode)) - { - // TODO : resources - error = ErrorBuilder.New() - .SetMessage("The value `{0}` is not an enum value.", value.Value ?? "null") - .SetExtension("variable", $"{path}") - .Build(); - return false; - } - - if (!enumType.Values.ContainsName((string)value.Value!)) - { - // TODO : resources - error = ErrorBuilder.New() - .SetMessage("The value `{0}` is not a valid value for the enum type `{1}`.", value.Value ?? "null", enumType.Name) - .SetExtension("variable", $"{path}") - .Build(); - return false; - } - - error = null; - return true; - } - - throw new NotSupportedException( - $"The type `{type.FullTypeName()}` is not a valid input type."); + value, + out variableValue, + out error); } private static IInputType AssertInputType( diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.WriteTo.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.WriteTo.cs index 52882b4fc07..91a30ece7c3 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.WriteTo.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Text/Json/SourceResultDocument.WriteTo.cs @@ -74,25 +74,26 @@ private void WriteObject(Cursor start, DbRow startRow) while (current < end) { - var row = document._parsedData.Get(current); - Debug.Assert(row.TokenType is JsonTokenType.PropertyName); + var nameRow = document._parsedData.Get(current); + Debug.Assert(nameRow.TokenType is JsonTokenType.PropertyName); // property name - writer.WritePropertyName(document.ReadRawValue(row, includeQuotes: false)); + writer.WritePropertyName(document.ReadRawValue(nameRow, includeQuotes: false)); // property value - current++; - row = document._parsedData.Get(current); - WriteValue(current, row); + var valueCursor = current + 1; + var valueRow = document._parsedData.Get(valueCursor); + current = valueCursor; + WriteValue(current, valueRow); // next property (move past value) - if (row.IsSimpleValue) + if (valueRow.IsSimpleValue) { current++; } else { - current += row.NumberOfRows; + current += valueRow.NumberOfRows; } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/InlineFragmentOperationRewriter.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/InlineFragmentOperationRewriter.cs index 7c95b57d7d2..25c7d5d2e22 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/InlineFragmentOperationRewriter.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/InlineFragmentOperationRewriter.cs @@ -6,6 +6,21 @@ namespace HotChocolate.Fusion.Rewriters; +/// +/// Rewrites GraphQL operation documents by inlining all fragment spreads and merging inline fragments +/// into a single flattened selection set. This eliminates fragment definitions and produces an operation +/// with all selections explicitly expanded, making it suitable for execution planning and optimization. +/// +/// +/// The rewriter performs the following transformations: +/// +/// Expands all fragment spreads by substituting them with their fragment definition's selection set +/// Merges inline fragments that share the same type condition +/// Combines duplicate field selections +/// Optionally removes selections with static @skip/@include directives +/// Detects @defer and @stream directives for incremental delivery support +/// +/// public sealed class InlineFragmentOperationRewriter( ISchemaDefinition schema, bool removeStaticallyExcludedSelections = false, @@ -23,12 +38,29 @@ [new DirectiveNode("fusion__empty")], ImmutableArray.Empty, null); - public DocumentNode RewriteDocument(DocumentNode document, string? operationName = null) + /// + /// Rewrites a GraphQL document by inlining all fragments and flattening the operation's selection set. + /// + /// The GraphQL document to rewrite. + /// + /// The name of the operation to rewrite. If null, the first or only operation in the document is used. + /// + /// + /// A result containing the rewritten document with all fragments inlined and a flag indicating + /// whether the document contains @defer or @stream directives for incremental delivery. + /// + /// + /// Thrown when the document references undefined fragments or invalid type conditions. + /// + public InlineFragmentOperationRewriterResult RewriteDocument( + DocumentNode document, + string? operationName = null) { + var hasIncrementalParts = false; var operation = document.GetOperation(operationName); var operationType = schema.GetOperationType(operation.Operation); var fragmentLookup = CreateFragmentLookup(document); - var context = new Context(operationType, fragmentLookup); + var context = new Context(operationType, fragmentLookup, ref hasIncrementalParts); CollectSelections(operation.SelectionSet, context); RewriteSelections(context); @@ -46,7 +78,8 @@ public DocumentNode RewriteDocument(DocumentNode document, string? operationName RewriteDirectives(operation.Directives), newSelectionSet); - return new DocumentNode(ImmutableArray.Empty.Add(newOperation)); + var rewrittenDocument = new DocumentNode(ImmutableArray.Empty.Add(newOperation)); + return new InlineFragmentOperationRewriterResult(rewrittenDocument, hasIncrementalParts); } internal void CollectSelections(SelectionSetNode selectionSet, Context context) @@ -56,6 +89,12 @@ internal void CollectSelections(SelectionSetNode selectionSet, Context context) switch (selection) { case FieldNode field: + // Check for @stream directive (only valid on fields) + if (HasStreamDirective(field.Directives)) + { + context.MarkAsIncremental(); + } + if (!removeStaticallyExcludedSelections || IsIncluded(field.Directives)) { context.AddField(field); @@ -198,6 +237,12 @@ private void RewriteField(FieldNode fieldNode, Context context) private void CollectInlineFragment(InlineFragmentNode inlineFragment, Context context) { + // Check for @defer directive (only valid on inline fragments) + if (HasDeferDirective(inlineFragment.Directives)) + { + context.MarkAsIncremental(); + } + if ((inlineFragment.TypeCondition is null || inlineFragment.TypeCondition.Name.Value.Equals(context.Type.Name, StringComparison.Ordinal)) && inlineFragment.Directives.Count == 0) @@ -259,6 +304,12 @@ private void CollectFragmentSpread( FragmentSpreadNode fragmentSpread, Context context) { + // Check for @defer directive (only valid on fragment spreads) + if (HasDeferDirective(fragmentSpread.Directives)) + { + context.MarkAsIncremental(); + } + var fragmentDefinition = context.GetFragmentDefinition(fragmentSpread.Name.Value); var typeName = fragmentDefinition.TypeCondition.Name.Value; @@ -568,7 +619,10 @@ private static IReadOnlyList RemoveStaticIncludeConditions( return result.Count == 0 ? [] : result; - static bool IsStaticIncludeCondition(DirectiveNode directive, ref bool skipChecked, ref bool includeChecked) + static bool IsStaticIncludeCondition( + DirectiveNode directive, + ref bool skipChecked, + ref bool includeChecked) { if (directive.Name.Value.Equals(DirectiveNames.Skip.Name, StringComparison.Ordinal)) { @@ -591,15 +645,72 @@ static bool IsStaticIncludeCondition(DirectiveNode directive, ref bool skipCheck } } - public readonly ref struct Context( - ITypeDefinition type, - Dictionary fragments, - ISelectionSetMergeObserver? mergeObserver = null) + private static bool HasDeferDirective(IReadOnlyList directives) + { + if (directives.Count == 0) + { + return false; + } + + if (directives.Count == 1) + { + return directives[0].Name.Value.Equals(DirectiveNames.Defer.Name, StringComparison.Ordinal); + } + + for (var i = 0; i < directives.Count; i++) + { + if (directives[i].Name.Value.Equals(DirectiveNames.Defer.Name, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + private static bool HasStreamDirective(IReadOnlyList directives) + { + if (directives.Count == 0) + { + return false; + } + + if (directives.Count == 1) + { + return directives[0].Name.Value.Equals(DirectiveNames.Stream.Name, StringComparison.Ordinal); + } + + for (var i = 0; i < directives.Count; i++) + { + if (directives[i].Name.Value.Equals(DirectiveNames.Stream.Name, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + public readonly ref struct Context { - public ITypeDefinition Type { get; } = type; + private readonly Dictionary _fragments; + private readonly ref bool _hasIncrementalParts; + + public Context( + ITypeDefinition type, + Dictionary fragments, + ref bool hasIncrementalParts, + ISelectionSetMergeObserver? mergeObserver = null) + { + _fragments = fragments; + _hasIncrementalParts = ref hasIncrementalParts; + Type = type; + Observer = mergeObserver ?? NoopSelectionSetMergeObserver.Instance; + } + + public ITypeDefinition Type { get; } - public ISelectionSetMergeObserver Observer { get; } = - mergeObserver ?? NoopSelectionSetMergeObserver.Instance; + public ISelectionSetMergeObserver Observer { get; } public ImmutableArray.Builder Selections { get; } = ImmutableArray.CreateBuilder(); @@ -610,7 +721,7 @@ public readonly ref struct Context( public FragmentDefinitionNode GetFragmentDefinition(string name) { - if (!fragments.TryGetValue(name, out var fragment)) + if (!_fragments.TryGetValue(name, out var fragment)) { throw new RewriterException(string.Format( InlineFragmentOperationRewriter_FragmentDoesNotExist, @@ -643,8 +754,11 @@ public void AddFragmentSpread(FragmentSpreadNode fragmentSpread) Selections.Add(fragmentSpread); } + public void MarkAsIncremental() + => _hasIncrementalParts = true; + public Context Branch(ITypeDefinition type) - => new(type, fragments, Observer); + => new(type, _fragments, ref _hasIncrementalParts, Observer); } private sealed class FieldComparer : IEqualityComparer diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/InlineFragmentOperationRewriterResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/InlineFragmentOperationRewriterResult.cs new file mode 100644 index 00000000000..7986d14a469 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/InlineFragmentOperationRewriterResult.cs @@ -0,0 +1,10 @@ +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Rewriters; + +/// +/// Represents the result of flattening a GraphQL document by inlining fragment spreads and merging inline fragments. +/// +/// The flattened document with all fragments inlined into the operation. +/// Indicates whether the document contains @defer or @stream directives for incremental delivery. +public readonly record struct InlineFragmentOperationRewriterResult(DocumentNode Document, bool HasIncrementalParts); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/MergeSelectionSetRewriter.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/MergeSelectionSetRewriter.cs index c2969dac308..cae6fe91ff5 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/MergeSelectionSetRewriter.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/MergeSelectionSetRewriter.cs @@ -24,7 +24,8 @@ public SelectionSetNode Merge( CopyTo(selectionSet1.Selections, selections, 0); CopyTo(selectionSet2.Selections, selections, selectionSet1.Selections.Count); - var context = new InlineFragmentOperationRewriter.Context(type, [], mergeObserver); + var hasIncrementalParts = false; + var context = new InlineFragmentOperationRewriter.Context(type, [], ref hasIncrementalParts, mergeObserver); var merged = new SelectionSetNode(null, selections); mergeObserver.OnMerge(selectionSet1, selectionSet2); @@ -43,7 +44,8 @@ public SelectionSetNode Merge( { mergeObserver ??= NoopSelectionSetMergeObserver.Instance; - var context = new InlineFragmentOperationRewriter.Context(type, [], mergeObserver); + var hasIncrementalParts = false; + var context = new InlineFragmentOperationRewriter.Context(type, [], ref hasIncrementalParts, mergeObserver); var merged = new SelectionSetNode(null, [.. selectionSets.SelectMany(t => t.Selections)]); mergeObserver.OnMerge(selectionSets); diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj index 517b93269ea..e78a96ef5cc 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj @@ -12,6 +12,7 @@ + diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files.yaml index a6a6d959d19..1c302d16789 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files.yaml @@ -69,8 +69,26 @@ sourceSchemas: files: [Upload!]! } + "Defines the possible serialization types for GraphQL scalar values." + enum ScalarSerializationType { + "The scalar serializes to a string value." + STRING + "The scalar serializes to a boolean value." + BOOLEAN + "The scalar serializes to an integer value." + INT + "The scalar serializes to a floating-point value." + FLOAT + "The scalar serializes to an object value." + OBJECT + "The scalar serializes to a list value." + LIST + } + + directive @serializeAs("The primitive type a scalar is serialized to." type: [ScalarSerializationType!]! "The ECMA-262 regex pattern that the serialized scalar value conforms to." pattern: String) on SCALAR + "The `Upload` scalar type represents a file upload." - scalar Upload + scalar Upload @serializeAs(type: STRING) interactions: - request: contentType: multipart/form-data; boundary="f56524ab-5626-4955-b296-234a097b44f6" diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files_In_Input_Object.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files_In_Input_Object.yaml index 08a29aa2a5b..1b8e730dcc7 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files_In_Input_Object.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files_In_Input_Object.yaml @@ -69,8 +69,26 @@ sourceSchemas: files: [Upload!]! } + "Defines the possible serialization types for GraphQL scalar values." + enum ScalarSerializationType { + "The scalar serializes to a string value." + STRING + "The scalar serializes to a boolean value." + BOOLEAN + "The scalar serializes to an integer value." + INT + "The scalar serializes to a floating-point value." + FLOAT + "The scalar serializes to an object value." + OBJECT + "The scalar serializes to a list value." + LIST + } + + directive @serializeAs("The primitive type a scalar is serialized to." type: [ScalarSerializationType!]! "The ECMA-262 regex pattern that the serialized scalar value conforms to." pattern: String) on SCALAR + "The `Upload` scalar type represents a file upload." - scalar Upload + scalar Upload @serializeAs(type: STRING) interactions: - request: contentType: multipart/form-data; boundary="f56524ab-5626-4955-b296-234a097b44f6" diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files_In_Input_Object_Inline.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files_In_Input_Object_Inline.yaml index e4d137429d4..b3d9448298f 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files_In_Input_Object_Inline.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_List_Of_Files_In_Input_Object_Inline.yaml @@ -69,8 +69,26 @@ sourceSchemas: files: [Upload!]! } + "Defines the possible serialization types for GraphQL scalar values." + enum ScalarSerializationType { + "The scalar serializes to a string value." + STRING + "The scalar serializes to a boolean value." + BOOLEAN + "The scalar serializes to an integer value." + INT + "The scalar serializes to a floating-point value." + FLOAT + "The scalar serializes to an object value." + OBJECT + "The scalar serializes to a list value." + LIST + } + + directive @serializeAs("The primitive type a scalar is serialized to." type: [ScalarSerializationType!]! "The ECMA-262 regex pattern that the serialized scalar value conforms to." pattern: String) on SCALAR + "The `Upload` scalar type represents a file upload." - scalar Upload + scalar Upload @serializeAs(type: STRING) interactions: - request: contentType: multipart/form-data; boundary="f56524ab-5626-4955-b296-234a097b44f6" diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File.yaml index ee9a4b5d430..2e3df203e3b 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File.yaml @@ -57,8 +57,26 @@ sourceSchemas: files: [Upload!]! } + "Defines the possible serialization types for GraphQL scalar values." + enum ScalarSerializationType { + "The scalar serializes to a string value." + STRING + "The scalar serializes to a boolean value." + BOOLEAN + "The scalar serializes to an integer value." + INT + "The scalar serializes to a floating-point value." + FLOAT + "The scalar serializes to an object value." + OBJECT + "The scalar serializes to a list value." + LIST + } + + directive @serializeAs("The primitive type a scalar is serialized to." type: [ScalarSerializationType!]! "The ECMA-262 regex pattern that the serialized scalar value conforms to." pattern: String) on SCALAR + "The `Upload` scalar type represents a file upload." - scalar Upload + scalar Upload @serializeAs(type: STRING) interactions: - request: contentType: multipart/form-data; boundary="f56524ab-5626-4955-b296-234a097b44f6" diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_In_Input_Object.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_In_Input_Object.yaml index c7c6f9f2f6b..ccbcda55412 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_In_Input_Object.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_In_Input_Object.yaml @@ -57,8 +57,26 @@ sourceSchemas: files: [Upload!]! } + "Defines the possible serialization types for GraphQL scalar values." + enum ScalarSerializationType { + "The scalar serializes to a string value." + STRING + "The scalar serializes to a boolean value." + BOOLEAN + "The scalar serializes to an integer value." + INT + "The scalar serializes to a floating-point value." + FLOAT + "The scalar serializes to an object value." + OBJECT + "The scalar serializes to a list value." + LIST + } + + directive @serializeAs("The primitive type a scalar is serialized to." type: [ScalarSerializationType!]! "The ECMA-262 regex pattern that the serialized scalar value conforms to." pattern: String) on SCALAR + "The `Upload` scalar type represents a file upload." - scalar Upload + scalar Upload @serializeAs(type: STRING) interactions: - request: contentType: multipart/form-data; boundary="f56524ab-5626-4955-b296-234a097b44f6" diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_In_Input_Object_Inline.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_In_Input_Object_Inline.yaml index 36bd4b3b71a..6ab657ea1ec 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_In_Input_Object_Inline.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_In_Input_Object_Inline.yaml @@ -57,8 +57,26 @@ sourceSchemas: files: [Upload!]! } + "Defines the possible serialization types for GraphQL scalar values." + enum ScalarSerializationType { + "The scalar serializes to a string value." + STRING + "The scalar serializes to a boolean value." + BOOLEAN + "The scalar serializes to an integer value." + INT + "The scalar serializes to a floating-point value." + FLOAT + "The scalar serializes to an object value." + OBJECT + "The scalar serializes to a list value." + LIST + } + + directive @serializeAs("The primitive type a scalar is serialized to." type: [ScalarSerializationType!]! "The ECMA-262 regex pattern that the serialized scalar value conforms to." pattern: String) on SCALAR + "The `Upload` scalar type represents a file upload." - scalar Upload + scalar Upload @serializeAs(type: STRING) interactions: - request: contentType: multipart/form-data; boundary="f56524ab-5626-4955-b296-234a097b44f6" diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type_Both_Have_Different_Dependencies.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type_Both_Have_Different_Dependencies.yaml index b6709c27b81..2dc29e7358c 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type_Both_Have_Different_Dependencies.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type_Both_Have_Different_Dependencies.yaml @@ -32,6 +32,10 @@ response: "__typename": "Item2", "id": "SXRlbTI6MQ==", "products": [ + { + "id": "UHJvZHVjdDox", + "name": "Product: UHJvZHVjdDox" + }, { "id": "UHJvZHVjdDoy", "name": "Product: UHJvZHVjdDoy" @@ -39,14 +43,10 @@ response: { "id": "UHJvZHVjdDoz", "name": "Product: UHJvZHVjdDoz" - }, - { - "id": "UHJvZHVjdDo0", - "name": "Product: UHJvZHVjdDo0" } ], "singularProduct": { - "name": "Product: UHJvZHVjdDox" + "name": "Product: UHJvZHVjdDo0" } } } @@ -118,17 +118,17 @@ sourceSchemas: "id": "SXRlbTI6MQ==", "products": [ { - "id": "UHJvZHVjdDoy" + "id": "UHJvZHVjdDox" }, { - "id": "UHJvZHVjdDoz" + "id": "UHJvZHVjdDoy" }, { - "id": "UHJvZHVjdDo0" + "id": "UHJvZHVjdDoz" } ], "singularProduct": { - "id": "UHJvZHVjdDox" + "id": "UHJvZHVjdDo0" } } } @@ -166,7 +166,7 @@ sourceSchemas: } variables: | { - "__fusion_1_id": "UHJvZHVjdDox" + "__fusion_1_id": "UHJvZHVjdDo0" } response: results: @@ -175,7 +175,7 @@ sourceSchemas: "data": { "node": { "__typename": "Product", - "name": "Product: UHJvZHVjdDox" + "name": "Product: UHJvZHVjdDo0" } } } @@ -194,13 +194,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDoy" + "__fusion_2_id": "UHJvZHVjdDox" }, { - "__fusion_2_id": "UHJvZHVjdDoz" + "__fusion_2_id": "UHJvZHVjdDoy" }, { - "__fusion_2_id": "UHJvZHVjdDo0" + "__fusion_2_id": "UHJvZHVjdDoz" } ] response: @@ -211,7 +211,7 @@ sourceSchemas: "data": { "node": { "__typename": "Product", - "name": "Product: UHJvZHVjdDoy" + "name": "Product: UHJvZHVjdDox" } } } @@ -220,7 +220,7 @@ sourceSchemas: "data": { "node": { "__typename": "Product", - "name": "Product: UHJvZHVjdDoz" + "name": "Product: UHJvZHVjdDoy" } } } @@ -229,7 +229,7 @@ sourceSchemas: "data": { "node": { "__typename": "Product", - "name": "Product: UHJvZHVjdDo0" + "name": "Product: UHJvZHVjdDoz" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Concrete_Type_Linked_Field_With_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Concrete_Type_Linked_Field_With_Dependency.yaml index 1c50a89b2a4..2e8119e876c 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Concrete_Type_Linked_Field_With_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Concrete_Type_Linked_Field_With_Dependency.yaml @@ -19,7 +19,7 @@ response: { "viewerCanVote": true, "author": { - "displayName": "Author: QXV0aG9yOjU=" + "displayName": "Author: QXV0aG9yOjQ=" } }, { @@ -28,7 +28,7 @@ response: { "viewerCanVote": true, "author": { - "displayName": "Author: QXV0aG9yOjQ=" + "displayName": "Author: QXV0aG9yOjU=" } } ] @@ -87,7 +87,7 @@ sourceSchemas: "__typename": "Discussion", "viewerCanVote": true, "author": { - "id": "QXV0aG9yOjU=" + "id": "QXV0aG9yOjQ=" } }, { @@ -98,7 +98,7 @@ sourceSchemas: "__typename": "Discussion", "viewerCanVote": true, "author": { - "id": "QXV0aG9yOjQ=" + "id": "QXV0aG9yOjU=" } } ] @@ -131,10 +131,10 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjU=" + "__fusion_1_id": "QXV0aG9yOjQ=" }, { - "__fusion_1_id": "QXV0aG9yOjQ=" + "__fusion_1_id": "QXV0aG9yOjU=" } ] response: @@ -144,7 +144,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjU=" + "displayName": "Author: QXV0aG9yOjQ=" } } } @@ -152,7 +152,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjQ=" + "displayName": "Author: QXV0aG9yOjU=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency.yaml index b76ac5260ed..e33fd1a6f79 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency.yaml @@ -16,8 +16,8 @@ response: "authorables": [ { "author": { - "id": "QXV0aG9yOjY=", - "displayName": "Author: QXV0aG9yOjY=" + "id": "QXV0aG9yOjQ=", + "displayName": "Author: QXV0aG9yOjQ=" } }, { @@ -28,8 +28,8 @@ response: }, { "author": { - "id": "QXV0aG9yOjQ=", - "displayName": "Author: QXV0aG9yOjQ=" + "id": "QXV0aG9yOjY=", + "displayName": "Author: QXV0aG9yOjY=" } } ] @@ -81,7 +81,7 @@ sourceSchemas: { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjY=" + "id": "QXV0aG9yOjQ=" } }, { @@ -93,7 +93,7 @@ sourceSchemas: { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjQ=" + "id": "QXV0aG9yOjY=" } } ] @@ -126,13 +126,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjY=" + "__fusion_1_id": "QXV0aG9yOjQ=" }, { "__fusion_1_id": "QXV0aG9yOjU=" }, { - "__fusion_1_id": "QXV0aG9yOjQ=" + "__fusion_1_id": "QXV0aG9yOjY=" } ] response: @@ -142,7 +142,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjY=" + "displayName": "Author: QXV0aG9yOjQ=" } } } @@ -158,7 +158,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjQ=" + "displayName": "Author: QXV0aG9yOjY=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml index c844eb1cec8..c5171eae478 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml @@ -21,9 +21,9 @@ response: "authorables": [ { "author": { - "id": "QXV0aG9yOjY=", - "displayName": "Author: QXV0aG9yOjY=", - "email": "Author: QXV0aG9yOjY=" + "id": "QXV0aG9yOjQ=", + "displayName": "Author: QXV0aG9yOjQ=", + "email": "Author: QXV0aG9yOjQ=" } }, { @@ -34,9 +34,9 @@ response: }, { "author": { - "id": "QXV0aG9yOjQ=", - "displayName": "Author: QXV0aG9yOjQ=", - "email": "Author: QXV0aG9yOjQ=" + "id": "QXV0aG9yOjY=", + "displayName": "Author: QXV0aG9yOjY=", + "email": "Author: QXV0aG9yOjY=" } } ] @@ -93,7 +93,7 @@ sourceSchemas: { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjY=" + "id": "QXV0aG9yOjQ=" } }, { @@ -105,7 +105,7 @@ sourceSchemas: { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjQ=" + "id": "QXV0aG9yOjY=" } } ] @@ -139,10 +139,10 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjY=" + "__fusion_1_id": "QXV0aG9yOjQ=" }, { - "__fusion_1_id": "QXV0aG9yOjQ=" + "__fusion_1_id": "QXV0aG9yOjY=" } ] response: @@ -152,7 +152,7 @@ sourceSchemas: { "data": { "authorById": { - "email": "Author: QXV0aG9yOjY=" + "email": "Author: QXV0aG9yOjQ=" } } } @@ -160,7 +160,7 @@ sourceSchemas: { "data": { "authorById": { - "email": "Author: QXV0aG9yOjQ=" + "email": "Author: QXV0aG9yOjY=" } } } @@ -176,13 +176,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "QXV0aG9yOjY=" + "__fusion_2_id": "QXV0aG9yOjQ=" }, { "__fusion_2_id": "QXV0aG9yOjU=" }, { - "__fusion_2_id": "QXV0aG9yOjQ=" + "__fusion_2_id": "QXV0aG9yOjY=" } ] response: @@ -192,7 +192,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjY=" + "displayName": "Author: QXV0aG9yOjQ=" } } } @@ -208,7 +208,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjQ=" + "displayName": "Author: QXV0aG9yOjY=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml index 34125ac70bf..8dc4faf67f4 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml @@ -22,8 +22,8 @@ response: "authorables": [ { "author": { - "id": "QXV0aG9yOjY=", - "displayName": "Author: QXV0aG9yOjY=" + "id": "QXV0aG9yOjQ=", + "displayName": "Author: QXV0aG9yOjQ=" } }, { @@ -34,8 +34,8 @@ response: }, { "author": { - "id": "QXV0aG9yOjQ=", - "displayName": "Author: QXV0aG9yOjQ=" + "id": "QXV0aG9yOjY=", + "displayName": "Author: QXV0aG9yOjY=" } } ] @@ -92,7 +92,7 @@ sourceSchemas: { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjY=" + "id": "QXV0aG9yOjQ=" } }, { @@ -104,7 +104,7 @@ sourceSchemas: { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjQ=" + "id": "QXV0aG9yOjY=" } } ] @@ -137,10 +137,10 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjY=" + "__fusion_1_id": "QXV0aG9yOjQ=" }, { - "__fusion_1_id": "QXV0aG9yOjQ=" + "__fusion_1_id": "QXV0aG9yOjY=" } ] response: @@ -150,7 +150,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjY=" + "displayName": "Author: QXV0aG9yOjQ=" } } } @@ -158,7 +158,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjQ=" + "displayName": "Author: QXV0aG9yOjY=" } } } @@ -174,13 +174,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "QXV0aG9yOjY=" + "__fusion_2_id": "QXV0aG9yOjQ=" }, { "__fusion_2_id": "QXV0aG9yOjU=" }, { - "__fusion_2_id": "QXV0aG9yOjQ=" + "__fusion_2_id": "QXV0aG9yOjY=" } ] response: @@ -190,7 +190,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjY=" + "displayName": "Author: QXV0aG9yOjQ=" } } } @@ -206,7 +206,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjQ=" + "displayName": "Author: QXV0aG9yOjY=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type.yaml index 1668db2e4eb..5072d3ecd66 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type.yaml @@ -19,7 +19,7 @@ response: { "votable": { "viewerCanVote": true, - "title": "Discussion: RGlzY3Vzc2lvbjo2" + "title": "Discussion: RGlzY3Vzc2lvbjo0" } }, { @@ -31,7 +31,7 @@ response: { "votable": { "viewerCanVote": true, - "title": "Discussion: RGlzY3Vzc2lvbjo0" + "title": "Discussion: RGlzY3Vzc2lvbjo2" } } ] @@ -90,7 +90,7 @@ sourceSchemas: "votable": { "__typename": "Discussion", "viewerCanVote": true, - "title": "Discussion: RGlzY3Vzc2lvbjo2" + "title": "Discussion: RGlzY3Vzc2lvbjo0" } }, { @@ -104,7 +104,7 @@ sourceSchemas: "votable": { "__typename": "Discussion", "viewerCanVote": true, - "title": "Discussion: RGlzY3Vzc2lvbjo0" + "title": "Discussion: RGlzY3Vzc2lvbjo2" } } ] diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type_Linked_Field_With_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type_Linked_Field_With_Dependency.yaml index 7aae9db06b4..f7928f2c3ca 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type_Linked_Field_With_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type_Linked_Field_With_Dependency.yaml @@ -22,7 +22,7 @@ response: "votable": { "viewerCanVote": true, "author": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=" } } }, @@ -38,7 +38,7 @@ response: "votable": { "viewerCanVote": true, "author": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=" } } } @@ -105,7 +105,7 @@ sourceSchemas: "__typename": "Discussion", "viewerCanVote": true, "author": { - "id": "QXV0aG9yOjc=" + "id": "QXV0aG9yOjk=" } } }, @@ -123,7 +123,7 @@ sourceSchemas: "__typename": "Discussion", "viewerCanVote": true, "author": { - "id": "QXV0aG9yOjk=" + "id": "QXV0aG9yOjc=" } } } @@ -157,13 +157,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjc=" + "__fusion_1_id": "QXV0aG9yOjk=" }, { "__fusion_1_id": "QXV0aG9yOjg=" }, { - "__fusion_1_id": "QXV0aG9yOjk=" + "__fusion_1_id": "QXV0aG9yOjc=" } ] response: @@ -173,7 +173,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=" } } } @@ -189,7 +189,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type_With_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type_With_Dependency.yaml index 8039b99d57e..b2743ed1ea3 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type_With_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Concrete_Type_With_Dependency.yaml @@ -90,7 +90,7 @@ sourceSchemas: "votable": { "__typename": "Discussion", "viewerCanVote": true, - "id": "RGlzY3Vzc2lvbjo2" + "id": "RGlzY3Vzc2lvbjo0" } }, { @@ -104,7 +104,7 @@ sourceSchemas: "votable": { "__typename": "Discussion", "viewerCanVote": true, - "id": "RGlzY3Vzc2lvbjo0" + "id": "RGlzY3Vzc2lvbjo2" } } ] @@ -137,13 +137,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "RGlzY3Vzc2lvbjo2" + "__fusion_1_id": "RGlzY3Vzc2lvbjo0" }, { "__fusion_1_id": "RGlzY3Vzc2lvbjo1" }, { - "__fusion_1_id": "RGlzY3Vzc2lvbjo0" + "__fusion_1_id": "RGlzY3Vzc2lvbjo2" } ] response: diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency.yaml index c67111a2557..60f862fb6ad 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency.yaml @@ -18,7 +18,7 @@ response: { "authorable": { "author": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=" } } }, @@ -32,7 +32,7 @@ response: { "authorable": { "author": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=" } } } @@ -92,7 +92,7 @@ sourceSchemas: "authorable": { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjc=" + "id": "QXV0aG9yOjk=" } } }, @@ -108,7 +108,7 @@ sourceSchemas: "authorable": { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjk=" + "id": "QXV0aG9yOjc=" } } } @@ -142,13 +142,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjc=" + "__fusion_1_id": "QXV0aG9yOjk=" }, { "__fusion_1_id": "QXV0aG9yOjg=" }, { - "__fusion_1_id": "QXV0aG9yOjk=" + "__fusion_1_id": "QXV0aG9yOjc=" } ] response: @@ -158,7 +158,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=" } } } @@ -174,7 +174,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml index a419789f622..43cb43ed090 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml @@ -23,8 +23,8 @@ response: { "authorable": { "author": { - "displayName": "Author: QXV0aG9yOjc=", - "email": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=", + "email": "Author: QXV0aG9yOjk=" } } }, @@ -39,8 +39,8 @@ response: { "authorable": { "author": { - "displayName": "Author: QXV0aG9yOjk=", - "email": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=", + "email": "Author: QXV0aG9yOjc=" } } } @@ -105,7 +105,7 @@ sourceSchemas: "authorable": { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjc=" + "id": "QXV0aG9yOjk=" } } }, @@ -121,7 +121,7 @@ sourceSchemas: "authorable": { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjk=" + "id": "QXV0aG9yOjc=" } } } @@ -156,13 +156,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjc=" + "__fusion_1_id": "QXV0aG9yOjk=" }, { "__fusion_1_id": "QXV0aG9yOjg=" }, { - "__fusion_1_id": "QXV0aG9yOjk=" + "__fusion_1_id": "QXV0aG9yOjc=" } ] response: @@ -172,7 +172,7 @@ sourceSchemas: { "data": { "authorById": { - "email": "Author: QXV0aG9yOjc=" + "email": "Author: QXV0aG9yOjk=" } } } @@ -188,7 +188,7 @@ sourceSchemas: { "data": { "authorById": { - "email": "Author: QXV0aG9yOjk=" + "email": "Author: QXV0aG9yOjc=" } } } @@ -204,13 +204,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "QXV0aG9yOjc=" + "__fusion_2_id": "QXV0aG9yOjk=" }, { "__fusion_2_id": "QXV0aG9yOjg=" }, { - "__fusion_2_id": "QXV0aG9yOjk=" + "__fusion_2_id": "QXV0aG9yOjc=" } ] response: @@ -220,7 +220,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=" } } } @@ -236,7 +236,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml index 60a8e56097e..c47b6be5f64 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml @@ -23,7 +23,7 @@ response: { "authorable": { "author": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=" } } }, @@ -37,7 +37,7 @@ response: { "authorable": { "author": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=" } } } @@ -102,7 +102,7 @@ sourceSchemas: "authorable": { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjc=" + "id": "QXV0aG9yOjk=" } } }, @@ -118,7 +118,7 @@ sourceSchemas: "authorable": { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjk=" + "id": "QXV0aG9yOjc=" } } } @@ -152,13 +152,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjc=" + "__fusion_1_id": "QXV0aG9yOjk=" }, { "__fusion_1_id": "QXV0aG9yOjg=" }, { - "__fusion_1_id": "QXV0aG9yOjk=" + "__fusion_1_id": "QXV0aG9yOjc=" } ] response: @@ -168,7 +168,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=" } } } @@ -184,7 +184,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=" } } } @@ -200,13 +200,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "QXV0aG9yOjc=" + "__fusion_2_id": "QXV0aG9yOjk=" }, { "__fusion_2_id": "QXV0aG9yOjg=" }, { - "__fusion_2_id": "QXV0aG9yOjk=" + "__fusion_2_id": "QXV0aG9yOjc=" } ] response: @@ -216,7 +216,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=" } } } @@ -232,7 +232,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjc=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Has_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Has_Dependency.yaml index 0c86c318ac8..c4a41df3cd0 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Has_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Has_Dependency.yaml @@ -20,7 +20,7 @@ response: "postEdges": [ { "node": { - "subgraph2": "Photo: UGhvdG86Ng==" + "subgraph2": "Photo: UGhvdG86NA==" } }, { @@ -30,7 +30,7 @@ response: }, { "node": { - "subgraph2": "Photo: UGhvdG86NA==" + "subgraph2": "Photo: UGhvdG86Ng==" } } ] @@ -86,7 +86,7 @@ sourceSchemas: { "node": { "__typename": "Photo", - "id": "UGhvdG86Ng==" + "id": "UGhvdG86NA==" } }, { @@ -98,7 +98,7 @@ sourceSchemas: { "node": { "__typename": "Photo", - "id": "UGhvdG86NA==" + "id": "UGhvdG86Ng==" } } ] @@ -131,13 +131,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "UGhvdG86Ng==" + "__fusion_1_id": "UGhvdG86NA==" }, { "__fusion_1_id": "UGhvdG86NQ==" }, { - "__fusion_1_id": "UGhvdG86NA==" + "__fusion_1_id": "UGhvdG86Ng==" } ] response: @@ -147,7 +147,7 @@ sourceSchemas: { "data": { "photoById": { - "subgraph2": "Photo: UGhvdG86Ng==" + "subgraph2": "Photo: UGhvdG86NA==" } } } @@ -163,7 +163,7 @@ sourceSchemas: { "data": { "photoById": { - "subgraph2": "Photo: UGhvdG86NA==" + "subgraph2": "Photo: UGhvdG86Ng==" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selection_Has_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selection_Has_Dependency.yaml index e3ea415d229..ad340c62cf7 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selection_Has_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selection_Has_Dependency.yaml @@ -25,7 +25,7 @@ response: { "node": { "product": { - "subgraph2": "Product: UHJvZHVjdDo3" + "subgraph2": "Product: UHJvZHVjdDo5" } } }, @@ -39,7 +39,7 @@ response: { "node": { "product": { - "subgraph2": "Product: UHJvZHVjdDo5" + "subgraph2": "Product: UHJvZHVjdDo3" } } } @@ -110,7 +110,7 @@ sourceSchemas: "node": { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo3" + "id": "UHJvZHVjdDo5" } } }, @@ -126,7 +126,7 @@ sourceSchemas: "node": { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo5" + "id": "UHJvZHVjdDo3" } } } @@ -160,13 +160,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDo3" + "__fusion_2_id": "UHJvZHVjdDo5" }, { "__fusion_2_id": "UHJvZHVjdDo4" }, { - "__fusion_2_id": "UHJvZHVjdDo5" + "__fusion_2_id": "UHJvZHVjdDo3" } ] response: @@ -176,7 +176,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo3" + "subgraph2": "Product: UHJvZHVjdDo5" } } } @@ -192,7 +192,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo5" + "subgraph2": "Product: UHJvZHVjdDo3" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml index a296d4099e9..092d79ed88a 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml @@ -25,7 +25,7 @@ response: { "node": { "product": { - "subgraph2": "Product: UHJvZHVjdDo3" + "subgraph2": "Product: UHJvZHVjdDo5" } } }, @@ -39,7 +39,7 @@ response: { "node": { "product": { - "subgraph2": "Product: UHJvZHVjdDo5" + "subgraph2": "Product: UHJvZHVjdDo3" } } } @@ -110,7 +110,7 @@ sourceSchemas: "node": { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo3" + "id": "UHJvZHVjdDo5" } } }, @@ -126,7 +126,7 @@ sourceSchemas: "node": { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo5" + "id": "UHJvZHVjdDo3" } } } @@ -166,13 +166,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDo3" + "__fusion_2_id": "UHJvZHVjdDo5" }, { "__fusion_2_id": "UHJvZHVjdDo4" }, { - "__fusion_2_id": "UHJvZHVjdDo5" + "__fusion_2_id": "UHJvZHVjdDo3" } ] response: @@ -182,7 +182,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo3" + "subgraph2": "Product: UHJvZHVjdDo5" } } } @@ -198,7 +198,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo5" + "subgraph2": "Product: UHJvZHVjdDo3" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Same_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Same_Dependency.yaml index b171af537c8..508c04a09c0 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Same_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Same_Dependency.yaml @@ -25,7 +25,7 @@ response: { "node": { "product": { - "subgraph2": "Product: UHJvZHVjdDo3" + "subgraph2": "Product: UHJvZHVjdDo5" } } }, @@ -39,7 +39,7 @@ response: { "node": { "product": { - "subgraph2": "Product: UHJvZHVjdDo5" + "subgraph2": "Product: UHJvZHVjdDo3" } } } @@ -106,7 +106,7 @@ sourceSchemas: "node": { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo3" + "id": "UHJvZHVjdDo5" } } }, @@ -122,7 +122,7 @@ sourceSchemas: "node": { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo5" + "id": "UHJvZHVjdDo3" } } } @@ -156,13 +156,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDo3" + "__fusion_2_id": "UHJvZHVjdDo5" }, { "__fusion_2_id": "UHJvZHVjdDo4" }, { - "__fusion_2_id": "UHJvZHVjdDo5" + "__fusion_2_id": "UHJvZHVjdDo3" } ] response: @@ -172,7 +172,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo3" + "subgraph2": "Product: UHJvZHVjdDo5" } } } @@ -188,7 +188,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo5" + "subgraph2": "Product: UHJvZHVjdDo3" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Has_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Has_Dependency.yaml index 942dc8e61e7..e6a01bf5f88 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Has_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Has_Dependency.yaml @@ -21,13 +21,13 @@ response: { "posts": [ { - "subgraph2": "Photo: UGhvdG86MTA=" + "subgraph2": "Photo: UGhvdG86NA==" }, { - "subgraph1": "Discussion: RGlzY3Vzc2lvbjoxMQ==" + "subgraph1": "Discussion: RGlzY3Vzc2lvbjo1" }, { - "subgraph2": "Photo: UGhvdG86MTI=" + "subgraph2": "Photo: UGhvdG86Ng==" } ] }, @@ -47,13 +47,13 @@ response: { "posts": [ { - "subgraph2": "Photo: UGhvdG86NA==" + "subgraph2": "Photo: UGhvdG86MTA=" }, { - "subgraph1": "Discussion: RGlzY3Vzc2lvbjo1" + "subgraph1": "Discussion: RGlzY3Vzc2lvbjoxMQ==" }, { - "subgraph2": "Photo: UGhvdG86Ng==" + "subgraph2": "Photo: UGhvdG86MTI=" } ] } @@ -111,15 +111,15 @@ sourceSchemas: "posts": [ { "__typename": "Photo", - "id": "UGhvdG86MTA=" + "id": "UGhvdG86NA==" }, { "__typename": "Discussion", - "subgraph1": "Discussion: RGlzY3Vzc2lvbjoxMQ==" + "subgraph1": "Discussion: RGlzY3Vzc2lvbjo1" }, { "__typename": "Photo", - "id": "UGhvdG86MTI=" + "id": "UGhvdG86Ng==" } ] }, @@ -143,15 +143,15 @@ sourceSchemas: "posts": [ { "__typename": "Photo", - "id": "UGhvdG86NA==" + "id": "UGhvdG86MTA=" }, { "__typename": "Discussion", - "subgraph1": "Discussion: RGlzY3Vzc2lvbjo1" + "subgraph1": "Discussion: RGlzY3Vzc2lvbjoxMQ==" }, { "__typename": "Photo", - "id": "UGhvdG86Ng==" + "id": "UGhvdG86MTI=" } ] } @@ -185,10 +185,10 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "UGhvdG86MTA=" + "__fusion_1_id": "UGhvdG86NA==" }, { - "__fusion_1_id": "UGhvdG86MTI=" + "__fusion_1_id": "UGhvdG86Ng==" }, { "__fusion_1_id": "UGhvdG86Nw==" @@ -197,10 +197,10 @@ sourceSchemas: "__fusion_1_id": "UGhvdG86OQ==" }, { - "__fusion_1_id": "UGhvdG86NA==" + "__fusion_1_id": "UGhvdG86MTA=" }, { - "__fusion_1_id": "UGhvdG86Ng==" + "__fusion_1_id": "UGhvdG86MTI=" } ] response: @@ -210,7 +210,7 @@ sourceSchemas: { "data": { "photoById": { - "subgraph2": "Photo: UGhvdG86MTA=" + "subgraph2": "Photo: UGhvdG86NA==" } } } @@ -218,7 +218,7 @@ sourceSchemas: { "data": { "photoById": { - "subgraph2": "Photo: UGhvdG86MTI=" + "subgraph2": "Photo: UGhvdG86Ng==" } } } @@ -242,7 +242,7 @@ sourceSchemas: { "data": { "photoById": { - "subgraph2": "Photo: UGhvdG86NA==" + "subgraph2": "Photo: UGhvdG86MTA=" } } } @@ -250,7 +250,7 @@ sourceSchemas: { "data": { "photoById": { - "subgraph2": "Photo: UGhvdG86Ng==" + "subgraph2": "Photo: UGhvdG86MTI=" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selection_Has_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selection_Has_Dependency.yaml index cd113aee09d..ee6ef700286 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selection_Has_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selection_Has_Dependency.yaml @@ -26,17 +26,17 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoxNQ==" + "subgraph2": "Product: UHJvZHVjdDoxOQ==" } }, { "author": { - "subgraph3": "Author: QXV0aG9yOjE0" + "subgraph3": "Author: QXV0aG9yOjIw" } }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxMw==" + "subgraph2": "Product: UHJvZHVjdDoyMQ==" } } ] @@ -45,7 +45,7 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoxOA==" + "subgraph2": "Product: UHJvZHVjdDoxNg==" } }, { @@ -55,7 +55,7 @@ response: }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxNg==" + "subgraph2": "Product: UHJvZHVjdDoxOA==" } } ] @@ -64,17 +64,17 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoyMQ==" + "subgraph2": "Product: UHJvZHVjdDoxMw==" } }, { "author": { - "subgraph3": "Author: QXV0aG9yOjIw" + "subgraph3": "Author: QXV0aG9yOjE0" } }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxOQ==" + "subgraph2": "Product: UHJvZHVjdDoxNQ==" } } ] @@ -147,19 +147,19 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxNQ==" + "id": "UHJvZHVjdDoxOQ==" } }, { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjE0" + "id": "QXV0aG9yOjIw" } }, { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxMw==" + "id": "UHJvZHVjdDoyMQ==" } } ] @@ -169,7 +169,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxOA==" + "id": "UHJvZHVjdDoxNg==" } }, { @@ -181,7 +181,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxNg==" + "id": "UHJvZHVjdDoxOA==" } } ] @@ -191,19 +191,19 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoyMQ==" + "id": "UHJvZHVjdDoxMw==" } }, { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjIw" + "id": "QXV0aG9yOjE0" } }, { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxOQ==" + "id": "UHJvZHVjdDoxNQ==" } } ] @@ -238,22 +238,22 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDoxNQ==" + "__fusion_2_id": "UHJvZHVjdDoxOQ==" }, { - "__fusion_2_id": "UHJvZHVjdDoxMw==" + "__fusion_2_id": "UHJvZHVjdDoyMQ==" }, { - "__fusion_2_id": "UHJvZHVjdDoxOA==" + "__fusion_2_id": "UHJvZHVjdDoxNg==" }, { - "__fusion_2_id": "UHJvZHVjdDoxNg==" + "__fusion_2_id": "UHJvZHVjdDoxOA==" }, { - "__fusion_2_id": "UHJvZHVjdDoyMQ==" + "__fusion_2_id": "UHJvZHVjdDoxMw==" }, { - "__fusion_2_id": "UHJvZHVjdDoxOQ==" + "__fusion_2_id": "UHJvZHVjdDoxNQ==" } ] response: @@ -263,7 +263,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxNQ==" + "subgraph2": "Product: UHJvZHVjdDoxOQ==" } } } @@ -271,7 +271,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxMw==" + "subgraph2": "Product: UHJvZHVjdDoyMQ==" } } } @@ -279,7 +279,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxOA==" + "subgraph2": "Product: UHJvZHVjdDoxNg==" } } } @@ -287,7 +287,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxNg==" + "subgraph2": "Product: UHJvZHVjdDoxOA==" } } } @@ -295,7 +295,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoyMQ==" + "subgraph2": "Product: UHJvZHVjdDoxMw==" } } } @@ -303,7 +303,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxOQ==" + "subgraph2": "Product: UHJvZHVjdDoxNQ==" } } } @@ -334,13 +334,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjE0" + "__fusion_1_id": "QXV0aG9yOjIw" }, { "__fusion_1_id": "QXV0aG9yOjE3" }, { - "__fusion_1_id": "QXV0aG9yOjIw" + "__fusion_1_id": "QXV0aG9yOjE0" } ] response: @@ -350,7 +350,7 @@ sourceSchemas: { "data": { "authorById": { - "subgraph3": "Author: QXV0aG9yOjE0" + "subgraph3": "Author: QXV0aG9yOjIw" } } } @@ -366,7 +366,7 @@ sourceSchemas: { "data": { "authorById": { - "subgraph3": "Author: QXV0aG9yOjIw" + "subgraph3": "Author: QXV0aG9yOjE0" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml index a5f9bd8d442..fe811549b47 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml @@ -26,17 +26,17 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoxNQ==" + "subgraph2": "Product: UHJvZHVjdDoxOQ==" } }, { "author": { - "subgraph2": "Author: QXV0aG9yOjE0" + "subgraph2": "Author: QXV0aG9yOjIw" } }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxMw==" + "subgraph2": "Product: UHJvZHVjdDoyMQ==" } } ] @@ -45,7 +45,7 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoxOA==" + "subgraph2": "Product: UHJvZHVjdDoxNg==" } }, { @@ -55,7 +55,7 @@ response: }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxNg==" + "subgraph2": "Product: UHJvZHVjdDoxOA==" } } ] @@ -64,17 +64,17 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoyMQ==" + "subgraph2": "Product: UHJvZHVjdDoxMw==" } }, { "author": { - "subgraph2": "Author: QXV0aG9yOjIw" + "subgraph2": "Author: QXV0aG9yOjE0" } }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxOQ==" + "subgraph2": "Product: UHJvZHVjdDoxNQ==" } } ] @@ -147,19 +147,19 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxNQ==" + "id": "UHJvZHVjdDoxOQ==" } }, { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjE0" + "id": "QXV0aG9yOjIw" } }, { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxMw==" + "id": "UHJvZHVjdDoyMQ==" } } ] @@ -169,7 +169,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxOA==" + "id": "UHJvZHVjdDoxNg==" } }, { @@ -181,7 +181,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxNg==" + "id": "UHJvZHVjdDoxOA==" } } ] @@ -191,19 +191,19 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoyMQ==" + "id": "UHJvZHVjdDoxMw==" } }, { "__typename": "Discussion", "author": { - "id": "QXV0aG9yOjIw" + "id": "QXV0aG9yOjE0" } }, { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxOQ==" + "id": "UHJvZHVjdDoxNQ==" } } ] @@ -244,13 +244,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "QXV0aG9yOjE0" + "__fusion_1_id": "QXV0aG9yOjIw" }, { "__fusion_1_id": "QXV0aG9yOjE3" }, { - "__fusion_1_id": "QXV0aG9yOjIw" + "__fusion_1_id": "QXV0aG9yOjE0" } ] response: @@ -260,7 +260,7 @@ sourceSchemas: { "data": { "authorById": { - "subgraph2": "Author: QXV0aG9yOjE0" + "subgraph2": "Author: QXV0aG9yOjIw" } } } @@ -276,7 +276,7 @@ sourceSchemas: { "data": { "authorById": { - "subgraph2": "Author: QXV0aG9yOjIw" + "subgraph2": "Author: QXV0aG9yOjE0" } } } @@ -292,22 +292,22 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDoxNQ==" + "__fusion_2_id": "UHJvZHVjdDoxOQ==" }, { - "__fusion_2_id": "UHJvZHVjdDoxMw==" + "__fusion_2_id": "UHJvZHVjdDoyMQ==" }, { - "__fusion_2_id": "UHJvZHVjdDoxOA==" + "__fusion_2_id": "UHJvZHVjdDoxNg==" }, { - "__fusion_2_id": "UHJvZHVjdDoxNg==" + "__fusion_2_id": "UHJvZHVjdDoxOA==" }, { - "__fusion_2_id": "UHJvZHVjdDoyMQ==" + "__fusion_2_id": "UHJvZHVjdDoxMw==" }, { - "__fusion_2_id": "UHJvZHVjdDoxOQ==" + "__fusion_2_id": "UHJvZHVjdDoxNQ==" } ] response: @@ -317,7 +317,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxNQ==" + "subgraph2": "Product: UHJvZHVjdDoxOQ==" } } } @@ -325,7 +325,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxMw==" + "subgraph2": "Product: UHJvZHVjdDoyMQ==" } } } @@ -333,7 +333,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxOA==" + "subgraph2": "Product: UHJvZHVjdDoxNg==" } } } @@ -341,7 +341,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxNg==" + "subgraph2": "Product: UHJvZHVjdDoxOA==" } } } @@ -349,7 +349,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoyMQ==" + "subgraph2": "Product: UHJvZHVjdDoxMw==" } } } @@ -357,7 +357,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxOQ==" + "subgraph2": "Product: UHJvZHVjdDoxNQ==" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml index 04ec7d0e74b..926852c7db3 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml @@ -26,17 +26,17 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoxNQ==" + "subgraph2": "Product: UHJvZHVjdDoxOQ==" } }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxNA==" + "subgraph2": "Product: UHJvZHVjdDoyMA==" } }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxMw==" + "subgraph2": "Product: UHJvZHVjdDoyMQ==" } } ] @@ -45,7 +45,7 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoxOA==" + "subgraph2": "Product: UHJvZHVjdDoxNg==" } }, { @@ -55,7 +55,7 @@ response: }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxNg==" + "subgraph2": "Product: UHJvZHVjdDoxOA==" } } ] @@ -64,17 +64,17 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDoyMQ==" + "subgraph2": "Product: UHJvZHVjdDoxMw==" } }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoyMA==" + "subgraph2": "Product: UHJvZHVjdDoxNA==" } }, { "product": { - "subgraph2": "Product: UHJvZHVjdDoxOQ==" + "subgraph2": "Product: UHJvZHVjdDoxNQ==" } } ] @@ -143,19 +143,19 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxNQ==" + "id": "UHJvZHVjdDoxOQ==" } }, { "__typename": "Discussion", "product": { - "id": "UHJvZHVjdDoxNA==" + "id": "UHJvZHVjdDoyMA==" } }, { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxMw==" + "id": "UHJvZHVjdDoyMQ==" } } ] @@ -165,7 +165,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxOA==" + "id": "UHJvZHVjdDoxNg==" } }, { @@ -177,7 +177,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxNg==" + "id": "UHJvZHVjdDoxOA==" } } ] @@ -187,19 +187,19 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoyMQ==" + "id": "UHJvZHVjdDoxMw==" } }, { "__typename": "Discussion", "product": { - "id": "UHJvZHVjdDoyMA==" + "id": "UHJvZHVjdDoxNA==" } }, { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDoxOQ==" + "id": "UHJvZHVjdDoxNQ==" } } ] @@ -234,13 +234,13 @@ sourceSchemas: variables: | [ { - "__fusion_1_id": "UHJvZHVjdDoxNA==" + "__fusion_1_id": "UHJvZHVjdDoyMA==" }, { "__fusion_1_id": "UHJvZHVjdDoxNw==" }, { - "__fusion_1_id": "UHJvZHVjdDoyMA==" + "__fusion_1_id": "UHJvZHVjdDoxNA==" } ] response: @@ -250,7 +250,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxNA==" + "subgraph2": "Product: UHJvZHVjdDoyMA==" } } } @@ -266,7 +266,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoyMA==" + "subgraph2": "Product: UHJvZHVjdDoxNA==" } } } @@ -282,22 +282,22 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDoxNQ==" + "__fusion_2_id": "UHJvZHVjdDoxOQ==" }, { - "__fusion_2_id": "UHJvZHVjdDoxMw==" + "__fusion_2_id": "UHJvZHVjdDoyMQ==" }, { - "__fusion_2_id": "UHJvZHVjdDoxOA==" + "__fusion_2_id": "UHJvZHVjdDoxNg==" }, { - "__fusion_2_id": "UHJvZHVjdDoxNg==" + "__fusion_2_id": "UHJvZHVjdDoxOA==" }, { - "__fusion_2_id": "UHJvZHVjdDoyMQ==" + "__fusion_2_id": "UHJvZHVjdDoxMw==" }, { - "__fusion_2_id": "UHJvZHVjdDoxOQ==" + "__fusion_2_id": "UHJvZHVjdDoxNQ==" } ] response: @@ -307,7 +307,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxNQ==" + "subgraph2": "Product: UHJvZHVjdDoxOQ==" } } } @@ -315,7 +315,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxMw==" + "subgraph2": "Product: UHJvZHVjdDoyMQ==" } } } @@ -323,7 +323,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxOA==" + "subgraph2": "Product: UHJvZHVjdDoxNg==" } } } @@ -331,7 +331,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxNg==" + "subgraph2": "Product: UHJvZHVjdDoxOA==" } } } @@ -339,7 +339,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoyMQ==" + "subgraph2": "Product: UHJvZHVjdDoxMw==" } } } @@ -347,7 +347,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxOQ==" + "subgraph2": "Product: UHJvZHVjdDoxNQ==" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selection_Has_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selection_Has_Dependency.yaml index aa191b84b34..4e501bdb5de 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selection_Has_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selection_Has_Dependency.yaml @@ -22,7 +22,7 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDo2" + "subgraph2": "Product: UHJvZHVjdDo0" } }, { @@ -32,7 +32,7 @@ response: }, { "product": { - "subgraph2": "Product: UHJvZHVjdDo0" + "subgraph2": "Product: UHJvZHVjdDo2" } } ] @@ -95,7 +95,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo2" + "id": "UHJvZHVjdDo0" } }, { @@ -107,7 +107,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo0" + "id": "UHJvZHVjdDo2" } } ] @@ -140,10 +140,10 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDo2" + "__fusion_2_id": "UHJvZHVjdDo0" }, { - "__fusion_2_id": "UHJvZHVjdDo0" + "__fusion_2_id": "UHJvZHVjdDo2" } ] response: @@ -153,7 +153,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo2" + "subgraph2": "Product: UHJvZHVjdDo0" } } } @@ -161,7 +161,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo0" + "subgraph2": "Product: UHJvZHVjdDo2" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml index 6aa59471114..b3b483c5898 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml @@ -22,7 +22,7 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDo2" + "subgraph2": "Product: UHJvZHVjdDo0" } }, { @@ -32,7 +32,7 @@ response: }, { "product": { - "subgraph2": "Product: UHJvZHVjdDo0" + "subgraph2": "Product: UHJvZHVjdDo2" } } ] @@ -95,7 +95,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo2" + "id": "UHJvZHVjdDo0" } }, { @@ -107,7 +107,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo0" + "id": "UHJvZHVjdDo2" } } ] @@ -169,10 +169,10 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDo2" + "__fusion_2_id": "UHJvZHVjdDo0" }, { - "__fusion_2_id": "UHJvZHVjdDo0" + "__fusion_2_id": "UHJvZHVjdDo2" } ] response: @@ -182,7 +182,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo2" + "subgraph2": "Product: UHJvZHVjdDo0" } } } @@ -190,7 +190,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo0" + "subgraph2": "Product: UHJvZHVjdDo2" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml index d28531ba1e0..5c83b3b995d 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml @@ -22,7 +22,7 @@ response: "posts": [ { "product": { - "subgraph2": "Product: UHJvZHVjdDo2" + "subgraph2": "Product: UHJvZHVjdDo0" } }, { @@ -32,7 +32,7 @@ response: }, { "product": { - "subgraph2": "Product: UHJvZHVjdDo0" + "subgraph2": "Product: UHJvZHVjdDo2" } } ] @@ -91,7 +91,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo2" + "id": "UHJvZHVjdDo0" } }, { @@ -103,7 +103,7 @@ sourceSchemas: { "__typename": "Photo", "product": { - "id": "UHJvZHVjdDo0" + "id": "UHJvZHVjdDo2" } } ] @@ -159,10 +159,10 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "UHJvZHVjdDo2" + "__fusion_2_id": "UHJvZHVjdDo0" }, { - "__fusion_2_id": "UHJvZHVjdDo0" + "__fusion_2_id": "UHJvZHVjdDo2" } ] response: @@ -172,7 +172,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo2" + "subgraph2": "Product: UHJvZHVjdDo0" } } } @@ -180,7 +180,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDo0" + "subgraph2": "Product: UHJvZHVjdDo2" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/VariableCoercionTests.OneOf_Only_One_Option_Provided_But_Value_Is_Null.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/VariableCoercionTests.OneOf_Only_One_Option_Provided_But_Value_Is_Null.yaml index b16b9651801..1ac9afa6bb5 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/VariableCoercionTests.OneOf_Only_One_Option_Provided_But_Value_Is_Null.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/VariableCoercionTests.OneOf_Only_One_Option_Provided_But_Value_Is_Null.yaml @@ -17,7 +17,7 @@ response: { "errors": [ { - "message": "\u0060null\u0060 was set to the field \u0060dog\u0060of the OneOf Input Object \u0060Pet\u0060. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", + "message": "\u0060null\u0060 was set to the field \u0060dog\u0060 of the OneOf Input Object \u0060Pet\u0060. OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null.", "path": [ "pet" ], diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values.yaml index 9b53fdd52a6..e0e0dd17893 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values.yaml @@ -28,8 +28,8 @@ response: "node": { "feedback": { "buyer": { - "relativeUrl": "User: VXNlcjoxMw==", - "displayName": "User: VXNlcjoxMw==" + "relativeUrl": "User: VXNlcjoxMQ==", + "displayName": "User: VXNlcjoxMQ==" } } } @@ -48,8 +48,8 @@ response: "node": { "feedback": { "buyer": { - "relativeUrl": "User: VXNlcjoxMQ==", - "displayName": "User: VXNlcjoxMQ==" + "relativeUrl": "User: VXNlcjoxMw==", + "displayName": "User: VXNlcjoxMw==" } } } @@ -95,13 +95,13 @@ sourceSchemas: variables: | [ { - "__fusion_3_id": "VXNlcjoxMw==" + "__fusion_3_id": "VXNlcjoxMQ==" }, { "__fusion_3_id": "VXNlcjoxMg==" }, { - "__fusion_3_id": "VXNlcjoxMQ==" + "__fusion_3_id": "VXNlcjoxMw==" } ] response: @@ -112,7 +112,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "displayName": "User: VXNlcjoxMw==" + "displayName": "User: VXNlcjoxMQ==" } } } @@ -130,7 +130,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "displayName": "User: VXNlcjoxMQ==" + "displayName": "User: VXNlcjoxMw==" } } } @@ -187,13 +187,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "VXNlcjoxMw==" + "__fusion_2_id": "VXNlcjoxMQ==" }, { "__fusion_2_id": "VXNlcjoxMg==" }, { - "__fusion_2_id": "VXNlcjoxMQ==" + "__fusion_2_id": "VXNlcjoxMw==" } ] response: @@ -204,7 +204,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "relativeUrl": "User: VXNlcjoxMw==" + "relativeUrl": "User: VXNlcjoxMQ==" } } } @@ -222,7 +222,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "relativeUrl": "User: VXNlcjoxMQ==" + "relativeUrl": "User: VXNlcjoxMw==" } } } @@ -302,7 +302,7 @@ sourceSchemas: "node": { "feedback": { "buyer": { - "id": "VXNlcjoxMw==" + "id": "VXNlcjoxMQ==" } } } @@ -320,7 +320,7 @@ sourceSchemas: "node": { "feedback": { "buyer": { - "id": "VXNlcjoxMQ==" + "id": "VXNlcjoxMw==" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values_And_Forwarded_Variable.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values_And_Forwarded_Variable.yaml index adc01c486a5..ba30c8b03ec 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values_And_Forwarded_Variable.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values_And_Forwarded_Variable.yaml @@ -3,7 +3,7 @@ request: document: | query( $arg1: String - $arg2: String + $arg2: String ) { userBySlug(slug: "me") { feedbacks { @@ -36,8 +36,8 @@ response: "node": { "feedback": { "buyer": { - "relativeUrl": "User: VXNlcjoxMw==", - "displayName": "User: VXNlcjoxMw==" + "relativeUrl": "User: VXNlcjoxMQ==", + "displayName": "User: VXNlcjoxMQ==" } } } @@ -56,8 +56,8 @@ response: "node": { "feedback": { "buyer": { - "relativeUrl": "User: VXNlcjoxMQ==", - "displayName": "User: VXNlcjoxMQ==" + "relativeUrl": "User: VXNlcjoxMw==", + "displayName": "User: VXNlcjoxMw==" } } } @@ -92,7 +92,7 @@ sourceSchemas: document: | query Op_b1ca18eb_4( $arg2: String - $__fusion_3_id: ID! + $__fusion_3_id: ID! ) { node(id: $__fusion_3_id) { __typename @@ -105,7 +105,7 @@ sourceSchemas: [ { "arg2": "def", - "__fusion_3_id": "VXNlcjoxMw==" + "__fusion_3_id": "VXNlcjoxMQ==" }, { "arg2": "def", @@ -113,7 +113,7 @@ sourceSchemas: }, { "arg2": "def", - "__fusion_3_id": "VXNlcjoxMQ==" + "__fusion_3_id": "VXNlcjoxMw==" } ] response: @@ -124,7 +124,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "displayName": "User: VXNlcjoxMw==" + "displayName": "User: VXNlcjoxMQ==" } } } @@ -142,7 +142,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "displayName": "User: VXNlcjoxMQ==" + "displayName": "User: VXNlcjoxMw==" } } } @@ -188,7 +188,7 @@ sourceSchemas: document: | query Op_b1ca18eb_3( $arg1: String - $__fusion_2_id: ID! + $__fusion_2_id: ID! ) { node(id: $__fusion_2_id) { __typename @@ -201,7 +201,7 @@ sourceSchemas: [ { "arg1": "abc", - "__fusion_2_id": "VXNlcjoxMw==" + "__fusion_2_id": "VXNlcjoxMQ==" }, { "arg1": "abc", @@ -209,7 +209,7 @@ sourceSchemas: }, { "arg1": "abc", - "__fusion_2_id": "VXNlcjoxMQ==" + "__fusion_2_id": "VXNlcjoxMw==" } ] response: @@ -220,7 +220,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "relativeUrl": "User: VXNlcjoxMw==" + "relativeUrl": "User: VXNlcjoxMQ==" } } } @@ -238,7 +238,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "relativeUrl": "User: VXNlcjoxMQ==" + "relativeUrl": "User: VXNlcjoxMw==" } } } @@ -318,7 +318,7 @@ sourceSchemas: "node": { "feedback": { "buyer": { - "id": "VXNlcjoxMw==" + "id": "VXNlcjoxMQ==" } } } @@ -336,7 +336,7 @@ sourceSchemas: "node": { "feedback": { "buyer": { - "id": "VXNlcjoxMQ==" + "id": "VXNlcjoxMw==" } } } @@ -351,7 +351,7 @@ operationPlan: - document: | query( $arg1: String - $arg2: String + $arg2: String ) { userBySlug(slug: "me") { feedbacks { @@ -420,7 +420,7 @@ operationPlan: operation: | query Op_b1ca18eb_3( $arg1: String - $__fusion_2_id: ID! + $__fusion_2_id: ID! ) { node(id: $__fusion_2_id) { __typename @@ -445,7 +445,7 @@ operationPlan: operation: | query Op_b1ca18eb_4( $arg2: String - $__fusion_3_id: ID! + $__fusion_3_id: ID! ) { node(id: $__fusion_3_id) { __typename diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values_Some_Items_Null.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values_Some_Items_Null.yaml index 2a007266a90..060cfac28a0 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values_Some_Items_Null.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.BatchExecutionState_With_Multiple_Variable_Values_Some_Items_Null.yaml @@ -28,8 +28,8 @@ response: "node": { "feedback": { "buyer": { - "relativeUrl": "User: VXNlcjoxMw==", - "displayName": "User: VXNlcjoxMw==" + "relativeUrl": "User: VXNlcjoxMQ==", + "displayName": "User: VXNlcjoxMQ==" } } } @@ -48,8 +48,8 @@ response: "node": { "feedback": { "buyer": { - "relativeUrl": "User: VXNlcjoxMQ==", - "displayName": "User: VXNlcjoxMQ==" + "relativeUrl": "User: VXNlcjoxMw==", + "displayName": "User: VXNlcjoxMw==" } } } @@ -95,13 +95,13 @@ sourceSchemas: variables: | [ { - "__fusion_3_id": "VXNlcjoxMw==" + "__fusion_3_id": "VXNlcjoxMQ==" }, { "__fusion_3_id": "VXNlcjoxMg==" }, { - "__fusion_3_id": "VXNlcjoxMQ==" + "__fusion_3_id": "VXNlcjoxMw==" } ] response: @@ -112,7 +112,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "displayName": "User: VXNlcjoxMw==" + "displayName": "User: VXNlcjoxMQ==" } } } @@ -130,7 +130,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "displayName": "User: VXNlcjoxMQ==" + "displayName": "User: VXNlcjoxMw==" } } } @@ -187,13 +187,13 @@ sourceSchemas: variables: | [ { - "__fusion_2_id": "VXNlcjoxMw==" + "__fusion_2_id": "VXNlcjoxMQ==" }, { "__fusion_2_id": "VXNlcjoxMg==" }, { - "__fusion_2_id": "VXNlcjoxMQ==" + "__fusion_2_id": "VXNlcjoxMw==" } ] response: @@ -204,7 +204,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "relativeUrl": "User: VXNlcjoxMw==" + "relativeUrl": "User: VXNlcjoxMQ==" } } } @@ -222,7 +222,7 @@ sourceSchemas: "data": { "node": { "__typename": "User", - "relativeUrl": "User: VXNlcjoxMQ==" + "relativeUrl": "User: VXNlcjoxMw==" } } } @@ -302,7 +302,7 @@ sourceSchemas: "node": { "feedback": { "buyer": { - "id": "VXNlcjoxMw==" + "id": "VXNlcjoxMQ==" } } } @@ -320,7 +320,7 @@ sourceSchemas: "node": { "feedback": { "buyer": { - "id": "VXNlcjoxMQ==" + "id": "VXNlcjoxMw==" } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Utilities.Tests/Rewriters/InlineFragmentOperationRewriterTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Utilities.Tests/Rewriters/InlineFragmentOperationRewriterTests.cs index 4a03c817556..23ed60aa5fd 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Utilities.Tests/Rewriters/InlineFragmentOperationRewriterTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Utilities.Tests/Rewriters/InlineFragmentOperationRewriterTests.cs @@ -28,10 +28,11 @@ fragment Product on Product { // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + Assert.False(result.HasIncrementalParts); + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -69,10 +70,11 @@ fragment Product2 on Product { // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + Assert.False(result.HasIncrementalParts); + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -104,10 +106,11 @@ public void Inline_Inline_Fragment_Into_ProductById_SelectionSet_1() // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + Assert.False(result.HasIncrementalParts); + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -147,10 +150,10 @@ fragment Product2 on Product { // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -183,10 +186,10 @@ ... @include(if: true) { // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -222,10 +225,10 @@ ... @include(if: false) { // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -260,10 +263,11 @@ fragment Product on Product { // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + Assert.False(result.HasIncrementalParts); + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -299,10 +303,10 @@ fragment Product on Product { // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -334,10 +338,10 @@ name @include(if: false) // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -365,10 +369,10 @@ id @include(if: false) // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ { productById(id: 1) { @@ -399,10 +403,10 @@ description @skip(if: false) // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ query( $skip: Boolean! @@ -441,10 +445,10 @@ name @skip(if: $skip) // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ query( $skip: Boolean! @@ -493,10 +497,10 @@ fragment ProductFragment2 on Product { // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ query( $slug: String! @@ -540,10 +544,10 @@ public void Merge_Fields_With_Aliases() // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ query( $slug: String! @@ -577,10 +581,10 @@ id @fusion__requirement // act var rewriter = new InlineFragmentOperationRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc, null); + var result = rewriter.RewriteDocument(doc, null); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ query( $skip: Boolean! @@ -721,10 +725,10 @@ name @include(if: $skip) var rewriter = new InlineFragmentOperationRewriter( schemaDefinition, removeStaticallyExcludedSelections: true); - var rewritten = rewriter.RewriteDocument(doc); + var result = rewriter.RewriteDocument(doc); // assert - rewritten.MatchInlineSnapshot( + result.Document.MatchInlineSnapshot( """ query( $skip: Boolean! @@ -736,4 +740,208 @@ name @include(if: $skip) } """); } + + [Fact] + public void Detect_Defer_On_Inline_Fragment() + { + // arrange + var sourceText = FileResource.Open("schema1.graphql"); + var schemaDefinition = SchemaParser.Parse(sourceText); + + var doc = Utf8GraphQLParser.Parse( + """ + { + productById(id: 1) { + id + ... @defer { + name + } + } + } + """); + + // act + var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); + var result = rewriter.RewriteDocument(doc); + + // assert + Assert.True(result.HasIncrementalParts); + } + + [Fact] + public void Detect_Defer_On_Fragment_Spread() + { + // arrange + var sourceText = FileResource.Open("schema1.graphql"); + var schemaDefinition = SchemaParser.Parse(sourceText); + + var doc = Utf8GraphQLParser.Parse( + """ + { + productById(id: 1) { + id + ... Product @defer + } + } + + fragment Product on Product { + name + } + """); + + // act + var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); + var result = rewriter.RewriteDocument(doc); + + // assert + Assert.True(result.HasIncrementalParts); + } + + [Fact] + public void Detect_Stream_On_Field() + { + // arrange + var sourceText = FileResource.Open("schema1.graphql"); + var schemaDefinition = SchemaParser.Parse(sourceText); + + var doc = Utf8GraphQLParser.Parse( + """ + { + productById(id: 1) { + id + reviews @stream { + nodes { + body + } + } + } + } + """); + + // act + var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); + var result = rewriter.RewriteDocument(doc); + + // assert + Assert.True(result.HasIncrementalParts); + } + + [Fact] + public void No_Incremental_Parts_Without_Defer_Or_Stream() + { + // arrange + var sourceText = FileResource.Open("schema1.graphql"); + var schemaDefinition = SchemaParser.Parse(sourceText); + + var doc = Utf8GraphQLParser.Parse( + """ + { + productById(id: 1) { + id + name + reviews { + nodes { + body + } + } + } + } + """); + + // act + var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); + var result = rewriter.RewriteDocument(doc); + + // assert + Assert.False(result.HasIncrementalParts); + } + + [Fact] + public void Detect_Multiple_Defer_Directives() + { + // arrange + var sourceText = FileResource.Open("schema1.graphql"); + var schemaDefinition = SchemaParser.Parse(sourceText); + + var doc = Utf8GraphQLParser.Parse( + """ + { + productById(id: 1) { + id + ... @defer { + name + } + ... @defer { + description + } + } + } + """); + + // act + var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); + var result = rewriter.RewriteDocument(doc); + + // assert + Assert.True(result.HasIncrementalParts); + } + + [Fact] + public void Detect_Defer_And_Stream_Together() + { + // arrange + var sourceText = FileResource.Open("schema1.graphql"); + var schemaDefinition = SchemaParser.Parse(sourceText); + + var doc = Utf8GraphQLParser.Parse( + """ + { + productById(id: 1) { + id + ... @defer { + name + } + reviews @stream { + nodes { + body + } + } + } + } + """); + + // act + var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); + var result = rewriter.RewriteDocument(doc); + + // assert + Assert.True(result.HasIncrementalParts); + } + + [Fact] + public void Detect_Defer_With_Label() + { + // arrange + var sourceText = FileResource.Open("schema1.graphql"); + var schemaDefinition = SchemaParser.Parse(sourceText); + + var doc = Utf8GraphQLParser.Parse( + """ + { + productById(id: 1) { + id + ... @defer(label: "productName") { + name + } + } + } + """); + + // act + var rewriter = new InlineFragmentOperationRewriter(schemaDefinition); + var result = rewriter.RewriteDocument(doc); + + // assert + Assert.True(result.HasIncrementalParts); + } } diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonNullIgnoreCondition.cs b/src/HotChocolate/Json/src/Json/JsonNullIgnoreCondition.cs similarity index 94% rename from src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonNullIgnoreCondition.cs rename to src/HotChocolate/Json/src/Json/JsonNullIgnoreCondition.cs index e6e05aa5d84..f7e6e4f5131 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/JsonNullIgnoreCondition.cs +++ b/src/HotChocolate/Json/src/Json/JsonNullIgnoreCondition.cs @@ -1,4 +1,4 @@ -namespace HotChocolate.Execution; +namespace HotChocolate.Text.Json; /// /// Specifies when null values are ignored. diff --git a/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.Literal.cs b/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.Literal.cs index 5289cf48350..e9a02d5f43b 100644 --- a/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.Literal.cs +++ b/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.Literal.cs @@ -13,6 +13,17 @@ public sealed partial class JsonWriter /// public void WriteNullValue() { + if (HasDeferredPropertyName) + { + DiscardDeferredPropertyName(); + return; + } + + if (IgnoreNullListElements && IsInArray) + { + return; + } + WriteLiteralByOptions(JsonConstants.NullValue); SetFlagToAddListSeparatorBeforeNextItem(); @@ -28,6 +39,8 @@ public void WriteNullValue() /// public void WriteBooleanValue(bool value) { + FlushDeferredPropertyName(); + if (value) { WriteLiteralByOptions(JsonConstants.TrueValue); diff --git a/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.Number.cs b/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.Number.cs index b5a6fb66ca2..e4dc0629437 100644 --- a/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.Number.cs +++ b/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.Number.cs @@ -22,6 +22,8 @@ public sealed partial class JsonWriter /// public void WriteNumberValue(ReadOnlySpan utf8FormattedNumber) { + FlushDeferredPropertyName(); + ValidateValue(utf8FormattedNumber); if (_indented) diff --git a/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.PropertyName.cs b/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.PropertyName.cs index bcaa3068f1e..8e86bde088a 100644 --- a/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.PropertyName.cs +++ b/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.PropertyName.cs @@ -44,6 +44,13 @@ public void WritePropertyName(string propertyName) /// public void WritePropertyName(ReadOnlySpan propertyName) { + FlushDeferredPropertyName(); + + if (IgnoreNullFields) + { + BeginDeferPropertyName(); + } + ValidateProperty(propertyName); var propertyIdx = NeedsEscaping(propertyName, _options.Encoder); @@ -189,6 +196,13 @@ private void WriteStringIndentedPropertyName(ReadOnlySpan escapedPropertyN /// public void WritePropertyName(ReadOnlySpan utf8PropertyName) { + FlushDeferredPropertyName(); + + if (IgnoreNullFields) + { + BeginDeferPropertyName(); + } + ValidateProperty(utf8PropertyName); var propertyIdx = NeedsEscaping(utf8PropertyName, _options.Encoder); @@ -210,6 +224,13 @@ public void WritePropertyName(ReadOnlySpan utf8PropertyName) private void WritePropertyNameUnescaped(ReadOnlySpan utf8PropertyName) { + FlushDeferredPropertyName(); + + if (IgnoreNullFields) + { + BeginDeferPropertyName(); + } + ValidateProperty(utf8PropertyName); WriteStringByOptionsPropertyName(utf8PropertyName); diff --git a/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.String.cs b/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.String.cs index 47822a393f8..10e3b187a62 100644 --- a/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.String.cs +++ b/src/HotChocolate/Json/src/Json/JsonWriter.WriteValues.String.cs @@ -51,6 +51,8 @@ public void WriteStringValue(string? value) /// public void WriteStringValue(ReadOnlySpan value) { + FlushDeferredPropertyName(); + WriteStringEscape(value); SetFlagToAddListSeparatorBeforeNextItem(); @@ -192,6 +194,8 @@ private void WriteStringEscapeValue(ReadOnlySpan value, int firstEscapeInd /// public void WriteStringValue(ReadOnlySpan utf8Value, bool skipEscaping = false) { + FlushDeferredPropertyName(); + ValidateValue(utf8Value); if (skipEscaping) @@ -392,6 +396,8 @@ private void WriteStringEscapeValue(ReadOnlySpan utf8Value, int firstEscap /// internal void WriteNumberValueAsStringUnescaped(ReadOnlySpan utf8Value) { + FlushDeferredPropertyName(); + // The value has been validated prior to calling this method. WriteStringByOptions(utf8Value); diff --git a/src/HotChocolate/Json/src/Json/JsonWriter.cs b/src/HotChocolate/Json/src/Json/JsonWriter.cs index b22be865812..c73b3aa927a 100644 --- a/src/HotChocolate/Json/src/Json/JsonWriter.cs +++ b/src/HotChocolate/Json/src/Json/JsonWriter.cs @@ -9,7 +9,7 @@ namespace HotChocolate.Text.Json; public sealed partial class JsonWriter { private readonly JsonWriterOptions _options; - private readonly IBufferWriter _writer; + private IBufferWriter _writer; // The highest order bit of _currentDepth is used to discern whether we are writing the first item in a list or not. // if (_currentDepth >> 31) == 1, add a list separator before writing the item @@ -26,10 +26,28 @@ public sealed partial class JsonWriter private readonly bool _indented; private readonly int _maxDepth; - public JsonWriter(IBufferWriter writer, JsonWriterOptions options) + // Deferred property name support for transparent null field omission. + // When IgnoreNullFields is true, WritePropertyName writes to _deferBuffer instead of + // _writer. If the next value is null, we discard the deferred bytes (rollback). + // If the next value is non-null, we flush the deferred bytes to the real writer first. + private DeferBuffer? _deferBuffer; + private IBufferWriter? _realWriter; + private int _savedCurrentDepth; + private JsonTokenType _savedTokenType; + + // Container type tracking for transparent null list element omission. + // Bit N = 1 means depth N is an array, bit N = 0 means object. + // Supports up to 64 levels of nesting. + private long _containerTypeStack; + + public JsonWriter( + IBufferWriter writer, + JsonWriterOptions options, + JsonNullIgnoreCondition nullIgnoreCondition = JsonNullIgnoreCondition.None) { _writer = writer; _options = options; + NullIgnoreCondition = nullIgnoreCondition; #if NET9_0_OR_GREATER Debug.Assert(options.NewLine is "\n" or "\r\n", "Invalid NewLine string."); @@ -52,6 +70,26 @@ public JsonWriter(IBufferWriter writer, JsonWriterOptions options) /// public JsonWriterOptions Options => _options; + /// + /// Gets the null ignore condition that controls whether null values + /// are omitted from the JSON output. + /// + public JsonNullIgnoreCondition NullIgnoreCondition { get; set; } + + /// + /// Getswha a value indicating whether null fields should be omitted + /// when writing JSON objects. + /// + public bool IgnoreNullFields + => (NullIgnoreCondition & JsonNullIgnoreCondition.Fields) == JsonNullIgnoreCondition.Fields; + + /// + /// Gets a value indicating whether null elements should be omitted + /// when writing JSON arrays. + /// + public bool IgnoreNullListElements + => (NullIgnoreCondition & JsonNullIgnoreCondition.Lists) == JsonNullIgnoreCondition.Lists; + private int Indentation => CurrentDepth * _indentLength; internal JsonTokenType TokenType => _tokenType; @@ -62,6 +100,87 @@ public JsonWriter(IBufferWriter writer, JsonWriterOptions options) /// public int CurrentDepth => _currentDepth & JsonConstants.RemoveFlagsBitMask; + private bool HasDeferredPropertyName + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _realWriter is not null; + } + + private bool IsInArray + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + var depth = CurrentDepth; + if (depth is 0 or > 64) + { + return false; + } + + return (_containerTypeStack & (1L << (depth - 1))) != 0; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FlushDeferredPropertyName() + { + if (_realWriter is not null) + { + FlushDeferredPropertyNameSlow(); + } + } + + private void FlushDeferredPropertyNameSlow() + { + var deferred = _deferBuffer!; + _writer = _realWriter!; + _realWriter = null; + + var span = _writer.GetSpan(deferred.WrittenCount); + deferred.WrittenSpan.CopyTo(span); + _writer.Advance(deferred.WrittenCount); + } + + private void DiscardDeferredPropertyName() + { + _writer = _realWriter!; + _realWriter = null; + _currentDepth = _savedCurrentDepth; + _tokenType = _savedTokenType; + } + + private void BeginDeferPropertyName() + { + _savedCurrentDepth = _currentDepth; + _savedTokenType = _tokenType; + + _deferBuffer ??= new DeferBuffer(); + _deferBuffer.Reset(); + + _realWriter = _writer; + _writer = _deferBuffer; + } + + private void SetContainerTypeArray() + { + var depth = CurrentDepth; + + if (depth is > 0 and <= 64) + { + _containerTypeStack |= 1L << (depth - 1); + } + } + + private void SetContainerTypeObject() + { + var depth = CurrentDepth; + + if (depth is > 0 and <= 64) + { + _containerTypeStack &= ~(1L << (depth - 1)); + } + } + /// /// Writes the beginning of a JSON array. /// @@ -71,8 +190,10 @@ public JsonWriter(IBufferWriter writer, JsonWriterOptions options) /// public void WriteStartArray() { + FlushDeferredPropertyName(); WriteStart(JsonConstants.OpenBracket); _tokenType = JsonTokenType.StartArray; + SetContainerTypeArray(); } /// @@ -84,8 +205,10 @@ public void WriteStartArray() /// public void WriteStartObject() { + FlushDeferredPropertyName(); WriteStart(JsonConstants.OpenBrace); _tokenType = JsonTokenType.StartObject; + SetContainerTypeObject(); } private void WriteStart(byte token) @@ -317,6 +440,8 @@ public static void ValidateValue(ReadOnlySpan value) /// The raw UTF-8 encoded JSON to write. public void WriteRawValue(ReadOnlySpan utf8Json) { + FlushDeferredPropertyName(); + var maxRequired = utf8Json.Length + 1; // Optionally, 1 list separator var bytesWritten = 0; @@ -334,4 +459,43 @@ public void WriteRawValue(ReadOnlySpan utf8Json) SetFlagToAddListSeparatorBeforeNextItem(); } + + /// + /// Internal buffer used for deferred property name writes. + /// + private sealed class DeferBuffer : IBufferWriter + { + private byte[] _buffer = new byte[256]; + private int _written; + + public int WrittenCount => _written; + + public ReadOnlySpan WrittenSpan => _buffer.AsSpan(0, _written); + + public void Reset() => _written = 0; + + public void Advance(int count) => _written += count; + + public Memory GetMemory(int sizeHint = 0) + { + EnsureCapacity(sizeHint); + return _buffer.AsMemory(_written); + } + + public Span GetSpan(int sizeHint = 0) + { + EnsureCapacity(sizeHint); + return _buffer.AsSpan(_written); + } + + private void EnsureCapacity(int sizeHint) + { + var required = _written + Math.Max(sizeHint, 1); + + if (required > _buffer.Length) + { + Array.Resize(ref _buffer, Math.Max(_buffer.Length * 2, required)); + } + } + } } diff --git a/src/HotChocolate/Json/test/Json.Tests/JsonWriterNullIgnoreTests.cs b/src/HotChocolate/Json/test/Json.Tests/JsonWriterNullIgnoreTests.cs new file mode 100644 index 00000000000..4ba1bf38e1c --- /dev/null +++ b/src/HotChocolate/Json/test/Json.Tests/JsonWriterNullIgnoreTests.cs @@ -0,0 +1,435 @@ +using System.Buffers; +using System.Text; +using System.Text.Json; + +namespace HotChocolate.Text.Json; + +public class JsonWriterNullIgnoreTests +{ + [Fact] + public void Default_NullIgnoreCondition_IsNone() + { + // arrange + var buffer = new ArrayBufferWriter(); + var options = new JsonWriterOptions { SkipValidation = true }; + var writer = new JsonWriter(buffer, options); + + // assert + Assert.Equal(JsonNullIgnoreCondition.None, writer.NullIgnoreCondition); + Assert.False(writer.IgnoreNullFields); + Assert.False(writer.IgnoreNullListElements); + } + + [Fact] + public void NullIgnoreCondition_Fields_SetsIgnoreNullFields() + { + // arrange + var buffer = new ArrayBufferWriter(); + var options = new JsonWriterOptions { SkipValidation = true }; + var writer = new JsonWriter(buffer, options, JsonNullIgnoreCondition.Fields); + + // assert + Assert.Equal(JsonNullIgnoreCondition.Fields, writer.NullIgnoreCondition); + Assert.True(writer.IgnoreNullFields); + Assert.False(writer.IgnoreNullListElements); + } + + [Fact] + public void NullIgnoreCondition_Lists_SetsIgnoreNullListElements() + { + // arrange + var buffer = new ArrayBufferWriter(); + var options = new JsonWriterOptions { SkipValidation = true }; + var writer = new JsonWriter(buffer, options, JsonNullIgnoreCondition.Lists); + + // assert + Assert.Equal(JsonNullIgnoreCondition.Lists, writer.NullIgnoreCondition); + Assert.False(writer.IgnoreNullFields); + Assert.True(writer.IgnoreNullListElements); + } + + [Fact] + public void NullIgnoreCondition_FieldsAndLists_SetsBoth() + { + // arrange + var buffer = new ArrayBufferWriter(); + var options = new JsonWriterOptions { SkipValidation = true }; + var writer = new JsonWriter(buffer, options, JsonNullIgnoreCondition.FieldsAndLists); + + // assert + Assert.Equal(JsonNullIgnoreCondition.FieldsAndLists, writer.NullIgnoreCondition); + Assert.True(writer.IgnoreNullFields); + Assert.True(writer.IgnoreNullListElements); + } + + [Fact] + public void IgnoreNullFields_OmitsNullFieldValues() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("name"); + writer.WriteStringValue("Alice"); + writer.WritePropertyName("age"); + writer.WriteNullValue(); + writer.WritePropertyName("email"); + writer.WriteStringValue("alice@example.com"); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"name":"Alice","email":"alice@example.com"}""", json); + } + + [Fact] + public void IgnoreNullFields_KeepsNonNullFields() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("a"); + writer.WriteNumberValue(1); + writer.WritePropertyName("b"); + writer.WriteBooleanValue(true); + writer.WritePropertyName("c"); + writer.WriteStringValue("hello"); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"a":1,"b":true,"c":"hello"}""", json); + } + + [Fact] + public void IgnoreNullFields_AllFieldsNull_WritesEmptyObject() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("a"); + writer.WriteNullValue(); + writer.WritePropertyName("b"); + writer.WriteNullValue(); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("{}", json); + } + + [Fact] + public void IgnoreNullFields_NullStringValue_OmitsField() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("name"); + writer.WriteStringValue((string?)null); + writer.WritePropertyName("value"); + writer.WriteNumberValue(42); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"value":42}""", json); + } + + [Fact] + public void IgnoreNullFields_NestedObject_OmitsNullFieldsAtAllLevels() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("outer"); + writer.WriteStartObject(); + writer.WritePropertyName("inner"); + writer.WriteStringValue("value"); + writer.WritePropertyName("nullField"); + writer.WriteNullValue(); + writer.WriteEndObject(); + writer.WritePropertyName("topNull"); + writer.WriteNullValue(); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"outer":{"inner":"value"}}""", json); + } + + [Fact] + public void IgnoreNullFields_PropertyFollowedByStartObject_FlushesPropertyName() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("data"); + writer.WriteStartObject(); + writer.WritePropertyName("id"); + writer.WriteNumberValue(1); + writer.WriteEndObject(); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"data":{"id":1}}""", json); + } + + [Fact] + public void IgnoreNullFields_PropertyFollowedByStartArray_FlushesPropertyName() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("items"); + writer.WriteStartArray(); + writer.WriteNumberValue(1); + writer.WriteNumberValue(2); + writer.WriteEndArray(); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"items":[1,2]}""", json); + } + + [Fact] + public void IgnoreNullFields_PropertyFollowedByRawValue_FlushesPropertyName() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("raw"); + writer.WriteRawValue("true"u8); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"raw":true}""", json); + } + + [Fact] + public void IgnoreNullListElements_OmitsNullsFromArray() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Lists, writer => + { + writer.WriteStartArray(); + writer.WriteNumberValue(1); + writer.WriteNullValue(); + writer.WriteNumberValue(2); + writer.WriteNullValue(); + writer.WriteNumberValue(3); + writer.WriteEndArray(); + }); + + // assert + Assert.Equal("[1,2,3]", json); + } + + [Fact] + public void IgnoreNullListElements_AllNulls_WritesEmptyArray() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Lists, writer => + { + writer.WriteStartArray(); + writer.WriteNullValue(); + writer.WriteNullValue(); + writer.WriteEndArray(); + }); + + // assert + Assert.Equal("[]", json); + } + + [Fact] + public void IgnoreNullListElements_NestedArray_OmitsNullsAtAllLevels() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Lists, writer => + { + writer.WriteStartArray(); + writer.WriteStartArray(); + writer.WriteNullValue(); + writer.WriteNumberValue(1); + writer.WriteEndArray(); + writer.WriteNullValue(); + writer.WriteEndArray(); + }); + + // assert + Assert.Equal("[[1]]", json); + } + + [Fact] + public void IgnoreNullListElements_DoesNotAffectObjectFields() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Lists, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("field"); + writer.WriteNullValue(); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"field":null}""", json); + } + + [Fact] + public void IgnoreNullFields_DoesNotAffectArrayElements() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartArray(); + writer.WriteNullValue(); + writer.WriteNumberValue(1); + writer.WriteEndArray(); + }); + + // assert + Assert.Equal("[null,1]", json); + } + + [Fact] + public void FieldsAndLists_OmitsBoth() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.FieldsAndLists, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("name"); + writer.WriteNullValue(); + writer.WritePropertyName("items"); + writer.WriteStartArray(); + writer.WriteNullValue(); + writer.WriteNumberValue(1); + writer.WriteNullValue(); + writer.WriteEndArray(); + writer.WritePropertyName("active"); + writer.WriteBooleanValue(true); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"items":[1],"active":true}""", json); + } + + [Fact] + public void None_WritesAllNulls() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.None, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("field"); + writer.WriteNullValue(); + writer.WritePropertyName("items"); + writer.WriteStartArray(); + writer.WriteNullValue(); + writer.WriteEndArray(); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"field":null,"items":[null]}""", json); + } + + [Fact] + public void IgnoreNullFields_ObjectInsideArray_OmitsNullFieldsInNestedObject() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartArray(); + writer.WriteStartObject(); + writer.WritePropertyName("id"); + writer.WriteNumberValue(1); + writer.WritePropertyName("name"); + writer.WriteNullValue(); + writer.WriteEndObject(); + writer.WriteEndArray(); + }); + + // assert + Assert.Equal("""[{"id":1}]""", json); + } + + [Fact] + public void IgnoreNullListElements_ArrayInsideObject_OmitsNullElements() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Lists, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("items"); + writer.WriteStartArray(); + writer.WriteNullValue(); + writer.WriteStringValue("a"); + writer.WriteEndArray(); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"items":["a"]}""", json); + } + + [Fact] + public void IgnoreNullFields_NullFieldBeforeLastField_CorrectCommas() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("a"); + writer.WriteNumberValue(1); + writer.WritePropertyName("b"); + writer.WriteNullValue(); + writer.WritePropertyName("c"); + writer.WriteNumberValue(3); + writer.WriteEndObject(); + }); + + // assert - verify no trailing comma after "a":1 + Assert.Equal("""{"a":1,"c":3}""", json); + } + + [Fact] + public void IgnoreNullFields_FirstFieldNull_CorrectOutput() + { + // arrange & act + var json = WriteJson(JsonNullIgnoreCondition.Fields, writer => + { + writer.WriteStartObject(); + writer.WritePropertyName("first"); + writer.WriteNullValue(); + writer.WritePropertyName("second"); + writer.WriteNumberValue(2); + writer.WriteEndObject(); + }); + + // assert + Assert.Equal("""{"second":2}""", json); + } + + private static string WriteJson(JsonNullIgnoreCondition condition, Action write) + { + var buffer = new ArrayBufferWriter(); + var options = new JsonWriterOptions { SkipValidation = true }; + var writer = new JsonWriter(buffer, options, condition); + + write(writer); + + return Encoding.UTF8.GetString(buffer.WrittenSpan); + } +} diff --git a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.QuerySyntax.cs b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.QuerySyntax.cs index 8254a3bac83..29c4ee3c314 100644 --- a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.QuerySyntax.cs +++ b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.QuerySyntax.cs @@ -75,7 +75,7 @@ private void VisitVariableDefinition(VariableDefinitionNode node, ISyntaxWriter if (node.DefaultValue is not null) { writer.Write(" = "); - writer.WriteValue(node.DefaultValue); + writer.WriteValue(node.DefaultValue, _indented); } WriteDirectives(node.Directives, writer); @@ -143,6 +143,7 @@ private void VisitSelectionSet(SelectionSetNode node, ISyntaxWriter writer) { writer.WriteLine(); writer.Indent(); + writer.WriteIndent(); separator = Environment.NewLine; } else @@ -187,8 +188,6 @@ private void VisitSelection(ISelectionNode node, ISyntaxWriter context) private void VisitField(FieldNode node, ISyntaxWriter writer) { - writer.WriteIndent(); - if (node.Alias is not null) { writer.WriteName(node.Alias); @@ -215,8 +214,6 @@ private void VisitField(FieldNode node, ISyntaxWriter writer) private void VisitFragmentSpread(FragmentSpreadNode node, ISyntaxWriter writer) { - writer.WriteIndent(); - writer.Write("... "); writer.WriteName(node.Name); @@ -225,8 +222,6 @@ private void VisitFragmentSpread(FragmentSpreadNode node, ISyntaxWriter writer) private void VisitInlineFragment(InlineFragmentNode node, ISyntaxWriter writer) { - writer.WriteIndent(); - writer.Write("..."); if (node.TypeCondition is not null) diff --git a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.SchemaSyntax.cs b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.SchemaSyntax.cs index 88394d76e47..a5148b4f433 100644 --- a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.SchemaSyntax.cs +++ b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.SchemaSyntax.cs @@ -403,7 +403,7 @@ private void WriteInputValueDefinition( writer.WriteSpace(); writer.Write("="); writer.WriteSpace(); - writer.WriteValue(value); + writer.WriteValue(value, _indented); } WriteDirectives(node.Directives, writer); diff --git a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.cs b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.cs index ce4a02627ad..28203897c12 100644 --- a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.cs +++ b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxSerializer.cs @@ -67,10 +67,10 @@ public void Serialize(ISyntaxNode node, ISyntaxWriter writer) case SyntaxKind.IntValue: case SyntaxKind.NullValue: case SyntaxKind.StringValue: - writer.WriteValue((IValueNode)node); + writer.WriteValue((IValueNode)node, _indented); break; case SyntaxKind.ObjectField: - writer.WriteObjectField((ObjectFieldNode)node); + writer.WriteObjectField((ObjectFieldNode)node, _indented); break; case SyntaxKind.SchemaDefinition: VisitSchemaDefinition((SchemaDefinitionNode)node, writer); diff --git a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxWriterExtensions.cs b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxWriterExtensions.cs index 4b8d8269d01..26344afd460 100644 --- a/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxWriterExtensions.cs +++ b/src/HotChocolate/Language/src/Language.SyntaxTree/Utilities/SyntaxWriterExtensions.cs @@ -25,11 +25,17 @@ public static void WriteMany( { if (items.Count > 0) { + var hasNewLine = separator.IndexOf('\n') >= 0 || separator.IndexOf('\r') >= 0; + action(items[0], writer); for (var i = 1; i < items.Count; i++) { writer.Write(separator); + if (hasNewLine) + { + writer.WriteIndent(); + } action(items[i], writer); } } @@ -56,6 +62,14 @@ public static void WriteMany( public static void WriteValue( this ISyntaxWriter writer, IValueNode? node) + { + WriteValue(writer, node, indented: false); + } + + public static void WriteValue( + this ISyntaxWriter writer, + IValueNode? node, + bool indented) { if (node is null) { @@ -103,11 +117,11 @@ public static void WriteValue( break; case SyntaxKind.ListValue: - WriteListValue(writer, (ListValueNode)node); + WriteListValue(writer, (ListValueNode)node, indented); break; case SyntaxKind.ObjectValue: - WriteObjectValue(writer, (ObjectValueNode)node); + WriteObjectValue(writer, (ObjectValueNode)node, indented); break; case SyntaxKind.Variable: @@ -247,21 +261,70 @@ public static void WriteNullValue(this ISyntaxWriter writer) public static void WriteListValue(this ISyntaxWriter writer, ListValueNode node) { - writer.Write("[ "); - writer.WriteMany(node.Items, (n, w) => w.WriteValue(n)); - writer.Write(" ]"); + WriteListValue(writer, node, indented: false); + } + + public static void WriteListValue(this ISyntaxWriter writer, ListValueNode node, bool indented) + { + writer.Write('['); + + if (indented && node.Items.Count > 0) + { + writer.WriteLine(); + writer.Indent(); + writer.WriteIndent(); + writer.WriteMany(node.Items, (n, w) => w.WriteValue(n, indented), "," + Environment.NewLine); + writer.WriteLine(); + writer.Unindent(); + writer.WriteIndent(); + } + else + { + writer.WriteSpace(); + writer.WriteMany(node.Items, (n, w) => w.WriteValue(n, indented)); + writer.WriteSpace(); + } + + writer.Write(']'); } public static void WriteObjectValue(this ISyntaxWriter writer, ObjectValueNode node) { - writer.Write("{ "); - writer.WriteMany(node.Fields, (n, w) => w.WriteObjectField(n)); - writer.Write(" }"); + WriteObjectValue(writer, node, indented: false); + } + + public static void WriteObjectValue(this ISyntaxWriter writer, ObjectValueNode node, bool indented) + { + writer.Write('{'); + + if (indented && node.Fields.Count > 0) + { + writer.WriteLine(); + writer.Indent(); + writer.WriteIndent(); + writer.WriteMany(node.Fields, (n, w) => w.WriteObjectField(n, indented), "," + Environment.NewLine); + writer.WriteLine(); + writer.Unindent(); + writer.WriteIndent(); + } + else + { + writer.WriteSpace(); + writer.WriteMany(node.Fields, (n, w) => w.WriteObjectField(n, indented)); + writer.WriteSpace(); + } + + writer.Write('}'); } public static void WriteObjectField(this ISyntaxWriter writer, ObjectFieldNode node) { - writer.WriteField(node.Name, node.Value); + WriteObjectField(writer, node, indented: false); + } + + public static void WriteObjectField(this ISyntaxWriter writer, ObjectFieldNode node, bool indented) + { + writer.WriteField(node.Name, node.Value, indented); } public static void WriteVariable(this ISyntaxWriter writer, VariableNode node) @@ -271,10 +334,15 @@ public static void WriteVariable(this ISyntaxWriter writer, VariableNode node) } public static void WriteField(this ISyntaxWriter writer, NameNode name, IValueNode value) + { + WriteField(writer, name, value, indented: false); + } + + public static void WriteField(this ISyntaxWriter writer, NameNode name, IValueNode value, bool indented) { writer.Write(name.Value); writer.Write(": "); - writer.WriteValue(value); + writer.WriteValue(value, indented); } public static void WriteArgument(this ISyntaxWriter writer, ArgumentNode node) diff --git a/src/HotChocolate/Language/src/Language.Utf8/HotChocolate.Language.Utf8.csproj b/src/HotChocolate/Language/src/Language.Utf8/HotChocolate.Language.Utf8.csproj index 2acd7961110..75ceee0c3e9 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/HotChocolate.Language.Utf8.csproj +++ b/src/HotChocolate/Language/src/Language.Utf8/HotChocolate.Language.Utf8.csproj @@ -18,6 +18,7 @@ + diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8MemoryBuilder.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8MemoryBuilder.cs index 6c0b03651da..cf46bfd3236 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8MemoryBuilder.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8MemoryBuilder.cs @@ -45,9 +45,7 @@ public ReadOnlyMemorySegment Write(ReadOnlySpan value) } public ReadOnlyMemorySegment GetMemorySegment(int start, int length) - { - return new ReadOnlyMemorySegment(this, start, length); - } + => new(this, start, length); public Memory GetMemory(int sizeHint = 0) { @@ -98,7 +96,9 @@ public void Seal() throw new InvalidOperationException("Memory is sealed."); } - var finalArray = _buffer.AsSpan().Slice(0, _written).ToArray(); + var finalArray = _written > 0 + ? _buffer.AsSpan().Slice(0, _written).ToArray() + : []; ArrayPool.Shared.Return(_buffer); _buffer = finalArray; } diff --git a/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs b/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs index 7e1ab3b545a..d5a20da7fe4 100644 --- a/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs +++ b/src/HotChocolate/Language/src/Language.Web/JsonValueParser.cs @@ -1,6 +1,6 @@ using System.Buffers; -using System.Text.Json; using System.Runtime.InteropServices; +using System.Text.Json; using HotChocolate.Buffers; namespace HotChocolate.Language; @@ -12,6 +12,7 @@ public ref struct JsonValueParser { private const int DefaultMaxAllowedDepth = 64; private readonly int _maxAllowedDepth; + private readonly bool _doNotSeal; internal Utf8MemoryBuilder? _memory; private readonly PooledArrayWriter? _externalBuffer; @@ -37,6 +38,12 @@ public JsonValueParser(PooledArrayWriter buffer) _externalBuffer = buffer; } + internal JsonValueParser(bool doNotSeal) + { + _maxAllowedDepth = DefaultMaxAllowedDepth; + _doNotSeal = doNotSeal; + } + public IValueNode Parse(JsonElement element) { if (element.ValueKind is JsonValueKind.Undefined) @@ -56,8 +63,11 @@ public IValueNode Parse(JsonElement element) } finally { - _memory?.Seal(); - _memory = null; + if (!_doNotSeal) + { + _memory?.Seal(); + _memory = null; + } } } @@ -213,8 +223,11 @@ public IValueNode Parse(ref Utf8JsonReader reader) } finally { - _memory?.Seal(); - _memory = null; + if (!_doNotSeal) + { + _memory?.Seal(); + _memory = null; + } } } diff --git a/src/HotChocolate/Language/src/Language.Web/OperationIdFormatException.cs b/src/HotChocolate/Language/src/Language.Web/OperationIdFormatException.cs deleted file mode 100644 index a6ab9f23530..00000000000 --- a/src/HotChocolate/Language/src/Language.Web/OperationIdFormatException.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace HotChocolate.Language; - -/// -/// Represents an error that occurs when the operation id has an invalid format. -/// -public sealed class OperationIdFormatException : SyntaxException -{ - /// - /// Initializes a new instance of . - /// - /// - /// The reader that encountered the syntax error. - /// - internal OperationIdFormatException(Utf8GraphQLReader reader) - : base(reader, "The operation id has an invalid format.") - { - } -} diff --git a/src/HotChocolate/Language/src/Language.Web/ThrowHelper.cs b/src/HotChocolate/Language/src/Language.Web/ThrowHelper.cs index c7ecde76130..fcbd9f39f72 100644 --- a/src/HotChocolate/Language/src/Language.Web/ThrowHelper.cs +++ b/src/HotChocolate/Language/src/Language.Web/ThrowHelper.cs @@ -19,6 +19,9 @@ public static InvalidGraphQLRequestException InvalidDocumentIdValue(JsonTokenTyp ThrowHelper_InvalidDocumentIdValue, tokenType)); + public static InvalidGraphQLRequestException InvalidDocumentIdFormat() + => new("The operation id has an invalid format."); + public static InvalidGraphQLRequestException InvalidOperationNameValue(JsonTokenType tokenType) => new(string.Format( CultureInfo.InvariantCulture, diff --git a/src/HotChocolate/Language/src/Language.Web/Utf8GraphQLRequestParser.cs b/src/HotChocolate/Language/src/Language.Web/Utf8GraphQLRequestParser.cs index 80895c63927..84aabf6cfad 100644 --- a/src/HotChocolate/Language/src/Language.Web/Utf8GraphQLRequestParser.cs +++ b/src/HotChocolate/Language/src/Language.Web/Utf8GraphQLRequestParser.cs @@ -200,9 +200,9 @@ private readonly GraphQLRequest ParseRequest(ref Utf8JsonReader reader, Operatio if (reader.TokenType == JsonTokenType.String) { var id = reader.GetString(); - if (!string.IsNullOrEmpty(id)) + if (!string.IsNullOrEmpty(id) && !OperationDocumentId.TryParse(id, out documentId)) { - documentId = new OperationDocumentId(id); + throw ThrowHelper.InvalidDocumentIdFormat(); } } else if (reader.TokenType == JsonTokenType.Null) diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/SyntaxWriterTests.cs b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/SyntaxWriterTests.cs new file mode 100644 index 00000000000..97e26effc3a --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/SyntaxWriterTests.cs @@ -0,0 +1,719 @@ +using HotChocolate.Language.Utilities; + +namespace HotChocolate.Language.SyntaxTree; + +public class SyntaxWriterTests +{ + [Fact] + public void WriteMany_WithNewlineSeparator_WritesIndentation() + { + // arrange + var writer = new StringSyntaxWriter(); + writer.Indent(); + var items = new[] { "item1", "item2", "item3" }; + + // act + writer.WriteMany( + items, + (item, w) => w.Write(item), + Environment.NewLine); + + var result = writer.ToString(); + + // assert + var expected = $"item1{Environment.NewLine} item2{Environment.NewLine} item3"; + Assert.Equal(expected, result); + } + + [Fact] + public void WriteMany_WithCommaSeparator_DoesNotWriteIndentation() + { + // arrange + var writer = new StringSyntaxWriter(); + writer.Indent(); + var items = new[] { "item1", "item2", "item3" }; + + // act + writer.WriteMany( + items, + (item, w) => w.Write(item), + ", "); + + var result = writer.ToString(); + + // assert + Assert.Equal("item1, item2, item3", result); + } + + [Fact] + public void WriteMany_WithMultipleIndentLevels_WritesCorrectIndentation() + { + // arrange + var writer = new StringSyntaxWriter(); + writer.Indent(); + writer.Indent(); + var items = new[] { "a", "b", "c" }; + + // act + writer.WriteMany( + items, + (item, w) => w.Write(item), + Environment.NewLine); + + var result = writer.ToString(); + + // assert + var expected = $"a{Environment.NewLine} b{Environment.NewLine} c"; + Assert.Equal(expected, result); + } + + [Fact] + public void WriteObjectValue_WithIndentation_FormatsCorrectly() + { + // arrange + var writer = new StringSyntaxWriter(); + var objectValue = new ObjectValueNode( + new ObjectFieldNode("field1", "value1"), + new ObjectFieldNode("field2", "value2"), + new ObjectFieldNode("field3", "value3")); + + // act + writer.WriteObjectValue(objectValue); + var result = writer.ToString(); + + // assert + Assert.Equal("{ field1: \"value1\", field2: \"value2\", field3: \"value3\" }", result); + } + + [Fact] + public void WriteListValue_WithIndentation_FormatsCorrectly() + { + // arrange + var writer = new StringSyntaxWriter(); + var listValue = new ListValueNode( + new IntValueNode(1), + new IntValueNode(2), + new IntValueNode(3)); + + // act + writer.WriteListValue(listValue); + var result = writer.ToString(); + + // assert + Assert.Equal("[ 1, 2, 3 ]", result); + } + + [Fact] + public void WriteMany_WithEmptyList_WritesNothing() + { + // arrange + var writer = new StringSyntaxWriter(); + var items = Array.Empty(); + + // act + writer.WriteMany( + items, + (item, w) => w.Write(item), + ", "); + + var result = writer.ToString(); + + // assert + Assert.Equal(string.Empty, result); + } + + [Fact] + public void WriteMany_WithSingleItem_DoesNotWriteSeparator() + { + // arrange + var writer = new StringSyntaxWriter(); + writer.Indent(); + var items = new[] { "single" }; + + // act + writer.WriteMany( + items, + (item, w) => w.Write(item), + Environment.NewLine); + + var result = writer.ToString(); + + // assert + Assert.Equal("single", result); + } + + [Fact] + public void SyntaxSerializer_WithIndentation_FormatsCorrectly() + { + // arrange + const string query = + """ + query GetUser($id: ID!) { + user(id: $id) { + name + email + } + } + """; + + var document = Utf8GraphQLParser.Parse(query); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + var result = writer.ToString(); + + // assert + // The result should have proper indentation with each field on its own line + Assert.Contains($"query GetUser({Environment.NewLine}", result); + Assert.Contains($"{Environment.NewLine} $id: ID!{Environment.NewLine}", result); + Assert.Contains($" user(id: $id) {{{Environment.NewLine}", result); + Assert.Contains($" name{Environment.NewLine}", result); + Assert.Contains($" email{Environment.NewLine}", result); + } + + [Fact] + public void WriteMany_WithCarriageReturnNewLine_WritesIndentation() + { + // arrange + var writer = new StringSyntaxWriter(); + writer.Indent(); + var items = new[] { "a", "b", "c" }; + + // act + writer.WriteMany( + items, + (item, w) => w.Write(item), + "\r\n"); + + var result = writer.ToString(); + + // assert + const string expected = "a\r\n b\r\n c"; + Assert.Equal(expected, result); + } + + [Fact] + public void WriteMany_WithOnlyCarriageReturn_WritesIndentation() + { + // arrange + var writer = new StringSyntaxWriter(); + writer.Indent(); + var items = new[] { "x", "y" }; + + // act + writer.WriteMany( + items, + (item, w) => w.Write(item), + "\r"); + + var result = writer.ToString(); + + // assert + const string expected = "x\r y"; + Assert.Equal(expected, result); + } + + [Fact] + public void ObjectValue_Indented_MatchesSnapshot() + { + // arrange + var objectValue = new ObjectValueNode( + new ObjectFieldNode("enum", new EnumValueNode("Foo")), + new ObjectFieldNode("enum2", new EnumValueNode("Bar")), + new ObjectFieldNode("nested", new ObjectValueNode( + new ObjectFieldNode("inner", "value")))); + + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(objectValue, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void ObjectValue_NotIndented_MatchesSnapshot() + { + // arrange + var objectValue = new ObjectValueNode( + new ObjectFieldNode("enum", new EnumValueNode("Foo")), + new ObjectFieldNode("enum2", new EnumValueNode("Bar")), + new ObjectFieldNode("nested", new ObjectValueNode( + new ObjectFieldNode("inner", "value")))); + + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = false }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(objectValue, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void ListValue_Indented_MatchesSnapshot() + { + // arrange + var listValue = new ListValueNode( + new ObjectValueNode( + new ObjectFieldNode("a", 1), + new ObjectFieldNode("b", 2)), + new ObjectValueNode( + new ObjectFieldNode("c", 3), + new ObjectFieldNode("d", 4)), + new IntValueNode(5)); + + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(listValue, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void ListValue_NotIndented_MatchesSnapshot() + { + // arrange + var listValue = new ListValueNode( + new ObjectValueNode( + new ObjectFieldNode("a", 1), + new ObjectFieldNode("b", 2)), + new ObjectValueNode( + new ObjectFieldNode("c", 3), + new ObjectFieldNode("d", 4)), + new IntValueNode(5)); + + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = false }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(listValue, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void VariableDefinition_WithDefaultObjectValue_Indented_MatchesSnapshot() + { + // arrange + const string query = """ + query GetUser($input: InputType = { field1: "value1", field2: "value2" }) { + user + } + """; + + var document = Utf8GraphQLParser.Parse(query); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void FieldArgument_WithObjectValue_Indented_MatchesSnapshot() + { + // arrange + const string query = """ + query { + user(filter: { name: "John", age: 30, active: true }) + } + """; + + var document = Utf8GraphQLParser.Parse(query); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void ObjectTypeDefinition_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + type User { + id: ID! + name: String! + email: String + posts: [Post!]! + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void InterfaceTypeDefinition_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + interface Node { + id: ID! + } + + type User implements Node { + id: ID! + name: String! + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void InputObjectTypeDefinition_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + input UserFilter { + name: String + age: Int + active: Boolean = true + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void EnumTypeDefinition_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + enum Role { + ADMIN + USER + GUEST + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void UnionTypeDefinition_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + union SearchResult = User | Post | Comment + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void TypeWithDirectives_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + type User @auth(requires: ADMIN) { + id: ID! @deprecated(reason: "Use uid instead") + uid: ID! + name: String! + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void ComplexSchemaDefinition_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + schema { + query: Query + mutation: Mutation + } + + type Query { + user(id: ID!): User + users(filter: UserFilter): [User!]! + } + + type Mutation { + createUser(input: CreateUserInput!): User! + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void QueryWithDirectivesOnField_Indented_MatchesSnapshot() + { + // arrange + const string query = """ + query { + user(id: "123") { + id @include(if: true) + name @skip(if: false) @deprecated + email @custom(arg1: "value1", arg2: 42) + } + } + """; + + var document = Utf8GraphQLParser.Parse(query); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void FragmentWithDirectives_Indented_MatchesSnapshot() + { + // arrange + const string query = """ + fragment UserFields on User @custom(value: "test") { + id + name @include(if: true) + email + } + + query { + user { + ...UserFields @defer + } + } + """; + + var document = Utf8GraphQLParser.Parse(query); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void InlineFragmentWithDirectives_Indented_MatchesSnapshot() + { + // arrange + const string query = """ + query { + search { + ... on User @defer { + id + name + } + ... on Post @defer @stream { + title + content + } + } + } + """; + + var document = Utf8GraphQLParser.Parse(query); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void TypeWithManyDirectives_NoWrapping_MatchesSnapshot() + { + // arrange + const string schema = """ + type User @auth @cache @log @validate @track { + id: ID! + name: String! @deprecated @private @readonly @indexed @unique + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void TypeWithManyDirectives_WithWrapping_MatchesSnapshot() + { + // arrange + const string schema = """ + type User @auth @cache @log @validate @track { + id: ID! + name: String! @deprecated @private @readonly @indexed @unique + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions + { + Indented = true, + MaxDirectivesPerLine = 2 + }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void DirectiveDefinition_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + directive @auth( + requires: Role = ADMIN + scopes: [String!] + ) repeatable on OBJECT | FIELD_DEFINITION + + directive @deprecated( + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ENUM_VALUE + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void ArgumentsWithDirectives_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + type Query { + user( + id: ID! @deprecated + filter: UserFilter @custom(value: "test") + ): User + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } + + [Fact] + public void EnumValueWithDirectives_Indented_MatchesSnapshot() + { + // arrange + const string schema = """ + enum Role { + ADMIN @description(text: "Administrator role") + USER @description(text: "Regular user") + GUEST @deprecated(reason: "Use USER instead") @internal + } + """; + + var document = Utf8GraphQLParser.Parse(schema); + var serializer = new SyntaxSerializer(new SyntaxSerializerOptions { Indented = true }); + var writer = new StringSyntaxWriter(); + + // act + serializer.Serialize(document, writer); + + // assert + writer.ToString().MatchSnapshot(extension: ".graphql"); + } +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ArgumentsWithDirectives_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ArgumentsWithDirectives_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..14592fae4fd --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ArgumentsWithDirectives_Indented_MatchesSnapshot.graphql @@ -0,0 +1,3 @@ +type Query { + user(id: ID! @deprecated filter: UserFilter @custom(value: "test")): User +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ComplexSchemaDefinition_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ComplexSchemaDefinition_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..65543f9e87e --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ComplexSchemaDefinition_Indented_MatchesSnapshot.graphql @@ -0,0 +1,13 @@ +schema { + query: Query + mutation: Mutation +} + +type Query { + user(id: ID!): User + users(filter: UserFilter): [User!]! +} + +type Mutation { + createUser(input: CreateUserInput!): User! +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.DirectiveDefinition_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.DirectiveDefinition_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..f8d7f1fe43f --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.DirectiveDefinition_Indented_MatchesSnapshot.graphql @@ -0,0 +1,3 @@ +directive @auth(requires: Role = ADMIN scopes: [String!]) repeatable on OBJECT | FIELD_DEFINITION + +directive @deprecated(reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.EnumTypeDefinition_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.EnumTypeDefinition_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..1b5fe4b33ee --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.EnumTypeDefinition_Indented_MatchesSnapshot.graphql @@ -0,0 +1,5 @@ +enum Role { + ADMIN + USER + GUEST +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.EnumValueWithDirectives_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.EnumValueWithDirectives_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..b7f0867ea5d --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.EnumValueWithDirectives_Indented_MatchesSnapshot.graphql @@ -0,0 +1,5 @@ +enum Role { + ADMIN @description(text: "Administrator role") + USER @description(text: "Regular user") + GUEST @deprecated(reason: "Use USER instead") @internal +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.FieldArgument_WithObjectValue_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.FieldArgument_WithObjectValue_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..718f9d252f2 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.FieldArgument_WithObjectValue_Indented_MatchesSnapshot.graphql @@ -0,0 +1,3 @@ +{ + user(filter: { name: "John", age: 30, active: true }) +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.FragmentWithDirectives_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.FragmentWithDirectives_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..683bff27555 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.FragmentWithDirectives_Indented_MatchesSnapshot.graphql @@ -0,0 +1,11 @@ +fragment UserFields on User @custom(value: "test") { + id + name @include(if: true) + email +} + +{ + user { + ... UserFields @defer + } +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InlineFragmentWithDirectives_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InlineFragmentWithDirectives_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..98073d65e6d --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InlineFragmentWithDirectives_Indented_MatchesSnapshot.graphql @@ -0,0 +1,12 @@ +{ + search { + ... on User @defer { + id + name + } + ... on Post @defer @stream { + title + content + } + } +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InputObjectTypeDefinition_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InputObjectTypeDefinition_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..8529ff41298 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InputObjectTypeDefinition_Indented_MatchesSnapshot.graphql @@ -0,0 +1,5 @@ +input UserFilter { + name: String + age: Int + active: Boolean = true +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InterfaceTypeDefinition_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InterfaceTypeDefinition_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..2b12f825d69 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.InterfaceTypeDefinition_Indented_MatchesSnapshot.graphql @@ -0,0 +1,8 @@ +interface Node { + id: ID! +} + +type User implements Node { + id: ID! + name: String! +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ListValue_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ListValue_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..7d18f9284f5 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ListValue_Indented_MatchesSnapshot.graphql @@ -0,0 +1,11 @@ +[ + { + a: 1, + b: 2 + }, + { + c: 3, + d: 4 + }, + 5 +] diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ListValue_NotIndented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ListValue_NotIndented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..96d0bfe9a56 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ListValue_NotIndented_MatchesSnapshot.graphql @@ -0,0 +1 @@ +[ { a: 1, b: 2 }, { c: 3, d: 4 }, 5 ] diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectTypeDefinition_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectTypeDefinition_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..0bf7783cc5e --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectTypeDefinition_Indented_MatchesSnapshot.graphql @@ -0,0 +1,6 @@ +type User { + id: ID! + name: String! + email: String + posts: [Post!]! +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectValue_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectValue_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..c9606b60719 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectValue_Indented_MatchesSnapshot.graphql @@ -0,0 +1,7 @@ +{ + enum: Foo, + enum2: Bar, + nested: { + inner: "value" + } +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectValue_NotIndented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectValue_NotIndented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..07b2989cbed --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.ObjectValue_NotIndented_MatchesSnapshot.graphql @@ -0,0 +1 @@ +{ enum: Foo, enum2: Bar, nested: { inner: "value" } } diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.QueryWithDirectivesOnField_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.QueryWithDirectivesOnField_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..bae31149913 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.QueryWithDirectivesOnField_Indented_MatchesSnapshot.graphql @@ -0,0 +1,7 @@ +{ + user(id: "123") { + id @include(if: true) + name @skip(if: false) @deprecated + email @custom(arg1: "value1", arg2: 42) + } +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithDirectives_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithDirectives_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..fd8af41a4a3 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithDirectives_Indented_MatchesSnapshot.graphql @@ -0,0 +1,5 @@ +type User @auth(requires: ADMIN) { + id: ID! @deprecated(reason: "Use uid instead") + uid: ID! + name: String! +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithManyDirectives_NoWrapping_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithManyDirectives_NoWrapping_MatchesSnapshot.graphql new file mode 100644 index 00000000000..02fa83dd3c6 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithManyDirectives_NoWrapping_MatchesSnapshot.graphql @@ -0,0 +1,4 @@ +type User @auth @cache @log @validate @track { + id: ID! + name: String! @deprecated @private @readonly @indexed @unique +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithManyDirectives_WithWrapping_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithManyDirectives_WithWrapping_MatchesSnapshot.graphql new file mode 100644 index 00000000000..5301dcb1182 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.TypeWithManyDirectives_WithWrapping_MatchesSnapshot.graphql @@ -0,0 +1,14 @@ +type User + @auth + @cache + @log + @validate + @track { + id: ID! + name: String! + @deprecated + @private + @readonly + @indexed + @unique +} diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.UnionTypeDefinition_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.UnionTypeDefinition_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..a45f161ccf1 --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.UnionTypeDefinition_Indented_MatchesSnapshot.graphql @@ -0,0 +1 @@ +union SearchResult = User | Post | Comment diff --git a/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.VariableDefinition_WithDefaultObjectValue_Indented_MatchesSnapshot.graphql b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.VariableDefinition_WithDefaultObjectValue_Indented_MatchesSnapshot.graphql new file mode 100644 index 00000000000..22a25d187ea --- /dev/null +++ b/src/HotChocolate/Language/test/Language.SyntaxTree.Tests/__snapshots__/SyntaxWriterTests.VariableDefinition_WithDefaultObjectValue_Indented_MatchesSnapshot.graphql @@ -0,0 +1,8 @@ +query GetUser( + $input: InputType = { + field1: "value1", + field2: "value2" + } +) { + user +} diff --git a/src/HotChocolate/Language/test/Language.Tests/Parser/__snapshots__/KitchenSinkParserTests.ParseFacebookKitchenSinkSchema.snap b/src/HotChocolate/Language/test/Language.Tests/Parser/__snapshots__/KitchenSinkParserTests.ParseFacebookKitchenSinkSchema.snap index ae7f33c5522..e97bf5bddd7 100644 --- a/src/HotChocolate/Language/test/Language.Tests/Parser/__snapshots__/KitchenSinkParserTests.ParseFacebookKitchenSinkSchema.snap +++ b/src/HotChocolate/Language/test/Language.Tests/Parser/__snapshots__/KitchenSinkParserTests.ParseFacebookKitchenSinkSchema.snap @@ -19,8 +19,13 @@ type Foo implements Bar & Baz { """ argument: InputType!): Type three(argument: InputType other: String): Int four(argument: String = "string"): String - five(argument: [String] = [ "string", "string" ]): String - six(argument: InputType = { key: "value" }): Type + five(argument: [String] = [ + "string", + "string" + ]): String + six(argument: InputType = { + key: "value" + }): Type seven(argument: Int): Type } diff --git a/src/HotChocolate/Language/test/Language.Tests/Visitors/__snapshots__/SyntaxRewriterTests.Rename_Field.snap b/src/HotChocolate/Language/test/Language.Tests/Visitors/__snapshots__/SyntaxRewriterTests.Rename_Field.snap index 3918309bb9c..03155cea738 100644 --- a/src/HotChocolate/Language/test/Language.Tests/Visitors/__snapshots__/SyntaxRewriterTests.Rename_Field.snap +++ b/src/HotChocolate/Language/test/Language.Tests/Visitors/__snapshots__/SyntaxRewriterTests.Rename_Field.snap @@ -1,4 +1,4 @@ -schema { +schema { query: QueryType mutation: MutationType } @@ -17,8 +17,13 @@ type Foo implements Bar & Baz { """ argument: InputType!): Type three_abc(argument: InputType other: String): Int four_abc(argument: String = "string"): String - five_abc(argument: [String] = [ "string", "string" ]): String - six_abc(argument: InputType = { key: "value" }): Type + five_abc(argument: [String] = [ + "string", + "string" + ]): String + six_abc(argument: InputType = { + key: "value" + }): Type seven_abc(argument: Int): Type } diff --git a/src/HotChocolate/Marten/test/Directory.Build.props b/src/HotChocolate/Marten/test/Directory.Build.props index 7b5e79f43f8..0433866e225 100644 --- a/src/HotChocolate/Marten/test/Directory.Build.props +++ b/src/HotChocolate/Marten/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/MongoDb/test/Directory.Build.props b/src/HotChocolate/MongoDb/test/Directory.Build.props index 7b5e79f43f8..0433866e225 100644 --- a/src/HotChocolate/MongoDb/test/Directory.Build.props +++ b/src/HotChocolate/MongoDb/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/MongoDb/test/Types.MongoDb/BsonTypeTests.cs b/src/HotChocolate/MongoDb/test/Types.MongoDb/BsonTypeTests.cs index 3483cec95f7..fd5b9214e09 100644 --- a/src/HotChocolate/MongoDb/test/Types.MongoDb/BsonTypeTests.cs +++ b/src/HotChocolate/MongoDb/test/Types.MongoDb/BsonTypeTests.cs @@ -288,23 +288,6 @@ public async Task ValueToLiteral_Should_ReturnNullValueNode_When_CalledWithNull( Assert.IsType(value); } - [Fact] - public async Task ValueToLiteral_Should_ThrowException_When_CalledWithNonBsonValue() - { - // arrange - var type = (await new ServiceCollection() - .AddGraphQL() - .AddBsonType() - .ModifyOptions(x => x.StrictValidation = false) - .BuildSchemaAsync()).Types.GetType("Bson"); - - // act - var result = Record.Exception(() => type.ValueToLiteral("Fails")); - - // assert - Assert.IsType(result); - } - [Fact] public async Task Output_Return_Object() { @@ -561,10 +544,11 @@ public async Task Input_Value_List_As_Variable() OperationRequestBuilder.New() .SetDocument("query ($foo: Bson) { foo(input: $foo) }") .SetVariableValues( - new Dictionary + """ { - { "foo", new List { "abc" } } - }) + "foo": ["abc"] + } + """) .Build()); // assert @@ -592,13 +576,15 @@ public async Task Input_Object_List_As_Variable() OperationRequestBuilder.New() .SetDocument("query ($foo: Bson) { foo(input: $foo) }") .SetVariableValues( - new Dictionary + """ { + "foo": [ { - "foo", - new List { new Dictionary { { "abc", "def" } } } + "abc": "def" } - }) + ] + } + """) .Build()); // assert @@ -625,7 +611,12 @@ public async Task Input_Value_String_As_Variable() var result = await executor.ExecuteAsync( OperationRequestBuilder.New() .SetDocument("query ($foo: Bson) { foo(input: $foo) }") - .SetVariableValues(new Dictionary { { "foo", "bar" } }) + .SetVariableValues( + """ + { + "foo": "bar" + } + """) .Build()); // assert @@ -652,7 +643,12 @@ public async Task Input_Value_Int_As_Variable() var result = await executor.ExecuteAsync( OperationRequestBuilder.New() .SetDocument("query ($foo: Bson) { foo(input: $foo) }") - .SetVariableValues(new Dictionary { { "foo", 123 } }) + .SetVariableValues( + """ + { + "foo": 123 + } + """) .Build()); // assert @@ -679,7 +675,12 @@ public async Task Input_Value_Float_As_Variable() var result = await executor.ExecuteAsync( OperationRequestBuilder.New() .SetDocument("query ($foo: Bson) { foo(input: $foo) }") - .SetVariableValues(new Dictionary { { "foo", 1.2 } }) + .SetVariableValues( + """ + { + "foo": 1.2 + } + """) .Build()); // assert @@ -697,7 +698,7 @@ public async Task Input_Value_BsonDocument_As_Variable() .Field("foo") .Type() .Argument("input", a => a.Type()) - .Resolve(ctx => ctx.ArgumentLiteral("input"))) + .Resolve(ctx => ctx.ArgumentValue("input"))) .Create(); var executor = schema.MakeExecutable(); @@ -707,10 +708,13 @@ public async Task Input_Value_BsonDocument_As_Variable() OperationRequestBuilder.New() .SetDocument("query ($foo: Bson) { foo(input: $foo) }") .SetVariableValues( - new Dictionary + """ { - { "foo", new BsonDocument { { "a", "b" } } } - }) + "foo": { + "a": "b" + } + } + """) .Build()); // assert @@ -737,7 +741,12 @@ public async Task Input_Value_Boolean_As_Variable() var result = await executor.ExecuteAsync( OperationRequestBuilder.New() .SetDocument("query ($foo: Bson) { foo(input: $foo) }") - .SetVariableValues(new Dictionary { { "foo", false } }) + .SetVariableValues( + """ + { + "foo": false + } + """) .Build()); // assert @@ -764,7 +773,12 @@ public async Task Input_Value_Null_As_Variable() var result = await executor.ExecuteAsync( OperationRequestBuilder.New() .SetDocument("query ($foo: Bson) { foo(input: $foo) }") - .SetVariableValues(new Dictionary { { "foo", null } }) + .SetVariableValues( + """ + { + "foo": null + } + """) .Build()); // assert @@ -952,7 +966,7 @@ public void IsInstanceOfType_NullValue_True() var result = type.IsValueCompatible(NullValueNode.Default); // assert - Assert.True(result); + Assert.False(result); } [Fact] diff --git a/src/HotChocolate/MongoDb/test/Types.MongoDb/__snapshots__/BsonTypeTests.Input_Value_BsonDocument_As_Variable.snap b/src/HotChocolate/MongoDb/test/Types.MongoDb/__snapshots__/BsonTypeTests.Input_Value_BsonDocument_As_Variable.snap index 5c8471d7832..a01deda1f64 100644 --- a/src/HotChocolate/MongoDb/test/Types.MongoDb/__snapshots__/BsonTypeTests.Input_Value_BsonDocument_As_Variable.snap +++ b/src/HotChocolate/MongoDb/test/Types.MongoDb/__snapshots__/BsonTypeTests.Input_Value_BsonDocument_As_Variable.snap @@ -1,5 +1,7 @@ { "data": { - "foo": "{ \"a\" : \"b\" }" + "foo": { + "a": "b" + } } } diff --git a/src/HotChocolate/OpenApi/test/Directory.Build.props b/src/HotChocolate/OpenApi/test/Directory.Build.props index 5b3630fd7db..24fb7f78e32 100644 --- a/src/HotChocolate/OpenApi/test/Directory.Build.props +++ b/src/HotChocolate/OpenApi/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/PersistedOperations/test/Directory.Build.props b/src/HotChocolate/PersistedOperations/test/Directory.Build.props index 7b5e79f43f8..0433866e225 100644 --- a/src/HotChocolate/PersistedOperations/test/Directory.Build.props +++ b/src/HotChocolate/PersistedOperations/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/Raven/test/Data.Raven.Projections.Tests/__snapshots__/QueryableProjectionFilterTests.Should_NotInitializeObject_When_ResultOfLeftJoinIsNull_Deep.snap b/src/HotChocolate/Raven/test/Data.Raven.Projections.Tests/__snapshots__/QueryableProjectionFilterTests.Should_NotInitializeObject_When_ResultOfLeftJoinIsNull_Deep.snap index db52a3a7a25..bee18c4f138 100644 --- a/src/HotChocolate/Raven/test/Data.Raven.Projections.Tests/__snapshots__/QueryableProjectionFilterTests.Should_NotInitializeObject_When_ResultOfLeftJoinIsNull_Deep.snap +++ b/src/HotChocolate/Raven/test/Data.Raven.Projections.Tests/__snapshots__/QueryableProjectionFilterTests.Should_NotInitializeObject_When_ResultOfLeftJoinIsNull_Deep.snap @@ -4,15 +4,9 @@ Result: "errors": [ { "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 9, - "column": 41 - } - ], "path": [ "root", - 2, + 1, "foo", "nestedObject", "foo", @@ -24,15 +18,9 @@ Result: }, { "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 9, - "column": 41 - } - ], "path": [ "root", - 1, + 2, "foo", "nestedObject", "foo", diff --git a/src/HotChocolate/Raven/test/Directory.Build.props b/src/HotChocolate/Raven/test/Directory.Build.props index 7b5e79f43f8..0433866e225 100644 --- a/src/HotChocolate/Raven/test/Directory.Build.props +++ b/src/HotChocolate/Raven/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/Spatial/test/Directory.Build.props b/src/HotChocolate/Spatial/test/Directory.Build.props index 7b5e79f43f8..0433866e225 100644 --- a/src/HotChocolate/Spatial/test/Directory.Build.props +++ b/src/HotChocolate/Spatial/test/Directory.Build.props @@ -15,6 +15,7 @@ + diff --git a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonLineStringSerializerTests.FormatValue_Should_Pass_When_Value.graphql b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonLineStringSerializerTests.FormatValue_Should_Pass_When_Value.graphql index 679f756d4f3..0003a181198 100644 --- a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonLineStringSerializerTests.FormatValue_Should_Pass_When_Value.graphql +++ b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonLineStringSerializerTests.FormatValue_Should_Pass_When_Value.graphql @@ -1 +1,18 @@ -{ type: LineString, coordinates: [ [ 30, 10 ], [ 10, 30 ], [ 40, 40 ] ], crs: 0 } +{ + type: LineString, + coordinates: [ + [ + 30, + 10 + ], + [ + 10, + 30 + ], + [ + 40, + 40 + ] + ], + crs: 0 +} diff --git a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiLineStringSerializerTests.FormatValue_Should_Pass_When_Value.graphql b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiLineStringSerializerTests.FormatValue_Should_Pass_When_Value.graphql index 2258e40575d..93d1c3c2a33 100644 --- a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiLineStringSerializerTests.FormatValue_Should_Pass_When_Value.graphql +++ b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiLineStringSerializerTests.FormatValue_Should_Pass_When_Value.graphql @@ -1 +1,38 @@ -{ type: MultiLineString, coordinates: [ [ [ 10, 10 ], [ 20, 20 ], [ 10, 40 ] ], [ [ 40, 40 ], [ 30, 30 ], [ 40, 20 ], [ 30, 10 ] ] ], crs: 0 } +{ + type: MultiLineString, + coordinates: [ + [ + [ + 10, + 10 + ], + [ + 20, + 20 + ], + [ + 10, + 40 + ] + ], + [ + [ + 40, + 40 + ], + [ + 30, + 30 + ], + [ + 40, + 20 + ], + [ + 30, + 10 + ] + ] + ], + crs: 0 +} diff --git a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiPointSerializerTests.FormatValue_Should_Pass_When_Value.graphql b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiPointSerializerTests.FormatValue_Should_Pass_When_Value.graphql index 85265655cc3..94f1f357e52 100644 --- a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiPointSerializerTests.FormatValue_Should_Pass_When_Value.graphql +++ b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiPointSerializerTests.FormatValue_Should_Pass_When_Value.graphql @@ -1 +1,22 @@ -{ type: MultiPoint, coordinates: [ [ 10, 40 ], [ 40, 30 ], [ 20, 20 ], [ 30, 10 ] ], crs: 0 } +{ + type: MultiPoint, + coordinates: [ + [ + 10, + 40 + ], + [ + 40, + 30 + ], + [ + 20, + 20 + ], + [ + 30, + 10 + ] + ], + crs: 0 +} diff --git a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiPolygonSerializerTests.FormatValue_Should_Pass_When_Value.graphql b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiPolygonSerializerTests.FormatValue_Should_Pass_When_Value.graphql index 11b657e16bb..5de8868372b 100644 --- a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiPolygonSerializerTests.FormatValue_Should_Pass_When_Value.graphql +++ b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonMultiPolygonSerializerTests.FormatValue_Should_Pass_When_Value.graphql @@ -1 +1,50 @@ -{ type: MultiPolygon, coordinates: [ [ [ [ 30, 20 ], [ 45, 40 ], [ 10, 40 ], [ 30, 20 ] ] ], [ [ [ 15, 5 ], [ 40, 10 ], [ 10, 20 ], [ 5, 15 ], [ 15, 5 ] ] ] ], crs: 0 } +{ + type: MultiPolygon, + coordinates: [ + [ + [ + [ + 30, + 20 + ], + [ + 45, + 40 + ], + [ + 10, + 40 + ], + [ + 30, + 20 + ] + ] + ], + [ + [ + [ + 15, + 5 + ], + [ + 40, + 10 + ], + [ + 10, + 20 + ], + [ + 5, + 15 + ], + [ + 15, + 5 + ] + ] + ] + ], + crs: 0 +} diff --git a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonPointSerializerTests.FormatValue_Should_Pass_When_Value.graphql b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonPointSerializerTests.FormatValue_Should_Pass_When_Value.graphql index 27a6e14a82f..bc6573ef262 100644 --- a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonPointSerializerTests.FormatValue_Should_Pass_When_Value.graphql +++ b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonPointSerializerTests.FormatValue_Should_Pass_When_Value.graphql @@ -1 +1,8 @@ -{ type: Point, coordinates: [ 30, 10 ], crs: 0 } +{ + type: Point, + coordinates: [ + 30, + 10 + ], + crs: 0 +} diff --git a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonPolygonSerializerTests.FormatValue_Should_Pass_When_Value.snap b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonPolygonSerializerTests.FormatValue_Should_Pass_When_Value.snap index b2669fa7c84..c883404c651 100644 --- a/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonPolygonSerializerTests.FormatValue_Should_Pass_When_Value.snap +++ b/src/HotChocolate/Spatial/test/Types.Tests/__snapshots__/GeoJsonPolygonSerializerTests.FormatValue_Should_Pass_When_Value.snap @@ -1 +1,28 @@ -{ type: Polygon, coordinates: [ [ [ 30, 10 ], [ 40, 40 ], [ 20, 40 ], [ 10, 20 ], [ 30, 10 ] ] ], crs: 0 } +{ + type: Polygon, + coordinates: [ + [ + [ + 30, + 10 + ], + [ + 40, + 40 + ], + [ + 20, + 40 + ], + [ + 10, + 20 + ], + [ + 30, + 10 + ] + ] + ], + crs: 0 +} diff --git a/src/StrawberryShake/Client/src/Transport.InMemory/DependencyInjection/InMemoryClient.cs b/src/StrawberryShake/Client/src/Transport.InMemory/DependencyInjection/InMemoryClient.cs index 32c29a389fb..38d632f3caf 100644 --- a/src/StrawberryShake/Client/src/Transport.InMemory/DependencyInjection/InMemoryClient.cs +++ b/src/StrawberryShake/Client/src/Transport.InMemory/DependencyInjection/InMemoryClient.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using HotChocolate; using HotChocolate.Execution; using HotChocolate.Language; @@ -64,7 +65,11 @@ public async ValueTask ExecuteAsync( } requestBuilder.SetOperationName(request.Name); - requestBuilder.SetVariableValues(CreateVariables(request)); + requestBuilder.SetVariableValues(CreateVariables(request, out var fileLookup)); + if (fileLookup is not null) + { + requestBuilder.Features.Set(fileLookup); + } requestBuilder.SetExtensions(request.GetExtensionsOrNull()); requestBuilder.SetGlobalState(request.GetContextDataOrNull()); @@ -81,11 +86,15 @@ await interceptor .ConfigureAwait(false); } - private IReadOnlyDictionary? CreateVariables(OperationRequest request) + private IReadOnlyDictionary? CreateVariables( + OperationRequest request, + out IFileLookup? fileLookup) { + fileLookup = null; + if (request.Variables is { } variables) { - var unflattened = MapFilesToLookup(request.Files); + var unflattened = MapFilesToLookup(request.Files, out fileLookup); var response = new Dictionary(); foreach (var pair in variables) @@ -127,9 +136,9 @@ await interceptor return response; } default: - if (fileValue is Upload upload) + if (variables is null && fileValue is string fileKey) { - return new StreamFile(upload.FileName, () => upload.Content, null, upload.ContentType); + return fileKey; } return variables; @@ -144,22 +153,35 @@ private static void GetFileValueOrDefault( value = (source, key) switch { (Dictionary s, string prop) when s.ContainsKey(prop) => s[prop], - (List l, int i) when i < l.Count => l[i], + (List l, int i) when i < l.Count => l[i], _ => null }; } - private static IReadOnlyDictionary MapFilesToLookup( - IReadOnlyDictionary files) + private static IReadOnlyDictionary MapFilesToLookup( + IReadOnlyDictionary files, + out IFileLookup? fileLookup) { if (files.Count == 0) { - return ImmutableDictionary.Empty; + fileLookup = null; + return ImmutableDictionary.Empty; } - var unflattened = new Dictionary(); + var unflattened = new Dictionary(); + var fileMap = new Dictionary(); + foreach (var file in files) { + if (!file.Value.HasValue) + { + continue; + } + + var upload = file.Value.Value; + fileMap[file.Key] = + new StreamFile(upload.FileName, () => upload.Content, null, upload.ContentType); + object? current = unflattened; var path = file.Key.Split('.').ToArray(); for (var i = 1; i < path.Length; i++) @@ -184,7 +206,7 @@ private static IReadOnlyDictionary MapFilesToLookup( if (nextSegment is null) { - currentList[index] = file.Value; + currentList[index] = file.Key; } else if (currentList.ElementAtOrDefault(index) is not null) { @@ -210,7 +232,7 @@ private static IReadOnlyDictionary MapFilesToLookup( if (nextSegment is null) { - currentDict[segment] = file.Value; + currentDict[segment] = file.Key; } else if (currentDict.TryGetValue(segment, out var o) && o is not null) { @@ -229,6 +251,22 @@ private static IReadOnlyDictionary MapFilesToLookup( } } + fileLookup = fileMap.Count == 0 ? null : new InMemoryFileLookup(fileMap); return unflattened; } + + private sealed class InMemoryFileLookup(IReadOnlyDictionary files) : IFileLookup + { + public bool TryGetFile(string name, [NotNullWhen(true)] out IFile? file) + { + if (files.TryGetValue(name, out var current)) + { + file = current; + return true; + } + + file = null; + return false; + } + } } diff --git a/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/__snapshots__/IntegrationTests.Execution_Error.md b/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/__snapshots__/IntegrationTests.Execution_Error.md index 62e12732ce0..4335b92f807 100644 --- a/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/__snapshots__/IntegrationTests.Execution_Error.md +++ b/src/StrawberryShake/Client/test/Transport.WebSocket.Tests/__snapshots__/IntegrationTests.Execution_Error.md @@ -7,12 +7,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -29,12 +23,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -51,12 +39,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -73,12 +55,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -95,12 +71,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -117,12 +87,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -139,12 +103,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -161,12 +119,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -183,12 +135,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] @@ -205,12 +151,6 @@ "errors": [ { "message": "Unexpected Execution Error", - "locations": [ - { - "line": 1, - "column": 21 - } - ], "path": [ "onTest" ] diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/AnyScalarDefaultSerializationTest.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/AnyScalarDefaultSerializationTest.cs index 52553997657..69feefb6510 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/AnyScalarDefaultSerializationTest.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/AnyScalarDefaultSerializationTest.cs @@ -19,7 +19,7 @@ public async Task Execute_AnyScalarDefaultSerialization_Test() // arrange using var cts = new CancellationTokenSource(20_000); using var host = TestServerHelper.CreateServer( - builder => builder.AddTypeExtension(), + builder => builder.AddTypeExtension().AddJsonTypeConverter(), out var port); var serviceCollection = new ServiceCollection(); serviceCollection.AddHttpClient( diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/MultiProfileTest.Client.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/MultiProfileTest.Client.cs index 2be6afab45a..89f60904c7d 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/MultiProfileTest.Client.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/MultiProfileTest.Client.cs @@ -1485,7 +1485,7 @@ public partial interface IOnReviewSubSubscription : global::StrawberryShake.IOpe /// /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename @@ -1523,7 +1523,7 @@ private CreateReviewMutMutationDocument() /// /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename @@ -1629,7 +1629,7 @@ private CreateReviewMutMutation(global::StrawberryShake.IOperationExecutor /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/StarWarsGetHeroWithFragmentIncludeAndSkipDirectiveTest.Client.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/StarWarsGetHeroWithFragmentIncludeAndSkipDirectiveTest.Client.cs index fe35c8227ff..bc55e9d51e5 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/StarWarsGetHeroWithFragmentIncludeAndSkipDirectiveTest.Client.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/StarWarsGetHeroWithFragmentIncludeAndSkipDirectiveTest.Client.cs @@ -657,7 +657,7 @@ public partial interface IGetHeroWithFragmentIncludeAndSkipDirective_Hero_Friend /// /// query GetHeroWithFragmentIncludeAndSkipDirective( /// $includePageInfo: Boolean = false - /// $skipPageInfo: Boolean = true + /// $skipPageInfo: Boolean = true /// ) { /// hero(episode: NEW_HOPE) { /// __typename @@ -723,7 +723,7 @@ private GetHeroWithFragmentIncludeAndSkipDirectiveQueryDocument() /// /// query GetHeroWithFragmentIncludeAndSkipDirective( /// $includePageInfo: Boolean = false - /// $skipPageInfo: Boolean = true + /// $skipPageInfo: Boolean = true /// ) { /// hero(episode: NEW_HOPE) { /// __typename @@ -863,7 +863,7 @@ private GetHeroWithFragmentIncludeAndSkipDirectiveQuery(global::StrawberryShake. /// /// query GetHeroWithFragmentIncludeAndSkipDirective( /// $includePageInfo: Boolean = false - /// $skipPageInfo: Boolean = true + /// $skipPageInfo: Boolean = true /// ) { /// hero(episode: NEW_HOPE) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/UploadScalarTest.Client.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/UploadScalarTest.Client.cs index 073de397d7b..6acad0a906c 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/UploadScalarTest.Client.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/UploadScalarTest.Client.cs @@ -556,12 +556,12 @@ public partial class BazInput : global::StrawberryShake.CodeGeneration.CSharp.In /// /// query TestUpload( /// $nonUpload: String - /// $single: Upload - /// $list: [Upload] - /// $nested: [[Upload]] - /// $object: TestInput - /// $objectList: [TestInput] - /// $objectNested: [[TestInput]] + /// $single: Upload + /// $list: [Upload] + /// $nested: [[Upload]] + /// $object: TestInput + /// $objectList: [TestInput] + /// $objectNested: [[TestInput]] /// ) { /// upload(nonUpload: $nonUpload, single: $single, list: $list, nested: $nested, object: $object, objectList: $objectList, objectNested: $objectNested) /// } @@ -595,12 +595,12 @@ private TestUploadQueryDocument() /// /// query TestUpload( /// $nonUpload: String - /// $single: Upload - /// $list: [Upload] - /// $nested: [[Upload]] - /// $object: TestInput - /// $objectList: [TestInput] - /// $objectNested: [[TestInput]] + /// $single: Upload + /// $list: [Upload] + /// $nested: [[Upload]] + /// $object: TestInput + /// $objectList: [TestInput] + /// $objectNested: [[TestInput]] /// ) { /// upload(nonUpload: $nonUpload, single: $single, list: $list, nested: $nested, object: $object, objectList: $objectList, objectNested: $objectNested) /// } @@ -978,12 +978,12 @@ private void MapFilesFromArgumentObjectNested(global::System.String path, global /// /// query TestUpload( /// $nonUpload: String - /// $single: Upload - /// $list: [Upload] - /// $nested: [[Upload]] - /// $object: TestInput - /// $objectList: [TestInput] - /// $objectNested: [[TestInput]] + /// $single: Upload + /// $list: [Upload] + /// $nested: [[Upload]] + /// $object: TestInput + /// $objectList: [TestInput] + /// $objectNested: [[TestInput]] /// ) { /// upload(nonUpload: $nonUpload, single: $single, list: $list, nested: $nested, object: $object, objectList: $objectList, objectNested: $objectNested) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/UploadScalar_InMemoryTest.Client.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/UploadScalar_InMemoryTest.Client.cs index 1fc02aba99f..8fd284d0326 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/UploadScalar_InMemoryTest.Client.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/UploadScalar_InMemoryTest.Client.cs @@ -556,12 +556,12 @@ public partial class BazInput : global::StrawberryShake.CodeGeneration.CSharp.In /// /// query TestUpload( /// $nonUpload: String - /// $single: Upload - /// $list: [Upload] - /// $nested: [[Upload]] - /// $object: TestInput - /// $objectList: [TestInput] - /// $objectNested: [[TestInput]] + /// $single: Upload + /// $list: [Upload] + /// $nested: [[Upload]] + /// $object: TestInput + /// $objectList: [TestInput] + /// $objectNested: [[TestInput]] /// ) { /// upload(nonUpload: $nonUpload, single: $single, list: $list, nested: $nested, object: $object, objectList: $objectList, objectNested: $objectNested) /// } @@ -595,12 +595,12 @@ private TestUploadQueryDocument() /// /// query TestUpload( /// $nonUpload: String - /// $single: Upload - /// $list: [Upload] - /// $nested: [[Upload]] - /// $object: TestInput - /// $objectList: [TestInput] - /// $objectNested: [[TestInput]] + /// $single: Upload + /// $list: [Upload] + /// $nested: [[Upload]] + /// $object: TestInput + /// $objectList: [TestInput] + /// $objectNested: [[TestInput]] /// ) { /// upload(nonUpload: $nonUpload, single: $single, list: $list, nested: $nested, object: $object, objectList: $objectList, objectNested: $objectNested) /// } @@ -978,12 +978,12 @@ private void MapFilesFromArgumentObjectNested(global::System.String path, global /// /// query TestUpload( /// $nonUpload: String - /// $single: Upload - /// $list: [Upload] - /// $nested: [[Upload]] - /// $object: TestInput - /// $objectList: [TestInput] - /// $objectNested: [[TestInput]] + /// $single: Upload + /// $list: [Upload] + /// $nested: [[Upload]] + /// $object: TestInput + /// $objectList: [TestInput] + /// $objectNested: [[TestInput]] /// ) { /// upload(nonUpload: $nonUpload, single: $single, list: $list, nested: $nested, object: $object, objectList: $objectList, objectNested: $objectNested) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/__snapshots__/StarWarsIntrospectionTest.Execute_StarWarsIntrospection_Test.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/__snapshots__/StarWarsIntrospectionTest.Execute_StarWarsIntrospection_Test.snap index 4935037e0db..8ff78c0829f 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/__snapshots__/StarWarsIntrospectionTest.Execute_StarWarsIntrospection_Test.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/__snapshots__/StarWarsIntrospectionTest.Execute_StarWarsIntrospection_Test.snap @@ -1027,7 +1027,7 @@ "Name": null, "OfType": { "Kind": "Scalar", - "Name": "JSON", + "Name": "Any", "OfType": null } }, @@ -1508,7 +1508,7 @@ "Args": [], "Type": { "Kind": "Scalar", - "Name": "JSON", + "Name": "Any", "OfType": null }, "IsDeprecated": false, @@ -1673,7 +1673,7 @@ "Args": [], "Type": { "Kind": "Scalar", - "Name": "JSON", + "Name": "Any", "OfType": null }, "IsDeprecated": false, @@ -1862,7 +1862,7 @@ "Args": [], "Type": { "Kind": "Scalar", - "Name": "JSON", + "Name": "Any", "OfType": null }, "IsDeprecated": false, @@ -2116,7 +2116,7 @@ }, { "Kind": "Scalar", - "Name": "JSON", + "Name": "Any", "Description": null, "Fields": null, "InputFields": null, @@ -4360,7 +4360,7 @@ "PossibleTypes": null, "OfType": { "__typename": "__Type", - "Name": "JSON", + "Name": "Any", "Kind": "Scalar", "Description": null, "Fields": null, @@ -5272,7 +5272,7 @@ "Args": [], "Type": { "__typename": "__Type", - "Name": "JSON", + "Name": "Any", "Kind": "Scalar", "Description": null, "Fields": null, @@ -5563,7 +5563,7 @@ "Args": [], "Type": { "__typename": "__Type", - "Name": "JSON", + "Name": "Any", "Kind": "Scalar", "Description": null, "Fields": null, @@ -5865,7 +5865,7 @@ "Args": [], "Type": { "__typename": "__Type", - "Name": "JSON", + "Name": "Any", "Kind": "Scalar", "Description": null, "Fields": null, @@ -6292,7 +6292,7 @@ }, { "__typename": "__Type", - "Name": "JSON", + "Name": "Any", "Kind": "Scalar", "Description": null, "Fields": null, diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityGeneratorTests.Generate_ChatClient_MapperMapsEntityOnRootCorrectly.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityGeneratorTests.Generate_ChatClient_MapperMapsEntityOnRootCorrectly.snap index 9280e75cf73..4c19e7e710f 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityGeneratorTests.Generate_ChatClient_MapperMapsEntityOnRootCorrectly.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityGeneratorTests.Generate_ChatClient_MapperMapsEntityOnRootCorrectly.snap @@ -1194,7 +1194,7 @@ namespace Foo.Bar /// /// mutation WriteMessage( /// $text: String! - /// $address: String! + /// $address: String! /// ) { /// sendMessage(input: { text: $text, recipientEmail: $address }) { /// __typename @@ -1252,7 +1252,7 @@ namespace Foo.Bar /// /// mutation WriteMessage( /// $text: String! - /// $address: String! + /// $address: String! /// ) { /// sendMessage(input: { text: $text, recipientEmail: $address }) { /// __typename @@ -1380,7 +1380,7 @@ namespace Foo.Bar /// /// mutation WriteMessage( /// $text: String! - /// $address: String! + /// $address: String! /// ) { /// sendMessage(input: { text: $text, recipientEmail: $address }) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityGeneratorTests.Generate_ChatClient_MapperMapsEntityOnRootCorrectly_With_Records.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityGeneratorTests.Generate_ChatClient_MapperMapsEntityOnRootCorrectly_With_Records.snap index 8f0a41ec59f..674703bcf27 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityGeneratorTests.Generate_ChatClient_MapperMapsEntityOnRootCorrectly_With_Records.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityGeneratorTests.Generate_ChatClient_MapperMapsEntityOnRootCorrectly_With_Records.snap @@ -1194,7 +1194,7 @@ namespace Foo.Bar /// /// mutation WriteMessage( /// $text: String! - /// $address: String! + /// $address: String! /// ) { /// sendMessage(input: { text: $text, recipientEmail: $address }) { /// __typename @@ -1252,7 +1252,7 @@ namespace Foo.Bar /// /// mutation WriteMessage( /// $text: String! - /// $address: String! + /// $address: String! /// ) { /// sendMessage(input: { text: $text, recipientEmail: $address }) { /// __typename @@ -1380,7 +1380,7 @@ namespace Foo.Bar /// /// mutation WriteMessage( /// $text: String! - /// $address: String! + /// $address: String! /// ) { /// sendMessage(input: { text: $text, recipientEmail: $address }) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityOrIdGeneratorTests.UnionWithNestedObject.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityOrIdGeneratorTests.UnionWithNestedObject.snap index b5cadd62689..f983c21967f 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityOrIdGeneratorTests.UnionWithNestedObject.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/EntityOrIdGeneratorTests.UnionWithNestedObject.snap @@ -525,8 +525,8 @@ namespace Foo.Bar /// /// mutation StoreUserSettingFor( /// $userId: Int! - /// $customerId: Int! - /// $input: StoreUserSettingForInput! + /// $customerId: Int! + /// $input: StoreUserSettingForInput! /// ) { /// storeUserSettingFor(userId: $userId, customerId: $customerId, input: $input) { /// __typename @@ -575,8 +575,8 @@ namespace Foo.Bar /// /// mutation StoreUserSettingFor( /// $userId: Int! - /// $customerId: Int! - /// $input: StoreUserSettingForInput! + /// $customerId: Int! + /// $input: StoreUserSettingForInput! /// ) { /// storeUserSettingFor(userId: $userId, customerId: $customerId, input: $input) { /// __typename @@ -699,8 +699,8 @@ namespace Foo.Bar /// /// mutation StoreUserSettingFor( /// $userId: Int! - /// $customerId: Int! - /// $input: StoreUserSettingForInput! + /// $customerId: Int! + /// $input: StoreUserSettingForInput! /// ) { /// storeUserSettingFor(userId: $userId, customerId: $customerId, input: $input) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Comments.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Comments.snap index df5e15bbfe1..dd5e34edc66 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Comments.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Comments.snap @@ -412,8 +412,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -447,8 +447,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -600,8 +600,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Comments_With_Input_Records.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Comments_With_Input_Records.snap index 80a600ac18a..7cc4c2c8787 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Comments_With_Input_Records.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Comments_With_Input_Records.snap @@ -392,8 +392,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -427,8 +427,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -580,8 +580,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Complex_Arguments.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Complex_Arguments.snap index 78ddfc000fa..90dbe5587d0 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Complex_Arguments.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_Complex_Arguments.snap @@ -406,8 +406,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -441,8 +441,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -594,8 +594,8 @@ namespace Foo.Bar /// /// query Test( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_FirstNonUpload.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_FirstNonUpload.snap index 983939023a0..c730f354e16 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_FirstNonUpload.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_FirstNonUpload.snap @@ -98,7 +98,7 @@ namespace Foo.Bar /// /// query Test( /// $string: String! - /// $upload: Upload! + /// $upload: Upload! /// ) { /// foo(string: $string, upload: $upload) /// } @@ -132,7 +132,7 @@ namespace Foo.Bar /// /// query Test( /// $string: String! - /// $upload: Upload! + /// $upload: Upload! /// ) { /// foo(string: $string, upload: $upload) /// } @@ -241,7 +241,7 @@ namespace Foo.Bar /// /// query Test( /// $string: String! - /// $upload: Upload! + /// $upload: Upload! /// ) { /// foo(string: $string, upload: $upload) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_LastNonUpload.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_LastNonUpload.snap index 7484bb0da81..08eb6597e73 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_LastNonUpload.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_LastNonUpload.snap @@ -98,7 +98,7 @@ namespace Foo.Bar /// /// query Test( /// $upload: Upload! - /// $string: String! + /// $string: String! /// ) { /// foo(string: $string, upload: $upload) /// } @@ -132,7 +132,7 @@ namespace Foo.Bar /// /// query Test( /// $upload: Upload! - /// $string: String! + /// $string: String! /// ) { /// foo(string: $string, upload: $upload) /// } @@ -241,7 +241,7 @@ namespace Foo.Bar /// /// query Test( /// $upload: Upload! - /// $string: String! + /// $string: String! /// ) { /// foo(string: $string, upload: $upload) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_UploadAsArg.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_UploadAsArg.snap index e6a72eef3a0..e901618af1e 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_UploadAsArg.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/InputGeneratorTests.Operation_With_UploadAsArg.snap @@ -98,11 +98,11 @@ namespace Foo.Bar /// /// query Test( /// $upload: Upload! - /// $uploadNullable: Upload - /// $list: [Upload!]! - /// $listNullable: [Upload!] - /// $nestedList: [[Upload!]!]! - /// $nestedListNullable: [[Upload!]] + /// $uploadNullable: Upload + /// $list: [Upload!]! + /// $listNullable: [Upload!] + /// $nestedList: [[Upload!]!]! + /// $nestedListNullable: [[Upload!]] /// ) { /// foo(upload: $upload, uploadNullable: $uploadNullable, list: $list, listNullable: $listNullable, nestedList: $nestedList, nestedListNullable: $nestedListNullable) /// } @@ -136,11 +136,11 @@ namespace Foo.Bar /// /// query Test( /// $upload: Upload! - /// $uploadNullable: Upload - /// $list: [Upload!]! - /// $listNullable: [Upload!] - /// $nestedList: [[Upload!]!]! - /// $nestedListNullable: [[Upload!]] + /// $uploadNullable: Upload + /// $list: [Upload!]! + /// $listNullable: [Upload!] + /// $nestedList: [[Upload!]!]! + /// $nestedListNullable: [[Upload!]] /// ) { /// foo(upload: $upload, uploadNullable: $uploadNullable, list: $list, listNullable: $listNullable, nestedList: $nestedList, nestedListNullable: $nestedListNullable) /// } @@ -407,11 +407,11 @@ namespace Foo.Bar /// /// query Test( /// $upload: Upload! - /// $uploadNullable: Upload - /// $list: [Upload!]! - /// $listNullable: [Upload!] - /// $nestedList: [[Upload!]!]! - /// $nestedListNullable: [[Upload!]] + /// $uploadNullable: Upload + /// $list: [Upload!]! + /// $listNullable: [Upload!] + /// $nestedList: [[Upload!]!]! + /// $nestedListNullable: [[Upload!]] /// ) { /// foo(upload: $upload, uploadNullable: $uploadNullable, list: $list, listNullable: $listNullable, nestedList: $nestedList, nestedListNullable: $nestedListNullable) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/NoStoreStarWarsGeneratorTests.Operation_With_Type_Argument.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/NoStoreStarWarsGeneratorTests.Operation_With_Type_Argument.snap index 835fa7036b6..1d443994ae1 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/NoStoreStarWarsGeneratorTests.Operation_With_Type_Argument.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/NoStoreStarWarsGeneratorTests.Operation_With_Type_Argument.snap @@ -361,7 +361,7 @@ namespace Foo.Bar /// /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename @@ -399,7 +399,7 @@ namespace Foo.Bar /// /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename @@ -505,7 +505,7 @@ namespace Foo.Bar /// /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/OperationGeneratorTests.Generate_ChatClient_AllOperations.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/OperationGeneratorTests.Generate_ChatClient_AllOperations.snap index 02f25ebc704..472a66593ae 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/OperationGeneratorTests.Generate_ChatClient_AllOperations.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/OperationGeneratorTests.Generate_ChatClient_AllOperations.snap @@ -3280,7 +3280,7 @@ namespace Foo.Bar /// /// mutation SendMessageMut( /// $email: String! - /// $text: String! + /// $text: String! /// ) { /// sendMessage(input: { recipientEmail: $email, text: $text }) { /// __typename @@ -3350,7 +3350,7 @@ namespace Foo.Bar /// /// mutation SendMessageMut( /// $email: String! - /// $text: String! + /// $text: String! /// ) { /// sendMessage(input: { recipientEmail: $email, text: $text }) { /// __typename @@ -3490,7 +3490,7 @@ namespace Foo.Bar /// /// mutation SendMessageMut( /// $email: String! - /// $text: String! + /// $text: String! /// ) { /// sendMessage(input: { recipientEmail: $email, text: $text }) { /// __typename @@ -6042,6 +6042,7 @@ namespace Microsoft.Extensions.DependencyInjection global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, new global::StrawberryShake.Serialization.UrlSerializer("Url")); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => new global::StrawberryShake.Serialization.SerializerResolver(global::System.Linq.Enumerable.Concat(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(parentServices), global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(sp)))); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton, global::Foo.Bar.State.GetPeopleResultFactory>(services); diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/OperationGeneratorTests.Operation_With_MultipleOperations.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/OperationGeneratorTests.Operation_With_MultipleOperations.snap index 8a0cacbe98c..5b3317c502f 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/OperationGeneratorTests.Operation_With_MultipleOperations.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/OperationGeneratorTests.Operation_With_MultipleOperations.snap @@ -552,8 +552,8 @@ namespace Foo.Bar /// /// query TestOperation( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -587,8 +587,8 @@ namespace Foo.Bar /// /// query TestOperation( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -740,8 +740,8 @@ namespace Foo.Bar /// /// query TestOperation( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -763,8 +763,8 @@ namespace Foo.Bar /// /// query TestOperation2( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -798,8 +798,8 @@ namespace Foo.Bar /// /// query TestOperation2( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -951,8 +951,8 @@ namespace Foo.Bar /// /// query TestOperation2( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -974,8 +974,8 @@ namespace Foo.Bar /// /// query TestOperation3( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -1009,8 +1009,8 @@ namespace Foo.Bar /// /// query TestOperation3( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } @@ -1162,8 +1162,8 @@ namespace Foo.Bar /// /// query TestOperation3( /// $single: Bar! - /// $list: [Bar!]! - /// $nestedList: [[Bar!]] + /// $list: [Bar!]! + /// $nestedList: [[Bar!]] /// ) { /// foo(single: $single, list: $list, nestedList: $nestedList) /// } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/ScalarGeneratorTests.Uri_Type.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/ScalarGeneratorTests.Uri_Type.snap index 22236a3232c..78db124c2a5 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/ScalarGeneratorTests.Uri_Type.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/ScalarGeneratorTests.Uri_Type.snap @@ -592,7 +592,6 @@ namespace Microsoft.Extensions.DependencyInjection global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, new global::StrawberryShake.Serialization.UriSerializer("Uri")); - global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, new global::StrawberryShake.Serialization.UriSerializer("URI")); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => new global::StrawberryShake.Serialization.SerializerResolver(global::System.Linq.Enumerable.Concat(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(parentServices), global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(sp)))); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton, global::Foo.Bar.State.GetPersonResultFactory>(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(sp)); diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_GetFeatsPage.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_GetFeatsPage.snap index 550d320531b..294d8f099a1 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_GetFeatsPage.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_GetFeatsPage.snap @@ -345,7 +345,7 @@ namespace Foo.Bar /// /// query GetFeatsPage( /// $skip: Int - /// $take: Int + /// $take: Int /// ) { /// feats(skip: $skip, take: $take) { /// __typename @@ -397,7 +397,7 @@ namespace Foo.Bar /// /// query GetFeatsPage( /// $skip: Int - /// $take: Int + /// $take: Int /// ) { /// feats(skip: $skip, take: $take) { /// __typename @@ -523,7 +523,7 @@ namespace Foo.Bar /// /// query GetFeatsPage( /// $skip: Int - /// $take: Int + /// $take: Int /// ) { /// feats(skip: $skip, take: $take) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_PeopleSearch_From_ActiveDirectory_Schema.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_PeopleSearch_From_ActiveDirectory_Schema.snap index a029eae6258..36b252ee4f6 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_PeopleSearch_From_ActiveDirectory_Schema.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_PeopleSearch_From_ActiveDirectory_Schema.snap @@ -651,9 +651,9 @@ namespace Foo.Bar /// /// query PeopleSearch( /// $term: String! - /// $skip: Int - /// $take: Int - /// $inactive: Boolean + /// $skip: Int + /// $take: Int + /// $inactive: Boolean /// ) { /// people: peopleSearch(term: $term, includeInactive: $inactive, skip: $skip, take: $take) { /// __typename @@ -728,9 +728,9 @@ namespace Foo.Bar /// /// query PeopleSearch( /// $term: String! - /// $skip: Int - /// $take: Int - /// $inactive: Boolean + /// $skip: Int + /// $take: Int + /// $inactive: Boolean /// ) { /// people: peopleSearch(term: $term, includeInactive: $inactive, skip: $skip, take: $take) { /// __typename @@ -909,9 +909,9 @@ namespace Foo.Bar /// /// query PeopleSearch( /// $term: String! - /// $skip: Int - /// $take: Int - /// $inactive: Boolean + /// $skip: Int + /// $take: Int + /// $inactive: Boolean /// ) { /// people: peopleSearch(term: $term, includeInactive: $inactive, skip: $skip, take: $take) { /// __typename @@ -1751,6 +1751,7 @@ namespace Microsoft.Extensions.DependencyInjection global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, new global::StrawberryShake.Serialization.UrlSerializer("Url")); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => new global::StrawberryShake.Serialization.SerializerResolver(global::System.Linq.Enumerable.Concat(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(parentServices), global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(sp)))); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton, global::Foo.Bar.State.PeopleSearchResultFactory>(services); global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(sp)); diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_Query_With_Skip_Take.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_Query_With_Skip_Take.snap index 94da8eaec7c..e8c9748a691 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_Query_With_Skip_Take.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.Create_Query_With_Skip_Take.snap @@ -266,8 +266,8 @@ namespace Foo.Bar /// /// query SearchNewsItems( /// $query: String! - /// $skip: Int - /// $take: Int + /// $skip: Int + /// $take: Int /// ) { /// newsItems(skip: $skip, take: $take, query: $query) { /// __typename @@ -312,8 +312,8 @@ namespace Foo.Bar /// /// query SearchNewsItems( /// $query: String! - /// $skip: Int - /// $take: Int + /// $skip: Int + /// $take: Int /// ) { /// newsItems(skip: $skip, take: $take, query: $query) { /// __typename @@ -446,8 +446,8 @@ namespace Foo.Bar /// /// query SearchNewsItems( /// $query: String! - /// $skip: Int - /// $take: Int + /// $skip: Int + /// $take: Int /// ) { /// newsItems(skip: $skip, take: $take, query: $query) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.FieldsWithUnderlineInName.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.FieldsWithUnderlineInName.snap index 18c04f503de..8b529e65743 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.FieldsWithUnderlineInName.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.FieldsWithUnderlineInName.snap @@ -2961,7 +2961,7 @@ namespace Foo.Bar /// /// query GetBwr_TimeSeries( /// $where: bwr_TimeSeriesFilterInput - /// $readDataInput: ReadDataInput! + /// $readDataInput: ReadDataInput! /// ) { /// bwr_TimeSeries(where: $where) { /// __typename @@ -3050,7 +3050,7 @@ namespace Foo.Bar /// /// query GetBwr_TimeSeries( /// $where: bwr_TimeSeriesFilterInput - /// $readDataInput: ReadDataInput! + /// $readDataInput: ReadDataInput! /// ) { /// bwr_TimeSeries(where: $where) { /// __typename @@ -3214,7 +3214,7 @@ namespace Foo.Bar /// /// query GetBwr_TimeSeries( /// $where: bwr_TimeSeriesFilterInput - /// $readDataInput: ReadDataInput! + /// $readDataInput: ReadDataInput! /// ) { /// bwr_TimeSeries(where: $where) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.QueryInterference.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.QueryInterference.snap index 622434fface..47a1dbddc93 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.QueryInterference.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/SchemaGeneratorTests.QueryInterference.snap @@ -2226,9 +2226,13 @@ namespace Foo.Bar /// /// query GetFeatsPage( /// $skip: Int! - /// $take: Int! - /// $searchTerm: String! = "" - /// $order: [FeatSortInput!] = [ { name: ASC } ] + /// $take: Int! + /// $searchTerm: String! = "" + /// $order: [FeatSortInput!] = [ + /// { + /// name: ASC + /// } + /// ] /// ) { /// feats(skip: $skip, take: $take, order: $order, where: { or: [ { name: { contains: $searchTerm } }, { traits: { some: { name: { contains: $searchTerm } } } } ] }) { /// __typename @@ -2286,9 +2290,13 @@ namespace Foo.Bar /// /// query GetFeatsPage( /// $skip: Int! - /// $take: Int! - /// $searchTerm: String! = "" - /// $order: [FeatSortInput!] = [ { name: ASC } ] + /// $take: Int! + /// $searchTerm: String! = "" + /// $order: [FeatSortInput!] = [ + /// { + /// name: ASC + /// } + /// ] /// ) { /// feats(skip: $skip, take: $take, order: $order, where: { or: [ { name: { contains: $searchTerm } }, { traits: { some: { name: { contains: $searchTerm } } } } ] }) { /// __typename @@ -2447,9 +2455,13 @@ namespace Foo.Bar /// /// query GetFeatsPage( /// $skip: Int! - /// $take: Int! - /// $searchTerm: String! = "" - /// $order: [FeatSortInput!] = [ { name: ASC } ] + /// $take: Int! + /// $searchTerm: String! = "" + /// $order: [FeatSortInput!] = [ + /// { + /// name: ASC + /// } + /// ] /// ) { /// feats(skip: $skip, take: $take, order: $order, where: { or: [ { name: { contains: $searchTerm } }, { traits: { some: { name: { contains: $searchTerm } } } } ] }) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/StarWarsGeneratorTests.Operation_With_Type_Argument.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/StarWarsGeneratorTests.Operation_With_Type_Argument.snap index c7caa88e278..1732078ae63 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/StarWarsGeneratorTests.Operation_With_Type_Argument.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/__snapshots__/StarWarsGeneratorTests.Operation_With_Type_Argument.snap @@ -361,7 +361,7 @@ namespace Foo.Bar /// /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename @@ -399,7 +399,7 @@ namespace Foo.Bar /// /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename @@ -505,7 +505,7 @@ namespace Foo.Bar /// /// mutation CreateReviewMut( /// $episode: Episode! - /// $review: ReviewInput! + /// $review: ReviewInput! /// ) { /// createReview(episode: $episode, review: $review) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/RazorGeneratorTests.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/RazorGeneratorTests.cs index b46ae17c344..506d2382e77 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/RazorGeneratorTests.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/RazorGeneratorTests.cs @@ -13,31 +13,35 @@ public void Query_And_Mutation() AssertResult( settings: new() { RazorComponents = true }, - @"query GetBars($a: String! $b: String) { - bars(a: $a b: $b) { - id - name - } + """ + query GetBars($a: String! $b: String) { + bars(a: $a b: $b) { + id + name + } } mutation SaveBars($a: String! $b: String) { - saveBar(a: $a b: $b) { - id - name - } - }", - @"type Query { - bars(a: String!, b: String): [Bar] + saveBar(a: $a b: $b) { + id + name + } + } + """, + """ + type Query { + bars(a: String!, b: String): [Bar] } type Mutation { - saveBar(a: String!, b: String): Bar + saveBar(a: String!, b: String): Bar } type Bar { - id: String! - name: String - }", + id: String! + name: String + } + """, "extend schema @key(fields: \"id\")"); } } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/__snapshots__/RazorGeneratorTests.Query_And_Mutation.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/__snapshots__/RazorGeneratorTests.Query_And_Mutation.snap index aa2bedd3935..6d5c236d1e9 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/__snapshots__/RazorGeneratorTests.Query_And_Mutation.snap +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/__snapshots__/RazorGeneratorTests.Query_And_Mutation.snap @@ -343,7 +343,7 @@ namespace Foo.Bar /// /// query GetBars( /// $a: String! - /// $b: String + /// $b: String /// ) { /// bars(a: $a, b: $b) { /// __typename @@ -384,7 +384,7 @@ namespace Foo.Bar /// /// query GetBars( /// $a: String! - /// $b: String + /// $b: String /// ) { /// bars(a: $a, b: $b) { /// __typename @@ -497,7 +497,7 @@ namespace Foo.Bar /// /// query GetBars( /// $a: String! - /// $b: String + /// $b: String /// ) { /// bars(a: $a, b: $b) { /// __typename @@ -526,7 +526,7 @@ namespace Foo.Bar /// /// mutation SaveBars( /// $a: String! - /// $b: String + /// $b: String /// ) { /// saveBar(a: $a, b: $b) { /// __typename @@ -567,7 +567,7 @@ namespace Foo.Bar /// /// mutation SaveBars( /// $a: String! - /// $b: String + /// $b: String /// ) { /// saveBar(a: $a, b: $b) { /// __typename @@ -680,7 +680,7 @@ namespace Foo.Bar /// /// mutation SaveBars( /// $a: String! - /// $b: String + /// $b: String /// ) { /// saveBar(a: $a, b: $b) { /// __typename diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Tests/Analyzers/DocumentAnalyzerTests.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Tests/Analyzers/DocumentAnalyzerTests.cs index 882edaf056a..b299d2022a7 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Tests/Analyzers/DocumentAnalyzerTests.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Tests/Analyzers/DocumentAnalyzerTests.cs @@ -71,7 +71,7 @@ query GetHero { }); } - [Fact] + [Fact(Skip = "We need to reimplement defer for the client.")] public async Task One_Fragment_One_Deferred_Fragment() { // arrange