Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TestServer support for GraphQLHttpClient #354 #357

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions GraphQL.Client.sln
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{89
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Example", "examples\GraphQL.Client.Example\GraphQL.Client.Example.csproj", "{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Client.TestHost", "src\GraphQL.Client.TestHost\GraphQL.Client.TestHost.csproj", "{01AE8466-3E48-4988-81F1-7F93F1531302}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -127,6 +129,10 @@ Global
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD}.Release|Any CPU.Build.0 = Release|Any CPU
{01AE8466-3E48-4988-81F1-7F93F1531302}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{01AE8466-3E48-4988-81F1-7F93F1531302}.Debug|Any CPU.Build.0 = Debug|Any CPU
{01AE8466-3E48-4988-81F1-7F93F1531302}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01AE8466-3E48-4988-81F1-7F93F1531302}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -146,6 +152,7 @@ Global
{0D307BAD-27AE-4A5D-8764-4AA2620B01E9} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C}
{7FFFEC00-D751-4FFC-9FD4-E91858F9A1C5} = {47C98B55-08F1-4428-863E-2C5C876DEEFE}
{6B13B87D-1EF4-485F-BC5D-891E2F4DA6CD} = {89AD33AB-64F6-4F82-822F-21DF7A10CEC0}
{01AE8466-3E48-4988-81F1-7F93F1531302} = {47C98B55-08F1-4428-863E-2C5C876DEEFE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {387AC1AC-F90C-4EF8-955A-04D495C75AF4}
Expand Down
15 changes: 15 additions & 0 deletions src/GraphQL.Client.Abstractions.Websocket/IWebSocketFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;

namespace GraphQL.Client.Abstractions.Websocket
{
/// <summary>
/// creates and returns a configured and connected <see cref="WebSocket"/> instance
/// </summary>
public interface IWebSocketFactory
{
Task<WebSocket> ConnectAsync(Uri webSocketUri, CancellationToken cancellationToken);
}
}
15 changes: 15 additions & 0 deletions src/GraphQL.Client.TestHost/GraphQL.Client.TestHost.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net461;netstandard2.1</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\GraphQL.Client\GraphQL.Client.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.2.0" />
</ItemGroup>

</Project>
13 changes: 13 additions & 0 deletions src/GraphQL.Client.TestHost/TestServerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using GraphQL.Client.Abstractions.Websocket;
using GraphQL.Client.Http;
using Microsoft.AspNetCore.TestHost;

namespace GraphQL.Client.TestHost
{
public static class TestServerExtensions
{
public static GraphQLHttpClient CreateGraphQLHttpClient(this TestServer testServer, GraphQLHttpClientOptions options, IGraphQLWebsocketJsonSerializer serializer)
=> new GraphQLHttpClient(options, serializer, testServer.CreateClient(), new TestServerWebSocketFactory(testServer));
}
}
27 changes: 27 additions & 0 deletions src/GraphQL.Client.TestHost/TestServerWebSocketFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using GraphQL.Client.Abstractions.Websocket;
using GraphQL.Client.Http.Websocket;
using Microsoft.AspNetCore.TestHost;

namespace GraphQL.Client.TestHost
{
public class TestServerWebSocketFactory: IWebSocketFactory
{
private readonly WebSocketClient _webSocketClient;

public TestServerWebSocketFactory(TestServer testServer)
{
_webSocketClient = testServer.CreateWebSocketClient();
_webSocketClient.ConfigureRequest = r =>
{
r.Headers["Sec-WebSocket-Protocol"] = "graphql-ws";
};
}

public Task<WebSocket> ConnectAsync(Uri webSocketUri, CancellationToken cancellationToken)
=> _webSocketClient.ConnectAsync(webSocketUri, cancellationToken);
}
}
15 changes: 10 additions & 5 deletions src/GraphQL.Client/GraphQLHttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.WebSockets;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using GraphQL.Client.Abstractions;
using GraphQL.Client.Abstractions.Websocket;
using GraphQL.Client.Http.Websocket;

[assembly:InternalsVisibleTo("GraphQL.Client.TestHost")]
[assembly:InternalsVisibleTo("GraphQL.Integration.Tests")]
namespace GraphQL.Client.Http
{
public class GraphQLHttpClient : IGraphQLClient
{
private readonly IWebSocketFactory _webSocketFactory;
private readonly Lazy<GraphQLHttpWebSocket> _lazyHttpWebSocket;
private GraphQLHttpWebSocket GraphQlHttpWebSocket => _lazyHttpWebSocket.Value;

Expand Down Expand Up @@ -63,16 +67,17 @@ public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJson
_disposeHttpClient = true;
}

public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJsonSerializer serializer, HttpClient httpClient)
public GraphQLHttpClient(GraphQLHttpClientOptions options, IGraphQLWebsocketJsonSerializer serializer, HttpClient httpClient, IWebSocketFactory? webSocketFactory = null)
{
Options = options ?? throw new ArgumentNullException(nameof(options));
JsonSerializer = serializer ?? throw new ArgumentNullException(nameof(serializer), "please configure the JSON serializer you want to use");
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_webSocketFactory = webSocketFactory ?? WebSocketFactoryHelper.GetDefaultWebSocketFactory(options);

if (!HttpClient.DefaultRequestHeaders.UserAgent.Any())
HttpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(GetType().Assembly.GetName().Name, GetType().Assembly.GetName().Version.ToString()));
_lazyHttpWebSocket = new Lazy<GraphQLHttpWebSocket>(CreateGraphQLHttpWebSocket);

_lazyHttpWebSocket = new Lazy<GraphQLHttpWebSocket>(()=>CreateGraphQLHttpWebSocket());
}

