API docs for SignalR (#26445)

* API docs for SignalR

* Apply suggestions from code review

Co-authored-by: Stephen Halter <halter73@gmail.com>
Co-authored-by: Pranav K <prkrishn@hotmail.com>

Co-authored-by: Stephen Halter <halter73@gmail.com>
Co-authored-by: Pranav K <prkrishn@hotmail.com>
This commit is contained in:
Brennan 2020-10-02 10:14:32 -07:00 committed by GitHub
parent cd94cd63b7
commit 659532b16c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 354 additions and 4 deletions

View File

@ -35,8 +35,19 @@ namespace Microsoft.AspNetCore.SignalR.Client
/// </remarks>
public partial class HubConnection : IAsyncDisposable
{
/// <summary>
/// The default timeout which specifies how long to wait for a message before closing the connection. Default is 30 seconds.
/// </summary>
public static readonly TimeSpan DefaultServerTimeout = TimeSpan.FromSeconds(30); // Server ping rate is 15 sec, this is 2 times that.
/// <summary>
/// The default timeout which specifies how long to wait for the handshake to respond before closing the connection. Default is 15 seconds.
/// </summary>
public static readonly TimeSpan DefaultHandshakeTimeout = TimeSpan.FromSeconds(15);
/// <summary>
/// The default interval that the client will send keep alive messages to let the server know to not close the connection. Default is 15 second interval.
/// </summary>
public static readonly TimeSpan DefaultKeepAliveInterval = TimeSpan.FromSeconds(15);
// This lock protects the connection state.

View File

@ -80,6 +80,9 @@ namespace Microsoft.AspNetCore.SignalR.Client
}
// Prevents from being displayed in intellisense
/// <summary>
/// Gets the <see cref="Type"/> of the current instance.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public new Type GetType()
{

View File

@ -4,6 +4,7 @@
<Description>Client for ASP.NET Core SignalR</Description>
<TargetFrameworks>$(DefaultNetFxTargetFramework);netstandard2.0;netstandard2.1</TargetFrameworks>
<RootNamespace>Microsoft.AspNetCore.SignalR.Client</RootNamespace>
<NoWarn>$(NoWarn.Replace('1591', ''))</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Client for ASP.NET Core SignalR</Description>
<TargetFrameworks>$(DefaultNetFxTargetFramework);netstandard2.0</TargetFrameworks>
<NoWarn>$(NoWarn.Replace('1591', ''))</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Client for ASP.NET Core Connection Handlers</Description>
<TargetFrameworks>$(DefaultNetFxTargetFramework);netstandard2.0;netstandard2.1</TargetFrameworks>
<NoWarn>$(NoWarn.Replace('1591', ''))</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -10,6 +10,10 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
/// </summary>
public class NoTransportSupportedException : Exception
{
/// <summary>
/// Constructs the <see cref="NoTransportSupportedException"/> exception with the provided <paramref name="message"/>.
/// </summary>
/// <param name="message">Message of the exception.</param>
public NoTransportSupportedException(string message)
: base(message)
{

View File

@ -10,8 +10,17 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
/// </summary>
public class TransportFailedException : Exception
{
/// <summary>
/// The name of the transport that failed to connect.
/// </summary>
public string TransportType { get; }
/// <summary>
/// Constructs a <see cref="TransportFailedException"/>.
/// </summary>
/// <param name="transportType">The name of the transport that failed to connect.</param>
/// <param name="message">The reason the transport failed.</param>
/// <param name="innerException">An optional extra exception if one was thrown while trying to connect.</param>
public TransportFailedException(string transportType, string message, Exception innerException = null)
: base($"{transportType} failed: {message}", innerException)
{

View File

@ -5,9 +5,19 @@ using System.Collections.Generic;
namespace Microsoft.AspNetCore.Http.Connections
{
/// <summary>
/// Part of the <see cref="NegotiationResponse"/> that represents an individual transport and the trasfer formats the transport supports.
/// </summary>
public class AvailableTransport
{
/// <summary>
/// A transport available on the server.
/// </summary>
public string Transport { get; set; }
/// <summary>
/// A list of formats supported by the transport. Examples include "Text" and "Binary".
/// </summary>
public IList<string> TransferFormats { get; set; }
}
}

View File

@ -7,6 +7,7 @@
<IsAspNetCoreApp>true</IsAspNetCoreApp>
<RootNamespace>Microsoft.AspNetCore.Http.Connections</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>$(NoWarn.Replace('1591', ''))</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -11,6 +11,9 @@ using Microsoft.AspNetCore.Internal;
namespace Microsoft.AspNetCore.Http.Connections
{
/// <summary>
/// The protocol for reading and writing negotiate requests and responses.
/// </summary>
public static class NegotiateProtocol
{
private const string ConnectionIdPropertyName = "connectionId";
@ -36,6 +39,11 @@ namespace Microsoft.AspNetCore.Http.Connections
// Used to detect ASP.NET SignalR Server connection attempt
private static ReadOnlySpan<byte> ProtocolVersionPropertyNameBytes => new byte[] { (byte)'P', (byte)'r', (byte)'o', (byte)'t', (byte)'o', (byte)'c', (byte)'o', (byte)'l', (byte)'V', (byte)'e', (byte)'r', (byte)'s', (byte)'i', (byte)'o', (byte)'n' };
/// <summary>
/// Writes the <paramref name="response"/> to the <paramref name="output"/>.
/// </summary>
/// <param name="response">The negotiation response generated in response to a negotiation request.</param>
/// <param name="output">Where the <paramref name="response"/> is written to as Json.</param>
public static void WriteResponse(NegotiationResponse response, IBufferWriter<byte> output)
{
var reusableWriter = ReusableUtf8JsonWriter.Get(output);
@ -124,6 +132,11 @@ namespace Microsoft.AspNetCore.Http.Connections
}
}
/// <summary>
/// Parses a <see cref="NegotiationResponse"/> from the <paramref name="content"/> as Json.
/// </summary>
/// <param name="content">The bytes of a Json payload that represents a <see cref="NegotiationResponse"/>.</param>
/// <returns>The parsed <see cref="NegotiationResponse"/>.</returns>
public static NegotiationResponse ParseResponse(ReadOnlySpan<byte> content)
{
try

View File

@ -5,14 +5,44 @@ using System.Collections.Generic;
namespace Microsoft.AspNetCore.Http.Connections
{
/// <summary>
/// A response to a '/negotiate' request.
/// </summary>
public class NegotiationResponse
{
/// <summary>
/// An optional Url to redirect the client to another endpoint.
/// </summary>
public string Url { get; set; }
/// <summary>
/// An optional access token to go along with the Url.
/// </summary>
public string AccessToken { get; set; }
/// <summary>
/// The public ID for the connection.
/// </summary>
public string ConnectionId { get; set; }
/// <summary>
/// The private ID for the connection.
/// </summary>
public string ConnectionToken { get; set; }
/// <summary>
/// The minimum value between the version the client sends and the maximum version the server supports.
/// </summary>
public int Version { get; set; }
/// <summary>
/// A list of transports the server supports.
/// </summary>
public IList<AvailableTransport> AvailableTransports { get; set; }
/// <summary>
/// An optional error during the negotiate. If this is not null the other properties on the response can be ignored.
/// </summary>
public string Error { get; set; }
}
}

View File

@ -11,6 +11,9 @@ using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Extension methods on <see cref="IEndpointRouteBuilder"/> that add routes for <see cref="ConnectionHandler"/>s.
/// </summary>
public static class ConnectionEndpointRouteBuilderExtensions
{
/// <summary>

View File

@ -5,6 +5,9 @@ using System;
namespace Microsoft.AspNetCore.Http.Connections
{
/// <summary>
/// Options used to change behavior of how connections are handled.
/// </summary>
public class ConnectionOptions
{
/// <summary>

View File

@ -6,10 +6,20 @@ using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Http.Connections
{
/// <summary>
/// Sets up <see cref="ConnectionOptions"/>.
/// </summary>
public class ConnectionOptionsSetup : IConfigureOptions<ConnectionOptions>
{
/// <summary>
/// Default timeout value for disconnecting idle connections.
/// </summary>
public static TimeSpan DefaultDisconectTimeout = TimeSpan.FromSeconds(15);
/// <summary>
/// Sets default values for options if they have not been set yet.
/// </summary>
/// <param name="options">The <see cref="ConnectionOptions"/>.</param>
public void Configure(ConnectionOptions options)
{
if (options.DisconnectTimeout == null)

View File

@ -1,15 +1,27 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Http.Connections.Features
{
/// <summary>
/// Feature set on the <see cref="ConnectionContext"/> that provides access to the underlying <see cref="Http.HttpContext"/>
/// associated with the connection if there is one.
/// </summary>
public interface IHttpContextFeature
{
/// <summary>
/// The <see cref="Http.HttpContext"/> associated with the connection if available.
/// </summary>
/// <remarks>
/// Connections can run on top of HTTP transports like WebSockets or Long Polling, or other non-HTTP transports. As a result,
/// this API can sometimes return <see langword="null"/> depending on the configuration of your application.
/// </remarks>
HttpContext HttpContext { get; set; }
}
}

View File

@ -4,12 +4,20 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Http.Connections.Features
{
/// <summary>
/// Feature set on the <see cref="ConnectionContext"/> that exposes the <see cref="HttpTransportType"/>
/// the connection is using.
/// </summary>
public interface IHttpTransportFeature
{
/// <summary>
/// The <see cref="HttpTransportType"/> the connection is using.
/// </summary>
HttpTransportType TransportType { get; }
}
}

View File

@ -7,6 +7,9 @@ using Microsoft.AspNetCore.Http.Connections.Features;
namespace Microsoft.AspNetCore.Http.Connections
{
/// <summary>
/// Extension method to get the underlying <see cref="HttpContext"/> of the connection if there is one.
/// </summary>
public static class HttpConnectionContextExtensions
{
/// <summary>

View File

@ -6,6 +6,7 @@
<IsAspNetCoreApp>true</IsAspNetCoreApp>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn.Replace('1591', ''))</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -6,8 +6,15 @@ using System.Collections.Generic;
namespace Microsoft.AspNetCore.Http.Connections
{
/// <summary>
/// Options used by the WebSockets transport to modify the transports behavior.
/// </summary>
public class WebSocketOptions
{
/// <summary>
/// Gets or sets the amount of time the WebSocket transport will wait for a graceful close before starting an ungraceful close.
/// </summary>
/// <value>Defaults to 5 seconds</value>
public TimeSpan CloseTimeout { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>

View File

@ -8,6 +8,7 @@
<RootNamespace>Microsoft.AspNetCore.SignalR</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn.Replace('1591', ''))</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -6,6 +6,9 @@ using Microsoft.AspNetCore.SignalR.Protocol;
namespace Microsoft.AspNetCore.SignalR
{
/// <summary>
/// The <see cref="MessagePackHubProtocol"/> options.
/// </summary>
public class MessagePackHubProtocolOptions
{
private MessagePackSerializerOptions? _messagePackSerializerOptions;

View File

@ -6,6 +6,7 @@
<RootNamespace>Microsoft.AspNetCore.SignalR</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn.Replace('1591', ''))</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -6,6 +6,7 @@
<RootNamespace>Microsoft.AspNetCore.SignalR</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn.Replace('1591', ''))</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -3,13 +3,34 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.SignalR.Protocol;
namespace Microsoft.AspNetCore.SignalR
{
/// <summary>
/// Class used by <see cref="IHubProtocol"/>s to get the <see cref="Type"/>(s) expected by the hub message being deserialized.
/// </summary>
public interface IInvocationBinder
{
/// <summary>
/// Gets the <see cref="Type"/> the invocation represented by the <paramref name="invocationId"/> is expected to contain.
/// </summary>
/// <param name="invocationId">The ID of the invocation being received.</param>
/// <returns>The <see cref="Type"/> the invocation is expected to contain.</returns>
Type GetReturnType(string invocationId);
/// <summary>
/// Gets the list of <see cref="Type"/>s the method represented by <paramref name="methodName"/> takes as arguments.
/// </summary>
/// <param name="methodName">The name of the method being called.</param>
/// <returns>A list of <see cref="Type"/>s the method takes as arguments.</returns>
IReadOnlyList<Type> GetParameterTypes(string methodName);
/// <summary>
/// Gets the <see cref="Type"/> the stream item is expected to contain.
/// </summary>
/// <param name="streamId">The ID of the stream the stream item is a part of.</param>
/// <returns>The <see cref="Type"/> of the item the stream contains.</returns>
Type GetStreamItemType(string streamId);
}
}

View File

@ -8,6 +8,7 @@
<RootNamespace>Microsoft.AspNetCore.SignalR</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn.Replace('1591', ''))</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -5,8 +5,15 @@ using System.Collections.Generic;
namespace Microsoft.AspNetCore.SignalR.Protocol
{
/// <summary>
/// The <see cref="CancelInvocationMessage"/> represents a cancellation of a streaming method.
/// </summary>
public class CancelInvocationMessage : HubInvocationMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="CancelInvocationMessage"/> class.
/// </summary>
/// <param name="invocationId">The ID of the hub method invocation being canceled.</param>
public CancelInvocationMessage(string invocationId) : base(invocationId)
{
}

View File

@ -5,12 +5,34 @@ using System;
namespace Microsoft.AspNetCore.SignalR.Protocol
{
/// <summary>
/// Represents an invocation that has completed. If there is an error then the invocation didn't complete successfully.
/// </summary>
public class CompletionMessage : HubInvocationMessage
{
/// <summary>
/// Optional error message if the invocation wasn't completed successfully. This must be null if there is a result.
/// </summary>
public string? Error { get; }
/// <summary>
/// Optional result from the invocation. This must be null if there is an error.
/// This can also be null if there wasn't a result from the method invocation.
/// </summary>
public object? Result { get; }
/// <summary>
/// Specifies whether the completion contains a result.
/// </summary>
public bool HasResult { get; }
/// <summary>
/// Constructs a <see cref="CompletionMessage"/>.
/// </summary>
/// <param name="invocationId">The ID of the invocation that has completed.</param>
/// <param name="error">An optional error if the invocation failed.</param>
/// <param name="result">An optional result if the invocation returns a result.</param>
/// <param name="hasResult">Specifies whether the completion contains a result.</param>
public CompletionMessage(string invocationId, string? error, object? result, bool hasResult)
: base(invocationId)
{
@ -24,6 +46,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
HasResult = hasResult;
}
/// <inheritdoc />
public override string ToString()
{
var errorStr = Error == null ? "<<null>>" : $"\"{Error}\"";
@ -33,12 +56,30 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
// Static factory methods. Don't want to use constructor overloading because it will break down
// if you need to send a payload statically-typed as a string. And because a static factory is clearer here
/// <summary>
/// Constructs a <see cref="CompletionMessage"/> with an error.
/// </summary>
/// <param name="invocationId">The ID of the invocation that is being completed.</param>
/// <param name="error">The error that occurred during the invocation.</param>
/// <returns>The constructed <see cref="CompletionMessage"/>.</returns>
public static CompletionMessage WithError(string invocationId, string error)
=> new CompletionMessage(invocationId, error, result: null, hasResult: false);
/// <summary>
/// Constructs a <see cref="CompletionMessage"/> with a result.
/// </summary>
/// <param name="invocationId">The ID of the invocation that is being completed.</param>
/// <param name="payload">The result from the invocation.</param>
/// <returns>The constructed <see cref="CompletionMessage"/>.</returns>
public static CompletionMessage WithResult(string invocationId, object payload)
=> new CompletionMessage(invocationId, error: null, result: payload, hasResult: true);
/// <summary>
/// Constructs a <see cref="CompletionMessage"/> without an error or result.
/// This means the invocation was successful but there is no return value.
/// </summary>
/// <param name="invocationId">The ID of the invocation that is being completed.</param>
/// <returns>The constructed <see cref="CompletionMessage"/>.</returns>
public static CompletionMessage Empty(string invocationId)
=> new CompletionMessage(invocationId, error: null, result: null, hasResult: false);
}

View File

@ -43,6 +43,11 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
}
}
/// <summary>
/// Gets the bytes of a successful handshake message.
/// </summary>
/// <param name="protocol">The protocol being used for the connection.</param>
/// <returns>The bytes of a successful handshake message.</returns>
public static ReadOnlySpan<byte> GetSuccessfulHandshake(IHubProtocol protocol) => _successHandshakeData.Span;
/// <summary>

View File

@ -3,8 +3,14 @@
namespace Microsoft.AspNetCore.SignalR.Protocol
{
/// <summary>
/// A keep-alive message to let the other side of the connection know that the connection is still alive.
/// </summary>
public class PingMessage : HubMessage
{
/// <summary>
/// A static instance of the PingMessage to remove unneeded allocations.
/// </summary>
public static readonly PingMessage Instance = new PingMessage();
private PingMessage()

View File

@ -3,15 +3,27 @@
namespace Microsoft.AspNetCore.SignalR.Protocol
{
/// <summary>
/// Represents a single item of an active stream.
/// </summary>
public class StreamItemMessage : HubInvocationMessage
{
/// <summary>
/// The single item from a stream.
/// </summary>
public object? Item { get; }
/// <summary>
/// Constructs a <see cref="StreamItemMessage"/>.
/// </summary>
/// <param name="invocationId">The ID of the stream.</param>
/// <param name="item">An item from the stream.</param>
public StreamItemMessage(string invocationId, object? item) : base(invocationId)
{
Item = item;
}
/// <inheritdoc />
public override string ToString()
{
return $"StreamItem {{ {nameof(InvocationId)}: \"{InvocationId}\", {nameof(Item)}: {Item ?? "<<null>>"} }}";

View File

@ -147,6 +147,12 @@ namespace Microsoft.AspNetCore.SignalR
// Currently used only for streaming methods
internal ConcurrentDictionary<string, CancellationTokenSource> ActiveRequestCancellationSources { get; } = new ConcurrentDictionary<string, CancellationTokenSource>(StringComparer.Ordinal);
/// <summary>
/// Write a <see cref="HubMessage"/> to the connection.
/// </summary>
/// <param name="message">The <see cref="HubMessage"/> being written.</param>
/// <param name="cancellationToken">Cancels the in progress write.</param>
/// <returns>A <see cref="ValueTask"/> that represents the completion of the write. If the write throws this task will still complete successfully.</returns>
[SuppressMessage("ApiDesign", "RS0026:Do not add multiple overloads with optional parameters", Justification = "Required to maintain compatibility")]
public virtual ValueTask WriteAsync(HubMessage message, CancellationToken cancellationToken = default)
{

View File

@ -8,11 +8,22 @@ using System.Collections.Generic;
namespace Microsoft.AspNetCore.SignalR
{
/// <summary>
/// Stores <see cref="HubConnectionContext"/>s by ID.
/// </summary>
/// <remarks>
/// This API is meant for internal usage.
/// </remarks>
public class HubConnectionStore
{
private readonly ConcurrentDictionary<string, HubConnectionContext> _connections =
new ConcurrentDictionary<string, HubConnectionContext>(StringComparer.Ordinal);
/// <summary>
/// Get the <see cref="HubConnectionContext"/> by connection ID.
/// </summary>
/// <param name="connectionId">The ID of the connection.</param>
/// <returns>The connection for the <paramref name="connectionId"/>, null if there is no connection.</returns>
public HubConnectionContext? this[string connectionId]
{
get
@ -22,40 +33,75 @@ namespace Microsoft.AspNetCore.SignalR
}
}
/// <summary>
/// The number of connections in the store.
/// </summary>
public int Count => _connections.Count;
/// <summary>
/// Add a <see cref="HubConnectionContext"/> to the store.
/// </summary>
/// <param name="connection">The connection to add.</param>
public void Add(HubConnectionContext connection)
{
_connections.TryAdd(connection.ConnectionId, connection);
}
/// <summary>
/// Removes a <see cref="HubConnectionContext"/> from the store.
/// </summary>
/// <param name="connection">The connection to remove.</param>
public void Remove(HubConnectionContext connection)
{
_connections.TryRemove(connection.ConnectionId, out _);
}
/// <summary>
/// Gets an enumerator over the connection store.
/// </summary>
/// <returns>The <see cref="Enumerator"/> over the connections.</returns>
public Enumerator GetEnumerator()
{
return new Enumerator(this);
}
/// <summary>
/// An <see cref="IEnumerator"/> over the <see cref="HubConnectionStore"/>
/// </summary>
public readonly struct Enumerator : IEnumerator<HubConnectionContext>
{
private readonly IEnumerator<KeyValuePair<string, HubConnectionContext>> _enumerator;
/// <summary>
/// Constructs the <see cref="Enumerator"/> over the <see cref="HubConnectionStore"/>.
/// </summary>
/// <param name="hubConnectionList">The store of connections to enumerate over.</param>
public Enumerator(HubConnectionStore hubConnectionList)
{
_enumerator = hubConnectionList._connections.GetEnumerator();
}
/// <summary>
/// The current connection the enumerator is on.
/// </summary>
public HubConnectionContext Current => _enumerator.Current.Value;
object IEnumerator.Current => Current;
/// <summary>
/// Disposes the enumerator.
/// </summary>
public void Dispose() => _enumerator.Dispose();
/// <summary>
/// Moves the enumerator to the next value.
/// </summary>
/// <returns>True if there is another connection. False if there are no more connections.</returns>
public bool MoveNext() => _enumerator.MoveNext();
/// <summary>
/// Resets the enumerator to the beginning.
/// </summary>
public void Reset() => _enumerator.Reset();
}
}

View File

@ -10,6 +10,10 @@ namespace Microsoft.AspNetCore.SignalR
/// </summary>
public class HubMetadata
{
/// <summary>
/// Constructs the <see cref="HubMetadata"/> of the given <see cref="Hub"/> type.
/// </summary>
/// <param name="hubType">The <see cref="Type"/> of the <see cref="Hub"/>.</param>
public HubMetadata(Type hubType)
{
HubType = hubType;

View File

@ -9,6 +9,9 @@ using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.SignalR
{
/// <summary>
/// Class to configure the <see cref="HubOptions"/>.
/// </summary>
public class HubOptionsSetup : IConfigureOptions<HubOptions>
{
internal static TimeSpan DefaultHandshakeTimeout => TimeSpan.FromSeconds(15);
@ -23,6 +26,10 @@ namespace Microsoft.AspNetCore.SignalR
private readonly List<string> _defaultProtocols = new List<string>();
/// <summary>
/// Constructs the <see cref="HubOptionsSetup"/> with a list of protocols added to Dependency Injection.
/// </summary>
/// <param name="protocols">The list of <see cref="IHubProtocol"/>s that are from Dependency Injection.</param>
public HubOptionsSetup(IEnumerable<IHubProtocol> protocols)
{
foreach (var hubProtocol in protocols)
@ -35,6 +42,10 @@ namespace Microsoft.AspNetCore.SignalR
}
}
/// <summary>
/// Configures the default values of the <see cref="HubOptions"/>.
/// </summary>
/// <param name="options">The <see cref="HubOptions"/> to configure.</param>
public void Configure(HubOptions options)
{
if (options.KeepAliveInterval == null)

View File

@ -7,14 +7,27 @@ using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.SignalR
{
/// <summary>
/// Class to configure the <see cref="HubOptions"/> for a specific <typeparamref name="THub"/>.
/// </summary>
/// <typeparam name="THub">The <see cref="Hub"/> type to configure.</typeparam>
public class HubOptionsSetup<THub> : IConfigureOptions<HubOptions<THub>> where THub : Hub
{
private readonly HubOptions _hubOptions;
/// <summary>
/// Constructs the options configuration class.
/// </summary>
/// <param name="options">The global <see cref="HubOptions"/> from Dependency Injection.</param>
public HubOptionsSetup(IOptions<HubOptions> options)
{
_hubOptions = options.Value;
}
/// <summary>
/// Configures the default values of the <see cref="HubOptions"/>.
/// </summary>
/// <param name="options">The options to configure.</param>
public void Configure(HubOptions<THub> options)
{
// Do a deep copy, otherwise users modifying the HubOptions<THub> list would be changing the global options list

View File

@ -7,6 +7,7 @@
<RootNamespace>Microsoft.AspNetCore.SignalR</RootNamespace>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn.Replace('1591', ''))</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -10,6 +10,9 @@ using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Extension methods on <see cref="IEndpointRouteBuilder"/> to add routes to <see cref="Hub"/>s.
/// </summary>
public static class HubEndpointRouteBuilderExtensions
{
private const DynamicallyAccessedMemberTypes HubAccessibility = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicMethods;

View File

@ -4,6 +4,7 @@
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<IsAspNetCoreApp>true</IsAspNetCoreApp>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn.Replace('1591', ''))</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Provides scale-out support for ASP.NET Core SignalR using a Redis server and the StackExchange.Redis client.</Description>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<NoWarn>$(NoWarn.Replace('1591', ''))</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -17,6 +17,10 @@ using StackExchange.Redis;
namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis
{
/// <summary>
/// The Redis scaleout provider for multi-server support.
/// </summary>
/// <typeparam name="THub">The type of <see cref="Hub"/> to manage connections for.</typeparam>
public class RedisHubLifetimeManager<THub> : HubLifetimeManager<THub>, IDisposable where THub : Hub
{
private readonly HubConnectionStore _connections = new HubConnectionStore();
@ -34,6 +38,12 @@ namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis
private readonly AckHandler _ackHandler;
private int _internalId;
/// <summary>
/// Constructs the <see cref="RedisHubLifetimeManager{THub}"/> with types from Dependency Injection.
/// </summary>
/// <param name="logger">The logger to write information about what the class is doing.</param>
/// <param name="options">The <see cref="RedisOptions"/> that influence behavior of the Redis connection.</param>
/// <param name="hubProtocolResolver">The <see cref="IHubProtocolResolver"/> to get an <see cref="IHubProtocol"/> instance when writing to connections.</param>
public RedisHubLifetimeManager(ILogger<RedisHubLifetimeManager<THub>> logger,
IOptions<RedisOptions> options,
IHubProtocolResolver hubProtocolResolver)
@ -41,6 +51,14 @@ namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis
{
}
/// <summary>
/// Constructs the <see cref="RedisHubLifetimeManager{THub}"/> with types from Dependency Injection.
/// </summary>
/// <param name="logger">The logger to write information about what the class is doing.</param>
/// <param name="options">The <see cref="RedisOptions"/> that influence behavior of the Redis connection.</param>
/// <param name="hubProtocolResolver">The <see cref="IHubProtocolResolver"/> to get an <see cref="IHubProtocol"/> instance when writing to connections.</param>
/// <param name="globalHubOptions">The global <see cref="HubOptions"/>.</param>
/// <param name="hubOptions">The <typeparamref name="THub"/> specific options.</param>
public RedisHubLifetimeManager(ILogger<RedisHubLifetimeManager<THub>> logger,
IOptions<RedisOptions> options,
IHubProtocolResolver hubProtocolResolver,
@ -65,6 +83,7 @@ namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis
_ = EnsureRedisServerConnection();
}
/// <inheritdoc />
public override async Task OnConnectedAsync(HubConnectionContext connection)
{
await EnsureRedisServerConnection();
@ -86,6 +105,7 @@ namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis
await Task.WhenAll(connectionTask, userTask);
}
/// <inheritdoc />
public override Task OnDisconnectedAsync(HubConnectionContext connection)
{
_connections.Remove(connection);
@ -119,18 +139,21 @@ namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis
return Task.WhenAll(tasks);
}
/// <inheritdoc />
public override Task SendAllAsync(string methodName, object[] args, CancellationToken cancellationToken = default)
{
var message = _protocol.WriteInvocation(methodName, args);
return PublishAsync(_channels.All, message);
}
/// <inheritdoc />
public override Task SendAllExceptAsync(string methodName, object[] args, IReadOnlyList<string> excludedConnectionIds, CancellationToken cancellationToken = default)
{
var message = _protocol.WriteInvocation(methodName, args, excludedConnectionIds);
return PublishAsync(_channels.All, message);
}
/// <inheritdoc />
public override Task SendConnectionAsync(string connectionId, string methodName, object[] args, CancellationToken cancellationToken = default)
{
if (connectionId == null)
@ -150,6 +173,7 @@ namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis
return PublishAsync(_channels.Connection(connectionId), message);
}
/// <inheritdoc />
public override Task SendGroupAsync(string groupName, string methodName, object[] args, CancellationToken cancellationToken = default)
{
if (groupName == null)
@ -161,6 +185,7 @@ namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis
return PublishAsync(_channels.Group(groupName), message);
}
/// <inheritdoc />
public override Task SendGroupExceptAsync(string groupName, string methodName, object[] args, IReadOnlyList<string> excludedConnectionIds, CancellationToken cancellationToken = default)
{
if (groupName == null)
@ -172,12 +197,14 @@ namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis
return PublishAsync(_channels.Group(groupName), message);
}
/// <inheritdoc />
public override Task SendUserAsync(string userId, string methodName, object[] args, CancellationToken cancellationToken = default)
{
var message = _protocol.WriteInvocation(methodName, args);
return PublishAsync(_channels.User(userId), message);
}
/// <inheritdoc />
public override Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default)
{
if (connectionId == null)
@ -200,6 +227,7 @@ namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis
return SendGroupActionAndWaitForAck(connectionId, groupName, GroupAction.Add);
}
/// <inheritdoc />
public override Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default)
{
if (connectionId == null)
@ -222,6 +250,7 @@ namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis
return SendGroupActionAndWaitForAck(connectionId, groupName, GroupAction.Remove);
}
/// <inheritdoc />
public override Task SendConnectionsAsync(IReadOnlyList<string> connectionIds, string methodName, object[] args, CancellationToken cancellationToken = default)
{
if (connectionIds == null)
@ -240,6 +269,7 @@ namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis
return Task.WhenAll(publishTasks);
}
/// <inheritdoc />
public override Task SendGroupsAsync(IReadOnlyList<string> groupNames, string methodName, object[] args, CancellationToken cancellationToken = default)
{
if (groupNames == null)
@ -260,6 +290,7 @@ namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis
return Task.WhenAll(publishTasks);
}
/// <inheritdoc />
public override Task SendUsersAsync(IReadOnlyList<string> userIds, string methodName, object[] args, CancellationToken cancellationToken = default)
{
if (userIds.Count > 0)
@ -352,6 +383,9 @@ namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis
});
}
/// <summary>
/// Cleans up the Redis connection.
/// </summary>
public void Dispose()
{
_bus?.UnsubscribeAll();