diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs index f99cb6c76e..94f3fff9dd 100644 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs @@ -661,6 +661,30 @@ namespace Microsoft.AspNetCore.SignalR.Tests return output.Reader; } + public async IAsyncEnumerable DerivedParameterInterfaceAsyncEnumerable(IDerivedParameterTestObject param) + { + await Task.Yield(); + yield return param.Value; + } + + public async IAsyncEnumerable DerivedParameterBaseClassAsyncEnumerable(DerivedParameterTestObjectBase param) + { + await Task.Yield(); + yield return param.Value; + } + + public async IAsyncEnumerable DerivedParameterInterfaceAsyncEnumerableWithCancellation(IDerivedParameterTestObject param, [EnumeratorCancellation] CancellationToken token) + { + await Task.Yield(); + yield return param.Value; + } + + public async IAsyncEnumerable DerivedParameterBaseClassAsyncEnumerableWithCancellation(DerivedParameterTestObjectBase param, [EnumeratorCancellation] CancellationToken token) + { + await Task.Yield(); + yield return param.Value; + } + public class AsyncEnumerableImpl : IAsyncEnumerable { private readonly IAsyncEnumerable _inner; @@ -753,6 +777,18 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } } + + public interface IDerivedParameterTestObject + { + public string Value { get; set; } + } + + public abstract class DerivedParameterTestObjectBase : IDerivedParameterTestObject + { + public string Value { get; set; } + } + + public class DerivedParameterTestObject : DerivedParameterTestObjectBase { } } public class SimpleHub : Hub diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs index 56b77c0b7a..dc58bd227b 100644 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs @@ -3576,6 +3576,62 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } + /// + /// Hub methods might be written by users in a way that accepts an interface or base class as a parameter + /// and deserialization could supply a derived class. + /// This test ensures implementation and subclass arguments are correctly bound for dispatch. + /// + [Theory] + [InlineData(nameof(StreamingHub.DerivedParameterInterfaceAsyncEnumerable))] + [InlineData(nameof(StreamingHub.DerivedParameterBaseClassAsyncEnumerable))] + [InlineData(nameof(StreamingHub.DerivedParameterInterfaceAsyncEnumerableWithCancellation))] + [InlineData(nameof(StreamingHub.DerivedParameterBaseClassAsyncEnumerableWithCancellation))] + public async Task CanPassDerivedParameterToStreamHubMethod(string method) + { + using (StartVerifiableLog()) + { + var argument = new StreamingHub.DerivedParameterTestObject { Value = "test" }; + var protocolOptions = new NewtonsoftJsonHubProtocolOptions + { + PayloadSerializerSettings = new JsonSerializerSettings() + { + // The usage of TypeNameHandling.All is a security risk. + // If you're implementing this in your own application instead use your own 'type' field and a custom JsonConverter + // or ensure you're restricting to only known types with a custom SerializationBinder. + // See https://github.com/aspnet/AspNetCore/issues/11495#issuecomment-505047422 + TypeNameHandling = TypeNameHandling.All + } + }; + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider( + services => services.AddSignalR() + .AddNewtonsoftJsonProtocol(o => o.PayloadSerializerSettings = protocolOptions.PayloadSerializerSettings), + LoggerFactory); + var connectionHandler = serviceProvider.GetService>(); + var invocationBinder = new Mock(); + invocationBinder.Setup(b => b.GetStreamItemType(It.IsAny())).Returns(typeof(string)); + + using (var client = new TestClient( + protocol: new NewtonsoftJsonHubProtocol(Options.Create(protocolOptions)), + invocationBinder: invocationBinder.Object)) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler); + + // Wait for a connection, or for the endpoint to fail. + await client.Connected.OrThrowIfOtherFails(connectionHandlerTask).OrTimeout(); + + var messages = await client.StreamAsync(method, argument).OrTimeout(); + + Assert.Equal(2, messages.Count); + HubConnectionHandlerTestUtils.AssertHubMessage(new StreamItemMessage(string.Empty, argument.Value), messages[0]); + HubConnectionHandlerTestUtils.AssertHubMessage(CompletionMessage.Empty(string.Empty), messages[1]); + + client.Dispose(); + + await connectionHandlerTask.OrTimeout(); + } + } + } + private class CustomHubActivator : IHubActivator where THub : Hub { public int ReleaseCount; diff --git a/src/SignalR/server/SignalR/test/Internal/DefaultHubDispatcherTests.cs b/src/SignalR/server/SignalR/test/Internal/DefaultHubDispatcherTests.cs deleted file mode 100644 index f28ec35cb1..0000000000 --- a/src/SignalR/server/SignalR/test/Internal/DefaultHubDispatcherTests.cs +++ /dev/null @@ -1,143 +0,0 @@ -// 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.IO.Pipelines; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.SignalR.Internal; -using Microsoft.AspNetCore.SignalR.Protocol; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Xunit; - -namespace Microsoft.AspNetCore.SignalR.Tests.Internal -{ - public class DefaultHubDispatcherTests - { - private class MockHubConnectionContext : HubConnectionContext - { - public TaskCompletionSource ReceivedCompleted = new TaskCompletionSource(); - public List Values = new List(); - - public MockHubConnectionContext(ConnectionContext connectionContext, HubConnectionContextOptions contextOptions, ILoggerFactory loggerFactory) - : base(connectionContext, contextOptions, loggerFactory) { } - - public override ValueTask WriteAsync(HubMessage message, CancellationToken cancellationToken) - { - if (message is StreamItemMessage streamItemMessage) - Values.Add((TValue)streamItemMessage.Item); - - else if (message is CompletionMessage completionMessage) - { - ReceivedCompleted.TrySetResult(null); - - if (!string.IsNullOrEmpty(completionMessage.Error)) - { - throw new Exception("Error invoking hub method: " + completionMessage.Error); - } - } - - else throw new NotImplementedException(); - - return default; - } - } - - private static DefaultHubDispatcher CreateDispatcher() where THub : Hub - { - var serviceCollection = new ServiceCollection(); - serviceCollection.TryAddScoped(typeof(IHubActivator<>), typeof(DefaultHubActivator<>)); - var provider = serviceCollection.BuildServiceProvider(); - var serviceScopeFactory = provider.GetService(); - - return new DefaultHubDispatcher( - serviceScopeFactory, - new HubContext(new DefaultHubLifetimeManager(NullLogger>.Instance)), - Options.Create(new HubOptions()), - Options.Create(new HubOptions()), - new Logger>(NullLoggerFactory.Instance)); - } - - private static MockHubConnectionContext CreateConnectionContext() - { - var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default); - var connection = new DefaultConnectionContext(Guid.NewGuid().ToString(), pair.Application, pair.Transport); - var contextOptions = new HubConnectionContextOptions() { KeepAliveInterval = TimeSpan.Zero }; - - return new MockHubConnectionContext( - connection, - contextOptions, - NullLoggerFactory.Instance); - } - - /// - /// For . - /// - private interface ITestDerivedParameter - { - public string Value { get; } - } - - /// - /// For . - /// - private abstract class TestDerivedParameterBase - { - public TestDerivedParameterBase(string value) => Value = value; - public string Value { get; } - } - - /// - /// For . - /// - private class TestDerivedParameter : TestDerivedParameterBase, ITestDerivedParameter - { - public TestDerivedParameter(string value) : base(value) { } - } - - /// - /// For . - /// - private class TestDerivedParameterHub : Hub - { - public async IAsyncEnumerable TestSubclass(TestDerivedParameterBase param, [EnumeratorCancellation]CancellationToken token) - { - await Task.Yield(); - yield return param.Value; - } - - public async IAsyncEnumerable TestImplementation(ITestDerivedParameter param, [EnumeratorCancellation]CancellationToken token) - { - await Task.Yield(); - yield return param.Value; - } - } - - /// - /// Hub methods might be written by users in a way that accepts an interface or base class as a parameter - /// and deserialization could supply a derived class (e.g. Json.NET's TypeNameHandling = TypeNameHandling.All). - /// This test ensures implementation and subclass arguments are correctly bound for dispatch. - /// - [Theory] - [InlineData(nameof(TestDerivedParameterHub.TestImplementation))] - [InlineData(nameof(TestDerivedParameterHub.TestSubclass))] - public async Task DispatchesDerivedArguments(string methodName) - { - var message = new TestDerivedParameter("Yup"); - var connectionContext = CreateConnectionContext(); - var dispatcher = CreateDispatcher(); - - await dispatcher.DispatchMessageAsync(connectionContext, new StreamInvocationMessage("123", methodName, new[] { message })); - await (connectionContext as MockHubConnectionContext).ReceivedCompleted.Task; - - Assert.Single(connectionContext.Values, message.Value); - } - } -}