#endregion
Expand Down Expand Up @@ -171,7 +176,7 @@ private GraphQLHttpWebSocket CreateGraphQLHttpWebSocket()
if (!webSocketEndpoint.HasWebSocketScheme())
throw new InvalidOperationException($"uri \"{webSocketEndpoint}\" is not a websocket endpoint");

return new GraphQLHttpWebSocket(webSocketEndpoint, this);
return new GraphQLHttpWebSocket(webSocketEndpoint, this, _webSocketFactory);
}

#endregion
Expand Down
65 changes: 65 additions & 0 deletions src/GraphQL.Client/Websocket/ClientWebSocketFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using GraphQL.Client.Abstractions.Websocket;

namespace GraphQL.Client.Http.Websocket
{
#if NETSTANDARD
/// <summary>
/// Default web socket factory for netstandard2.0
/// </summary>
public class ClientWebSocketFactory : IWebSocketFactory
{
private readonly GraphQLHttpClientOptions _options;

public ClientWebSocketFactory(GraphQLHttpClientOptions options)
{
_options = options;
}

public async Task<WebSocket> ConnectAsync(Uri webSocketUri, CancellationToken cancellationToken)
{
var webSocket = new ClientWebSocket();
webSocket.Options.AddSubProtocol("graphql-ws");

// the following properties are not supported in Blazor WebAssembly and throw a PlatformNotSupportedException error when accessed
try
{
webSocket.Options.ClientCertificates = ((HttpClientHandler) _options.HttpMessageHandler).ClientCertificates;
}
catch (NotImplementedException)
{
Debug.WriteLine("property 'ClientWebSocketOptions.ClientCertificates' not implemented by current platform");
}
catch (PlatformNotSupportedException)
{
Debug.WriteLine("property 'ClientWebSocketOptions.ClientCertificates' not supported by current platform");
}

try
{
webSocket.Options.UseDefaultCredentials =
((HttpClientHandler) _options.HttpMessageHandler).UseDefaultCredentials;
}
catch (NotImplementedException)
{
Debug.WriteLine("property 'ClientWebSocketOptions.UseDefaultCredentials' not implemented by current platform");
}
catch (PlatformNotSupportedException)
{
Debug.WriteLine("Property 'ClientWebSocketOptions.UseDefaultCredentials' not supported by current platform");
}

_options.ConfigureWebsocketOptions(webSocket.Options);

Debug.WriteLine($"opening websocket {webSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})");
await webSocket.ConnectAsync(webSocketUri, cancellationToken);
return webSocket;
}
}
#endif
}
72 changes: 7 additions & 65 deletions src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ internal class GraphQLHttpWebSocket : IDisposable
private readonly BehaviorSubject<GraphQLWebsocketConnectionState> _stateSubject =
new BehaviorSubject<GraphQLWebsocketConnectionState>(GraphQLWebsocketConnectionState.Disconnected);
private readonly IDisposable _requestSubscription;
private readonly IWebSocketFactory _webSocketFactory;

private int _connectionAttempt = 0;
private IConnectableObservable<WebsocketMessageWrapper> _incomingMessages;
Expand All @@ -39,11 +40,7 @@ internal class GraphQLHttpWebSocket : IDisposable
private Task _initializeWebSocketTask = Task.CompletedTask;
private readonly object _initializeLock = new object();

#if NETFRAMEWORK
private WebSocket _clientWebSocket = null;
#else
private ClientWebSocket _clientWebSocket = null;
#endif
private WebSocket _clientWebSocket = null;

#endregion

Expand Down Expand Up @@ -71,11 +68,12 @@ internal class GraphQLHttpWebSocket : IDisposable

#endregion

public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client)
public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClient client, IWebSocketFactory webSocketFactory)
{
_internalCancellationToken = _internalCancellationTokenSource.Token;
_webSocketUri = webSocketUri;
_client = client;
_webSocketFactory = webSocketFactory;
_buffer = new ArraySegment<byte>(new byte[8192]);
IncomingMessageStream = GetMessageStream();

Expand Down Expand Up @@ -182,7 +180,7 @@ public IObservable<GraphQLResponse<TResponse>> CreateSubscriptionStream<TRespons
catch (OperationCanceledException) { }
})
);

