diff --git a/src/SignalR/perf/Microbenchmarks/DefaultHubDispatcherBenchmark.cs b/src/SignalR/perf/Microbenchmarks/DefaultHubDispatcherBenchmark.cs index 6910c55c5835..896088fe63c5 100644 --- a/src/SignalR/perf/Microbenchmarks/DefaultHubDispatcherBenchmark.cs +++ b/src/SignalR/perf/Microbenchmarks/DefaultHubDispatcherBenchmark.cs @@ -33,6 +33,7 @@ public void GlobalSetup() serviceScopeFactory, new HubContext(new DefaultHubLifetimeManager(NullLogger>.Instance)), enableDetailedErrors: false, + disableImplicitFromServiceParameters: true, new Logger>(NullLoggerFactory.Instance), hubFilters: null); diff --git a/src/SignalR/server/Core/src/HubConnectionHandler.cs b/src/SignalR/server/Core/src/HubConnectionHandler.cs index 5a80edb721e7..f3e7343c0c3a 100644 --- a/src/SignalR/server/Core/src/HubConnectionHandler.cs +++ b/src/SignalR/server/Core/src/HubConnectionHandler.cs @@ -62,6 +62,7 @@ IServiceScopeFactory serviceScopeFactory _userIdProvider = userIdProvider; _enableDetailedErrors = false; + bool disableImplicitFromServiceParameters; List? hubFilters = null; if (_hubOptions.UserHasSetValues) @@ -69,6 +70,7 @@ IServiceScopeFactory serviceScopeFactory _maximumMessageSize = _hubOptions.MaximumReceiveMessageSize; _enableDetailedErrors = _hubOptions.EnableDetailedErrors ?? _enableDetailedErrors; _maxParallelInvokes = _hubOptions.MaximumParallelInvocationsPerClient; + disableImplicitFromServiceParameters = _hubOptions.DisableImplicitFromServiceParameters; if (_hubOptions.HubFilters != null) { @@ -80,6 +82,7 @@ IServiceScopeFactory serviceScopeFactory _maximumMessageSize = _globalHubOptions.MaximumReceiveMessageSize; _enableDetailedErrors = _globalHubOptions.EnableDetailedErrors ?? _enableDetailedErrors; _maxParallelInvokes = _globalHubOptions.MaximumParallelInvocationsPerClient; + disableImplicitFromServiceParameters = _globalHubOptions.DisableImplicitFromServiceParameters; if (_globalHubOptions.HubFilters != null) { @@ -91,6 +94,7 @@ IServiceScopeFactory serviceScopeFactory serviceScopeFactory, new HubContext(lifetimeManager), _enableDetailedErrors, + disableImplicitFromServiceParameters, new Logger>(loggerFactory), hubFilters); } diff --git a/src/SignalR/server/Core/src/HubOptions.cs b/src/SignalR/server/Core/src/HubOptions.cs index fbd3f81aa930..0f183658336f 100644 --- a/src/SignalR/server/Core/src/HubOptions.cs +++ b/src/SignalR/server/Core/src/HubOptions.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.Extensions.DependencyInjection; + namespace Microsoft.AspNetCore.SignalR; /// @@ -70,4 +73,13 @@ public int MaximumParallelInvocationsPerClient _maximumParallelInvocationsPerClient = value; } } + + /// + /// When , determines if a Hub method parameter will be injected from the DI container. + /// Parameters can be explicitly marked with an attribute that implements with or without this option set. + /// + /// + /// False by default. Hub method arguments will be resolved from a DI container if possible. + /// + public bool DisableImplicitFromServiceParameters { get; set; } } diff --git a/src/SignalR/server/Core/src/HubOptionsSetup`T.cs b/src/SignalR/server/Core/src/HubOptionsSetup`T.cs index ac72f0509f75..7c2fb0ca621f 100644 --- a/src/SignalR/server/Core/src/HubOptionsSetup`T.cs +++ b/src/SignalR/server/Core/src/HubOptionsSetup`T.cs @@ -37,6 +37,7 @@ public void Configure(HubOptions options) options.MaximumReceiveMessageSize = _hubOptions.MaximumReceiveMessageSize; options.StreamBufferCapacity = _hubOptions.StreamBufferCapacity; options.MaximumParallelInvocationsPerClient = _hubOptions.MaximumParallelInvocationsPerClient; + options.DisableImplicitFromServiceParameters = _hubOptions.DisableImplicitFromServiceParameters; options.UserHasSetValues = true; diff --git a/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs b/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs index fbdd2223878f..b3f51f7fe4bb 100644 --- a/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs +++ b/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs @@ -28,13 +28,13 @@ internal partial class DefaultHubDispatcher : HubDispatcher where TH private readonly Func? _onDisconnectedMiddleware; public DefaultHubDispatcher(IServiceScopeFactory serviceScopeFactory, IHubContext hubContext, bool enableDetailedErrors, - ILogger> logger, List? hubFilters) + bool disableImplicitFromServiceParameters, ILogger> logger, List? hubFilters) { _serviceScopeFactory = serviceScopeFactory; _hubContext = hubContext; _enableDetailedErrors = enableDetailedErrors; _logger = logger; - DiscoverHubMethods(); + DiscoverHubMethods(disableImplicitFromServiceParameters); var count = hubFilters?.Count ?? 0; if (count != 0) @@ -307,7 +307,7 @@ await SendInvocationError(hubMethodInvocationMessage.InvocationId, connection, CancellationTokenSource? cts = null; if (descriptor.HasSyntheticArguments) { - ReplaceArguments(descriptor, hubMethodInvocationMessage, isStreamCall, connection, ref arguments, out cts); + ReplaceArguments(descriptor, hubMethodInvocationMessage, isStreamCall, connection, scope, ref arguments, out cts); } if (isStreamResponse) @@ -601,7 +601,7 @@ await connection.WriteAsync(CompletionMessage.WithError(hubMethodInvocationMessa } private void ReplaceArguments(HubMethodDescriptor descriptor, HubMethodInvocationMessage hubMethodInvocationMessage, bool isStreamCall, - HubConnectionContext connection, ref object?[] arguments, out CancellationTokenSource? cts) + HubConnectionContext connection, AsyncServiceScope scope, ref object?[] arguments, out CancellationTokenSource? cts) { cts = null; // In order to add the synthetic arguments we need a new array because the invocation array is too small (it doesn't know about synthetic arguments) @@ -626,6 +626,10 @@ private void ReplaceArguments(HubMethodDescriptor descriptor, HubMethodInvocatio cts = CancellationTokenSource.CreateLinkedTokenSource(connection.ConnectionAborted); arguments[parameterPointer] = cts.Token; } + else if (descriptor.IsServiceArgument(parameterPointer)) + { + arguments[parameterPointer] = scope.ServiceProvider.GetRequiredService(descriptor.OriginalParameterTypes[parameterPointer]); + } else if (isStreamCall && ReflectionHelper.IsStreamingType(descriptor.OriginalParameterTypes[parameterPointer], mustBeDirectType: true)) { Log.StartingParameterStream(_logger, hubMethodInvocationMessage.StreamIds![streamPointer]); @@ -644,12 +648,20 @@ private void ReplaceArguments(HubMethodDescriptor descriptor, HubMethodInvocatio } } - private void DiscoverHubMethods() + private void DiscoverHubMethods(bool disableImplicitFromServiceParameters) { var hubType = typeof(THub); var hubTypeInfo = hubType.GetTypeInfo(); var hubName = hubType.Name; + using var scope = _serviceScopeFactory.CreateScope(); + + IServiceProviderIsService? serviceProviderIsService = null; + if (!disableImplicitFromServiceParameters) + { + serviceProviderIsService = scope.ServiceProvider.GetService(); + } + foreach (var methodInfo in HubReflectionHelper.GetHubMethods(hubType)) { if (methodInfo.IsGenericMethod) @@ -668,7 +680,7 @@ private void DiscoverHubMethods() var executor = ObjectMethodExecutor.Create(methodInfo, hubTypeInfo); var authorizeAttributes = methodInfo.GetCustomAttributes(inherit: true); - _methods[methodName] = new HubMethodDescriptor(executor, authorizeAttributes); + _methods[methodName] = new HubMethodDescriptor(executor, serviceProviderIsService, authorizeAttributes); Log.HubMethodBound(_logger, hubName, methodName); } diff --git a/src/SignalR/server/Core/src/Internal/HubMethodDescriptor.cs b/src/SignalR/server/Core/src/Internal/HubMethodDescriptor.cs index d494ff8dc133..79d364d0ecab 100644 --- a/src/SignalR/server/Core/src/Internal/HubMethodDescriptor.cs +++ b/src/SignalR/server/Core/src/Internal/HubMethodDescriptor.cs @@ -6,6 +6,8 @@ using System.Reflection; using System.Threading.Channels; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.SignalR.Internal; @@ -22,8 +24,10 @@ internal class HubMethodDescriptor private readonly MethodInfo? _makeCancelableEnumeratorMethodInfo; private Func>? _makeCancelableEnumerator; + // bitset to store which parameters come from DI up to 64 arguments + private ulong _isServiceArgument; - public HubMethodDescriptor(ObjectMethodExecutor methodExecutor, IEnumerable policies) + public HubMethodDescriptor(ObjectMethodExecutor methodExecutor, IServiceProviderIsService? serviceProviderIsService, IEnumerable policies) { MethodExecutor = methodExecutor; @@ -56,7 +60,7 @@ public HubMethodDescriptor(ObjectMethodExecutor methodExecutor, IEnumerable + ParameterTypes = methodExecutor.MethodParameters.Where((p, index) => { // Only streams can take CancellationTokens currently if (IsStreamResponse && p.ParameterType == typeof(CancellationToken)) @@ -75,6 +79,18 @@ public HubMethodDescriptor(ObjectMethodExecutor methodExecutor, IEnumerable typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)) || + serviceProviderIsService?.IsService(p.ParameterType) == true) + { + if (index >= 64) + { + throw new InvalidOperationException( + "Hub methods can't use services from DI in the parameters after the 64th parameter."); + } + _isServiceArgument |= (1UL << index); + HasSyntheticArguments = true; + return false; + } return true; }).Select(p => p.ParameterType).ToArray(); @@ -104,6 +120,11 @@ public HubMethodDescriptor(ObjectMethodExecutor methodExecutor, IEnumerable FromReturnedStream(object stream, CancellationToken cancellationToken) { // there is the potential for compile to be called times but this has no harmful effect other than perf diff --git a/src/SignalR/server/Core/src/PublicAPI.Unshipped.txt b/src/SignalR/server/Core/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..0454de320f7f 100644 --- a/src/SignalR/server/Core/src/PublicAPI.Unshipped.txt +++ b/src/SignalR/server/Core/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.SignalR.HubOptions.DisableImplicitFromServiceParameters.get -> bool +Microsoft.AspNetCore.SignalR.HubOptions.DisableImplicitFromServiceParameters.set -> void diff --git a/src/SignalR/server/SignalR/test/AddSignalRTests.cs b/src/SignalR/server/SignalR/test/AddSignalRTests.cs index b39856af1a48..80ef3e2b6c85 100644 --- a/src/SignalR/server/SignalR/test/AddSignalRTests.cs +++ b/src/SignalR/server/SignalR/test/AddSignalRTests.cs @@ -111,6 +111,7 @@ public void HubSpecificOptionsHaveSameValuesAsGlobalHubOptions() Assert.Equal(globalHubOptions.SupportedProtocols, hubOptions.SupportedProtocols); Assert.Equal(globalHubOptions.ClientTimeoutInterval, hubOptions.ClientTimeoutInterval); Assert.Equal(globalHubOptions.MaximumParallelInvocationsPerClient, hubOptions.MaximumParallelInvocationsPerClient); + Assert.Equal(globalHubOptions.DisableImplicitFromServiceParameters, hubOptions.DisableImplicitFromServiceParameters); Assert.True(hubOptions.UserHasSetValues); } @@ -145,6 +146,7 @@ public void UserSpecifiedOptionsRunAfterDefaultOptions() options.SupportedProtocols = null; options.ClientTimeoutInterval = TimeSpan.FromSeconds(1); options.MaximumParallelInvocationsPerClient = 3; + options.DisableImplicitFromServiceParameters = true; }); var serviceProvider = serviceCollection.BuildServiceProvider(); @@ -158,6 +160,7 @@ public void UserSpecifiedOptionsRunAfterDefaultOptions() Assert.Null(globalOptions.SupportedProtocols); Assert.Equal(3, globalOptions.MaximumParallelInvocationsPerClient); Assert.Equal(TimeSpan.FromSeconds(1), globalOptions.ClientTimeoutInterval); + Assert.True(globalOptions.DisableImplicitFromServiceParameters); } [Fact] diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs index 592b4b33bec7..8bf7ee7ae869 100644 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs @@ -1,16 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Runtime.CompilerServices; using System.Text; -using System.Threading; using System.Threading.Channels; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http.Metadata; using Newtonsoft.Json.Serialization; namespace Microsoft.AspNetCore.SignalR.Tests; @@ -1247,3 +1243,65 @@ public void SetCaller(IClientProxy caller) Caller = caller; } } + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] +public class FromService : Attribute, IFromServiceMetadata +{ } +public class Service1 +{ } +public class Service2 +{ } +public class Service3 +{ } + +public class ServicesHub : TestHub +{ + public bool SingleService([FromService] Service1 service) + { + return true; + } + + public bool MultipleServices([FromService] Service1 service, [FromService] Service2 service2, [FromService] Service3 service3) + { + return true; + } + + public async Task ServicesAndParams(int value, [FromService] Service1 service, ChannelReader channelReader, [FromService] Service2 service2, bool value2) + { + int total = 0; + while (await channelReader.WaitToReadAsync()) + { + total += await channelReader.ReadAsync(); + } + return total + value; + } + + public int ServiceWithoutAttribute(Service1 service) + { + return 1; + } + + public int ServiceWithAndWithoutAttribute(Service1 service, [FromService] Service2 service2) + { + return 1; + } + + public async Task Stream(ChannelReader channelReader) + { + while (await channelReader.WaitToReadAsync()) + { + await channelReader.ReadAsync(); + } + } +} + +public class TooManyParamsHub : Hub +{ + public void ManyParams(int a1, string a2, bool a3, float a4, string a5, int a6, int a7, int a8, int a9, int a10, int a11, + int a12, int a13, int a14, int a15, int a16, int a17, int a18, int a19, int a20, int a21, int a22, int a23, int a24, + int a25, int a26, int a27, int a28, int a29, int a30, int a31, int a32, int a33, int a34, int a35, int a36, int a37, + int a38, int a39, int a40, int a41, int a42, int a43, int a44, int a45, int a46, int a47, int a48, int a49, int a50, + int a51, int a52, int a53, int a54, int a55, int a56, int a57, int a58, int a59, int a60, int a61, int a62, int a63, + int a64, [FromService] Service1 service) + { } +} diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs index 55ba1b321be7..8a9c43830065 100644 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs @@ -1,18 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Buffers; -using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.IO; using System.IO.Pipelines; -using System.Linq; using System.Security.Claims; using System.Text; -using System.Threading; -using System.Threading.Tasks; +using System.Threading.Channels; using MessagePack; using MessagePack.Formatters; using MessagePack.Resolvers; @@ -32,7 +27,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; -using Xunit; namespace Microsoft.AspNetCore.SignalR.Tests; @@ -4597,6 +4591,247 @@ public async Task CanSendThroughIHubContextBaseHub() } } + [Fact] + public async Task HubMethodFailsIfServiceNotFound() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(provider => + { + provider.AddSignalR(o => o.EnableDetailedErrors = true); + }); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout(); + var res = await client.InvokeAsync(nameof(ServicesHub.SingleService)).DefaultTimeout(); + Assert.Equal("An unexpected error occurred invoking 'SingleService' on the server. InvalidOperationException: No service for type 'Microsoft.AspNetCore.SignalR.Tests.Service1' has been registered.", res.Error); + } + } + + [Fact] + public async Task HubMethodCanInjectService() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(provider => + { + provider.AddSingleton(); + }); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout(); + var res = await client.InvokeAsync(nameof(ServicesHub.SingleService)).DefaultTimeout(); + Assert.True(Assert.IsType(res.Result)); + } + } + + [Fact] + public async Task HubMethodCanInjectMultipleServices() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(provider => + { + provider.AddSingleton(); + provider.AddSingleton(); + provider.AddSingleton(); + }); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout(); + var res = await client.InvokeAsync(nameof(ServicesHub.MultipleServices)).DefaultTimeout(); + Assert.True(Assert.IsType(res.Result)); + } + } + + [Fact] + public async Task HubMethodCanInjectServicesWithOtherParameters() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(provider => + { + provider.AddSingleton(); + provider.AddSingleton(); + provider.AddSingleton(); + }); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout(); + await client.BeginUploadStreamAsync("0", nameof(ServicesHub.ServicesAndParams), new string[] { "1" }, 10, true).DefaultTimeout(); + + await client.SendHubMessageAsync(new StreamItemMessage("1", 1)).DefaultTimeout(); + await client.SendHubMessageAsync(new StreamItemMessage("1", 14)).DefaultTimeout(); + + await client.SendHubMessageAsync(CompletionMessage.Empty("1")).DefaultTimeout(); + + var response = Assert.IsType(await client.ReadAsync().DefaultTimeout()); + Assert.Equal(25L, response.Result); + } + } + + [Fact] + public async Task StreamFromServiceDoesNotWork() + { + var channel = Channel.CreateBounded(10); + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(provider => + { + provider.AddSingleton(channel.Reader); + }); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout(); + var res = await client.InvokeAsync(nameof(ServicesHub.Stream)).DefaultTimeout(); + Assert.Equal("An unexpected error occurred invoking 'Stream' on the server. HubException: Client sent 0 stream(s), Hub method expects 1.", res.Error); + } + } + + [Fact] + public async Task ServiceNotResolvedWithoutAttribute_WithSettingDisabledGlobally() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(provider => + { + provider.AddSignalR(options => + { + options.EnableDetailedErrors = true; + options.DisableImplicitFromServiceParameters = true; + }); + provider.AddSingleton(); + }); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout(); + var res = await client.InvokeAsync(nameof(ServicesHub.ServiceWithoutAttribute)).DefaultTimeout(); + Assert.Equal("Failed to invoke 'ServiceWithoutAttribute' due to an error on the server. InvalidDataException: Invocation provides 0 argument(s) but target expects 1.", res.Error); + } + } + + [Fact] + public async Task ServiceResolvedWithoutAttribute() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(provider => + { + provider.AddSignalR(options => + { + options.EnableDetailedErrors = true; + }); + provider.AddSingleton(); + }); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout(); + var res = await client.InvokeAsync(nameof(ServicesHub.ServiceWithoutAttribute)).DefaultTimeout(); + Assert.Equal(1L, res.Result); + } + } + + [Fact] + public async Task ServiceResolvedWithoutAttribute_WithHubSpecificSettingEnabled() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(provider => + { + provider.AddSignalR(options => + { + options.EnableDetailedErrors = true; + options.DisableImplicitFromServiceParameters = true; + }).AddHubOptions(options => + { + options.DisableImplicitFromServiceParameters = false; + }); + provider.AddSingleton(); + }); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout(); + var res = await client.InvokeAsync(nameof(ServicesHub.ServiceWithoutAttribute)).DefaultTimeout(); + Assert.Equal(1L, res.Result); + } + } + + [Fact] + public async Task ServiceNotResolvedWithAndWithoutAttribute_WithOptionDisabled() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(provider => + { + provider.AddSignalR(options => + { + options.EnableDetailedErrors = true; + options.DisableImplicitFromServiceParameters = true; + }); + provider.AddSingleton(); + provider.AddSingleton(); + }); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout(); + var res = await client.InvokeAsync(nameof(ServicesHub.ServiceWithAndWithoutAttribute)).DefaultTimeout(); + Assert.Equal("Failed to invoke 'ServiceWithAndWithoutAttribute' due to an error on the server. InvalidDataException: Invocation provides 0 argument(s) but target expects 1.", res.Error); + } + } + + [Fact] + public async Task ServiceResolvedWithAndWithoutAttribute() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(provider => + { + provider.AddSignalR(options => + { + options.EnableDetailedErrors = true; + }); + provider.AddSingleton(); + provider.AddSingleton(); + }); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout(); + var res = await client.InvokeAsync(nameof(ServicesHub.ServiceWithAndWithoutAttribute)).DefaultTimeout(); + Assert.Equal(1L, res.Result); + } + } + + [Fact] + public async Task ServiceNotResolvedIfNotInDI() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(provider => + { + provider.AddSignalR(options => + { + options.EnableDetailedErrors = true; + }); + }); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).DefaultTimeout(); + var res = await client.InvokeAsync(nameof(ServicesHub.ServiceWithoutAttribute)).DefaultTimeout(); + Assert.Equal("Failed to invoke 'ServiceWithoutAttribute' due to an error on the server. InvalidDataException: Invocation provides 0 argument(s) but target expects 1.", res.Error); + } + } + + [Fact] + public void TooManyParametersWithServiceThrows() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(provider => + { + provider.AddSingleton(); + }); + Assert.Throws( + () => serviceProvider.GetService>()); + } + private class CustomHubActivator : IHubActivator where THub : Hub { public int ReleaseCount;