diff --git a/src/HotChocolate/AspNetCore/src/Transport.Abstractions/Serialization/Utf8JsonWriterHelper.cs b/src/HotChocolate/AspNetCore/src/Transport.Abstractions/Serialization/Utf8JsonWriterHelper.cs index de62d173e94..7292a3917fa 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Abstractions/Serialization/Utf8JsonWriterHelper.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Abstractions/Serialization/Utf8JsonWriterHelper.cs @@ -206,14 +206,7 @@ private static void WriteList( for (var i = 0; i < list.Count; i++) { - var element = list[i]; - - if (element is null) - { - continue; - } - - WriteFieldValue(writer, element); + WriteFieldValue(writer, list[i]); } writer.WriteEndArray(); diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs index 2d63c01c651..dc0747a6ff1 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs @@ -53,6 +53,11 @@ public GraphQLHttpResponse(HttpResponseMessage message) /// public bool IsSuccessStatusCode => _message.IsSuccessStatusCode; + /// + /// Gets the reason phrase which typically is sent by servers together with the status code. + /// + public string? ReasonPhrase => _message.ReasonPhrase; + /// /// Throws an exception if the HTTP response was unsuccessful. /// diff --git a/src/HotChocolate/Utilities/src/Utilities/HotChocolate.Utilities.csproj b/src/HotChocolate/Utilities/src/Utilities/HotChocolate.Utilities.csproj index 379a690277f..056320160bc 100644 --- a/src/HotChocolate/Utilities/src/Utilities/HotChocolate.Utilities.csproj +++ b/src/HotChocolate/Utilities/src/Utilities/HotChocolate.Utilities.csproj @@ -20,6 +20,7 @@ + diff --git a/src/StrawberryShake/Client/StrawberryShake.Client.sln b/src/StrawberryShake/Client/StrawberryShake.Client.sln index 461f94d5ba9..403d8513044 100644 --- a/src/StrawberryShake/Client/StrawberryShake.Client.sln +++ b/src/StrawberryShake/Client/StrawberryShake.Client.sln @@ -37,6 +37,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StrawberryShake.Persistence EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StrawberryShake.Razor", "src\Razor\StrawberryShake.Razor.csproj", "{2A834588-BA60-4906-B111-20AF9FD5B1E6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Transport.Http", "..\..\HotChocolate\AspNetCore\src\Transport.Http\HotChocolate.Transport.Http.csproj", "{1A765D49-CF64-4C34-AE37-E6171362A858}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Utilities", "..\..\HotChocolate\Utilities\src\Utilities\HotChocolate.Utilities.csproj", "{62478E17-8EAE-4473-9C8E-5933042B25A0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -218,6 +222,30 @@ Global {2A834588-BA60-4906-B111-20AF9FD5B1E6}.Release|x64.Build.0 = Release|Any CPU {2A834588-BA60-4906-B111-20AF9FD5B1E6}.Release|x86.ActiveCfg = Release|Any CPU {2A834588-BA60-4906-B111-20AF9FD5B1E6}.Release|x86.Build.0 = Release|Any CPU + {1A765D49-CF64-4C34-AE37-E6171362A858}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A765D49-CF64-4C34-AE37-E6171362A858}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A765D49-CF64-4C34-AE37-E6171362A858}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A765D49-CF64-4C34-AE37-E6171362A858}.Debug|x64.Build.0 = Debug|Any CPU + {1A765D49-CF64-4C34-AE37-E6171362A858}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A765D49-CF64-4C34-AE37-E6171362A858}.Debug|x86.Build.0 = Debug|Any CPU + {1A765D49-CF64-4C34-AE37-E6171362A858}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A765D49-CF64-4C34-AE37-E6171362A858}.Release|Any CPU.Build.0 = Release|Any CPU + {1A765D49-CF64-4C34-AE37-E6171362A858}.Release|x64.ActiveCfg = Release|Any CPU + {1A765D49-CF64-4C34-AE37-E6171362A858}.Release|x64.Build.0 = Release|Any CPU + {1A765D49-CF64-4C34-AE37-E6171362A858}.Release|x86.ActiveCfg = Release|Any CPU + {1A765D49-CF64-4C34-AE37-E6171362A858}.Release|x86.Build.0 = Release|Any CPU + {62478E17-8EAE-4473-9C8E-5933042B25A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62478E17-8EAE-4473-9C8E-5933042B25A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62478E17-8EAE-4473-9C8E-5933042B25A0}.Debug|x64.ActiveCfg = Debug|Any CPU + {62478E17-8EAE-4473-9C8E-5933042B25A0}.Debug|x64.Build.0 = Debug|Any CPU + {62478E17-8EAE-4473-9C8E-5933042B25A0}.Debug|x86.ActiveCfg = Debug|Any CPU + {62478E17-8EAE-4473-9C8E-5933042B25A0}.Debug|x86.Build.0 = Debug|Any CPU + {62478E17-8EAE-4473-9C8E-5933042B25A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62478E17-8EAE-4473-9C8E-5933042B25A0}.Release|Any CPU.Build.0 = Release|Any CPU + {62478E17-8EAE-4473-9C8E-5933042B25A0}.Release|x64.ActiveCfg = Release|Any CPU + {62478E17-8EAE-4473-9C8E-5933042B25A0}.Release|x64.Build.0 = Release|Any CPU + {62478E17-8EAE-4473-9C8E-5933042B25A0}.Release|x86.ActiveCfg = Release|Any CPU + {62478E17-8EAE-4473-9C8E-5933042B25A0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {61894B9B-EC9A-4FC0-AAAF-9922978A5AB8} = {5F283FE4-BB0A-4806-B079-42DBFE0B2EDF} @@ -234,5 +262,7 @@ Global {DFFA574E-439F-4FCE-BF6D-CE5BEACA360A} = {5F283FE4-BB0A-4806-B079-42DBFE0B2EDF} {9A5345E8-A2EB-410F-BD72-6F268794D2DC} = {4E0B9145-BE75-4F46-906B-092DFF4AD5CC} {2A834588-BA60-4906-B111-20AF9FD5B1E6} = {5F283FE4-BB0A-4806-B079-42DBFE0B2EDF} + {1A765D49-CF64-4C34-AE37-E6171362A858} = {264FDF20-A58F-476E-9E02-9F082FB65612} + {62478E17-8EAE-4473-9C8E-5933042B25A0} = {264FDF20-A58F-476E-9E02-9F082FB65612} EndGlobalSection EndGlobal diff --git a/src/StrawberryShake/Client/src/Resources/Properties/Resources.Designer.cs b/src/StrawberryShake/Client/src/Resources/Properties/Resources.Designer.cs index abc3c75adb8..b63691b1bf4 100644 --- a/src/StrawberryShake/Client/src/Resources/Properties/Resources.Designer.cs +++ b/src/StrawberryShake/Client/src/Resources/Properties/Resources.Designer.cs @@ -278,5 +278,11 @@ internal static string ResponseEnumerator_HttpNoSuccessStatusCode { return ResourceManager.GetString("ResponseEnumerator_HttpNoSuccessStatusCode", resourceCulture); } } + + internal static string HttpConnection_FileMapDoesNotMatch { + get { + return ResourceManager.GetString("HttpConnection_FileMapDoesNotMatch", resourceCulture); + } + } } } diff --git a/src/StrawberryShake/Client/src/Resources/Properties/Resources.resx b/src/StrawberryShake/Client/src/Resources/Properties/Resources.resx index 110dd126178..4615c8ef898 100644 --- a/src/StrawberryShake/Client/src/Resources/Properties/Resources.resx +++ b/src/StrawberryShake/Client/src/Resources/Properties/Resources.resx @@ -135,4 +135,7 @@ Response status code does not indicate success: {0} ({1}). + + Could not map the file uploads to the variables. The data structures did not match. Could not find path {0} in the variables. + diff --git a/src/StrawberryShake/Client/src/Transport.Http/HttpConnection.cs b/src/StrawberryShake/Client/src/Transport.Http/HttpConnection.cs index bfeebe2da83..4291a844737 100644 --- a/src/StrawberryShake/Client/src/Transport.Http/HttpConnection.cs +++ b/src/StrawberryShake/Client/src/Transport.Http/HttpConnection.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using System.Net.Http; -using System.Net.Http.Headers; -using System.Net.Http.Json; +using System.Text; using System.Text.Json; -using StrawberryShake.Internal; +using HotChocolate.Transport.Http; +using HotChocolate.Utilities; using StrawberryShake.Json; +using static StrawberryShake.Properties.Resources; using static StrawberryShake.Transport.Http.ResponseEnumerable; namespace StrawberryShake.Transport.Http; @@ -13,7 +14,6 @@ namespace StrawberryShake.Transport.Http; public sealed class HttpConnection : IHttpConnection { private readonly Func _createClient; - private readonly JsonOperationRequestSerializer _serializer = new(); public HttpConnection(Func createClient) { @@ -21,54 +21,200 @@ public HttpConnection(Func createClient) } public IAsyncEnumerable> ExecuteAsync(OperationRequest request) - => Create(_createClient, () => CreateRequestMessage(request)); + => Create(_createClient, () => MapRequest(request)); - private HttpRequestMessage CreateRequestMessage(OperationRequest request) + private static GraphQLHttpRequest MapRequest(OperationRequest request) { - var operation = CreateRequestMessageBody(request); + var (id, name, document, variables, extensions, _, files, _) = request; - var content = request.Files.Count == 0 - ? CreateRequestContent(operation) - : CreateMultipartContent(request, operation); +#if NETSTANDARD2_0 + var body = Encoding.UTF8.GetString(document.Body.ToArray()); +#else + var body = Encoding.UTF8.GetString(document.Body); +#endif - return new HttpRequestMessage { Method = HttpMethod.Post, Content = content }; + var hasFiles = files is { Count: > 0 }; + + variables = MapVariables(variables); + if (hasFiles && variables is not null) + { + variables = MapFilesToVariables(variables, files!); + } + + var operation = + new HotChocolate.Transport.OperationRequest(body, id, name, variables, extensions); + + return new GraphQLHttpRequest(operation) { EnableFileUploads = hasFiles }; } - private byte[] CreateRequestMessageBody(OperationRequest request) + /// + /// Converts the variables into a dictionary that can be serialized. This is necessary + /// because the variables can contain lists of key value pairs which are not supported + /// by HotChocolate.Transport.Http + /// + /// + /// We only convert the variables if necessary to avoid unnecessary allocations. + /// + private static IReadOnlyDictionary? MapVariables( + IReadOnlyDictionary variables) { - using var arrayWriter = new ArrayWriter(); - _serializer.Serialize(request, arrayWriter); - var buffer = new byte[arrayWriter.Length]; - arrayWriter.Body.Span.CopyTo(buffer); - return buffer; + if (variables.Count == 0) + { + return null; + } + + Dictionary? copy = null; + foreach (var variable in variables) + { + var value = variable.Value; + // the value can be a List of key value pairs and not only a dictionary. We do expect + // to just have lists here, but in case we have a dictionary this should also just work. + if (value is IEnumerable> items) + { + copy ??= CreateDictionary(variables); + + value = MapVariables(CreateDictionary(items)); + } + else if (value is List list) + { + // the lists are mutable so we can just update the value in the list + MapVariables(list); + } + + if (copy is not null) + { + copy[variable.Key] = value; + } + } + + return copy ?? variables; } - private static HttpContent CreateRequestContent(byte[] operation) + private static void MapVariables(List variables) { - var content = new ByteArrayContent(operation); - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - return content; + if (variables.Count == 0) + { + return; + } + + for (var index = 0; index < variables.Count; index++) + { + switch (variables[index]) + { + case IEnumerable> items: + variables[index] = MapVariables(CreateDictionary(items)); + break; + + case List list: + MapVariables(list); + break; + } + } } - private static HttpContent CreateMultipartContent(OperationRequest request, byte[] operation) + private static Dictionary CreateDictionary( + IEnumerable> values) { - var fileMap = new Dictionary(); - var form = new MultipartFormDataContent +#if NETSTANDARD2_0 + var dictionary = new Dictionary(); + + foreach (var value in values) { - { new ByteArrayContent(operation), "operations" }, - { JsonContent.Create(fileMap), "map" } - }; + dictionary[value.Key] = value.Value; + } - foreach (var file in request.Files) + return dictionary; +#else + return new Dictionary(values); +#endif + } + + private static IReadOnlyDictionary MapFilesToVariables( + IReadOnlyDictionary variables, + IReadOnlyDictionary files) + { + foreach (var file in files) { - if (file.Value is { } fileContent) + var path = file.Key; + var upload = file.Value; + + if (!upload.HasValue) + { + continue; + } + + var currentPath = path.Substring("variables.".Length); + object? currentObject = variables; + int index; + while ((index = currentPath.IndexOf('.')) >= 0) { - var identifier = (fileMap.Count + 1).ToString(); - fileMap.Add(identifier, new[] { file.Key }); - form.Add(new StreamContent(fileContent.Content), identifier, fileContent.FileName); + var segment = currentPath.Substring(0, index); + switch (currentObject) + { + case Dictionary dictionary: + if (!dictionary.TryGetValue(segment, out currentObject)) + { + throw new InvalidOperationException( + string.Format(HttpConnection_FileMapDoesNotMatch, path)); + } + + break; + + case List array: + if (!int.TryParse(segment, out var arrayIndex)) + { + throw new InvalidOperationException( + string.Format(HttpConnection_FileMapDoesNotMatch, path)); + } + + if (arrayIndex >= array.Count) + { + throw new InvalidOperationException( + string.Format(HttpConnection_FileMapDoesNotMatch, path)); + } + + currentObject = array[arrayIndex]; + break; + + default: + throw new InvalidOperationException( + string.Format(HttpConnection_FileMapDoesNotMatch, path)); + } + + currentPath = currentPath.Substring(index + 1); + } + + switch (currentObject) + { + case Dictionary result: + result[currentPath] = + new FileReference(upload.Value.Content, upload.Value.FileName); + break; + + case List array: + if (!int.TryParse(currentPath, out var arrayIndex)) + { + throw new InvalidOperationException( + string.Format(HttpConnection_FileMapDoesNotMatch, path)); + } + + if (arrayIndex >= array.Count) + { + throw new InvalidOperationException( + string.Format(HttpConnection_FileMapDoesNotMatch, path)); + } + + array[arrayIndex] = + new FileReference(upload.Value.Content, upload.Value.FileName); + + break; + + default: + throw new InvalidOperationException( + string.Format(HttpConnection_FileMapDoesNotMatch, path)); } } - return form; + return variables; } } diff --git a/src/StrawberryShake/Client/src/Transport.Http/ResponseEnumerable.cs b/src/StrawberryShake/Client/src/Transport.Http/ResponseEnumerable.cs index 6cb11603411..d0a36057d39 100644 --- a/src/StrawberryShake/Client/src/Transport.Http/ResponseEnumerable.cs +++ b/src/StrawberryShake/Client/src/Transport.Http/ResponseEnumerable.cs @@ -1,31 +1,174 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Net.Http; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Transport.Http; +using StrawberryShake.Internal; +using static StrawberryShake.Properties.Resources; namespace StrawberryShake.Transport.Http; internal sealed class ResponseEnumerable : IAsyncEnumerable> { private readonly Func _createClient; - private readonly Func _createRequest; + private readonly Func _createRequest; private ResponseEnumerable( Func createClient, - Func createRequest) + Func createRequest) { _createClient = createClient; _createRequest = createRequest; } - public IAsyncEnumerator> GetAsyncEnumerator( + public async IAsyncEnumerator> GetAsyncEnumerator( CancellationToken cancellationToken = default) - => new ResponseEnumerator(_createClient, _createRequest, cancellationToken); + { + using var client = new DefaultGraphQLHttpClient(_createClient()); + var request = _createRequest(); + + var result = await client.SendAsync(request, cancellationToken); + + Exception? transportError = null; + if (!result.IsSuccessStatusCode) + { + transportError = CreateError(result); + } + + ConfiguredCancelableAsyncEnumerable.Enumerator + enumerator = default!; + try + { + enumerator = result + .ReadAsResultStreamAsync(cancellationToken) + .ConfigureAwait(false) + .WithCancellation(cancellationToken) + .GetAsyncEnumerator(); + } + catch (Exception ex) + { + transportError ??= ex; + } + + var hasNext = true; + while (hasNext) + { + try + { + hasNext = await enumerator.MoveNextAsync(); + } + catch (Exception ex) + { + hasNext = false; + transportError ??= ex; + } + + // in case we have a result we will still parse and return it even if we have a + // transport error as we will continue to read the stream until the end. + if (hasNext) + { + var parsedResult = ParseResult(enumerator.Current); + + yield return new Response(parsedResult, transportError); + + transportError = null; + } + else if (transportError is not null) + { + var errorBody = CreateBodyFromException(transportError); + yield return new Response(errorBody, transportError); + } + } + } + + private static JsonDocument? ParseResult(HotChocolate.Transport.OperationResult? result) + { + if (result is null) + { + return null; + } + + var buffer = new HotChocolate.Utilities.ArrayWriter(); + using var writer = new Utf8JsonWriter(buffer); + + writer.WriteStartObject(); + WriteProperty(writer, "data", result.Data); + + // in case we have just a "Internal Execution Error" we will not write the errors as this + // is a internal error of HotChocolate.Transport.Http. In strawberry shake we are used to + // handle the transport errors our self. + // Strawberry Shake only outputs the exceptions though if there is no error in the errors + // field + if (result.Errors.ValueKind is not JsonValueKind.Array || + result.Errors.GetArrayLength() != 1 || + !result.Errors[0].TryGetProperty("message", out var message) || + message.GetString() is not "Internal Execution Error") + { + WriteProperty(writer, "errors", result.Errors); + } + + WriteProperty(writer, "extensions", result.Extensions); + writer.WriteEndObject(); + + writer.Flush(); + + return JsonDocument.Parse(buffer.GetWrittenMemory()); + } + + private static void WriteProperty(Utf8JsonWriter writer, string propertyName, JsonElement value) + { + if (value.ValueKind != JsonValueKind.Undefined) + { + writer.WritePropertyName(propertyName); + value.WriteTo(writer); + } + } + + private static Exception CreateError(GraphQLHttpResponse response) + { +#if NET5_0_OR_GREATER + return new HttpRequestException( + string.Format( + ResponseEnumerator_HttpNoSuccessStatusCode, + (int)response.StatusCode, + response.ReasonPhrase), + null, + response.StatusCode); +#else + return new HttpRequestException( + string.Format( + ResponseEnumerator_HttpNoSuccessStatusCode, + (int)response.StatusCode, + response.ReasonPhrase), + null); +#endif + } + + internal static JsonDocument CreateBodyFromException(Exception exception) + { + using var bufferWriter = new ArrayWriter(); + + using var jsonWriter = new Utf8JsonWriter(bufferWriter); + jsonWriter.WriteStartObject(); + jsonWriter.WritePropertyName("errors"); + jsonWriter.WriteStartArray(); + jsonWriter.WriteStartObject(); + jsonWriter.WriteString("message", exception.Message); + jsonWriter.WriteEndObject(); + jsonWriter.WriteEndArray(); + jsonWriter.WriteEndObject(); + jsonWriter.Flush(); + + return JsonDocument.Parse(bufferWriter.Body); + } public static ResponseEnumerable Create( Func createClient, - Func createRequest) + Func createRequest) { if (createClient is null) { diff --git a/src/StrawberryShake/Client/src/Transport.Http/ResponseEnumerator.cs b/src/StrawberryShake/Client/src/Transport.Http/ResponseEnumerator.cs deleted file mode 100644 index 1aa5e719538..00000000000 --- a/src/StrawberryShake/Client/src/Transport.Http/ResponseEnumerator.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.WebUtilities; -using static System.Net.Http.HttpCompletionOption; -using static System.StringComparison; -using static StrawberryShake.Properties.Resources; -using static StrawberryShake.Transport.Http.ResponseHelper; - -namespace StrawberryShake.Transport.Http; - -internal sealed class ResponseEnumerator : IAsyncEnumerator> -{ - private readonly Func _createClient; - private readonly Func _createRequest; - private readonly CancellationToken _abort; - private ConnectionContext? _context; - private MultipartReader? _reader; - private bool _completed; - private bool _disposed; - - public ResponseEnumerator( - Func createClient, - Func createRequest, - CancellationToken abort) - { - _createClient = createClient; - _createRequest = createRequest; - _abort = abort; - } - - public Response Current { get; private set; } = default!; - - public async ValueTask MoveNextAsync() - { - if (_completed || _disposed) - { - return false; - } - - if (_context is null || _reader is null) - { - var client = _createClient(); - var request = _createRequest(); - var response = - await client.SendAsync(request, ResponseHeadersRead, _abort).ConfigureAwait(false); - -#if NET5_0_OR_GREATER - var stream = await response.Content.ReadAsStreamAsync(_abort).ConfigureAwait(false); -#else - var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); -#endif - _context = new ConnectionContext(client, request, response, stream); - - if (response.Content.Headers.ContentType is { } contentType && - string.Equals(contentType.MediaType, "multipart/mixed")) - { - var boundary = contentType.Parameters.First( - t => string.Equals(t.Name, "boundary", Ordinal)); - _reader = new MultipartReader(boundary.Value!.Trim('"'), stream); - } - else - { - try - { - Exception? transportError = null; - - // If we detect that the response has a non-success status code we will - // create a transport error that will be added to the response. - if (!response.IsSuccessStatusCode) - { -#if NET5_0_OR_GREATER - transportError = - new HttpRequestException( - string.Format( - ResponseEnumerator_HttpNoSuccessStatusCode, - (int)response.StatusCode, - response.ReasonPhrase), - null, - response.StatusCode); -#else - transportError = - new HttpRequestException( - string.Format( - ResponseEnumerator_HttpNoSuccessStatusCode, - (int)response.StatusCode, - response.ReasonPhrase), - null); -#endif - } - - // We now try to parse the possible GraphQL response, this step could fail - // as the response might not be a GraphQL response. It could in some cases - // be a HTML error page. - Current = await stream.TryParseResponse(transportError, _abort) - .ConfigureAwait(false); - } - catch (Exception ex) - { - Current = new Response(CreateBodyFromException(ex), ex); - } - _completed = true; - return true; - } - } - - var multipartSection = await _reader.ReadNextSectionAsync(_abort).ConfigureAwait(false); - - if (multipartSection is null) - { - Current = default!; - return false; - } - -#if NETCOREAPP3_1_OR_GREATER - await using var body = multipartSection.Body; -#else - using var body = multipartSection.Body; -#endif - - Current = await body.TryParseResponse(null, _abort).ConfigureAwait(false); - - if (Current.Exception is not null) - { - _completed = true; - } - - return true; - } - - public ValueTask DisposeAsync() - { - if (!_disposed) - { - Current = default!; - _reader = null; - _context?.Dispose(); - _disposed = true; - } - - return default; - } - - private sealed class ConnectionContext : IDisposable - { - public ConnectionContext( - HttpClient client, - HttpRequestMessage requestMessage, - HttpResponseMessage responseMessage, - Stream stream) - { - Client = client; - RequestMessage = requestMessage; - ResponseMessage = responseMessage; - Stream = stream; - } - - public HttpClient Client { get; } - - public HttpRequestMessage RequestMessage { get; } - - public HttpResponseMessage ResponseMessage { get; } - - public Stream Stream { get; } - - public void Dispose() - { - Client.Dispose(); - RequestMessage.Dispose(); - ResponseMessage.Dispose(); - Stream.Dispose(); - } - } -} diff --git a/src/StrawberryShake/Client/src/Transport.Http/ResponseHelper.cs b/src/StrawberryShake/Client/src/Transport.Http/ResponseHelper.cs deleted file mode 100644 index 8405e5917a1..00000000000 --- a/src/StrawberryShake/Client/src/Transport.Http/ResponseHelper.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Buffers; -using System.IO; -using System.Net.Http; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using StrawberryShake.Internal; -using static StrawberryShake.ResultFields; - -namespace StrawberryShake.Transport.Http; - -internal static class ResponseHelper -{ - public static async Task> TryParseResponse( - this Stream stream, - Exception? transportError, - CancellationToken cancellationToken) - { - try - { - var document = - await JsonDocument.ParseAsync( - stream, - new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip }, - cancellationToken) - .ConfigureAwait(false); - - if (document.RootElement.ValueKind is JsonValueKind.Object) - { - var hasNext = false; - var isPatch = document.RootElement.TryGetProperty(ResultFields.Path, out _); - - if (document.RootElement.TryGetProperty(HasNext, out var hasNextProp) && - hasNextProp.GetBoolean()) - { - hasNext = true; - } - - return new Response(document, transportError, isPatch, hasNext); - } - - return new Response(document, transportError); - } - catch (Exception ex) - { - var error = transportError ?? ex; - return new Response(CreateBodyFromException(error), error); - } - } - - internal static JsonDocument CreateBodyFromException(Exception exception) - { - using var bufferWriter = new ArrayWriter(); - - using var jsonWriter = new Utf8JsonWriter(bufferWriter); - jsonWriter.WriteStartObject(); - jsonWriter.WritePropertyName("errors"); - jsonWriter.WriteStartArray(); - jsonWriter.WriteStartObject(); - jsonWriter.WriteString("message", exception.Message); - jsonWriter.WriteEndObject(); - jsonWriter.WriteEndArray(); - jsonWriter.WriteEndObject(); - jsonWriter.Flush(); - - return JsonDocument.Parse(bufferWriter.Body); - } -} diff --git a/src/StrawberryShake/Client/src/Transport.Http/StrawberryShake.Transport.Http.csproj b/src/StrawberryShake/Client/src/Transport.Http/StrawberryShake.Transport.Http.csproj index a2c86b540ab..7d520ff19b1 100644 --- a/src/StrawberryShake/Client/src/Transport.Http/StrawberryShake.Transport.Http.csproj +++ b/src/StrawberryShake/Client/src/Transport.Http/StrawberryShake.Transport.Http.csproj @@ -17,6 +17,7 @@ + diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/StarWarsOnReviewSubGraphQLSSETest.Client.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/StarWarsOnReviewSubGraphQLSSETest.Client.cs new file mode 100644 index 00000000000..893e071cacf --- /dev/null +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/StarWarsOnReviewSubGraphQLSSETest.Client.cs @@ -0,0 +1,595 @@ +// ReSharper disable BuiltInTypeReferenceStyle +// ReSharper disable RedundantNameQualifier +// ReSharper disable ArrangeObjectCreationWhenTypeEvident +// ReSharper disable UnusedType.Global +// ReSharper disable PartialTypeWithSinglePart +// ReSharper disable UnusedMethodReturnValue.Local +// ReSharper disable ConvertToAutoProperty +// ReSharper disable UnusedMember.Global +// ReSharper disable SuggestVarOrType_SimpleTypes +// ReSharper disable InconsistentNaming + +// StarWarsOnReviewSubGraphQLSSEClient + +// +#nullable enable + +namespace Microsoft.Extensions.DependencyInjection +{ + // StrawberryShake.CodeGeneration.CSharp.Generators.DependencyInjectionGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public static partial class StarWarsOnReviewSubGraphQLSSEClientServiceCollectionExtensions + { + public static global::StrawberryShake.IClientBuilder AddStarWarsOnReviewSubGraphQLSSEClient(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::StrawberryShake.ExecutionStrategy strategy = global::StrawberryShake.ExecutionStrategy.NetworkOnly) + { + var serviceCollection = new global::Microsoft.Extensions.DependencyInjection.ServiceCollection(); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => + { + ConfigureClientdefault(sp, serviceCollection, strategy); + return new ClientServiceProvider(global::Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(serviceCollection)); + }); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => new global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.State.StarWarsOnReviewSubGraphQLSSEClientStoreAccessor(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp)), global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp)), global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp)), global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp)), global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp)))); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp))); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp))); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp))); + return new global::StrawberryShake.ClientBuilder("StarWarsOnReviewSubGraphQLSSEClient", services, serviceCollection); + } + + private static global::Microsoft.Extensions.DependencyInjection.IServiceCollection ConfigureClientdefault(global::System.IServiceProvider parentServices, global::Microsoft.Extensions.DependencyInjection.ServiceCollection services, global::StrawberryShake.ExecutionStrategy strategy = global::StrawberryShake.ExecutionStrategy.NetworkOnly) + { + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton(services, sp => new global::StrawberryShake.OperationStore(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp))); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => + { + var clientFactory = global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(parentServices); + return new global::StrawberryShake.Transport.Http.HttpConnection(() => clientFactory.CreateClient("StarWarsOnReviewSubGraphQLSSEClient")); + }); + 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); + 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); + 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); + 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); + 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::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.State.OnReviewSubResultFactory>(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(sp)); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp)); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton, global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.State.OnReviewSubBuilder>(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton>(services, sp => new global::StrawberryShake.OperationExecutor(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp), () => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(sp), () => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(sp), global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp), strategy)); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton, global::StrawberryShake.Json.JsonResultPatcher>(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp)); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp)); + return services; + } + + private sealed class ClientServiceProvider : System.IServiceProvider, System.IDisposable + { + private readonly System.IServiceProvider _provider; + public ClientServiceProvider(System.IServiceProvider provider) + { + _provider = provider; + } + + public object? GetService(System.Type serviceType) + { + return _provider.GetService(serviceType); + } + + public void Dispose() + { + if (_provider is System.IDisposable d) + { + d.Dispose(); + } + } + } + } +} + +namespace StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE +{ + // StrawberryShake.CodeGeneration.CSharp.Generators.ResultTypeGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class OnReviewSubResult : global::System.IEquatable, IOnReviewSubResult + { + public OnReviewSubResult(global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.IOnReviewSub_OnReview onReview) + { + OnReview = onReview; + } + + public global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.IOnReviewSub_OnReview OnReview { get; } + + public virtual global::System.Boolean Equals(OnReviewSubResult? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other.GetType() != GetType()) + { + return false; + } + + return (OnReview.Equals(other.OnReview)); + } + + public override global::System.Boolean Equals(global::System.Object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((OnReviewSubResult)obj); + } + + public override global::System.Int32 GetHashCode() + { + unchecked + { + int hash = 5; + hash ^= 397 * OnReview.GetHashCode(); + return hash; + } + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ResultTypeGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class OnReviewSub_OnReview_Review : global::System.IEquatable, IOnReviewSub_OnReview_Review + { + public OnReviewSub_OnReview_Review(global::System.String __typename, global::System.Int32 stars, global::System.String? commentary) + { + this.__typename = __typename; + Stars = stars; + Commentary = commentary; + } + + /// + /// The name of the current Object type at runtime. + /// + public global::System.String __typename { get; } + + public global::System.Int32 Stars { get; } + + public global::System.String? Commentary { get; } + + public virtual global::System.Boolean Equals(OnReviewSub_OnReview_Review? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other.GetType() != GetType()) + { + return false; + } + + return (__typename.Equals(other.__typename)) && Stars == other.Stars && ((Commentary is null && other.Commentary is null) || Commentary != null && Commentary.Equals(other.Commentary)); + } + + public override global::System.Boolean Equals(global::System.Object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((OnReviewSub_OnReview_Review)obj); + } + + public override global::System.Int32 GetHashCode() + { + unchecked + { + int hash = 5; + hash ^= 397 * __typename.GetHashCode(); + hash ^= 397 * Stars.GetHashCode(); + if (Commentary != null) + { + hash ^= 397 * Commentary.GetHashCode(); + } + + return hash; + } + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ResultInterfaceGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial interface IOnReviewSubResult + { + public global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.IOnReviewSub_OnReview OnReview { get; } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ResultInterfaceGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial interface IOnReviewSub_OnReview + { + /// + /// The name of the current Object type at runtime. + /// + public global::System.String __typename { get; } + + public global::System.Int32 Stars { get; } + + public global::System.String? Commentary { get; } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ResultInterfaceGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial interface IOnReviewSub_OnReview_Review : IOnReviewSub_OnReview + { + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.OperationDocumentGenerator + /// + /// Represents the operation service of the OnReviewSub GraphQL operation + /// + /// subscription OnReviewSub { + /// onReview(episode: NEW_HOPE) { + /// __typename + /// stars + /// commentary + /// } + /// } + /// + /// + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class OnReviewSubSubscriptionDocument : global::StrawberryShake.IDocument + { + private OnReviewSubSubscriptionDocument() + { + } + + public static OnReviewSubSubscriptionDocument Instance { get; } = new OnReviewSubSubscriptionDocument(); + public global::StrawberryShake.OperationKind Kind => global::StrawberryShake.OperationKind.Subscription; + public global::System.ReadOnlySpan Body => new global::System.Byte[]{0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x4f, 0x6e, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x53, 0x75, 0x62, 0x20, 0x7b, 0x20, 0x6f, 0x6e, 0x52, 0x65, 0x76, 0x69, 0x65, 0x77, 0x28, 0x65, 0x70, 0x69, 0x73, 0x6f, 0x64, 0x65, 0x3a, 0x20, 0x4e, 0x45, 0x57, 0x5f, 0x48, 0x4f, 0x50, 0x45, 0x29, 0x20, 0x7b, 0x20, 0x5f, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x73, 0x74, 0x61, 0x72, 0x73, 0x20, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x72, 0x79, 0x20, 0x7d, 0x20, 0x7d}; + public global::StrawberryShake.DocumentHash Hash { get; } = new global::StrawberryShake.DocumentHash("sha1Hash", "92220fce37342d7ade3d63a2a81342eb1fb14bac"); + public override global::System.String ToString() + { +#if NETCOREAPP3_1_OR_GREATER + return global::System.Text.Encoding.UTF8.GetString(Body); +#else + return global::System.Text.Encoding.UTF8.GetString(Body.ToArray()); +#endif + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.OperationServiceGenerator + /// + /// Represents the operation service of the OnReviewSub GraphQL operation + /// + /// subscription OnReviewSub { + /// onReview(episode: NEW_HOPE) { + /// __typename + /// stars + /// commentary + /// } + /// } + /// + /// + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class OnReviewSubSubscription : global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.IOnReviewSubSubscription + { + private readonly global::StrawberryShake.IOperationExecutor _operationExecutor; + public OnReviewSubSubscription(global::StrawberryShake.IOperationExecutor operationExecutor) + { + _operationExecutor = operationExecutor ?? throw new global::System.ArgumentNullException(nameof(operationExecutor)); + } + + global::System.Type global::StrawberryShake.IOperationRequestFactory.ResultType => typeof(IOnReviewSubResult); + public global::System.IObservable> Watch(global::StrawberryShake.ExecutionStrategy? strategy = null) + { + var request = CreateRequest(); + return _operationExecutor.Watch(request, strategy); + } + + private global::StrawberryShake.OperationRequest CreateRequest() + { + return CreateRequest(null); + } + + private global::StrawberryShake.OperationRequest CreateRequest(global::System.Collections.Generic.IReadOnlyDictionary? variables) + { + return new global::StrawberryShake.OperationRequest(id: OnReviewSubSubscriptionDocument.Instance.Hash.Value, name: "OnReviewSub", document: OnReviewSubSubscriptionDocument.Instance, strategy: global::StrawberryShake.RequestStrategy.Default); + } + + global::StrawberryShake.OperationRequest global::StrawberryShake.IOperationRequestFactory.Create(global::System.Collections.Generic.IReadOnlyDictionary? variables) + { + return CreateRequest(); + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.OperationServiceInterfaceGenerator + /// + /// Represents the operation service of the OnReviewSub GraphQL operation + /// + /// subscription OnReviewSub { + /// onReview(episode: NEW_HOPE) { + /// __typename + /// stars + /// commentary + /// } + /// } + /// + /// + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial interface IOnReviewSubSubscription : global::StrawberryShake.IOperationRequestFactory + { + global::System.IObservable> Watch(global::StrawberryShake.ExecutionStrategy? strategy = null); + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ClientGenerator + /// + /// Represents the StarWarsOnReviewSubGraphQLSSEClient GraphQL client + /// + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class StarWarsOnReviewSubGraphQLSSEClient : global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.IStarWarsOnReviewSubGraphQLSSEClient + { + private readonly global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.IOnReviewSubSubscription _onReviewSub; + public StarWarsOnReviewSubGraphQLSSEClient(global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.IOnReviewSubSubscription onReviewSub) + { + _onReviewSub = onReviewSub ?? throw new global::System.ArgumentNullException(nameof(onReviewSub)); + } + + public static global::System.String ClientName => "StarWarsOnReviewSubGraphQLSSEClient"; + public global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.IOnReviewSubSubscription OnReviewSub => _onReviewSub; + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ClientInterfaceGenerator + /// + /// Represents the StarWarsOnReviewSubGraphQLSSEClient GraphQL client + /// + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial interface IStarWarsOnReviewSubGraphQLSSEClient + { + global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.IOnReviewSubSubscription OnReviewSub { get; } + } +} + +namespace StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.State +{ + // StrawberryShake.CodeGeneration.CSharp.Generators.ResultDataFactoryGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class OnReviewSubResultFactory : global::StrawberryShake.IOperationResultDataFactory + { + private readonly global::StrawberryShake.IEntityStore _entityStore; + public OnReviewSubResultFactory(global::StrawberryShake.IEntityStore entityStore) + { + _entityStore = entityStore ?? throw new global::System.ArgumentNullException(nameof(entityStore)); + } + + global::System.Type global::StrawberryShake.IOperationResultDataFactory.ResultType => typeof(global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.IOnReviewSubResult); + public OnReviewSubResult Create(global::StrawberryShake.IOperationResultDataInfo dataInfo, global::StrawberryShake.IEntityStoreSnapshot? snapshot = null) + { + if (snapshot is null) + { + snapshot = _entityStore.CurrentSnapshot; + } + + if (dataInfo is OnReviewSubResultInfo info) + { + return new OnReviewSubResult(MapNonNullableIOnReviewSub_OnReview(info.OnReview, snapshot)); + } + + throw new global::System.ArgumentException("OnReviewSubResultInfo expected."); + } + + private global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.IOnReviewSub_OnReview MapNonNullableIOnReviewSub_OnReview(global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.State.ReviewData data, global::StrawberryShake.IEntityStoreSnapshot snapshot) + { + IOnReviewSub_OnReview returnValue = default !; + if (data.__typename.Equals("Review", global::System.StringComparison.Ordinal)) + { + returnValue = new OnReviewSub_OnReview_Review(data.__typename ?? throw new global::System.ArgumentNullException(), data.Stars ?? throw new global::System.ArgumentNullException(), data.Commentary); + } + else + { + throw new global::System.NotSupportedException(); + } + + return returnValue; + } + + global::System.Object global::StrawberryShake.IOperationResultDataFactory.Create(global::StrawberryShake.IOperationResultDataInfo dataInfo, global::StrawberryShake.IEntityStoreSnapshot? snapshot) + { + return Create(dataInfo, snapshot); + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ResultInfoGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class OnReviewSubResultInfo : global::StrawberryShake.IOperationResultDataInfo + { + private readonly global::System.Collections.Generic.IReadOnlyCollection _entityIds; + private readonly global::System.UInt64 _version; + public OnReviewSubResultInfo(global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.State.ReviewData onReview, global::System.Collections.Generic.IReadOnlyCollection entityIds, global::System.UInt64 version) + { + OnReview = onReview; + _entityIds = entityIds ?? throw new global::System.ArgumentNullException(nameof(entityIds)); + _version = version; + } + + public global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.State.ReviewData OnReview { get; } + + public global::System.Collections.Generic.IReadOnlyCollection EntityIds => _entityIds; + public global::System.UInt64 Version => _version; + public global::StrawberryShake.IOperationResultDataInfo WithVersion(global::System.UInt64 version) + { + return new OnReviewSubResultInfo(OnReview, _entityIds, version); + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.JsonResultBuilderGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class OnReviewSubBuilder : global::StrawberryShake.OperationResultBuilder + { + private readonly global::StrawberryShake.IEntityStore _entityStore; + private readonly global::StrawberryShake.IEntityIdSerializer _idSerializer; + private readonly global::StrawberryShake.Serialization.ILeafValueParser _stringParser; + private readonly global::StrawberryShake.Serialization.ILeafValueParser _intParser; + public OnReviewSubBuilder(global::StrawberryShake.IEntityStore entityStore, global::StrawberryShake.IEntityIdSerializer idSerializer, global::StrawberryShake.IOperationResultDataFactory resultDataFactory, global::StrawberryShake.Serialization.ISerializerResolver serializerResolver) + { + _entityStore = entityStore ?? throw new global::System.ArgumentNullException(nameof(entityStore)); + _idSerializer = idSerializer ?? throw new global::System.ArgumentNullException(nameof(idSerializer)); + ResultDataFactory = resultDataFactory ?? throw new global::System.ArgumentNullException(nameof(resultDataFactory)); + _stringParser = serializerResolver.GetLeafValueParser("String") ?? throw new global::System.ArgumentException("No serializer for type `String` found."); + _intParser = serializerResolver.GetLeafValueParser("Int") ?? throw new global::System.ArgumentException("No serializer for type `Int` found."); + } + + protected override global::StrawberryShake.IOperationResultDataFactory ResultDataFactory { get; } + + protected override global::StrawberryShake.IOperationResultDataInfo BuildData(global::System.Text.Json.JsonElement obj) + { + var entityIds = new global::System.Collections.Generic.HashSet(); + global::StrawberryShake.IEntityStoreSnapshot snapshot = default !; + _entityStore.Update(session => + { + snapshot = session.CurrentSnapshot; + }); + return new OnReviewSubResultInfo(Deserialize_NonNullableIOnReviewSub_OnReview(global::StrawberryShake.Json.JsonElementExtensions.GetPropertyOrNull(obj, "onReview")), entityIds, snapshot.Version); + } + + private global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.State.ReviewData Deserialize_NonNullableIOnReviewSub_OnReview(global::System.Text.Json.JsonElement? obj) + { + if (!obj.HasValue) + { + throw new global::System.ArgumentNullException(); + } + + var typename = obj.Value.GetProperty("__typename").GetString(); + if (typename?.Equals("Review", global::System.StringComparison.Ordinal) ?? false) + { + return new global::StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE.State.ReviewData(typename, stars: Deserialize_NonNullableInt32(global::StrawberryShake.Json.JsonElementExtensions.GetPropertyOrNull(obj, "stars")), commentary: Deserialize_String(global::StrawberryShake.Json.JsonElementExtensions.GetPropertyOrNull(obj, "commentary"))); + } + + throw new global::System.NotSupportedException(); + } + + private global::System.String Deserialize_NonNullableString(global::System.Text.Json.JsonElement? obj) + { + if (!obj.HasValue) + { + throw new global::System.ArgumentNullException(); + } + + return _stringParser.Parse(obj.Value.GetString()!); + } + + private global::System.Int32 Deserialize_NonNullableInt32(global::System.Text.Json.JsonElement? obj) + { + if (!obj.HasValue) + { + throw new global::System.ArgumentNullException(); + } + + return _intParser.Parse(obj.Value.GetInt32()!); + } + + private global::System.String? Deserialize_String(global::System.Text.Json.JsonElement? obj) + { + if (!obj.HasValue) + { + return null; + } + + return _stringParser.Parse(obj.Value.GetString()!); + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.DataTypeGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "0.0.0.0")] + public partial class ReviewData + { + public ReviewData(global::System.String __typename, global::System.Int32? stars = default !, global::System.String? commentary = default !) + { + this.__typename = __typename ?? throw new global::System.ArgumentNullException(nameof(__typename)); + Stars = stars; + Commentary = commentary; + } + + public global::System.String __typename { get; } + + public global::System.Int32? Stars { get; } + + public global::System.String? Commentary { get; } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.EntityIdFactoryGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class StarWarsOnReviewSubGraphQLSSEClientEntityIdFactory : global::StrawberryShake.IEntityIdSerializer + { + private static readonly global::System.Text.Json.JsonWriterOptions _options = new global::System.Text.Json.JsonWriterOptions() + {Indented = false}; + public global::StrawberryShake.EntityId Parse(global::System.Text.Json.JsonElement obj) + { + global::System.String __typename = obj.GetProperty("__typename").GetString()!; + return __typename switch + { + _ => throw new global::System.NotSupportedException()}; + } + + public global::System.String Format(global::StrawberryShake.EntityId entityId) + { + return entityId.Name switch + { + _ => throw new global::System.NotSupportedException()}; + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.StoreAccessorGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class StarWarsOnReviewSubGraphQLSSEClientStoreAccessor : global::StrawberryShake.StoreAccessor + { + public StarWarsOnReviewSubGraphQLSSEClientStoreAccessor(global::StrawberryShake.IOperationStore operationStore, global::StrawberryShake.IEntityStore entityStore, global::StrawberryShake.IEntityIdSerializer entityIdSerializer, global::System.Collections.Generic.IEnumerable requestFactories, global::System.Collections.Generic.IEnumerable resultDataFactories) : base(operationStore, entityStore, entityIdSerializer, requestFactories, resultDataFactories) + { + } + } +} + + diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/StarWarsOnReviewSubGraphQLSSETest.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/StarWarsOnReviewSubGraphQLSSETest.cs new file mode 100644 index 00000000000..c719f7b6193 --- /dev/null +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/StarWarsOnReviewSubGraphQLSSETest.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.AspNetCore.Tests.Utilities; +using HotChocolate.StarWars.Models; +using HotChocolate.Subscriptions; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using StrawberryShake.Transport.WebSockets; +using Xunit; +using static HotChocolate.StarWars.Types.Subscriptions; + +namespace StrawberryShake.CodeGeneration.CSharp.Integration.StarWarsOnReviewSubGraphQLSSE; + +public class StarWarsOnReviewSubGraphQLSSETest : ServerTestBase +{ + public StarWarsOnReviewSubGraphQLSSETest(TestServerFactory serverFactory) : base(serverFactory) + { + } + + [Fact] + public async Task Execute_StarWarsOnReviewSubGraphQLSSE_Test() + { + // arrange + using var cts = new CancellationTokenSource(20_000); + var ct = cts.Token; + using var host = TestServerHelper.CreateServer( + _ => { }, + out var port); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddHttpClient( + StarWarsOnReviewSubGraphQLSSEClient.ClientName, + c => c.BaseAddress = new Uri("http://localhost:" + port + "/graphql")); + serviceCollection.AddWebSocketClient( + StarWarsOnReviewSubGraphQLSSEClient.ClientName, + c => c.Uri = new Uri("ws://localhost:" + port + "/graphql")); + serviceCollection.AddStarWarsOnReviewSubGraphQLSSEClient(); + IServiceProvider services = serviceCollection.BuildServiceProvider(); + var client = services.GetRequiredService(); + + // act + var topicEventSender = host.Services.GetRequiredService(); + var topic = Episode.NewHope; + + var connectCompletionSource = new TaskCompletionSource(); + var subscribeCompletionSource = new TaskCompletionSource(); + var session = client.OnReviewSub.Watch() + .Subscribe( + _ => connectCompletionSource.TrySetResult(), + () => subscribeCompletionSource.TrySetResult()); + + // make sure the subscription connection is successful + while (!connectCompletionSource.Task.IsCompleted) + { + await topicEventSender.SendAsync( + $"{OnReview}_{topic}", + new Review { Stars = 1, Commentary = "Commentary" }, + ct); + await Task.Delay(1_000, ct); + } + + // complete the topic of subscription from server + await topicEventSender.CompleteAsync($"{OnReview}_{topic}"); + var completedTask = await Task.WhenAny( + subscribeCompletionSource.Task, + Task.Delay(Timeout.Infinite, ct)); + + // assert + Assert.True(subscribeCompletionSource.Task == completedTask); + } +} diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/TestGeneration.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/TestGeneration.cs index fdb9f5519e1..ff48b1159b6 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/TestGeneration.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/TestGeneration.cs @@ -254,6 +254,21 @@ public void StarWarsOnReviewSubNoStore() => } }"); + [Fact] + public void StarWarsOnReviewSubGraphQLSSE() => + AssertStarWarsResult( + CreateIntegrationTest(profiles: new[] + { + new TransportProfile("default", TransportType.Http) + }), + @"subscription OnReviewSub { + onReview(episode: NEW_HOPE) { + __typename + stars + commentary + } + }"); + /* [Fact] public void StarWarsGetFriendsDeferredData() =>