diff --git a/src/HotChocolate/Core/src/Execution/Processing/DirectiveContext.cs b/src/HotChocolate/Core/src/Execution/Processing/DirectiveContext.cs index 81995903f18..3a3ee1e474e 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/DirectiveContext.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/DirectiveContext.cs @@ -27,6 +27,8 @@ public DirectiveContext(IMiddlewareContext middlewareContext, IDirective directi public IOperation Operation => _middlewareContext.Operation; + public IOperationResultBuilder OperationResult => _middlewareContext.OperationResult; + public ISelection Selection => _middlewareContext.Selection; public IVariableValueCollection Variables => _middlewareContext.Variables; diff --git a/src/HotChocolate/Core/src/Execution/Processing/MiddlewareContext.Global.cs b/src/HotChocolate/Core/src/Execution/Processing/MiddlewareContext.Global.cs index 5e65109607c..ffaf3a3300c 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/MiddlewareContext.Global.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/MiddlewareContext.Global.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using HotChocolate.Execution.Properties; +using HotChocolate.Execution.Serialization; using HotChocolate.Resolvers; using HotChocolate.Types; using Microsoft.Extensions.DependencyInjection; @@ -12,6 +13,7 @@ namespace HotChocolate.Execution.Processing; internal partial class MiddlewareContext : IMiddlewareContext { + private readonly OperationResultBuilderFacade _operationResultBuilder = new(); private readonly List> _cleanupTasks = new(); private OperationContext _operationContext = default!; private IServiceProvider _services = default!; @@ -29,6 +31,8 @@ public IServiceProvider Services public IOperation Operation => _operationContext.Operation; + public IOperationResultBuilder OperationResult => _operationResultBuilder; + public IDictionary ContextData => _operationContext.ContextData; public IVariableValueCollection Variables => _operationContext.Variables; @@ -69,6 +73,7 @@ public IReadOnlyList GetSelections( for (var i = 0; i < selectionCount; i++) { var childSelection = Unsafe.Add(ref selectionRef, i); + if (childSelection.IsIncluded(operationIncludeFlags, allowInternals)) { finalFields.Add(childSelection); @@ -90,11 +95,12 @@ public void ReportError(string errorMessage) nameof(errorMessage)); } - ReportError(ErrorBuilder.New() - .SetMessage(errorMessage) - .SetPath(Path) - .AddLocation(_selection.SyntaxNode) - .Build()); + ReportError( + ErrorBuilder.New() + .SetMessage(errorMessage) + .SetPath(Path) + .AddLocation(_selection.SyntaxNode) + .Build()); } public void ReportError(Exception exception, Action? configure = null) @@ -182,7 +188,9 @@ public async ValueTask ResolveAsync() _hasResolverResult = true; } - return _resolverResult is null ? default! : (T)_resolverResult; + return _resolverResult is null + ? default! + : (T)_resolverResult; } public T Resolver() => @@ -257,4 +265,15 @@ public IMiddlewareContext Clone() IResolverContext IResolverContext.Clone() => Clone(); + + private sealed class OperationResultBuilderFacade : IOperationResultBuilder + { + public OperationContext Context { get; set; } = default!; + + public void SetResultState(string key, object? value) + => Context.Result.SetContextData(key, value); + + public void SetExtension(string key, TValue value) + => Context.Result.SetExtension(key, new NeedsFormatting(value)); + } } diff --git a/src/HotChocolate/Core/src/Execution/Processing/MiddlewareContext.Pooling.cs b/src/HotChocolate/Core/src/Execution/Processing/MiddlewareContext.Pooling.cs index dcdba66eb87..ee56a02f00f 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/MiddlewareContext.Pooling.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/MiddlewareContext.Pooling.cs @@ -24,6 +24,7 @@ public void Initialize( IImmutableDictionary scopedContextData) { _operationContext = operationContext; + _operationResultBuilder.Context = _operationContext; _services = operationContext.Services; _selection = selection; ParentResult = parentResult; @@ -49,6 +50,7 @@ public void Clean() _hasResolverResult = false; _result = default; _parser = default!; + _operationResultBuilder.Context = default!; Path = default!; ScopedContextData = default!; diff --git a/src/HotChocolate/Core/src/Execution/Serialization/JsonResultFormatter.cs b/src/HotChocolate/Core/src/Execution/Serialization/JsonResultFormatter.cs index f079ad0401d..689e92b7781 100644 --- a/src/HotChocolate/Core/src/Execution/Serialization/JsonResultFormatter.cs +++ b/src/HotChocolate/Core/src/Execution/Serialization/JsonResultFormatter.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; @@ -12,6 +11,7 @@ using System.Threading.Tasks; using HotChocolate.Execution.Processing; using HotChocolate.Utilities; +using static System.Text.Json.JsonSerializerDefaults; using static HotChocolate.Execution.ThrowHelper; namespace HotChocolate.Execution.Serialization; @@ -22,6 +22,7 @@ namespace HotChocolate.Execution.Serialization; public sealed partial class JsonResultFormatter : IQueryResultFormatter, IExecutionResultFormatter { private readonly JsonWriterOptions _options; + private readonly JsonSerializerOptions _serializerOptions; /// /// Initializes a new instance of . @@ -34,7 +35,8 @@ public sealed partial class JsonResultFormatter : IQueryResultFormatter, IExecut /// public JsonResultFormatter(bool indented = false, JavaScriptEncoder? encoder = null) { - _options = new JsonWriterOptions { Indented = indented, Encoder = encoder }; + _options = new() { Indented = indented, Encoder = encoder }; + _serializerOptions = new(Web) { WriteIndented = indented, Encoder = encoder }; } /// @@ -606,7 +608,7 @@ private void WriteFieldValue( WriteListResult(writer, resultMapList); break; -#if NET5_0_OR_GREATER +#if NET6_0_OR_GREATER case JsonElement element: WriteJsonElement(writer, element); break; @@ -615,6 +617,10 @@ private void WriteFieldValue( writer.WriteRawValue(rawJsonValue.Value.Span, true); break; #endif + case NeedsFormatting unformatted: + unformatted.FormatValue(writer, _serializerOptions); + break; + case Dictionary dict: WriteDictionary(writer, dict); break; diff --git a/src/HotChocolate/Core/src/Execution/Serialization/NeedsFormatting.cs b/src/HotChocolate/Core/src/Execution/Serialization/NeedsFormatting.cs new file mode 100644 index 00000000000..70bfa92f79f --- /dev/null +++ b/src/HotChocolate/Core/src/Execution/Serialization/NeedsFormatting.cs @@ -0,0 +1,61 @@ +using System.Text.Json; + +namespace HotChocolate.Execution.Serialization; + +/// +/// This helper class allows us to indicate to the formatters that the inner value +/// has a custom formatter. +/// +/// +/// The downside of this helper is that we bind it explicitly to JSON. +/// If there were alternative query formatter that use different formats we would get +/// into trouble with this. +/// +/// This is also the reason for keeping this internal. +/// +internal abstract class NeedsFormatting +{ + /// + /// Formats the value as JSON + /// + /// + /// The JSON writer. + /// + /// + /// The JSON serializer options. + /// + public abstract void FormatValue(Utf8JsonWriter writer, JsonSerializerOptions options); +} + +/// +/// This helper class allows us to indicate to the formatters that the inner value +/// has a custom formatter. +/// +/// +/// The downside of this helper is that we bind it explicitly to JSON. +/// If there were alternative query formatter that use different formats we would get +/// into trouble with this. +/// +/// This is also the reason for keeping this internal. +/// +internal sealed class NeedsFormatting : NeedsFormatting +{ + private readonly TValue _value; + + public NeedsFormatting(TValue value) + { + _value = value; + } + + /// + /// Formats the value as JSON + /// + /// + /// The JSON writer. + /// + /// + /// The JSON serializer options. + /// + public override void FormatValue(Utf8JsonWriter writer, JsonSerializerOptions options) + => JsonSerializer.Serialize(writer, _value, options); +} diff --git a/src/HotChocolate/Core/src/Execution/Serialization/RawJsonValue.cs b/src/HotChocolate/Core/src/Execution/Serialization/RawJsonValue.cs index e5a24679cb6..5062c96761b 100644 --- a/src/HotChocolate/Core/src/Execution/Serialization/RawJsonValue.cs +++ b/src/HotChocolate/Core/src/Execution/Serialization/RawJsonValue.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json; namespace HotChocolate.Execution.Serialization; @@ -7,6 +8,13 @@ namespace HotChocolate.Execution.Serialization; /// The JSON query result formatter will take the inner /// and writes it without validation to the JSON response object. /// +/// +/// The downside of this helper is that we bind it explicitly to JSON. +/// If there were alternative query formatter that use different formats we would get +/// into trouble with this. +/// +/// This is also the reason for keeping this internal. +/// internal readonly struct RawJsonValue { /// diff --git a/src/HotChocolate/Core/src/Types/Resolvers/IMiddlewareContext.cs b/src/HotChocolate/Core/src/Types/Resolvers/IMiddlewareContext.cs index f250b93de9f..51f5a2012a6 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/IMiddlewareContext.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/IMiddlewareContext.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using HotChocolate.Execution; using HotChocolate.Types; #nullable enable @@ -30,6 +29,11 @@ public interface IMiddlewareContext : IResolverContext /// bool IsResultModified { get; } + /// + /// Allows to modify some aspects of the overall operation result. + /// + IOperationResultBuilder OperationResult { get; } + /// /// Executes the field resolver and returns its result. /// diff --git a/src/HotChocolate/Core/src/Types/Resolvers/IOperationResultBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/IOperationResultBuilder.cs new file mode 100644 index 00000000000..1a3d93ef64a --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Resolvers/IOperationResultBuilder.cs @@ -0,0 +1,34 @@ +#nullable enable +namespace HotChocolate.Resolvers; + +/// +/// This helper allows modifying some aspects of the overall operation result object. +/// +public interface IOperationResultBuilder +{ + /// + /// Sets a property on the result context data which can be used for further processing + /// in the request pipeline. + /// + /// The key. + /// The value. + void SetResultState(string key, object? value); + + /// + /// Sets a property on the result extension data which will + /// be serialized and send to the consumer. + /// + /// + /// { + /// ... + /// "extensions": { + /// "yourKey": "yourValue" + /// } + /// } + /// + /// + /// + /// The key. + /// The value. + void SetExtension(string key, TValue value); +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/MiddlewareContextTests.cs b/src/HotChocolate/Core/test/Execution.Tests/MiddlewareContextTests.cs index 56860549879..32b886e2adb 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/MiddlewareContextTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/MiddlewareContextTests.cs @@ -204,7 +204,7 @@ public async Task ReplaceArguments_Delegate() result.MatchSnapshot(); } - [Fact] + [Fact] public async Task ReplaceArguments_Delegate_ReplaceWithNull_ShouldFail() { var result = await new ServiceCollection() @@ -230,6 +230,101 @@ public async Task ReplaceArguments_Delegate_ReplaceWithNull_ShouldFail() result.MatchSnapshot(); } + [Fact] + public async Task SetResultContextData() + { + var result = await new ServiceCollection() + .AddGraphQL() + .AddQueryType( + d => + { + d.Field("abc") + .Argument("a", t => t.Type()) + .Resolve(ctx => ctx.ArgumentValue("a")) + .Use( + next => async context => + { + context.OperationResult.SetResultState("abc", "def"); + await next(context); + }); + }) + .ExecuteRequestAsync("{ abc(a: \"abc\") }"); + + Assert.NotNull(result.ContextData); + Assert.True(result.ContextData.TryGetValue("abc", out var value)); + Assert.Equal("def", value); + } + + [Fact] + public async Task SetResultExtensionData_With_IntValue() + { + var result = await new ServiceCollection() + .AddGraphQL() + .AddQueryType( + d => + { + d.Field("abc") + .Argument("a", t => t.Type()) + .Resolve(ctx => ctx.ArgumentValue("a")) + .Use( + next => async context => + { + context.OperationResult.SetExtension("abc", 1); + await next(context); + }); + }) + .ExecuteRequestAsync("{ abc(a: \"abc\") }"); + + Snapshot + .Create() + .Add(result) + .MatchInline( + @"{ + ""data"": { + ""abc"": ""abc"" + }, + ""extensions"": { + ""abc"": 1 + } + }"); + } + + [Fact] + public async Task SetResultExtensionData_With_ObjectValue() + { + var result = await new ServiceCollection() + .AddGraphQL() + .AddQueryType( + d => + { + d.Field("abc") + .Argument("a", t => t.Type()) + .Resolve(ctx => ctx.ArgumentValue("a")) + .Use( + next => async context => + { + context.OperationResult.SetExtension("abc", new SomeData("def")); + await next(context); + }); + }) + .ExecuteRequestAsync("{ abc(a: \"abc\") }"); + + Snapshot + .Create() + .Add(result) + .MatchInline( + @"{ + ""data"": { + ""abc"": ""abc"" + }, + ""extensions"": { + ""abc"": { + ""someField"": ""def"" + } + } + }"); + } + private static void CollectSelections( IResolverContext context, ISelection selection, @@ -248,4 +343,6 @@ private static void CollectSelections( } } } + + private record SomeData(string SomeField); } diff --git a/src/HotChocolate/Data/src/Data/Projections/Extensions/ProjectionObjectFieldDescriptorExtensions.cs b/src/HotChocolate/Data/src/Data/Projections/Extensions/ProjectionObjectFieldDescriptorExtensions.cs index ee229d87679..445f767b0c2 100644 --- a/src/HotChocolate/Data/src/Data/Projections/Extensions/ProjectionObjectFieldDescriptorExtensions.cs +++ b/src/HotChocolate/Data/src/Data/Projections/Extensions/ProjectionObjectFieldDescriptorExtensions.cs @@ -237,6 +237,8 @@ public MiddlewareContextProxy( public IOperation Operation => _context.Operation; + public IOperationResultBuilder OperationResult => _context.OperationResult; + public ISelection Selection { get; } public IVariableValueCollection Variables => _context.Variables; diff --git a/src/HotChocolate/Utilities/src/Utilities/ArrayWriter.cs b/src/HotChocolate/Utilities/src/Utilities/ArrayWriter.cs index bcf9cdd2127..388eccda4ea 100644 --- a/src/HotChocolate/Utilities/src/Utilities/ArrayWriter.cs +++ b/src/HotChocolate/Utilities/src/Utilities/ArrayWriter.cs @@ -25,7 +25,7 @@ public ArrayWriter() public ReadOnlyMemory Body => _buffer.AsMemory().Slice(0, _start); public byte[] GetInternalBuffer() => _buffer; - + public void Advance(int count) { _start += count;