Skip to content

Commit

Permalink
Introduce new incremental result structure. (#5396)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib committed Sep 14, 2022
1 parent 6ffb369 commit d4e9c3b
Show file tree
Hide file tree
Showing 62 changed files with 745 additions and 439 deletions.
Expand Up @@ -8,5 +8,25 @@ internal sealed class ExecutionResultSnapshotValueFormatter
: SnapshotValueFormatter<IExecutionResult>
{
protected override void Format(IBufferWriter<byte> snapshot, IExecutionResult value)
=> snapshot.Append(value.ToJson());
{
if (value.Kind is ExecutionResultKind.SingleResult)
{
snapshot.Append(value.ToJson());
}
else
{
FormatStreamAsync(snapshot, (IResponseStream)value).Wait();
}
}

private static async Task FormatStreamAsync(
IBufferWriter<byte> snapshot,
IResponseStream stream)
{
await foreach (var queryResult in stream.ReadResultsAsync().ConfigureAwait(false))
{
snapshot.Append(queryResult.ToJson());
snapshot.AppendLine();
}
}
}
Expand Up @@ -17,9 +17,9 @@ namespace HotChocolate.AspNetCore.Serialization;
/// </summary>
public class DefaultHttpResponseFormatter : IHttpResponseFormatter
{
private readonly JsonQueryResultFormatter _jsonFormatter;
private readonly MultiPartResponseStreamFormatter _multiPartFormatter;
private readonly EventStreamFormatter _eventStreamFormatter;
private readonly JsonResultFormatter _jsonFormatter;
private readonly MultiPartResultFormatter _multiPartFormatter;
private readonly EventStreamResultFormatter _eventStreamResultFormatter;

/// <summary>
/// Creates a new instance of <see cref="DefaultHttpResponseFormatter" />.
Expand All @@ -38,9 +38,9 @@ public class DefaultHttpResponseFormatter : IHttpResponseFormatter
bool indented = false,
JavaScriptEncoder? encoder = null)
{
_jsonFormatter = new JsonQueryResultFormatter(indented, encoder);
_multiPartFormatter = new MultiPartResponseStreamFormatter(_jsonFormatter);
_eventStreamFormatter = new EventStreamFormatter(indented, encoder);
_jsonFormatter = new JsonResultFormatter(indented, encoder);
_multiPartFormatter = new MultiPartResultFormatter(_jsonFormatter);
_eventStreamResultFormatter = new EventStreamResultFormatter(indented, encoder);
}

public GraphQLRequestFlags CreateRequestFlags(
Expand Down Expand Up @@ -275,7 +275,7 @@ public class DefaultHttpResponseFormatter : IHttpResponseFormatter
formatInfo = new FormatInfo(
ContentType.EventStream,
ResponseContentType.EventStream,
_eventStreamFormatter);
_eventStreamResultFormatter);
return true;
}

Expand Down Expand Up @@ -333,7 +333,7 @@ public class DefaultHttpResponseFormatter : IHttpResponseFormatter
formatInfo = new FormatInfo(
ContentType.EventStream,
ResponseContentType.EventStream,
_eventStreamFormatter);
_eventStreamResultFormatter);
return true;
}

Expand Down Expand Up @@ -408,7 +408,7 @@ public class DefaultHttpResponseFormatter : IHttpResponseFormatter
formatInfo = new FormatInfo(
ContentType.EventStream,
ResponseContentType.EventStream,
_eventStreamFormatter);
_eventStreamResultFormatter);
return true;
}

Expand Down
Expand Up @@ -15,7 +15,7 @@ namespace HotChocolate.AspNetCore.Subscriptions.Protocols.Apollo;

