diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index ebfa343e..bf61490a 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -1007,7 +1007,7 @@ protected virtual Task WriteJsonResponseAsync(HttpContext context, Http /// /// Gets a list of WebSocket sub-protocols supported. /// - protected virtual IEnumerable SupportedWebSocketSubProtocols => _supportedSubProtocols; + protected virtual IEnumerable SupportedWebSocketSubProtocols => _options.WebSockets.SupportedWebSocketSubProtocols; /// /// Creates an , a WebSocket message pump. diff --git a/src/Transports.AspNetCore/WebSockets/BaseSubscriptionServer.cs b/src/Transports.AspNetCore/WebSockets/BaseSubscriptionServer.cs index 7ba030bd..ca0186e7 100644 --- a/src/Transports.AspNetCore/WebSockets/BaseSubscriptionServer.cs +++ b/src/Transports.AspNetCore/WebSockets/BaseSubscriptionServer.cs @@ -259,10 +259,33 @@ protected virtual Task OnNotAuthorizedPolicyAsync(OperationMessage message, Auth ///

/// Otherwise, the connection is acknowledged via , /// is called to indicate that this WebSocket connection is ready to accept requests, - /// and keep-alive messages are sent via if configured to do so. - /// Keep-alive messages are only sent if no messages have been sent over the WebSockets connection for the - /// length of time configured in . + /// and is called to start sending keep-alive messages if configured to do so. ///
+ protected virtual async Task OnConnectionInitAsync(OperationMessage message) + { + if (!await AuthorizeAsync(message)) + { + return; + } + await OnConnectionAcknowledgeAsync(message); + if (TryInitialize() == false) + return; + + _ = OnKeepAliveLoopAsync(); + } + + /// + /// Executes when the client is attempting to initialize the connection. + ///

+ /// By default, this first checks to validate that the + /// request has passed authentication. If validation fails, the connection is closed with an Access + /// Denied message. + ///

+ /// Otherwise, the connection is acknowledged via , + /// is called to indicate that this WebSocket connection is ready to accept requests, + /// and is called to start sending keep-alive messages if configured to do so. + ///
+ [Obsolete($"Please use the {nameof(OnConnectionInitAsync)}(message) and {nameof(OnKeepAliveLoopAsync)} methods instead. This method will be removed in a future version of this library.")] protected virtual async Task OnConnectionInitAsync(OperationMessage message, bool smartKeepAlive) { if (!await AuthorizeAsync(message)) @@ -277,12 +300,49 @@ protected virtual async Task OnConnectionInitAsync(OperationMessage message, boo if (keepAliveTimeout > TimeSpan.Zero) { if (smartKeepAlive) - _ = StartSmartKeepAliveLoopAsync(); + _ = OnKeepAliveLoopAsync(keepAliveTimeout, KeepAliveMode.Timeout); else - _ = StartKeepAliveLoopAsync(); + _ = OnKeepAliveLoopAsync(keepAliveTimeout, KeepAliveMode.Interval); + } + } + + /// + /// Starts sending keep-alive messages if configured to do so. Inspects the configured + /// and passes control to + /// if keep-alive messages are enabled. + /// + protected virtual Task OnKeepAliveLoopAsync() + { + return OnKeepAliveLoopAsync( + _options.KeepAliveTimeout ?? DefaultKeepAliveTimeout, + _options.KeepAliveMode); + } + + /// + /// Sends keep-alive messages according to the specified timeout period and method. + /// See for implementation details for each supported mode. + /// + protected virtual async Task OnKeepAliveLoopAsync(TimeSpan keepAliveTimeout, KeepAliveMode keepAliveMode) + { + if (keepAliveTimeout <= TimeSpan.Zero) + return; + + switch (keepAliveMode) + { + case KeepAliveMode.Default: + case KeepAliveMode.Timeout: + await StartSmartKeepAliveLoopAsync(); + break; + case KeepAliveMode.Interval: + await StartDumbKeepAliveLoopAsync(); + break; + case KeepAliveMode.TimeoutWithPayload: + throw new NotImplementedException($"{nameof(KeepAliveMode.TimeoutWithPayload)} is not implemented within the {nameof(BaseSubscriptionServer)} class."); + default: + throw new ArgumentOutOfRangeException(nameof(keepAliveMode)); } - async Task StartKeepAliveLoopAsync() + async Task StartDumbKeepAliveLoopAsync() { while (!CancellationToken.IsCancellationRequested) { diff --git a/src/Transports.AspNetCore/WebSockets/GraphQLWebSocketOptions.cs b/src/Transports.AspNetCore/WebSockets/GraphQLWebSocketOptions.cs index f6444d97..858d26bf 100644 --- a/src/Transports.AspNetCore/WebSockets/GraphQLWebSocketOptions.cs +++ b/src/Transports.AspNetCore/WebSockets/GraphQLWebSocketOptions.cs @@ -25,6 +25,12 @@ public class GraphQLWebSocketOptions /// public TimeSpan? KeepAliveTimeout { get; set; } + /// + /// Gets or sets the keep-alive mode used for websocket subscriptions. + /// This property is only applicable when using the GraphQLWs protocol. + /// + public KeepAliveMode KeepAliveMode { get; set; } = KeepAliveMode.Default; + /// /// The amount of time to wait to attempt a graceful teardown of the WebSockets protocol. /// A value of indicates the default value defined by the implementation. @@ -42,4 +48,17 @@ public class GraphQLWebSocketOptions /// Disconnects a subscription from the client in the event of any GraphQL errors during a subscription. The default value is . /// public bool DisconnectAfterAnyError { get; set; } + + /// + /// The list of supported WebSocket sub-protocols. + /// Defaults to and . + /// Adding other sub-protocols require the method + /// to be overridden to handle the new sub-protocol. + /// + /// + /// When the is set to , you may wish to remove + /// from this list to prevent clients from using + /// protocols which do not support the keep-alive mode. + /// + public List SupportedWebSocketSubProtocols { get; set; } = [GraphQLWs.SubscriptionServer.SubProtocol, SubscriptionsTransportWs.SubscriptionServer.SubProtocol]; } diff --git a/src/Transports.AspNetCore/WebSockets/GraphQLWs/PingPayload.cs b/src/Transports.AspNetCore/WebSockets/GraphQLWs/PingPayload.cs new file mode 100644 index 00000000..a673cf8e --- /dev/null +++ b/src/Transports.AspNetCore/WebSockets/GraphQLWs/PingPayload.cs @@ -0,0 +1,12 @@ +namespace GraphQL.Server.Transports.AspNetCore.WebSockets.GraphQLWs; + +/// +/// The payload of the ping message. +/// +public class PingPayload +{ + /// + /// The unique identifier of the ping message. + /// + public string? id { get; set; } +} diff --git a/src/Transports.AspNetCore/WebSockets/GraphQLWs/SubscriptionServer.cs b/src/Transports.AspNetCore/WebSockets/GraphQLWs/SubscriptionServer.cs index 3235d16c..728a3391 100644 --- a/src/Transports.AspNetCore/WebSockets/GraphQLWs/SubscriptionServer.cs +++ b/src/Transports.AspNetCore/WebSockets/GraphQLWs/SubscriptionServer.cs @@ -4,6 +4,11 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets.GraphQLWs; public class SubscriptionServer : BaseSubscriptionServer { private readonly IWebSocketAuthenticationService? _authenticationService; + private readonly IGraphQLSerializer _serializer; + private readonly GraphQLWebSocketOptions _options; + private DateTime _lastPongReceivedUtc; + private string? _lastPingId; + private readonly object _lastPingLock = new(); /// /// The WebSocket sub-protocol used for this protocol. @@ -67,6 +72,8 @@ public SubscriptionServer( UserContextBuilder = userContextBuilder ?? throw new ArgumentNullException(nameof(userContextBuilder)); Serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); _authenticationService = authenticationService; + _serializer = serializer; + _options = options; } /// @@ -90,7 +97,9 @@ public override async Task OnMessageReceivedAsync(OperationMessage message) } else { +#pragma warning disable CS0618 // Type or member is obsolete await OnConnectionInitAsync(message, true); +#pragma warning restore CS0618 // Type or member is obsolete } return; } @@ -113,6 +122,69 @@ public override async Task OnMessageReceivedAsync(OperationMessage message) } } + /// + [Obsolete($"Please use the {nameof(OnConnectionInitAsync)} and {nameof(OnKeepAliveLoopAsync)} methods instead. This method will be removed in a future version of this library.")] + protected override Task OnConnectionInitAsync(OperationMessage message, bool smartKeepAlive) + { + if (smartKeepAlive) + return base.OnConnectionInitAsync(message); + else + return base.OnConnectionInitAsync(message, smartKeepAlive); + } + + /// + protected override Task OnKeepAliveLoopAsync(TimeSpan keepAliveTimeout, KeepAliveMode keepAliveMode) + { + if (keepAliveMode == KeepAliveMode.TimeoutWithPayload) + { + if (keepAliveTimeout <= TimeSpan.Zero) + return Task.CompletedTask; + return SecureKeepAliveLoopAsync(keepAliveTimeout, keepAliveTimeout); + } + return base.OnKeepAliveLoopAsync(keepAliveTimeout, keepAliveMode); + + // pingInterval is the time since the last pong was received before sending a new ping + // pongInterval is the time to wait for a pong after a ping was sent before forcibly closing the connection + async Task SecureKeepAliveLoopAsync(TimeSpan pingInterval, TimeSpan pongInterval) + { + lock (_lastPingLock) + _lastPongReceivedUtc = DateTime.UtcNow; + while (!CancellationToken.IsCancellationRequested) + { + // Wait for the next ping interval + TimeSpan interval; + var now = DateTime.UtcNow; + DateTime lastPongReceivedUtc; + lock (_lastPingLock) + { + lastPongReceivedUtc = _lastPongReceivedUtc; + } + var nextPing = _lastPongReceivedUtc.Add(pingInterval); + interval = nextPing.Subtract(now); + if (interval > TimeSpan.Zero) // could easily be zero or less, if pongInterval is equal or greater than pingInterval + await Task.Delay(interval, CancellationToken); + + // Send a new ping message + await OnSendKeepAliveAsync(); + + // Wait for the pong response + await Task.Delay(pongInterval, CancellationToken); + bool abort; + lock (_lastPingLock) + { + abort = _lastPongReceivedUtc == lastPongReceivedUtc; + } + if (abort) + { + // Forcibly close the connection if the client has not responded to the keep-alive message. + // Do not send a close message to the client or wait for a response. + Connection.HttpContext.Abort(); + return; + } + } + } + } + /// /// Pong is a required response to a ping, and also a unidirectional keep-alive packet, /// whereas ping is a bidirectional keep-alive packet. @@ -129,11 +201,46 @@ protected virtual Task OnPingAsync(OperationMessage message) /// Executes when a pong message is received. /// protected virtual Task OnPongAsync(OperationMessage message) - => Task.CompletedTask; + { + if (_options.KeepAliveMode == KeepAliveMode.TimeoutWithPayload) + { + try + { + var pingId = _serializer.ReadNode(message.Payload)?.id; + lock (_lastPingLock) + { + if (_lastPingId == pingId) + _lastPongReceivedUtc = DateTime.UtcNow; + } + } + catch { } // ignore deserialization errors in case the pong message does not match the expected format + } + return Task.CompletedTask; + } /// protected override Task OnSendKeepAliveAsync() - => Connection.SendMessageAsync(_pongMessage); + { + if (_options.KeepAliveMode == KeepAliveMode.TimeoutWithPayload) + { + var lastPingId = Guid.NewGuid().ToString("N"); + lock (_lastPingLock) + { + _lastPingId = lastPingId; + } + return Connection.SendMessageAsync( + new() + { + Type = MessageType.Ping, + Payload = new PingPayload { id = lastPingId } + } + ); + } + else + { + return Connection.SendMessageAsync(_pongMessage); + } + } private static readonly OperationMessage _connectionAckMessage = new() { Type = MessageType.ConnectionAck }; /// diff --git a/src/Transports.AspNetCore/WebSockets/KeepAliveMode.cs b/src/Transports.AspNetCore/WebSockets/KeepAliveMode.cs new file mode 100644 index 00000000..2b02463e --- /dev/null +++ b/src/Transports.AspNetCore/WebSockets/KeepAliveMode.cs @@ -0,0 +1,36 @@ +namespace GraphQL.Server.Transports.AspNetCore.WebSockets; + +/// +/// Specifies the mode of keep-alive behavior. +/// +public enum KeepAliveMode +{ + /// + /// Same as : Sends a unidirectional keep-alive message when no message has been received within the specified timeout period. + /// + Default = 0, + + /// + /// Sends a unidirectional keep-alive message when no message has been received within the specified timeout period. + /// + Timeout = 1, + + /// + /// Sends a unidirectional keep-alive message at a fixed interval, regardless of message activity. + /// + Interval = 2, + + /// + /// Sends a Ping message with a payload after the specified timeout from the last received Pong, + /// and waits for a corresponding Pong response. Requires that the client reflects the payload + /// in the response. Forcibly disconnects the client if the client does not respond with a Pong + /// message within the specified timeout. This means that a dead connection will be closed after + /// a maximum of double the period. + /// + /// + /// This mode is particularly useful when backpressure causes subscription messages to be delayed + /// due to a slow or unresponsive client connection. The server can detect that the client is not + /// processing messages in a timely manner and disconnect the client to free up resources. + /// + TimeoutWithPayload = 3, +} diff --git a/src/Transports.AspNetCore/WebSockets/SubscriptionsTransportWs/SubscriptionServer.cs b/src/Transports.AspNetCore/WebSockets/SubscriptionsTransportWs/SubscriptionServer.cs index b0cf7c53..ccdebe6b 100644 --- a/src/Transports.AspNetCore/WebSockets/SubscriptionsTransportWs/SubscriptionServer.cs +++ b/src/Transports.AspNetCore/WebSockets/SubscriptionsTransportWs/SubscriptionServer.cs @@ -85,7 +85,9 @@ public override async Task OnMessageReceivedAsync(OperationMessage message) } else { +#pragma warning disable CS0618 // Type or member is obsolete await OnConnectionInitAsync(message, false); +#pragma warning restore CS0618 // Type or member is obsolete } return; } @@ -108,6 +110,26 @@ public override async Task OnMessageReceivedAsync(OperationMessage message) } } + /// + [Obsolete($"Please use the {nameof(OnConnectionInitAsync)} and {nameof(OnKeepAliveLoopAsync)} methods instead. This method will be removed in a future version of this library.")] + protected override Task OnConnectionInitAsync(OperationMessage message, bool smartKeepAlive) + { + if (!smartKeepAlive) + return base.OnConnectionInitAsync(message); + else + return base.OnConnectionInitAsync(message, smartKeepAlive); + } + + /// + /// + /// This implementation overrides to + /// as this protocol does not support the other modes. Override this method to support your own implementation. + /// + protected override Task OnKeepAliveLoopAsync(TimeSpan keepAliveTimeout, KeepAliveMode keepAliveMode) + => base.OnKeepAliveLoopAsync( + keepAliveTimeout, + KeepAliveMode.Interval); + private static readonly OperationMessage _keepAliveMessage = new() { Type = MessageType.GQL_CONNECTION_KEEP_ALIVE }; /// protected override Task OnSendKeepAliveAsync() diff --git a/tests/ApiApprovalTests/net50+net60+net80/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/net50+net60+net80/GraphQL.Server.Transports.AspNetCore.approved.txt index 066e8a44..4039cd6e 100644 --- a/tests/ApiApprovalTests/net50+net60+net80/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/net50+net60+net80/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -274,8 +274,13 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets public virtual System.Threading.Tasks.Task InitializeConnectionAsync() { } protected virtual System.Threading.Tasks.Task OnCloseConnectionAsync() { } protected abstract System.Threading.Tasks.Task OnConnectionAcknowledgeAsync(GraphQL.Transport.OperationMessage message); + protected virtual System.Threading.Tasks.Task OnConnectionInitAsync(GraphQL.Transport.OperationMessage message) { } + [System.Obsolete("Please use the OnConnectionInitAsync(message) and OnKeepAliveLoopAsync methods in" + + "stead. This method will be removed in a future version of this library.")] protected virtual System.Threading.Tasks.Task OnConnectionInitAsync(GraphQL.Transport.OperationMessage message, bool smartKeepAlive) { } protected virtual System.Threading.Tasks.Task OnConnectionInitWaitTimeoutAsync() { } + protected virtual System.Threading.Tasks.Task OnKeepAliveLoopAsync() { } + protected virtual System.Threading.Tasks.Task OnKeepAliveLoopAsync(System.TimeSpan keepAliveTimeout, GraphQL.Server.Transports.AspNetCore.WebSockets.KeepAliveMode keepAliveMode) { } public abstract System.Threading.Tasks.Task OnMessageReceivedAsync(GraphQL.Transport.OperationMessage message); protected virtual System.Threading.Tasks.Task OnNotAuthenticatedAsync(GraphQL.Transport.OperationMessage message) { } protected virtual System.Threading.Tasks.Task OnNotAuthorizedPolicyAsync(GraphQL.Transport.OperationMessage message, Microsoft.AspNetCore.Authorization.AuthorizationResult result) { } @@ -300,7 +305,9 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets public bool DisconnectAfterAnyError { get; set; } public bool DisconnectAfterErrorEvent { get; set; } public System.TimeSpan? DisconnectionTimeout { get; set; } + public GraphQL.Server.Transports.AspNetCore.WebSockets.KeepAliveMode KeepAliveMode { get; set; } public System.TimeSpan? KeepAliveTimeout { get; set; } + public System.Collections.Generic.List SupportedWebSocketSubProtocols { get; set; } } public interface IOperationMessageProcessor : System.IDisposable { @@ -321,6 +328,13 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets System.Threading.Tasks.Task ExecuteAsync(GraphQL.Server.Transports.AspNetCore.WebSockets.IOperationMessageProcessor operationMessageProcessor); System.Threading.Tasks.Task SendMessageAsync(GraphQL.Transport.OperationMessage message); } + public enum KeepAliveMode + { + Default = 0, + Timeout = 1, + Interval = 2, + TimeoutWithPayload = 3, + } public sealed class SubscriptionList : System.IDisposable { public SubscriptionList() { } @@ -364,6 +378,11 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets.GraphQLWs public const string Pong = "pong"; public const string Subscribe = "subscribe"; } + public class PingPayload + { + public PingPayload() { } + public string? id { get; set; } + } public class SubscriptionServer : GraphQL.Server.Transports.AspNetCore.WebSockets.BaseSubscriptionServer { public SubscriptionServer(GraphQL.Server.Transports.AspNetCore.WebSockets.IWebSocketConnection connection, GraphQL.Server.Transports.AspNetCore.WebSockets.GraphQLWebSocketOptions options, GraphQL.Server.Transports.AspNetCore.IAuthorizationOptions authorizationOptions, GraphQL.IDocumentExecuter executer, GraphQL.IGraphQLSerializer serializer, Microsoft.Extensions.DependencyInjection.IServiceScopeFactory serviceScopeFactory, GraphQL.Server.Transports.AspNetCore.IUserContextBuilder userContextBuilder, GraphQL.Server.Transports.AspNetCore.WebSockets.IWebSocketAuthenticationService? authenticationService = null) { } @@ -377,6 +396,10 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets.GraphQLWs protected override System.Threading.Tasks.Task ExecuteRequestAsync(GraphQL.Transport.OperationMessage message) { } protected virtual System.Threading.Tasks.Task OnCompleteAsync(GraphQL.Transport.OperationMessage message) { } protected override System.Threading.Tasks.Task OnConnectionAcknowledgeAsync(GraphQL.Transport.OperationMessage message) { } + [System.Obsolete("Please use the OnConnectionInitAsync and OnKeepAliveLoopAsync methods instead. Th" + + "is method will be removed in a future version of this library.")] + protected override System.Threading.Tasks.Task OnConnectionInitAsync(GraphQL.Transport.OperationMessage message, bool smartKeepAlive) { } + protected override System.Threading.Tasks.Task OnKeepAliveLoopAsync(System.TimeSpan keepAliveTimeout, GraphQL.Server.Transports.AspNetCore.WebSockets.KeepAliveMode keepAliveMode) { } public override System.Threading.Tasks.Task OnMessageReceivedAsync(GraphQL.Transport.OperationMessage message) { } protected virtual System.Threading.Tasks.Task OnPingAsync(GraphQL.Transport.OperationMessage message) { } protected virtual System.Threading.Tasks.Task OnPongAsync(GraphQL.Transport.OperationMessage message) { } @@ -415,6 +438,10 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets.SubscriptionsTransport protected override System.Threading.Tasks.Task ErrorAccessDeniedAsync() { } protected override System.Threading.Tasks.Task ExecuteRequestAsync(GraphQL.Transport.OperationMessage message) { } protected override System.Threading.Tasks.Task OnConnectionAcknowledgeAsync(GraphQL.Transport.OperationMessage message) { } + [System.Obsolete("Please use the OnConnectionInitAsync and OnKeepAliveLoopAsync methods instead. Th" + + "is method will be removed in a future version of this library.")] + protected override System.Threading.Tasks.Task OnConnectionInitAsync(GraphQL.Transport.OperationMessage message, bool smartKeepAlive) { } + protected override System.Threading.Tasks.Task OnKeepAliveLoopAsync(System.TimeSpan keepAliveTimeout, GraphQL.Server.Transports.AspNetCore.WebSockets.KeepAliveMode keepAliveMode) { } public override System.Threading.Tasks.Task OnMessageReceivedAsync(GraphQL.Transport.OperationMessage message) { } protected override System.Threading.Tasks.Task OnSendKeepAliveAsync() { } protected virtual System.Threading.Tasks.Task OnStartAsync(GraphQL.Transport.OperationMessage message) { } diff --git a/tests/ApiApprovalTests/netcoreapp21+netstandard20/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/netcoreapp21+netstandard20/GraphQL.Server.Transports.AspNetCore.approved.txt index 1216fed0..a3dc75d6 100644 --- a/tests/ApiApprovalTests/netcoreapp21+netstandard20/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/netcoreapp21+netstandard20/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -292,8 +292,13 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets public virtual System.Threading.Tasks.Task InitializeConnectionAsync() { } protected virtual System.Threading.Tasks.Task OnCloseConnectionAsync() { } protected abstract System.Threading.Tasks.Task OnConnectionAcknowledgeAsync(GraphQL.Transport.OperationMessage message); + protected virtual System.Threading.Tasks.Task OnConnectionInitAsync(GraphQL.Transport.OperationMessage message) { } + [System.Obsolete("Please use the OnConnectionInitAsync(message) and OnKeepAliveLoopAsync methods in" + + "stead. This method will be removed in a future version of this library.")] protected virtual System.Threading.Tasks.Task OnConnectionInitAsync(GraphQL.Transport.OperationMessage message, bool smartKeepAlive) { } protected virtual System.Threading.Tasks.Task OnConnectionInitWaitTimeoutAsync() { } + protected virtual System.Threading.Tasks.Task OnKeepAliveLoopAsync() { } + protected virtual System.Threading.Tasks.Task OnKeepAliveLoopAsync(System.TimeSpan keepAliveTimeout, GraphQL.Server.Transports.AspNetCore.WebSockets.KeepAliveMode keepAliveMode) { } public abstract System.Threading.Tasks.Task OnMessageReceivedAsync(GraphQL.Transport.OperationMessage message); protected virtual System.Threading.Tasks.Task OnNotAuthenticatedAsync(GraphQL.Transport.OperationMessage message) { } protected virtual System.Threading.Tasks.Task OnNotAuthorizedPolicyAsync(GraphQL.Transport.OperationMessage message, Microsoft.AspNetCore.Authorization.AuthorizationResult result) { } @@ -318,7 +323,9 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets public bool DisconnectAfterAnyError { get; set; } public bool DisconnectAfterErrorEvent { get; set; } public System.TimeSpan? DisconnectionTimeout { get; set; } + public GraphQL.Server.Transports.AspNetCore.WebSockets.KeepAliveMode KeepAliveMode { get; set; } public System.TimeSpan? KeepAliveTimeout { get; set; } + public System.Collections.Generic.List SupportedWebSocketSubProtocols { get; set; } } public interface IOperationMessageProcessor : System.IDisposable { @@ -339,6 +346,13 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets System.Threading.Tasks.Task ExecuteAsync(GraphQL.Server.Transports.AspNetCore.WebSockets.IOperationMessageProcessor operationMessageProcessor); System.Threading.Tasks.Task SendMessageAsync(GraphQL.Transport.OperationMessage message); } + public enum KeepAliveMode + { + Default = 0, + Timeout = 1, + Interval = 2, + TimeoutWithPayload = 3, + } public sealed class SubscriptionList : System.IDisposable { public SubscriptionList() { } @@ -382,6 +396,11 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets.GraphQLWs public const string Pong = "pong"; public const string Subscribe = "subscribe"; } + public class PingPayload + { + public PingPayload() { } + public string? id { get; set; } + } public class SubscriptionServer : GraphQL.Server.Transports.AspNetCore.WebSockets.BaseSubscriptionServer { public SubscriptionServer(GraphQL.Server.Transports.AspNetCore.WebSockets.IWebSocketConnection connection, GraphQL.Server.Transports.AspNetCore.WebSockets.GraphQLWebSocketOptions options, GraphQL.Server.Transports.AspNetCore.IAuthorizationOptions authorizationOptions, GraphQL.IDocumentExecuter executer, GraphQL.IGraphQLSerializer serializer, Microsoft.Extensions.DependencyInjection.IServiceScopeFactory serviceScopeFactory, GraphQL.Server.Transports.AspNetCore.IUserContextBuilder userContextBuilder, GraphQL.Server.Transports.AspNetCore.WebSockets.IWebSocketAuthenticationService? authenticationService = null) { } @@ -395,6 +414,10 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets.GraphQLWs protected override System.Threading.Tasks.Task ExecuteRequestAsync(GraphQL.Transport.OperationMessage message) { } protected virtual System.Threading.Tasks.Task OnCompleteAsync(GraphQL.Transport.OperationMessage message) { } protected override System.Threading.Tasks.Task OnConnectionAcknowledgeAsync(GraphQL.Transport.OperationMessage message) { } + [System.Obsolete("Please use the OnConnectionInitAsync and OnKeepAliveLoopAsync methods instead. Th" + + "is method will be removed in a future version of this library.")] + protected override System.Threading.Tasks.Task OnConnectionInitAsync(GraphQL.Transport.OperationMessage message, bool smartKeepAlive) { } + protected override System.Threading.Tasks.Task OnKeepAliveLoopAsync(System.TimeSpan keepAliveTimeout, GraphQL.Server.Transports.AspNetCore.WebSockets.KeepAliveMode keepAliveMode) { } public override System.Threading.Tasks.Task OnMessageReceivedAsync(GraphQL.Transport.OperationMessage message) { } protected virtual System.Threading.Tasks.Task OnPingAsync(GraphQL.Transport.OperationMessage message) { } protected virtual System.Threading.Tasks.Task OnPongAsync(GraphQL.Transport.OperationMessage message) { } @@ -433,6 +456,10 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets.SubscriptionsTransport protected override System.Threading.Tasks.Task ErrorAccessDeniedAsync() { } protected override System.Threading.Tasks.Task ExecuteRequestAsync(GraphQL.Transport.OperationMessage message) { } protected override System.Threading.Tasks.Task OnConnectionAcknowledgeAsync(GraphQL.Transport.OperationMessage message) { } + [System.Obsolete("Please use the OnConnectionInitAsync and OnKeepAliveLoopAsync methods instead. Th" + + "is method will be removed in a future version of this library.")] + protected override System.Threading.Tasks.Task OnConnectionInitAsync(GraphQL.Transport.OperationMessage message, bool smartKeepAlive) { } + protected override System.Threading.Tasks.Task OnKeepAliveLoopAsync(System.TimeSpan keepAliveTimeout, GraphQL.Server.Transports.AspNetCore.WebSockets.KeepAliveMode keepAliveMode) { } public override System.Threading.Tasks.Task OnMessageReceivedAsync(GraphQL.Transport.OperationMessage message) { } protected override System.Threading.Tasks.Task OnSendKeepAliveAsync() { } protected virtual System.Threading.Tasks.Task OnStartAsync(GraphQL.Transport.OperationMessage message) { } diff --git a/tests/ApiApprovalTests/netcoreapp31/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/netcoreapp31/GraphQL.Server.Transports.AspNetCore.approved.txt index 685396c3..34257df1 100644 --- a/tests/ApiApprovalTests/netcoreapp31/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/netcoreapp31/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -274,8 +274,13 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets public virtual System.Threading.Tasks.Task InitializeConnectionAsync() { } protected virtual System.Threading.Tasks.Task OnCloseConnectionAsync() { } protected abstract System.Threading.Tasks.Task OnConnectionAcknowledgeAsync(GraphQL.Transport.OperationMessage message); + protected virtual System.Threading.Tasks.Task OnConnectionInitAsync(GraphQL.Transport.OperationMessage message) { } + [System.Obsolete("Please use the OnConnectionInitAsync(message) and OnKeepAliveLoopAsync methods in" + + "stead. This method will be removed in a future version of this library.")] protected virtual System.Threading.Tasks.Task OnConnectionInitAsync(GraphQL.Transport.OperationMessage message, bool smartKeepAlive) { } protected virtual System.Threading.Tasks.Task OnConnectionInitWaitTimeoutAsync() { } + protected virtual System.Threading.Tasks.Task OnKeepAliveLoopAsync() { } + protected virtual System.Threading.Tasks.Task OnKeepAliveLoopAsync(System.TimeSpan keepAliveTimeout, GraphQL.Server.Transports.AspNetCore.WebSockets.KeepAliveMode keepAliveMode) { } public abstract System.Threading.Tasks.Task OnMessageReceivedAsync(GraphQL.Transport.OperationMessage message); protected virtual System.Threading.Tasks.Task OnNotAuthenticatedAsync(GraphQL.Transport.OperationMessage message) { } protected virtual System.Threading.Tasks.Task OnNotAuthorizedPolicyAsync(GraphQL.Transport.OperationMessage message, Microsoft.AspNetCore.Authorization.AuthorizationResult result) { } @@ -300,7 +305,9 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets public bool DisconnectAfterAnyError { get; set; } public bool DisconnectAfterErrorEvent { get; set; } public System.TimeSpan? DisconnectionTimeout { get; set; } + public GraphQL.Server.Transports.AspNetCore.WebSockets.KeepAliveMode KeepAliveMode { get; set; } public System.TimeSpan? KeepAliveTimeout { get; set; } + public System.Collections.Generic.List SupportedWebSocketSubProtocols { get; set; } } public interface IOperationMessageProcessor : System.IDisposable { @@ -321,6 +328,13 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets System.Threading.Tasks.Task ExecuteAsync(GraphQL.Server.Transports.AspNetCore.WebSockets.IOperationMessageProcessor operationMessageProcessor); System.Threading.Tasks.Task SendMessageAsync(GraphQL.Transport.OperationMessage message); } + public enum KeepAliveMode + { + Default = 0, + Timeout = 1, + Interval = 2, + TimeoutWithPayload = 3, + } public sealed class SubscriptionList : System.IDisposable { public SubscriptionList() { } @@ -364,6 +378,11 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets.GraphQLWs public const string Pong = "pong"; public const string Subscribe = "subscribe"; } + public class PingPayload + { + public PingPayload() { } + public string? id { get; set; } + } public class SubscriptionServer : GraphQL.Server.Transports.AspNetCore.WebSockets.BaseSubscriptionServer { public SubscriptionServer(GraphQL.Server.Transports.AspNetCore.WebSockets.IWebSocketConnection connection, GraphQL.Server.Transports.AspNetCore.WebSockets.GraphQLWebSocketOptions options, GraphQL.Server.Transports.AspNetCore.IAuthorizationOptions authorizationOptions, GraphQL.IDocumentExecuter executer, GraphQL.IGraphQLSerializer serializer, Microsoft.Extensions.DependencyInjection.IServiceScopeFactory serviceScopeFactory, GraphQL.Server.Transports.AspNetCore.IUserContextBuilder userContextBuilder, GraphQL.Server.Transports.AspNetCore.WebSockets.IWebSocketAuthenticationService? authenticationService = null) { } @@ -377,6 +396,10 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets.GraphQLWs protected override System.Threading.Tasks.Task ExecuteRequestAsync(GraphQL.Transport.OperationMessage message) { } protected virtual System.Threading.Tasks.Task OnCompleteAsync(GraphQL.Transport.OperationMessage message) { } protected override System.Threading.Tasks.Task OnConnectionAcknowledgeAsync(GraphQL.Transport.OperationMessage message) { } + [System.Obsolete("Please use the OnConnectionInitAsync and OnKeepAliveLoopAsync methods instead. Th" + + "is method will be removed in a future version of this library.")] + protected override System.Threading.Tasks.Task OnConnectionInitAsync(GraphQL.Transport.OperationMessage message, bool smartKeepAlive) { } + protected override System.Threading.Tasks.Task OnKeepAliveLoopAsync(System.TimeSpan keepAliveTimeout, GraphQL.Server.Transports.AspNetCore.WebSockets.KeepAliveMode keepAliveMode) { } public override System.Threading.Tasks.Task OnMessageReceivedAsync(GraphQL.Transport.OperationMessage message) { } protected virtual System.Threading.Tasks.Task OnPingAsync(GraphQL.Transport.OperationMessage message) { } protected virtual System.Threading.Tasks.Task OnPongAsync(GraphQL.Transport.OperationMessage message) { } @@ -415,6 +438,10 @@ namespace GraphQL.Server.Transports.AspNetCore.WebSockets.SubscriptionsTransport protected override System.Threading.Tasks.Task ErrorAccessDeniedAsync() { } protected override System.Threading.Tasks.Task ExecuteRequestAsync(GraphQL.Transport.OperationMessage message) { } protected override System.Threading.Tasks.Task OnConnectionAcknowledgeAsync(GraphQL.Transport.OperationMessage message) { } + [System.Obsolete("Please use the OnConnectionInitAsync and OnKeepAliveLoopAsync methods instead. Th" + + "is method will be removed in a future version of this library.")] + protected override System.Threading.Tasks.Task OnConnectionInitAsync(GraphQL.Transport.OperationMessage message, bool smartKeepAlive) { } + protected override System.Threading.Tasks.Task OnKeepAliveLoopAsync(System.TimeSpan keepAliveTimeout, GraphQL.Server.Transports.AspNetCore.WebSockets.KeepAliveMode keepAliveMode) { } public override System.Threading.Tasks.Task OnMessageReceivedAsync(GraphQL.Transport.OperationMessage message) { } protected override System.Threading.Tasks.Task OnSendKeepAliveAsync() { } protected virtual System.Threading.Tasks.Task OnStartAsync(GraphQL.Transport.OperationMessage message) { } diff --git a/tests/Transports.AspNetCore.Tests/WebSockets/TestBaseSubscriptionServer.cs b/tests/Transports.AspNetCore.Tests/WebSockets/TestBaseSubscriptionServer.cs index 15e6df12..c5749d5c 100644 --- a/tests/Transports.AspNetCore.Tests/WebSockets/TestBaseSubscriptionServer.cs +++ b/tests/Transports.AspNetCore.Tests/WebSockets/TestBaseSubscriptionServer.cs @@ -49,7 +49,9 @@ public Task Do_ErrorAccessDeniedAsync() => ErrorAccessDeniedAsync(); public Task Do_OnConnectionInitAsync(OperationMessage message, bool smartKeepAlive) +#pragma warning disable CS0618 // Type or member is obsolete => OnConnectionInitAsync(message, smartKeepAlive); +#pragma warning restore CS0618 // Type or member is obsolete public Task Do_SubscribeAsync(OperationMessage message, bool overwrite) => SubscribeAsync(message, overwrite);