From 194c152b94b2b52510e09b9f3e8241f96732624f Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Sun, 15 Mar 2020 23:18:19 +0100 Subject: [PATCH 1/2] allow access to HttpResponseHeaders --- src/GraphQL.Client/GraphQLHttpClient.cs | 61 +++++++++++++++++-- src/GraphQL.Client/GraphQLHttpResponse.cs | 12 +++- .../QueryAndMutationTests/Base.cs | 15 +++++ 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index 30e85fd0..4e9cbf04 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -64,10 +65,12 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient #region IGraphQLClient /// - public Task> SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { - return Options.UseWebSocketForQueriesAndMutations - ? this.graphQlHttpWebSocket.SendRequest(request, cancellationToken) - : this.SendHttpPostRequestAsync(request, cancellationToken); + public async Task> SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { + if(Options.UseWebSocketForQueriesAndMutations) + return await this.graphQlHttpWebSocket.SendRequest(request, cancellationToken); + + var response = await this.SendHttpPostRequestAsync(request, cancellationToken); + return response.Response; } /// @@ -114,18 +117,64 @@ public IObservable> CreateSubscriptionStream public Task InitializeWebsocketConnection() => graphQlHttpWebSocket.InitializeWebSocket(); + /// + /// Sends a query to the GraphQL server and deserializes the response. Provides access to the HTTP response headers. This method will never utilize the websocket connection! + /// + /// + /// + /// + /// + public Task> SendQueryHttpAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { + return this.SendHttpPostRequestAsync(request, cancellationToken); + } + /// + public Task> SendQueryHttpAsync(GraphQLRequest request, + Func defineResponseType, CancellationToken cancellationToken = default) + => SendQueryHttpAsync(request, cancellationToken); + /// + public Task> SendQueryHttpAsync(string query, object? variables = null, + string? operationName = null, Func defineResponseType = null, CancellationToken cancellationToken = default) { + return SendQueryHttpAsync(new GraphQLRequest(query, variables, operationName), cancellationToken: cancellationToken); + } + + /// + /// Sends a mutation to the GraphQL server and deserializes the response. Provides access to the HTTP response headers. This method will never utilize the websocket connection! + /// + /// + /// + /// + /// + public Task> SendMutationHttpAsync(GraphQLRequest request, + CancellationToken cancellationToken = default) + => SendQueryHttpAsync(request, cancellationToken); + /// + public Task> SendMutationHttpAsync(GraphQLRequest request, + Func defineResponseType, CancellationToken cancellationToken = default) + => SendQueryHttpAsync(request, cancellationToken); + /// + public Task> SendMutationHttpAsync(string query, object? variables = null, + string? operationName = null, Func defineResponseType = null, CancellationToken cancellationToken = default) { + return SendQueryHttpAsync(new GraphQLRequest(query, variables, operationName), cancellationToken: cancellationToken); + } + #region Private Methods - private async Task> SendHttpPostRequestAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { + private async Task> SendHttpPostRequestAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { var preprocessedRequest = await Options.PreprocessRequest(request, this); using var httpRequestMessage = this.GenerateHttpRequestMessage(preprocessedRequest); using var httpResponseMessage = await this.HttpClient.SendAsync(httpRequestMessage, cancellationToken); if (!httpResponseMessage.IsSuccessStatusCode) { throw new GraphQLHttpException(httpResponseMessage); } + + var response = new GraphQLHttpResponse { + ResponseHeaders = httpResponseMessage.Headers, + StatusCode = httpResponseMessage.StatusCode + }; var bodyStream = await httpResponseMessage.Content.ReadAsStreamAsync(); - return await JsonSerializer.DeserializeFromUtf8StreamAsync(bodyStream, cancellationToken); + response.Response = await JsonSerializer.DeserializeFromUtf8StreamAsync(bodyStream, cancellationToken); + return response; } private HttpRequestMessage GenerateHttpRequestMessage(GraphQLRequest request) { diff --git a/src/GraphQL.Client/GraphQLHttpResponse.cs b/src/GraphQL.Client/GraphQLHttpResponse.cs index 1dbfea30..30282b00 100644 --- a/src/GraphQL.Client/GraphQLHttpResponse.cs +++ b/src/GraphQL.Client/GraphQLHttpResponse.cs @@ -1,5 +1,15 @@ +using System.Net; +using System.Net.Http.Headers; + namespace GraphQL.Client.Http { - public class GraphQLHttpResponse : GraphQLResponse { + public class GraphQLHttpResponse { + public GraphQLHttpResponse() + { + } + + public GraphQLResponse Response { get; set; } + public HttpResponseHeaders ResponseHeaders { get; set; } + public HttpStatusCode StatusCode { get; set; } } } diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index e60c26f0..51328c7b 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -1,3 +1,5 @@ +using System; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -46,6 +48,19 @@ public async void QueryTheory(int id, string name) { Assert.Equal(name, response.Data.Human.Name); } + [Theory] + [ClassData(typeof(StarWarsHumans))] + public async void QueryHttpTheory(int id, string name) { + var graphQLRequest = new GraphQLRequest($"{{ human(id: \"{id}\") {{ name }} }}"); + var httpResponse = await StarWarsClient.SendQueryHttpAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }); + + httpResponse.Response.Errors.Should().BeNull(); + httpResponse.Response.Data.Human.Name.Should().Be(name); + + httpResponse.StatusCode.Should().BeEquivalentTo(HttpStatusCode.OK); + httpResponse.ResponseHeaders.Date.Should().BeCloseTo(DateTimeOffset.Now, TimeSpan.FromMinutes(1)); + } + [Theory] [ClassData(typeof(StarWarsHumans))] public async void QueryWithDynamicReturnTypeTheory(int id, string name) { From 78eac27a30a440bf27fe7806b9c6c8ac5041bffd Mon Sep 17 00:00:00 2001 From: Alexander Rose Date: Fri, 20 Mar 2020 21:01:04 +0100 Subject: [PATCH 2/2] let GraphQLHttpResponse inherit from GraphQLResponse --- src/GraphQL.Client/GraphQLHttpClient.cs | 58 ++----------------- src/GraphQL.Client/GraphQLHttpResponse.cs | 31 ++++++++-- .../QueryAndMutationTests/Base.cs | 14 +++-- 3 files changed, 42 insertions(+), 61 deletions(-) diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs index 4e9cbf04..5f000dda 100644 --- a/src/GraphQL.Client/GraphQLHttpClient.cs +++ b/src/GraphQL.Client/GraphQLHttpClient.cs @@ -66,11 +66,10 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient /// public async Task> SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { - if(Options.UseWebSocketForQueriesAndMutations) + if (Options.UseWebSocketForQueriesAndMutations) return await this.graphQlHttpWebSocket.SendRequest(request, cancellationToken); - - var response = await this.SendHttpPostRequestAsync(request, cancellationToken); - return response.Response; + + return await this.SendHttpPostRequestAsync(request, cancellationToken); } /// @@ -116,47 +115,7 @@ public IObservable> CreateSubscriptionStream /// public Task InitializeWebsocketConnection() => graphQlHttpWebSocket.InitializeWebSocket(); - - /// - /// Sends a query to the GraphQL server and deserializes the response. Provides access to the HTTP response headers. This method will never utilize the websocket connection! - /// - /// - /// - /// - /// - public Task> SendQueryHttpAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { - return this.SendHttpPostRequestAsync(request, cancellationToken); - } - /// - public Task> SendQueryHttpAsync(GraphQLRequest request, - Func defineResponseType, CancellationToken cancellationToken = default) - => SendQueryHttpAsync(request, cancellationToken); - /// - public Task> SendQueryHttpAsync(string query, object? variables = null, - string? operationName = null, Func defineResponseType = null, CancellationToken cancellationToken = default) { - return SendQueryHttpAsync(new GraphQLRequest(query, variables, operationName), cancellationToken: cancellationToken); - } - - /// - /// Sends a mutation to the GraphQL server and deserializes the response. Provides access to the HTTP response headers. This method will never utilize the websocket connection! - /// - /// - /// - /// - /// - public Task> SendMutationHttpAsync(GraphQLRequest request, - CancellationToken cancellationToken = default) - => SendQueryHttpAsync(request, cancellationToken); - /// - public Task> SendMutationHttpAsync(GraphQLRequest request, - Func defineResponseType, CancellationToken cancellationToken = default) - => SendQueryHttpAsync(request, cancellationToken); - /// - public Task> SendMutationHttpAsync(string query, object? variables = null, - string? operationName = null, Func defineResponseType = null, CancellationToken cancellationToken = default) { - return SendQueryHttpAsync(new GraphQLRequest(query, variables, operationName), cancellationToken: cancellationToken); - } - + #region Private Methods private async Task> SendHttpPostRequestAsync(GraphQLRequest request, CancellationToken cancellationToken = default) { @@ -166,15 +125,10 @@ private async Task> SendHttpPostRequestAsync { - ResponseHeaders = httpResponseMessage.Headers, - StatusCode = httpResponseMessage.StatusCode - }; var bodyStream = await httpResponseMessage.Content.ReadAsStreamAsync(); - response.Response = await JsonSerializer.DeserializeFromUtf8StreamAsync(bodyStream, cancellationToken); - return response; + var response = await JsonSerializer.DeserializeFromUtf8StreamAsync(bodyStream, cancellationToken); + return response.ToGraphQLHttpResponse(httpResponseMessage.Headers, httpResponseMessage.StatusCode); } private HttpRequestMessage GenerateHttpRequestMessage(GraphQLRequest request) { diff --git a/src/GraphQL.Client/GraphQLHttpResponse.cs b/src/GraphQL.Client/GraphQLHttpResponse.cs index 30282b00..b14d8790 100644 --- a/src/GraphQL.Client/GraphQLHttpResponse.cs +++ b/src/GraphQL.Client/GraphQLHttpResponse.cs @@ -1,15 +1,36 @@ +using System; using System.Net; using System.Net.Http.Headers; namespace GraphQL.Client.Http { - public class GraphQLHttpResponse { - public GraphQLHttpResponse() - { + public class GraphQLHttpResponse: GraphQLResponse { + public GraphQLHttpResponse(GraphQLResponse response, HttpResponseHeaders responseHeaders, HttpStatusCode statusCode) { + Data = response.Data; + Errors = response.Errors; + Extensions = response.Extensions; + ResponseHeaders = responseHeaders; + StatusCode = statusCode; } - - public GraphQLResponse Response { get; set; } + public HttpResponseHeaders ResponseHeaders { get; set; } public HttpStatusCode StatusCode { get; set; } } + + public static class GraphQLResponseExtensions { + public static GraphQLHttpResponse ToGraphQLHttpResponse(this GraphQLResponse response, HttpResponseHeaders responseHeaders, HttpStatusCode statusCode) { + return new GraphQLHttpResponse(response, responseHeaders, statusCode); + } + + /// + /// Casts to . Throws ig the cast fails. + /// + /// + /// + /// is not a + /// + public static GraphQLHttpResponse AsGraphQLHttpResponse(this GraphQLResponse response) { + return (GraphQLHttpResponse) response; + } + } } diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs index 51328c7b..e55fd2c6 100644 --- a/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs +++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests/Base.cs @@ -50,12 +50,18 @@ public async void QueryTheory(int id, string name) { [Theory] [ClassData(typeof(StarWarsHumans))] - public async void QueryHttpTheory(int id, string name) { + public async void QueryAsHttpResponseTheory(int id, string name) { var graphQLRequest = new GraphQLRequest($"{{ human(id: \"{id}\") {{ name }} }}"); - var httpResponse = await StarWarsClient.SendQueryHttpAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }); + var responseType = new {Human = new {Name = string.Empty}}; + var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => responseType ); - httpResponse.Response.Errors.Should().BeNull(); - httpResponse.Response.Data.Human.Name.Should().Be(name); + FluentActions.Invoking(() => response.AsGraphQLHttpResponse()).Should() + .NotThrow("because the returned object is a GraphQLHttpResponse"); + + var httpResponse = response.AsGraphQLHttpResponse(); + + httpResponse.Errors.Should().BeNull(); + httpResponse.Data.Human.Name.Should().Be(name); httpResponse.StatusCode.Should().BeEquivalentTo(HttpStatusCode.OK); httpResponse.ResponseHeaders.Date.Should().BeCloseTo(DateTimeOffset.Now, TimeSpan.FromMinutes(1));