internal sealed class ApolloSubscriptionProtocolHandler : IProtocolHandler
{
private readonly JsonQueryResultFormatter _formatter = new();
private readonly JsonResultFormatter _formatter = new();
private readonly ISocketSessionInterceptor _interceptor;

public ApolloSubscriptionProtocolHandler(ISocketSessionInterceptor interceptor)
Expand Down
Expand Up @@ -14,7 +14,7 @@ namespace HotChocolate.AspNetCore.Subscriptions.Protocols.GraphQLOverWebSocket;

internal sealed class GraphQLOverWebSocketProtocolHandler : IGraphQLOverWebSocketProtocolHandler
{
private readonly JsonQueryResultFormatter _formatter = new();
private readonly JsonResultFormatter _formatter = new();
private readonly ISocketSessionInterceptor _interceptor;

public GraphQLOverWebSocketProtocolHandler(ISocketSessionInterceptor interceptor)
Expand Down
Expand Up @@ -193,7 +193,8 @@ public async Task Legacy_With_Stream_1()
---
Content-Type: application/json; charset=utf-8
{""path"":[],""data"":{""__typename"":""Query""},""hasNext"":false}
{""incremental"":[{""data"":{""__typename"":""Query""}," +
@"""path"":[]}],""hasNext"":false}
-----
");
}
Expand Down Expand Up @@ -763,7 +764,8 @@ public async Task New_Query_With_Streams_1()
---
Content-Type: application/json; charset=utf-8
{""path"":[],""data"":{""__typename"":""Query""},""hasNext"":false}
{""incremental"":[{""data"":{""__typename"":""Query""}," +
@"""path"":[]}],""hasNext"":false}
-----
");
}
Expand Down
Expand Up @@ -6,5 +6,5 @@ Content-Type: application/json; charset=utf-8
---
Content-Type: application/json; charset=utf-8

{"label":"my_id","path":["hero"],"data":{"id":"2001"},"hasNext":false}
{"incremental":[{"data":{"id":"2001"},"label":"my_id","path":["hero"]}],"hasNext":false}
-----
Expand Up @@ -6,5 +6,5 @@ Content-Type: application/json; charset=utf-8
---
Content-Type: application/json; charset=utf-8

{"label":"my_id","path":["hero"],"data":{"id":"2001"},"hasNext":false}
{"incremental":[{"data":{"id":"2001"},"label":"my_id","path":["hero"]}],"hasNext":false}
-----
Expand Up @@ -6,11 +6,7 @@ Content-Type: application/json; charset=utf-8
---
Content-Type: application/json; charset=utf-8

{"label":"foo","path":["hero","friends","nodes",1],"data":{"name":"Han Solo"},"hasNext":true}
---
Content-Type: application/json; charset=utf-8

{"label":"foo","path":["hero","friends","nodes",2],"data":{"name":"Leia Organa"},"hasNext":true}
{"incremental":[{"data":{"name":"Han Solo"},"label":"foo","path":["hero","friends","nodes",1]},{"data":{"name":"Leia Organa"},"label":"foo","path":["hero","friends","nodes",2]}],"hasNext":true}
---
Content-Type: application/json; charset=utf-8

Expand Down
@@ -1,5 +1,5 @@
{
"ContentType": "multipart/mixed; boundary=\"-\"; charset=utf-8",
"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{\"label\":\"my_id\",\"path\":[\"hero\"],\"data\":{\"id\":\"2001\"},\"hasNext\":false}\r\n-----\r\n"
"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\":false}\r\n-----\r\n"
}
Expand Up @@ -40,6 +40,11 @@ public interface IQueryResult : IExecutionResult
/// </summary>
IReadOnlyDictionary<string, object?>? Extensions { get; }

/// <summary>
/// Gets the incremental patches provided with this result.
/// </summary>
IReadOnlyList<IQueryResult>? Incremental { get; }

/// <summary>
/// A boolean that is present and <c>true</c> when there are more payloads
/// that will be sent for this operation. The last payload in a multi payload response
Expand Down
11 changes: 9 additions & 2 deletions src/HotChocolate/Core/src/Abstractions/Execution/QueryResult.cs
Expand Up @@ -17,13 +17,14 @@ public sealed class QueryResult : ExecutionResult, IQueryResult
IReadOnlyList<IError>? errors,
IReadOnlyDictionary<string, object?>? extension,
IReadOnlyDictionary<string, object?>? contextData,
IReadOnlyList<IQueryResult>? incremental,
string? label,
Path? path,
bool? hasNext,
Func<ValueTask>[] cleanupTasks)
: base(cleanupTasks)
{
if (data is null && errors is null && hasNext is not false)
if (data is null && errors is null && incremental is null && hasNext is not false)
{
throw new ArgumentException(
AbstractionResources.QueryResult_DataAndResultAreNull,
Expand All @@ -34,6 +35,7 @@ public sealed class QueryResult : ExecutionResult, IQueryResult
Errors = errors;
Extensions = extension;
ContextData = contextData;
Incremental = incremental;
Label = label;
Path = path;
HasNext = hasNext;
Expand All @@ -47,11 +49,12 @@ public sealed class QueryResult : ExecutionResult, IQueryResult
IReadOnlyList<IError>? errors = null,
IReadOnlyDictionary<string, object?>? extension = null,
IReadOnlyDictionary<string, object?>? contextData = null,
IReadOnlyList<IQueryResult>? incremental = null,
string? label = null,
Path? path = null,
bool? hasNext = null)
{
if (data is null && errors is null && hasNext is not false)
if (data is null && errors is null && incremental is null && hasNext is not false)
{
throw new ArgumentException(
AbstractionResources.QueryResult_DataAndResultAreNull,
Expand All @@ -62,6 +65,7 @@ public sealed class QueryResult : ExecutionResult, IQueryResult
Errors = errors;
Extensions = extension;
ContextData = contextData;
Incremental = incremental;
Label = label;
Path = path;
HasNext = hasNext;
Expand All @@ -85,6 +89,9 @@ public sealed class QueryResult : ExecutionResult, IQueryResult
/// <inheritdoc />
public IReadOnlyDictionary<string, object?>? Extensions { get; }

/// <inheritdoc />
public IReadOnlyList<IQueryResult>? Incremental { get; }

/// <inheritdoc />
public override IReadOnlyDictionary<string, object?>? ContextData { get; }

Expand Down
Expand Up @@ -12,6 +12,7 @@ public class QueryResultBuilder : IQueryResultBuilder
private List<IError>? _errors;
private ExtensionData? _extensionData;
private ExtensionData? _contextData;
private List<IQueryResult>? _incremental;
private string? _label;
private Path? _path;
private bool? _hasNext;
Expand Down Expand Up @@ -109,6 +110,18 @@ public IQueryResultBuilder SetContextData(IReadOnlyDictionary<string, object?>?
return this;
}

public IQueryResultBuilder AddPatch(IQueryResult patch)
{
if (patch is null)
{
throw new ArgumentNullException(nameof(patch));
}

_incremental ??= new List<IQueryResult>();
_incremental.Add(patch);
return this;
}

public IQueryResultBuilder SetLabel(string? label)
{
_label = label;
Expand Down Expand Up @@ -146,6 +159,7 @@ public IQueryResult Create()
_errors?.Count > 0 ? _errors : null,
_extensionData?.Count > 0 ? _extensionData : null,
_contextData?.Count > 0 ? _contextData : null,
_incremental,
_label,
_path,
_hasNext,
Expand Down
Expand Up @@ -25,6 +25,9 @@
<AutoGen>True</AutoGen>
<DependentUpon>AbstractionResources.resx</DependentUpon>
</Compile>
<Compile Update="Execution\IQueryRequestBuilder.cs">
<DependentUpon>IQueryResult.cs</DependentUpon>
</Compile>
</ItemGroup>

<ItemGroup>
Expand Down
16 changes: 16 additions & 0 deletions src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs
Expand Up @@ -136,4 +136,20 @@ public static class WellKnownContextData
/// The key to overwrite the root type instance for a request.
/// </summary>
public const string InitialValue = "HotChocolate.Execution.InitialValue";

/// <summary>
/// The key to lookup significant results that were removed during execution.
/// </summary>
public const string RemovedResults = "HotChocolate.Execution.RemovedResults";

/// <summary>
/// The key to lookup result sets that expect data patches.
/// </summary>
public const string ExpectedPatches = "HotChocolate.Execution.ExpectedPatches";

/// <summary>
/// The key to the patch ID of a result set. The patch ID references the result into which
/// the result set containing the patch ID shall be patched into.
/// </summary>
public const string PatchId = "HotChocolate.Execution.PatchId";
}
Expand Up @@ -9,8 +9,8 @@ namespace HotChocolate;

public static class ExecutionResultExtensions
{
private static readonly JsonQueryResultFormatter _formatter = new(false);
private static readonly JsonQueryResultFormatter _formatterIndented = new(true);
private static readonly JsonResultFormatter _formatter = new(false);
private static readonly JsonResultFormatter _formatterIndented = new(true);

public static void WriteTo(
this IQueryResult result,
Expand Down
Expand Up @@ -113,6 +113,14 @@ internal static class OperationContextExtensions
return context;
}

public static OperationContext SetPatchId(
this OperationContext context,
uint patchId)
{
context.Result.SetContextData(WellKnownContextData.PatchId, patchId);
return context;
}

public static OperationContext ClearResult(
this OperationContext context)
{
Expand Down
Expand Up @@ -54,7 +54,8 @@ internal sealed class DeferredFragment : DeferredExecutionTask
protected override async Task ExecuteAsync(
OperationContextOwner operationContextOwner,
uint resultId,
uint parentResultId)
uint parentResultId,
uint patchId)
{
try
{
Expand All @@ -80,6 +81,7 @@ internal sealed class DeferredFragment : DeferredExecutionTask
.SetLabel(Label)
.SetPath(Path)
.SetData(parentResult)
.SetPatchId(patchId)
.BuildResultBuilder();

// complete the task and provide the result
Expand Down
Expand Up @@ -71,7 +71,8 @@ internal sealed class DeferredStream : DeferredExecutionTask
protected override async Task ExecuteAsync(
OperationContextOwner operationContextOwner,
uint resultId,
uint parentResultId)
uint parentResultId,
uint patchId)
{
var operationContext = operationContextOwner.OperationContext;
var aborted = operationContext.RequestAborted;
Expand All @@ -97,12 +98,13 @@ internal sealed class DeferredStream : DeferredExecutionTask
.SetLabel(Label)
.SetPath(operationContext.PathFactory.Append(Path, Index))
.SetData((ObjectResult)_task.ChildTask.ParentResult[0].Value!)
.SetPatchId(patchId)
.BuildResultBuilder();

await _task.ChildTask.CompleteUnsafeAsync().ConfigureAwait(false);

// we will register this same task again to get the next item.
operationContext.DeferredScheduler.Register(this);
operationContext.DeferredScheduler.Register(this, patchId);
operationContext.DeferredScheduler.Complete(new(resultId, parentResultId, result));
}
catch
Expand Down

0 comments on commit d4e9c3b

Please sign in to comment.