Debug.WriteLine($"sending start message on subscription {startRequest.Id}");
// send subscription request
try
Expand Down Expand Up @@ -379,59 +377,6 @@ public Task InitializeWebSocket()

// else (re-)create websocket and connect
_clientWebSocket?.Dispose();

#if NETFRAMEWORK
// fix websocket not supported on win 7 using
// https://github.com/PingmanTools/System.Net.WebSockets.Client.Managed
_clientWebSocket = SystemClientWebSocket.CreateClientWebSocket();
switch (_clientWebSocket) {
case ClientWebSocket nativeWebSocket:
nativeWebSocket.Options.AddSubProtocol("graphql-ws");
nativeWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates;
nativeWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials;
Options.ConfigureWebsocketOptions(nativeWebSocket.Options);
break;
case System.Net.WebSockets.Managed.ClientWebSocket managedWebSocket:
managedWebSocket.Options.AddSubProtocol("graphql-ws");
managedWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates;
managedWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials;
break;
default:
throw new NotSupportedException($"unknown websocket type {_clientWebSocket.GetType().Name}");
}
#else
_clientWebSocket = new ClientWebSocket();
_clientWebSocket.Options.AddSubProtocol("graphql-ws");

// the following properties are not supported in Blazor WebAssembly and throw a PlatformNotSupportedException error when accessed
try
{
_clientWebSocket.Options.ClientCertificates = ((HttpClientHandler)Options.HttpMessageHandler).ClientCertificates;
}
catch (NotImplementedException)
{
Debug.WriteLine("property 'ClientWebSocketOptions.ClientCertificates' not implemented by current platform");
}
catch (PlatformNotSupportedException)
{
Debug.WriteLine("property 'ClientWebSocketOptions.ClientCertificates' not supported by current platform");
}

try
{
_clientWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)Options.HttpMessageHandler).UseDefaultCredentials;
}
catch (NotImplementedException)
{
Debug.WriteLine("property 'ClientWebSocketOptions.UseDefaultCredentials' not implemented by current platform");
}
catch (PlatformNotSupportedException)
{
Debug.WriteLine("Property 'ClientWebSocketOptions.UseDefaultCredentials' not supported by current platform");
}

Options.ConfigureWebsocketOptions(_clientWebSocket.Options);
#endif
return _initializeWebSocketTask = ConnectAsync(_internalCancellationToken);
}
}
Expand All @@ -442,8 +387,7 @@ private async Task ConnectAsync(CancellationToken token)
{
await BackOff();
_stateSubject.OnNext(GraphQLWebsocketConnectionState.Connecting);
Debug.WriteLine($"opening websocket {_clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})");
await _clientWebSocket.ConnectAsync(_webSocketUri, token);
_clientWebSocket = await _webSocketFactory.ConnectAsync(_webSocketUri, token);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rose-a @andreyleskov regarding 4. Tests stability:

I recommend you ALWAYS use ConfigureAwait(false) in library code especially when using XUnit package as a test framework because of Xunit.Sdk.MaxConcurrencySyncContext. In my case maximumConcurrencyLevel was 6 and some tests randomly failed. So please look through entire codebase and add ConfigureAwait(false) to awaitable calls. Perhaps it will help.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for the comment, it makes sense from my point of view. This PR is following the existing await style and focuses on just WebSockets. I assume repository owners prefer to do repository-wide changes as await style in a dedicated PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im on to this in in #370...

_stateSubject.OnNext(GraphQLWebsocketConnectionState.Connected);
Debug.WriteLine($"connection established on websocket {_clientWebSocket.GetHashCode()}, invoking Options.OnWebsocketConnected()");
await (Options.OnWebsocketConnected?.Invoke(_client) ?? Task.CompletedTask);
Expand Down Expand Up @@ -479,7 +423,7 @@ private async Task ConnectAsync(CancellationToken token)
Debug.WriteLine($"new incoming message stream {_incomingMessages.GetHashCode()} created");

_incomingMessagesConnection = new CompositeDisposable(maintenanceSubscription, connection);

var initRequest = new GraphQLWebSocketRequest
{
Type = GraphQLWebSocketMessageType.GQL_CONNECTION_INIT,
Expand Down Expand Up @@ -608,8 +552,6 @@ private async Task<WebsocketMessageWrapper> ReceiveWebsocketMessagesAsync()
return response;

case WebSocketMessageType.Close:
var closeResponse = await _client.JsonSerializer.DeserializeToWebsocketResponseWrapperAsync(ms);
closeResponse.MessageBytes = ms.ToArray();
Debug.WriteLine($"Connection closed by the server.");
throw new Exception("Connection closed by the server.");

Expand Down
Loading