diff --git a/src/SignalR/Directory.Build.targets b/src/SignalR/Directory.Build.targets index a5761bff02..923e244638 100644 --- a/src/SignalR/Directory.Build.targets +++ b/src/SignalR/Directory.Build.targets @@ -1,7 +1,5 @@  - $(MicrosoftNETCoreApp22PackageVersion) - - 99.9 + $(MicrosoftNETCoreAppPackageVersion) diff --git a/src/SignalR/SignalR.sln b/src/SignalR/SignalR.sln index 7f925af865..6f8900fb5f 100644 --- a/src/SignalR/SignalR.sln +++ b/src/SignalR/SignalR.sln @@ -33,8 +33,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.C EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Core", "src\Microsoft.AspNetCore.SignalR.Core\Microsoft.AspNetCore.SignalR.Core.csproj", "{42E76F87-92B6-45AB-BF07-6B811C0F2CAC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Redis", "src\Microsoft.AspNetCore.SignalR.Redis\Microsoft.AspNetCore.SignalR.Redis.csproj", "{59319B72-38BE-4041-8E5C-FF6938874CE8}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SocialWeather", "samples\SocialWeather\SocialWeather.csproj", "{8D789F94-CB74-45FD-ACE7-92AF6E55042E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Tests", "test\Microsoft.AspNetCore.SignalR.Tests\Microsoft.AspNetCore.SignalR.Tests.csproj", "{1CE2B3BE-056C-41E3-A5F5-6A1EF1D288BA}" @@ -69,8 +67,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Client", "src\Microsoft.AspNetCore.SignalR.Client\Microsoft.AspNetCore.SignalR.Client.csproj", "{BE982591-F4BB-42D9-ABD4-A5D44C65971E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Redis.Tests", "test\Microsoft.AspNetCore.SignalR.Redis.Tests\Microsoft.AspNetCore.SignalR.Redis.Tests.csproj", "{0B083AE6-86CA-4E0B-AE02-59154D1FD005}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JwtSample", "samples\JwtSample\JwtSample.csproj", "{6A7491D3-3C97-49BD-A71C-433AED657F30}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JwtClientSample", "samples\JwtClientSample\JwtClientSample.csproj", "{1A953296-E869-4DE2-A693-FD5FCDE27057}" @@ -113,10 +109,6 @@ Global {42E76F87-92B6-45AB-BF07-6B811C0F2CAC}.Debug|Any CPU.Build.0 = Debug|Any CPU {42E76F87-92B6-45AB-BF07-6B811C0F2CAC}.Release|Any CPU.ActiveCfg = Release|Any CPU {42E76F87-92B6-45AB-BF07-6B811C0F2CAC}.Release|Any CPU.Build.0 = Release|Any CPU - {59319B72-38BE-4041-8E5C-FF6938874CE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {59319B72-38BE-4041-8E5C-FF6938874CE8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {59319B72-38BE-4041-8E5C-FF6938874CE8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {59319B72-38BE-4041-8E5C-FF6938874CE8}.Release|Any CPU.Build.0 = Release|Any CPU {8D789F94-CB74-45FD-ACE7-92AF6E55042E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8D789F94-CB74-45FD-ACE7-92AF6E55042E}.Debug|Any CPU.Build.0 = Debug|Any CPU {8D789F94-CB74-45FD-ACE7-92AF6E55042E}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -177,10 +169,6 @@ Global {BE982591-F4BB-42D9-ABD4-A5D44C65971E}.Debug|Any CPU.Build.0 = Debug|Any CPU {BE982591-F4BB-42D9-ABD4-A5D44C65971E}.Release|Any CPU.ActiveCfg = Release|Any CPU {BE982591-F4BB-42D9-ABD4-A5D44C65971E}.Release|Any CPU.Build.0 = Release|Any CPU - {0B083AE6-86CA-4E0B-AE02-59154D1FD005}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0B083AE6-86CA-4E0B-AE02-59154D1FD005}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0B083AE6-86CA-4E0B-AE02-59154D1FD005}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0B083AE6-86CA-4E0B-AE02-59154D1FD005}.Release|Any CPU.Build.0 = Release|Any CPU {6A7491D3-3C97-49BD-A71C-433AED657F30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6A7491D3-3C97-49BD-A71C-433AED657F30}.Debug|Any CPU.Build.0 = Debug|Any CPU {6A7491D3-3C97-49BD-A71C-433AED657F30}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -233,7 +221,6 @@ Global {C4AEAB04-F341-4539-B6C0-52368FB4BF9E} = {C4BC9889-B49F-41B6-806B-F84941B2549B} {AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9} = {6A35B453-52EC-48AF-89CA-D4A69800F131} {42E76F87-92B6-45AB-BF07-6B811C0F2CAC} = {DA69F624-5398-4884-87E4-B816698CDE65} - {59319B72-38BE-4041-8E5C-FF6938874CE8} = {DA69F624-5398-4884-87E4-B816698CDE65} {8D789F94-CB74-45FD-ACE7-92AF6E55042E} = {C4BC9889-B49F-41B6-806B-F84941B2549B} {1CE2B3BE-056C-41E3-A5F5-6A1EF1D288BA} = {6A35B453-52EC-48AF-89CA-D4A69800F131} {BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9} = {C4BC9889-B49F-41B6-806B-F84941B2549B} @@ -249,7 +236,6 @@ Global {B0243F99-2D3F-4CC6-AD71-E3F891B64724} = {DA69F624-5398-4884-87E4-B816698CDE65} {E081EE41-D95F-4AD2-BC0B-4B562C0A2A47} = {DA69F624-5398-4884-87E4-B816698CDE65} {BE982591-F4BB-42D9-ABD4-A5D44C65971E} = {DA69F624-5398-4884-87E4-B816698CDE65} - {0B083AE6-86CA-4E0B-AE02-59154D1FD005} = {6A35B453-52EC-48AF-89CA-D4A69800F131} {6A7491D3-3C97-49BD-A71C-433AED657F30} = {C4BC9889-B49F-41B6-806B-F84941B2549B} {1A953296-E869-4DE2-A693-FD5FCDE27057} = {C4BC9889-B49F-41B6-806B-F84941B2549B} {0A0A6135-EA24-4307-95C2-CE1B7E164A5E} = {6A35B453-52EC-48AF-89CA-D4A69800F131} diff --git a/src/SignalR/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj b/src/SignalR/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj index 67a14ce4e7..cc5036ad1a 100644 --- a/src/SignalR/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj +++ b/src/SignalR/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj @@ -1,9 +1,7 @@  - netcoreapp2.2 - - $([System.String]::Copy($(MicrosoftAspNetCoreAllPackageVersion)).Replace('2.2', '1.1')) + netcoreapp3.0 @@ -14,7 +12,7 @@ - + @@ -23,8 +21,8 @@ - - + + diff --git a/src/SignalR/benchmarkapps/BenchmarkServer/Startup.cs b/src/SignalR/benchmarkapps/BenchmarkServer/Startup.cs index 3363b8de58..a9f167cae9 100644 --- a/src/SignalR/benchmarkapps/BenchmarkServer/Startup.cs +++ b/src/SignalR/benchmarkapps/BenchmarkServer/Startup.cs @@ -28,7 +28,7 @@ namespace BenchmarkServer var redisConnectionString = _config["SignalRRedis"]; if (!string.IsNullOrEmpty(redisConnectionString)) { - signalrBuilder.AddRedis(redisConnectionString); + signalrBuilder.AddStackExchangeRedis(redisConnectionString); } } diff --git a/src/SignalR/benchmarkapps/BenchmarkServer/signalr.json b/src/SignalR/benchmarkapps/BenchmarkServer/signalr.json index 23c53f7088..d8d47bba67 100644 --- a/src/SignalR/benchmarkapps/BenchmarkServer/signalr.json +++ b/src/SignalR/benchmarkapps/BenchmarkServer/signalr.json @@ -3,7 +3,7 @@ "Client": "SignalR", "Source": { "Repository": "https://github.com/aspnet/SignalR.git", - "BranchOrCommit": "release/2.2", + "BranchOrCommit": "dev", "Project": "benchmarkapps/BenchmarkServer/BenchmarkServer.csproj" }, "Connections": 10, diff --git a/src/SignalR/benchmarkapps/Crankier/Crankier.csproj b/src/SignalR/benchmarkapps/Crankier/Crankier.csproj index d73db04ff4..d8dbc064ff 100644 --- a/src/SignalR/benchmarkapps/Crankier/Crankier.csproj +++ b/src/SignalR/benchmarkapps/Crankier/Crankier.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.2 + netcoreapp3.0 Microsoft.AspNetCore.SignalR.CranksRevenge diff --git a/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/DefaultHubDispatcherBenchmark.cs b/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/DefaultHubDispatcherBenchmark.cs index 3da8b6d0cc..d7431ed28b 100644 --- a/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/DefaultHubDispatcherBenchmark.cs +++ b/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/DefaultHubDispatcherBenchmark.cs @@ -54,6 +54,7 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks { public string Name { get; } public int Version => 1; + public int MinorVersion => 0; public TransferFormat TransferFormat { get; } public bool IsVersionSupported(int version) diff --git a/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/HandshakeProtocolBenchmark.cs b/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/HandshakeProtocolBenchmark.cs new file mode 100644 index 0000000000..2a36dc648e --- /dev/null +++ b/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/HandshakeProtocolBenchmark.cs @@ -0,0 +1,127 @@ +// 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.Buffers; +using System.Text; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.SignalR.Protocol; + +namespace Microsoft.AspNetCore.SignalR.Microbenchmarks +{ + public class HandshakeProtocolBenchmark + { + ReadOnlySequence _requestMessage1; + ReadOnlySequence _requestMessage2; + ReadOnlySequence _requestMessage3; + ReadOnlySequence _requestMessage4; + + ReadOnlySequence _responseMessage1; + ReadOnlySequence _responseMessage2; + ReadOnlySequence _responseMessage3; + ReadOnlySequence _responseMessage4; + ReadOnlySequence _responseMessage5; + ReadOnlySequence _responseMessage6; + + [GlobalSetup] + public void GlobalSetup() + { + _requestMessage1 = new ReadOnlySequence(Encoding.UTF8.GetBytes("{\"protocol\":\"dummy\",\"version\":1}\u001e")); + _requestMessage2 = new ReadOnlySequence(Encoding.UTF8.GetBytes("{\"protocol\":\"\",\"version\":10}\u001e")); + _requestMessage3 = new ReadOnlySequence(Encoding.UTF8.GetBytes("{\"protocol\":\"\",\"version\":10,\"unknown\":null}\u001e")); + _requestMessage4 = new ReadOnlySequence(Encoding.UTF8.GetBytes("42")); + + _responseMessage1 = new ReadOnlySequence(Encoding.UTF8.GetBytes("{\"error\":\"dummy\"}\u001e")); + _responseMessage2 = new ReadOnlySequence(Encoding.UTF8.GetBytes("{\"error\":\"\"}\u001e")); + _responseMessage3 = new ReadOnlySequence(Encoding.UTF8.GetBytes("{}\u001e")); + _responseMessage4 = new ReadOnlySequence(Encoding.UTF8.GetBytes("{\"unknown\":null}\u001e")); + _responseMessage5 = new ReadOnlySequence(Encoding.UTF8.GetBytes("{\"error\":\"\",\"minorVersion\":34}\u001e")); + _responseMessage6 = new ReadOnlySequence(Encoding.UTF8.GetBytes("{\"error\":\"flump flump flump\",\"minorVersion\":112}\u001e")); + } + + [Benchmark] + public void HandShakeWriteResponseEmpty_MemoryBufferWriter() + { + var writer = MemoryBufferWriter.Get(); + try + { + HandshakeProtocol.WriteResponseMessage(HandshakeResponseMessage.Empty, writer); + } + finally + { + MemoryBufferWriter.Return(writer); + } + } + + [Benchmark] + public void HandShakeWriteResponse_MemoryBufferWriter() + { + ReadOnlyMemory result; + var memoryBufferWriter = MemoryBufferWriter.Get(); + try + { + HandshakeProtocol.WriteResponseMessage(new HandshakeResponseMessage(1), memoryBufferWriter); + result = memoryBufferWriter.ToArray(); + } + finally + { + MemoryBufferWriter.Return(memoryBufferWriter); + } + } + + [Benchmark] + public void HandShakeWriteRequest_MemoryBufferWriter() + { + var memoryBufferWriter = MemoryBufferWriter.Get(); + try + { + HandshakeProtocol.WriteRequestMessage(new HandshakeRequestMessage("json", 1), memoryBufferWriter); + } + finally + { + MemoryBufferWriter.Return(memoryBufferWriter); + } + } + + [Benchmark] + public void ParsingHandshakeRequestMessage_ValidMessage1() + => HandshakeProtocol.TryParseRequestMessage(ref _requestMessage1, out var deserializedMessage); + + [Benchmark] + public void ParsingHandshakeRequestMessage_ValidMessage2() + => HandshakeProtocol.TryParseRequestMessage(ref _requestMessage2, out var deserializedMessage); + + [Benchmark] + public void ParsingHandshakeRequestMessage_ValidMessage3() + => HandshakeProtocol.TryParseRequestMessage(ref _requestMessage3, out var deserializedMessage); + + [Benchmark] + public void ParsingHandshakeRequestMessage_NotComplete1() + => HandshakeProtocol.TryParseRequestMessage(ref _requestMessage4, out _); + + [Benchmark] + public void ParsingHandshakeResponseMessage_ValidMessages1() + => HandshakeProtocol.TryParseResponseMessage(ref _responseMessage1, out var response); + + [Benchmark] + public void ParsingHandshakeResponseMessage_ValidMessages2() + => HandshakeProtocol.TryParseResponseMessage(ref _responseMessage2, out var response); + + [Benchmark] + public void ParsingHandshakeResponseMessage_ValidMessages3() + => HandshakeProtocol.TryParseResponseMessage(ref _responseMessage3, out var response); + + [Benchmark] + public void ParsingHandshakeResponseMessage_ValidMessages4() + => HandshakeProtocol.TryParseResponseMessage(ref _responseMessage4, out var response); + + [Benchmark] + public void ParsingHandshakeResponseMessage_GivesMinorVersion1() + => HandshakeProtocol.TryParseResponseMessage(ref _responseMessage5, out var response); + + [Benchmark] + public void ParsingHandshakeResponseMessage_GivesMinorVersion2() + => HandshakeProtocol.TryParseResponseMessage(ref _responseMessage6, out var response); + } +} \ No newline at end of file diff --git a/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks.csproj b/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks.csproj index 0143f5ffae..4f0e19b517 100644 --- a/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks.csproj +++ b/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.2 + netcoreapp3.0 diff --git a/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/NegotiateProtocolBenchmark.cs b/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/NegotiateProtocolBenchmark.cs index 2e4120e187..909b4217d0 100644 --- a/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/NegotiateProtocolBenchmark.cs +++ b/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/NegotiateProtocolBenchmark.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; +using System.Text; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Http.Connections; @@ -15,6 +16,12 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks private NegotiationResponse _negotiateResponse; private Stream _stream; + private byte[] _responseData1; + private byte[] _responseData2; + private byte[] _responseData3; + private byte[] _responseData4; + private byte[] _responseData5; + [GlobalSetup] public void GlobalSetup() { @@ -35,6 +42,15 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks } }; _stream = Stream.Null; + + _responseData1 = Encoding.UTF8.GetBytes("{\"connectionId\":\"123\",\"availableTransports\":[]}"); + _responseData2 = Encoding.UTF8.GetBytes("{\"url\": \"http://foo.com/chat\"}"); + _responseData3 = Encoding.UTF8.GetBytes("{\"url\": \"http://foo.com/chat\", \"accessToken\": \"token\"}"); + _responseData4 = Encoding.UTF8.GetBytes("{\"connectionId\":\"123\",\"availableTransports\":[{\"transport\":\"test\",\"transferFormats\":[]}]}"); + + var writer = new MemoryBufferWriter(); + NegotiateProtocol.WriteResponse(_negotiateResponse, writer); + _responseData5 = writer.ToArray(); } [Benchmark] @@ -51,5 +67,25 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks writer.Reset(); } } + + [Benchmark] + public void ParsingNegotiateResponseMessageSuccessForValid1() + => NegotiateProtocol.ParseResponse(new MemoryStream(_responseData1)); + + [Benchmark] + public void ParsingNegotiateResponseMessageSuccessForValid2() + => NegotiateProtocol.ParseResponse(new MemoryStream(_responseData2)); + + [Benchmark] + public void ParsingNegotiateResponseMessageSuccessForValid3() + => NegotiateProtocol.ParseResponse(new MemoryStream(_responseData3)); + + [Benchmark] + public void ParsingNegotiateResponseMessageSuccessForValid4() + => NegotiateProtocol.ParseResponse(new MemoryStream(_responseData4)); + + [Benchmark] + public void ParsingNegotiateResponseMessageSuccessForValid5() + => NegotiateProtocol.ParseResponse(new MemoryStream(_responseData5)); } -} +} \ No newline at end of file diff --git a/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/RedisHubLifetimeManagerBenchmark.cs b/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/RedisHubLifetimeManagerBenchmark.cs index 852fbdfa3c..606c53673c 100644 --- a/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/RedisHubLifetimeManagerBenchmark.cs +++ b/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/RedisHubLifetimeManagerBenchmark.cs @@ -178,6 +178,7 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks public string Name => _name; public int Version => _innerProtocol.Version; + public int MinorVersion => _innerProtocol.MinorVersion; public TransferFormat TransferFormat => _innerProtocol.TransferFormat; diff --git a/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/RedisProtocolBenchmark.cs b/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/RedisProtocolBenchmark.cs index f5e02e489b..c87d0e5226 100644 --- a/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/RedisProtocolBenchmark.cs +++ b/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/RedisProtocolBenchmark.cs @@ -126,6 +126,7 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks public string Name { get; } public int Version => 1; + public int MinorVersion => 0; public TransferFormat TransferFormat => TransferFormat.Text; diff --git a/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/TestBinder.cs b/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/TestBinder.cs index 1e25041d4f..bbcb8a1bde 100644 --- a/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/TestBinder.cs +++ b/src/SignalR/benchmarks/Microsoft.AspNetCore.SignalR.Microbenchmarks/TestBinder.cs @@ -59,5 +59,10 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks } throw new InvalidOperationException("Unexpected binder call"); } + + public Type GetStreamItemType(string streamId) + { + throw new NotImplementedException(); + } } } diff --git a/src/SignalR/build/dependencies.props b/src/SignalR/build/dependencies.props index 361fce7a5f..719ea4d5a2 100644 --- a/src/SignalR/build/dependencies.props +++ b/src/SignalR/build/dependencies.props @@ -5,74 +5,73 @@ 0.10.13 3.1.0 - 2.2.0-rtm-181106-13 - 2.2.0-preview2-20181011.2 + 3.0.0-preview-181113-11 + 3.0.0-build-20181114.5 1.7.3.4 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-35661 - 2.2.0-preview3-35457 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-35661 - 4.5.0 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-35661 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-27105-02 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-preview-181113-11 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-alpha1-10742 + 3.0.0-preview-181113-11 + 3.0.0-alpha1-10742 + 4.6.0-preview1-26907-04 + 3.0.0-preview-181109-02 + 3.0.0-preview-181109-02 + 3.0.0-preview-181109-02 + 3.0.0-alpha1-10727 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview-181113-11 + 3.0.0-preview1-26907-05 15.6.1 4.10.0 2.0.3 11.0.2 2.0.513 - 1.2.6 - 4.5.0 - 4.5.2 - 4.5.1 - 4.5.0 + 4.6.0-preview1-26907-04 + 4.6.0-preview1-26907-04 + 4.6.0-preview1-26717-04 + 4.6.0-preview1-26907-04 3.1.1 4.3.0 - 4.5.1 - 4.5.0 - 4.5.0 - 4.5.1 + 4.6.0-preview1-26907-04 + 4.6.0-preview1-26907-04 + 4.6.0-preview1-26907-04 + 4.6.0-preview1-26907-04 2.3.1 2.3.1 2.3.1 diff --git a/src/SignalR/build/publish-apps.ps1 b/src/SignalR/build/publish-apps.ps1 index a2d0062e79..6a45f50a0f 100644 --- a/src/SignalR/build/publish-apps.ps1 +++ b/src/SignalR/build/publish-apps.ps1 @@ -1,4 +1,4 @@ -param($RootDirectory = (Get-Location), $Framework = "netcoreapp2.2", $Runtime = "win7-x64", $CommitHash, $BranchName, $BuildNumber) +param($RootDirectory = (Get-Location), $Framework = "netcoreapp3.0", $Runtime = "win7-x64", $CommitHash, $BranchName, $BuildNumber) # De-Powershell the path $RootDirectory = (Convert-Path $RootDirectory) @@ -45,4 +45,4 @@ $Apps.Keys | ForEach-Object { } finally { Remove-Item $MetadataPath } -} \ No newline at end of file +} diff --git a/src/SignalR/build/repo.props b/src/SignalR/build/repo.props index fc909de537..16207d928b 100644 --- a/src/SignalR/build/repo.props +++ b/src/SignalR/build/repo.props @@ -15,11 +15,10 @@ Internal.AspNetCore.Universe.Lineup - 2.2.0-* https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json - + diff --git a/src/SignalR/clients/java/signalr/build.gradle b/src/SignalR/clients/java/signalr/build.gradle index 70eaa9fc45..20c3661bda 100644 --- a/src/SignalR/clients/java/signalr/build.gradle +++ b/src/SignalR/clients/java/signalr/build.gradle @@ -35,7 +35,7 @@ dependencies { testCompile 'org.slf4j:slf4j-jdk14:1.7.25' implementation 'com.google.code.gson:gson:2.8.5' implementation 'com.squareup.okhttp3:okhttp:3.11.0' - api 'io.reactivex.rxjava2:rxjava:2.2.2' + api 'io.reactivex.rxjava2:rxjava:2.2.3' implementation 'org.slf4j:slf4j-api:1.7.25' } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CancelInvocationMessage.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CancelInvocationMessage.java new file mode 100644 index 0000000000..096c49faf0 --- /dev/null +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/CancelInvocationMessage.java @@ -0,0 +1,18 @@ +// 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. + +package com.microsoft.signalr; + +final class CancelInvocationMessage extends HubMessage { + private final int type = HubMessageType.CANCEL_INVOCATION.value; + private final String invocationId; + + public CancelInvocationMessage(String invocationId) { + this.invocationId = invocationId; + } + + @Override + public HubMessageType getMessageType() { + return HubMessageType.CANCEL_INVOCATION; + } +} diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java index 485137d0a6..5b255ec9bd 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java @@ -21,9 +21,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.reactivex.Completable; +import io.reactivex.Observable; import io.reactivex.Single; -import io.reactivex.subjects.CompletableSubject; -import io.reactivex.subjects.SingleSubject; +import io.reactivex.subjects.*; /** * A connection used to invoke hub methods on a SignalR Server. @@ -58,7 +58,6 @@ public class HubConnection { private long handshakeResponseTimeout = 15*1000; private final Logger logger = LoggerFactory.getLogger(HubConnection.class); - /** * Sets the server timeout interval for the connection. * @@ -202,8 +201,17 @@ public class HubConnection { } irq.complete(completionMessage); break; - case STREAM_INVOCATION: case STREAM_ITEM: + StreamItem streamItem = (StreamItem)message; + InvocationRequest streamInvocationRequest = connectionState.getInvocation(streamItem.getInvocationId()); + if (streamInvocationRequest == null) { + logger.warn("Dropped unsolicited Completion message for invocation '{}'.", streamItem.getInvocationId()); + continue; + } + + streamInvocationRequest.addItem(streamItem); + break; + case STREAM_INVOCATION: case CANCEL_INVOCATION: logger.error("This client does not support {} messages.", message.getMessageType()); @@ -481,7 +489,7 @@ public class HubConnection { // forward the invocation result or error to the user // run continuations on a separate thread - Single pendingCall = irq.getPendingCall(); + Subject pendingCall = irq.getPendingCall(); pendingCall.subscribe(result -> { // Primitive types can't be cast with the Class cast function if (returnType.isPrimitive()) { @@ -498,10 +506,54 @@ public class HubConnection { return subject; } + /** + * Invokes a streaming hub method on the server using the specified name and arguments. + * + * @param returnType The expected return type of the stream items. + * @param method The name of the server method to invoke. + * @param args The arguments used to invoke the server method. + * @param The expected return type. + * @return An observable that yields the streaming results from the server. + */ + @SuppressWarnings("unchecked") + public Observable stream(Class returnType, String method, Object ... args) { + String invocationId = connectionState.getNextInvocationId(); + AtomicInteger subscriptionCount = new AtomicInteger(); + StreamInvocationMessage streamInvocationMessage = new StreamInvocationMessage(invocationId, method, args); + InvocationRequest irq = new InvocationRequest(returnType, invocationId); + connectionState.addInvocation(irq); + ReplaySubject subject = ReplaySubject.create(); + + Subject pendingCall = irq.getPendingCall(); + pendingCall.subscribe(result -> { + // Primitive types can't be cast with the Class cast function + if (returnType.isPrimitive()) { + subject.onNext((T)result); + } else { + subject.onNext(returnType.cast(result)); + } + }, error -> subject.onError(error), + () -> subject.onComplete()); + + sendHubMessage(streamInvocationMessage); + Observable observable = subject.doOnSubscribe((subscriber) -> subscriptionCount.incrementAndGet()); + + return observable.doOnDispose(() -> { + if (subscriptionCount.decrementAndGet() == 0) { + CancelInvocationMessage cancelInvocationMessage = new CancelInvocationMessage(invocationId); + sendHubMessage(cancelInvocationMessage); + connectionState.tryRemoveInvocation(invocationId); + subject.onComplete(); + } + }); + } + private void sendHubMessage(HubMessage message) { String serializedMessage = protocol.writeMessage(message); - if (message.getMessageType() == HubMessageType.INVOCATION) { + if (message.getMessageType() == HubMessageType.INVOCATION ) { logger.debug("Sending {} message '{}'.", message.getMessageType().name(), ((InvocationMessage)message).getInvocationId()); + } else if (message.getMessageType() == HubMessageType.STREAM_INVOCATION) { + logger.debug("Sending {} message '{}'.", message.getMessageType().name(), ((StreamInvocationMessage)message).getInvocationId()); } else { logger.debug("Sending {} message.", message.getMessageType().name()); } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationRequest.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationRequest.java index 3d12d39d04..7de8890db7 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationRequest.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/InvocationRequest.java @@ -5,12 +5,12 @@ package com.microsoft.signalr; import java.util.concurrent.CancellationException; -import io.reactivex.Single; -import io.reactivex.subjects.SingleSubject; +import io.reactivex.subjects.ReplaySubject; +import io.reactivex.subjects.Subject; class InvocationRequest { private final Class returnType; - private final SingleSubject pendingCall = SingleSubject.create(); + private final Subject pendingCall = ReplaySubject.create(); private final String invocationId; InvocationRequest(Class returnType, String invocationId) { @@ -19,13 +19,22 @@ class InvocationRequest { } public void complete(CompletionMessage completion) { - if (completion.getResult() != null) { - pendingCall.onSuccess(completion.getResult()); + if (completion.getError() == null) { + if (completion.getResult() != null) { + pendingCall.onNext(completion.getResult()); + } + pendingCall.onComplete(); } else { pendingCall.onError(new HubException(completion.getError())); } } + public void addItem(StreamItem streamItem) { + if (streamItem.getItem() != null) { + pendingCall.onNext(streamItem.getItem()); + } + } + public void fail(Exception ex) { pendingCall.onError(ex); } @@ -34,7 +43,7 @@ class InvocationRequest { pendingCall.onError(new CancellationException("Invocation was canceled.")); } - public Single getPendingCall() { + public Subject getPendingCall() { return pendingCall; } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/JsonHubProtocol.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/JsonHubProtocol.java index 8ff1024b4b..57bb5d1a7b 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/JsonHubProtocol.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/JsonHubProtocol.java @@ -73,15 +73,13 @@ class JsonHubProtocol implements HubProtocol { error = reader.nextString(); break; case "result": - if (invocationId == null) { + case "item": + if (invocationId == null || binder.getReturnType(invocationId) == null) { resultToken = jsonParser.parse(reader); } else { result = gson.fromJson(reader, binder.getReturnType(invocationId)); } break; - case "item": - reader.skipValue(); - break; case "arguments": if (target != null) { boolean startedArray = false; @@ -142,12 +140,19 @@ class JsonHubProtocol implements HubProtocol { break; case COMPLETION: if (resultToken != null) { - result = gson.fromJson(resultToken, binder.getReturnType(invocationId)); + Class returnType = binder.getReturnType(invocationId); + result = gson.fromJson(resultToken, returnType != null ? returnType : Object.class); } hubMessages.add(new CompletionMessage(invocationId, result, error)); break; - case STREAM_INVOCATION: case STREAM_ITEM: + if (resultToken != null) { + Class returnType = binder.getReturnType(invocationId); + result = gson.fromJson(resultToken, returnType != null ? returnType : Object.class); + } + hubMessages.add(new StreamItem(invocationId, result)); + break; + case STREAM_INVOCATION: case CANCEL_INVOCATION: throw new UnsupportedOperationException(String.format("The message type %s is not supported yet.", messageType)); case PING: diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamInvocationMessage.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamInvocationMessage.java index eb4ae0e479..f897195667 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamInvocationMessage.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamInvocationMessage.java @@ -3,11 +3,28 @@ package com.microsoft.signalr; -class StreamInvocationMessage extends InvocationMessage { +final class StreamInvocationMessage extends HubMessage { private final int type = HubMessageType.STREAM_INVOCATION.value; + private final String invocationId; + private final String target; + private final Object[] arguments; - public StreamInvocationMessage(String invocationId, String target, Object[] arguments) { - super(invocationId, target, arguments); + public StreamInvocationMessage(String invocationId, String target, Object[] args) { + this.invocationId = invocationId; + this.target = target; + this.arguments = args; + } + + public String getInvocationId() { + return invocationId; + } + + public String getTarget() { + return target; + } + + public Object[] getArguments() { + return arguments; } @Override diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamItem.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamItem.java new file mode 100644 index 0000000000..3b422daf46 --- /dev/null +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/StreamItem.java @@ -0,0 +1,28 @@ +// 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. + +package com.microsoft.signalr; + +final class StreamItem extends HubMessage { + private final int type = HubMessageType.STREAM_ITEM.value; + private final String invocationId; + private final Object item; + + public StreamItem(String invocationId, Object item) { + this.invocationId = invocationId; + this.item = item; + } + + public String getInvocationId() { + return invocationId; + } + + public Object getItem() { + return item; + } + + @Override + public HubMessageType getMessageType() { + return HubMessageType.STREAM_ITEM; + } +} diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HubConnectionTest.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HubConnectionTest.java index 6c29be1c82..95f4e2c191 100644 --- a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HubConnectionTest.java +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/HubConnectionTest.java @@ -5,6 +5,7 @@ package com.microsoft.signalr; import static org.junit.jupiter.api.Assertions.*; +import java.util.Iterator; import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; @@ -15,7 +16,9 @@ import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; +import io.reactivex.Observable; import io.reactivex.Single; +import io.reactivex.disposables.Disposable; import io.reactivex.subjects.SingleSubject; class HubConnectionTest { @@ -367,6 +370,222 @@ class HubConnectionTest { assertEquals(Double.valueOf(24), value.get()); } + @Test + public void checkStreamSingleItem() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean completed = new AtomicBoolean(); + AtomicBoolean onNextCalled = new AtomicBoolean(); + Observable result = hubConnection.stream(String.class, "echo", "message"); + result.subscribe((item) -> onNextCalled.set(true), + (error) -> {}, + () -> completed.set(true)); + + assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertFalse(completed.get()); + assertFalse(onNextCalled.get()); + + mockTransport.receiveMessage("{\"type\":2,\"invocationId\":\"1\",\"item\":\"First\"}" + RECORD_SEPARATOR); + + assertTrue(onNextCalled.get()); + + mockTransport.receiveMessage("{\"type\":3,\"invocationId\":\"1\",\"result\":\"hello\"}" + RECORD_SEPARATOR); + assertTrue(completed.get()); + + assertEquals("First", result.timeout(1000, TimeUnit.MILLISECONDS).blockingFirst()); + } + + @Test + public void checkStreamCompletionResult() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean completed = new AtomicBoolean(); + AtomicBoolean onNextCalled = new AtomicBoolean(); + Observable result = hubConnection.stream(String.class, "echo", "message"); + result.subscribe((item) -> onNextCalled.set(true), + (error) -> {}, + () -> completed.set(true)); + + assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertFalse(completed.get()); + assertFalse(onNextCalled.get()); + + mockTransport.receiveMessage("{\"type\":2,\"invocationId\":\"1\",\"item\":\"First\"}" + RECORD_SEPARATOR); + + assertTrue(onNextCalled.get()); + + mockTransport.receiveMessage("{\"type\":3,\"invocationId\":\"1\",\"result\":\"COMPLETED\"}" + RECORD_SEPARATOR); + assertTrue(completed.get()); + + assertEquals("First", result.timeout(1000, TimeUnit.MILLISECONDS).blockingFirst()); + assertEquals("COMPLETED", result.timeout(1000, TimeUnit.MILLISECONDS).blockingLast()); + + } + + @Test + public void checkStreamCompletionError() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean onErrorCalled = new AtomicBoolean(); + AtomicBoolean onNextCalled = new AtomicBoolean(); + Observable result = hubConnection.stream(String.class, "echo", "message"); + result.subscribe((item) -> onNextCalled.set(true), + (error) -> onErrorCalled.set(true), + () -> {}); + + assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertFalse(onErrorCalled.get()); + assertFalse(onNextCalled.get()); + + mockTransport.receiveMessage("{\"type\":2,\"invocationId\":\"1\",\"item\":\"First\"}" + RECORD_SEPARATOR); + + assertTrue(onNextCalled.get()); + + mockTransport.receiveMessage("{\"type\":3,\"invocationId\":\"1\",\"error\":\"There was an error\"}" + RECORD_SEPARATOR); + assertTrue(onErrorCalled.get()); + + assertEquals("First", result.timeout(1000, TimeUnit.MILLISECONDS).blockingFirst()); + Throwable exception = assertThrows(HubException.class, () -> result.timeout(1000, TimeUnit.MILLISECONDS).blockingLast()); + assertEquals("There was an error", exception.getMessage()); + } + + @Test + public void checkStreamMultipleItems() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean completed = new AtomicBoolean(); + Observable result = hubConnection.stream(String.class, "echo", "message"); + result.subscribe((item) -> {/*OnNext*/ }, + (error) -> {/*OnError*/}, + () -> {/*OnCompleted*/completed.set(true);}); + + assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertFalse(completed.get()); + + mockTransport.receiveMessage("{\"type\":2,\"invocationId\":\"1\",\"item\":\"First\"}" + RECORD_SEPARATOR); + mockTransport.receiveMessage("{\"type\":2,\"invocationId\":\"1\",\"item\":\"Second\"}" + RECORD_SEPARATOR); + mockTransport.receiveMessage("{\"type\":3,\"invocationId\":\"1\",\"result\":\"null\"}" + RECORD_SEPARATOR); + + Iterator resultIterator = result.timeout(1000, TimeUnit.MILLISECONDS).blockingIterable().iterator(); + assertEquals("First", resultIterator.next()); + assertEquals("Second", resultIterator.next()); + assertTrue(completed.get()); + } + + @Test + public void checkCancelIsSentAfterDispose() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean completed = new AtomicBoolean(); + Observable result = hubConnection.stream(String.class, "echo", "message"); + Disposable subscription = result.subscribe((item) -> {/*OnNext*/ }, + (error) -> {/*OnError*/}, + () -> {/*OnCompleted*/completed.set(true);}); + + assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertFalse(completed.get()); + + subscription.dispose(); + assertEquals("{\"type\":5,\"invocationId\":\"1\"}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[2]); + } + + @Test + public void checkCancelIsSentAfterAllSubscriptionsAreDisposed() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + Observable result = hubConnection.stream(String.class, "echo", "message"); + Disposable subscription = result.subscribe((item) -> {/*OnNext*/ }, + (error) -> {/*OnError*/}, + () -> {/*OnCompleted*/}); + + Disposable secondSubscription = result.subscribe((item) -> {/*OnNext*/ }, + (error) -> {/*OnError*/}, + () -> {/*OnCompleted*/}); + + subscription.dispose(); + assertEquals(2, mockTransport.getSentMessages().length); + assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, + mockTransport.getSentMessages()[mockTransport.getSentMessages().length - 1]); + + secondSubscription.dispose(); + assertEquals(3, mockTransport.getSentMessages().length); + assertEquals("{\"type\":5,\"invocationId\":\"1\"}" + RECORD_SEPARATOR, + mockTransport.getSentMessages()[mockTransport.getSentMessages().length - 1]); + } + + @Test + public void checkStreamWithDispose() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + Observable result = hubConnection.stream(String.class, "echo", "message"); + Disposable subscription = result.subscribe((item) -> {/*OnNext*/}, + (error) -> {/*OnError*/}, + () -> {/*OnCompleted*/}); + + assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + + mockTransport.receiveMessage("{\"type\":2,\"invocationId\":\"1\",\"item\":\"First\"}" + RECORD_SEPARATOR); + + subscription.dispose(); + mockTransport.receiveMessage("{\"type\":2,\"invocationId\":\"1\",\"item\":\"Second\"}" + RECORD_SEPARATOR); + + assertEquals("First", result.timeout(1000, TimeUnit.MILLISECONDS).blockingLast()); + } + + @Test + public void checkStreamWithDisposeWithMultipleSubscriptions() { + MockTransport mockTransport = new MockTransport(); + HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); + + hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait(); + + AtomicBoolean completed = new AtomicBoolean(); + Observable result = hubConnection.stream(String.class, "echo", "message"); + Disposable subscription = result.subscribe((item) -> {/*OnNext*/}, + (error) -> {/*OnError*/}, + () -> {/*OnCompleted*/}); + + Disposable subscription2 = result.subscribe((item) -> {/*OnNext*/}, + (error) -> {/*OnError*/}, + () -> {/*OnCompleted*/completed.set(true);}); + + assertEquals("{\"type\":4,\"invocationId\":\"1\",\"target\":\"echo\",\"arguments\":[\"message\"]}" + RECORD_SEPARATOR, mockTransport.getSentMessages()[1]); + assertFalse(completed.get()); + + mockTransport.receiveMessage("{\"type\":2,\"invocationId\":\"1\",\"item\":\"First\"}" + RECORD_SEPARATOR); + + subscription.dispose(); + mockTransport.receiveMessage("{\"type\":2,\"invocationId\":\"1\",\"item\":\"Second\"}" + RECORD_SEPARATOR); + + mockTransport.receiveMessage("{\"type\":3,\"invocationId\":\"1\"}" + RECORD_SEPARATOR); + assertTrue(completed.get()); + assertEquals("First", result.timeout(1000, TimeUnit.MILLISECONDS).blockingFirst()); + + subscription2.dispose(); + assertEquals("Second", result.timeout(1000, TimeUnit.MILLISECONDS).blockingLast()); + } + @Test public void invokeWaitsForCompletionMessage() { MockTransport mockTransport = new MockTransport(); diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/JsonHubProtocolTest.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/JsonHubProtocolTest.java index 8a8ad9f3cd..f4df7a24b6 100644 --- a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/JsonHubProtocolTest.java +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/JsonHubProtocolTest.java @@ -108,15 +108,6 @@ class JsonHubProtocolTest { assertEquals(42, messageResult); } - @Test - public void parseSingleUnsupportedStreamItemMessage() { - String stringifiedMessage = "{\"type\":2,\"Id\":1,\"Item\":42}\u001E"; - TestBinder binder = new TestBinder(null); - - Throwable exception = assertThrows(UnsupportedOperationException.class, () -> jsonHubProtocol.parseMessages(stringifiedMessage, binder)); - assertEquals("The message type STREAM_ITEM is not supported yet.", exception.getMessage()); - } - @Test public void parseSingleUnsupportedStreamInvocationMessage() { String stringifiedMessage = "{\"type\":4,\"Id\":1,\"target\":\"test\",\"arguments\":[42]}\u001E"; diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/sample/Chat.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/sample/Chat.java index 2e0990d70a..0acb379433 100644 --- a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/sample/Chat.java +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/sample/Chat.java @@ -8,6 +8,7 @@ import java.util.Scanner; import com.microsoft.signalr.HubConnection; import com.microsoft.signalr.HubConnectionBuilder; + public class Chat { public static void main(String[] args) { System.out.println("Enter the URL of the SignalR Chat you want to join"); diff --git a/src/SignalR/clients/ts/FunctionalTests/FunctionalTests.csproj b/src/SignalR/clients/ts/FunctionalTests/FunctionalTests.csproj index 06948653fc..1f2d67dfcd 100644 --- a/src/SignalR/clients/ts/FunctionalTests/FunctionalTests.csproj +++ b/src/SignalR/clients/ts/FunctionalTests/FunctionalTests.csproj @@ -1,7 +1,7 @@ - netcoreapp2.2 + netcoreapp3.0 True 2.8 diff --git a/src/SignalR/clients/ts/FunctionalTests/package-lock.json b/src/SignalR/clients/ts/FunctionalTests/package-lock.json index 98ea76c951..e2b24fb2bb 100644 --- a/src/SignalR/clients/ts/FunctionalTests/package-lock.json +++ b/src/SignalR/clients/ts/FunctionalTests/package-lock.json @@ -1827,12 +1827,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1847,17 +1849,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -1974,7 +1979,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -1986,6 +1992,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2000,6 +2007,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2007,12 +2015,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -2031,6 +2041,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2111,7 +2122,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2123,6 +2135,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -2244,6 +2257,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/src/SignalR/clients/ts/FunctionalTests/scripts/run-tests.ts b/src/SignalR/clients/ts/FunctionalTests/scripts/run-tests.ts index 7dd600eded..5ae55bad6c 100644 --- a/src/SignalR/clients/ts/FunctionalTests/scripts/run-tests.ts +++ b/src/SignalR/clients/ts/FunctionalTests/scripts/run-tests.ts @@ -191,7 +191,7 @@ function runJest(httpsUrl: string, httpUrl: string) { (async () => { try { - const serverPath = path.resolve(__dirname, "..", "bin", configuration, "netcoreapp2.2", "FunctionalTests.dll"); + const serverPath = path.resolve(__dirname, "..", "bin", configuration, "netcoreapp3.0", "FunctionalTests.dll"); debug(`Launching Functional Test Server: ${serverPath}`); let desiredServerUrl = "https://127.0.0.1:0;http://127.0.0.1:0"; diff --git a/src/SignalR/clients/ts/common/package-lock.json b/src/SignalR/clients/ts/common/package-lock.json index 8b3564efc7..7ad4e6a391 100644 --- a/src/SignalR/clients/ts/common/package-lock.json +++ b/src/SignalR/clients/ts/common/package-lock.json @@ -2601,7 +2601,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -3016,7 +3017,8 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3072,6 +3074,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3115,12 +3118,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/src/SignalR/clients/ts/signalr-protocol-msgpack/package-lock.json b/src/SignalR/clients/ts/signalr-protocol-msgpack/package-lock.json index 433ef5cd6e..68493e002a 100644 --- a/src/SignalR/clients/ts/signalr-protocol-msgpack/package-lock.json +++ b/src/SignalR/clients/ts/signalr-protocol-msgpack/package-lock.json @@ -1,6 +1,6 @@ { "name": "@aspnet/signalr-protocol-msgpack", - "version": "1.1.0-rtm-t000", + "version": "3.0.0-alpha1-t000", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/SignalR/clients/ts/signalr/package-lock.json b/src/SignalR/clients/ts/signalr/package-lock.json index 1f09c86a5b..9bedb9db53 100644 --- a/src/SignalR/clients/ts/signalr/package-lock.json +++ b/src/SignalR/clients/ts/signalr/package-lock.json @@ -1,6 +1,6 @@ { "name": "@aspnet/signalr", - "version": "1.1.0-rtm-t000", + "version": "3.0.0-alpha1-t000", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/SignalR/clients/ts/signalr/src/HandshakeProtocol.ts b/src/SignalR/clients/ts/signalr/src/HandshakeProtocol.ts index af4bf7ca52..4c306e80b8 100644 --- a/src/SignalR/clients/ts/signalr/src/HandshakeProtocol.ts +++ b/src/SignalR/clients/ts/signalr/src/HandshakeProtocol.ts @@ -13,6 +13,7 @@ export interface HandshakeRequestMessage { /** @private */ export interface HandshakeResponseMessage { readonly error: string; + readonly minorVersion: number; } /** @private */ diff --git a/src/SignalR/samples/ClientSample/ClientSample.csproj b/src/SignalR/samples/ClientSample/ClientSample.csproj index b38755fe73..7449e1e91a 100644 --- a/src/SignalR/samples/ClientSample/ClientSample.csproj +++ b/src/SignalR/samples/ClientSample/ClientSample.csproj @@ -1,7 +1,7 @@  - netcoreapp2.2;net461 + netcoreapp3.0;net461 false Exe diff --git a/src/SignalR/samples/ClientSample/Program.cs b/src/SignalR/samples/ClientSample/Program.cs index d049ce921a..0a581dbe6f 100644 --- a/src/SignalR/samples/ClientSample/Program.cs +++ b/src/SignalR/samples/ClientSample/Program.cs @@ -26,6 +26,8 @@ namespace ClientSample RawSample.Register(app); HubSample.Register(app); + StreamingSample.Register(app); + UploadSample.Register(app); app.Command("help", cmd => { diff --git a/src/SignalR/samples/ClientSample/StreamingSample.cs b/src/SignalR/samples/ClientSample/StreamingSample.cs new file mode 100644 index 0000000000..9ae84da9c2 --- /dev/null +++ b/src/SignalR/samples/ClientSample/StreamingSample.cs @@ -0,0 +1,46 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.CommandLineUtils; + +namespace ClientSample +{ + internal class StreamingSample + { + internal static void Register(CommandLineApplication app) + { + app.Command("streaming", cmd => + { + cmd.Description = "Tests a streaming connection to a hub"; + + var baseUrlArgument = cmd.Argument("", "The URL to the Chat Hub to test"); + + cmd.OnExecute(() => ExecuteAsync(baseUrlArgument.Value)); + }); + } + + public static async Task ExecuteAsync(string baseUrl) + { + var connection = new HubConnectionBuilder() + .WithUrl(baseUrl) + .Build(); + + await connection.StartAsync(); + + var reader = await connection.StreamAsChannelAsync("ChannelCounter", 10, 2000); + + while (await reader.WaitToReadAsync()) + { + while (reader.TryRead(out var item)) + { + Console.WriteLine($"received: {item}"); + } + } + + return 0; + } + } +} diff --git a/src/SignalR/samples/ClientSample/Tcp/SocketReceiver.cs b/src/SignalR/samples/ClientSample/Tcp/SocketReceiver.cs index f8e5d87b61..b5d664dbc9 100644 --- a/src/SignalR/samples/ClientSample/Tcp/SocketReceiver.cs +++ b/src/SignalR/samples/ClientSample/Tcp/SocketReceiver.cs @@ -22,7 +22,7 @@ namespace ClientSample public SocketAwaitable ReceiveAsync(Memory buffer) { -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 _eventArgs.SetBuffer(buffer); #else var segment = buffer.GetArray(); diff --git a/src/SignalR/samples/ClientSample/Tcp/SocketSender.cs b/src/SignalR/samples/ClientSample/Tcp/SocketSender.cs index 49a44fbb27..13bdbd1e5a 100644 --- a/src/SignalR/samples/ClientSample/Tcp/SocketSender.cs +++ b/src/SignalR/samples/ClientSample/Tcp/SocketSender.cs @@ -32,7 +32,7 @@ namespace ClientSample return SendAsync(buffers.First); } -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 if (!_eventArgs.MemoryBuffer.Equals(Memory.Empty)) #else if (_eventArgs.Buffer != null) @@ -59,7 +59,7 @@ namespace ClientSample _eventArgs.BufferList = null; } -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 _eventArgs.SetBuffer(MemoryMarshal.AsMemory(memory)); #else var segment = memory.GetArray(); diff --git a/src/SignalR/samples/ClientSample/UploadSample.cs b/src/SignalR/samples/ClientSample/UploadSample.cs new file mode 100644 index 0000000000..b1b173b746 --- /dev/null +++ b/src/SignalR/samples/ClientSample/UploadSample.cs @@ -0,0 +1,90 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.CommandLineUtils; + +namespace ClientSample +{ + internal class UploadSample + { + internal static void Register(CommandLineApplication app) + { + app.Command("uploading", cmd => + { + cmd.Description = "Tests a streaming invocation from client to hub"; + + var baseUrlArgument = cmd.Argument("", "The URL to the Chat Hub to test"); + + cmd.OnExecute(() => ExecuteAsync(baseUrlArgument.Value)); + }); + } + + public static async Task ExecuteAsync(string baseUrl) + { + var connection = new HubConnectionBuilder() + .WithUrl(baseUrl) + .Build(); + await connection.StartAsync(); + + await BasicInvoke(connection); + //await MultiParamInvoke(connection); + //await AdditionalArgs(connection); + + return 0; + } + + public static async Task BasicInvoke(HubConnection connection) + { + var channel = Channel.CreateUnbounded(); + var invokeTask = connection.InvokeAsync("UploadWord", channel.Reader); + + foreach (var c in "hello") + { + await channel.Writer.WriteAsync(c.ToString()); + } + channel.Writer.TryComplete(); + + var result = await invokeTask; + Debug.WriteLine($"You message was: {result}"); + } + + private static async Task WriteStreamAsync(IEnumerable sequence, ChannelWriter writer) + { + foreach (T element in sequence) + { + await writer.WriteAsync(element); + await Task.Delay(100); + } + + writer.TryComplete(); + } + + public static async Task MultiParamInvoke(HubConnection connection) + { + var letters = Channel.CreateUnbounded(); + var numbers = Channel.CreateUnbounded(); + + _ = WriteStreamAsync(new[] { "h", "i", "!" }, letters.Writer); + _ = WriteStreamAsync(new[] { 1, 2, 3, 4, 5 }, numbers.Writer); + + var result = await connection.InvokeAsync("DoubleStreamUpload", letters.Reader, numbers.Reader); + + Debug.WriteLine(result); + } + + public static async Task AdditionalArgs(HubConnection connection) + { + var channel = Channel.CreateUnbounded(); + _ = WriteStreamAsync("main message".ToCharArray(), channel.Writer); + + var result = await connection.InvokeAsync("UploadWithSuffix", channel.Reader, " + wooh I'm a suffix"); + Debug.WriteLine($"Your message was: {result}"); + } + } +} + diff --git a/src/SignalR/samples/JwtClientSample/JwtClientSample.csproj b/src/SignalR/samples/JwtClientSample/JwtClientSample.csproj index 7746780816..5cc83d46b2 100644 --- a/src/SignalR/samples/JwtClientSample/JwtClientSample.csproj +++ b/src/SignalR/samples/JwtClientSample/JwtClientSample.csproj @@ -1,7 +1,7 @@  - netcoreapp2.2 + netcoreapp3.0 false Exe diff --git a/src/SignalR/samples/JwtSample/JwtSample.csproj b/src/SignalR/samples/JwtSample/JwtSample.csproj index dfadf14bef..8e7b28a97b 100644 --- a/src/SignalR/samples/JwtSample/JwtSample.csproj +++ b/src/SignalR/samples/JwtSample/JwtSample.csproj @@ -1,7 +1,7 @@  - netcoreapp2.2 + netcoreapp3.0 diff --git a/src/SignalR/samples/SignalRSamples/Hubs/UploadHub.cs b/src/SignalR/samples/SignalRSamples/Hubs/UploadHub.cs new file mode 100644 index 0000000000..bd821b5066 --- /dev/null +++ b/src/SignalR/samples/SignalRSamples/Hubs/UploadHub.cs @@ -0,0 +1,110 @@ +// 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.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; + +namespace SignalRSamples.Hubs +{ + public class UploadHub : Hub + { + public async Task DoubleStreamUpload(ChannelReader letters, ChannelReader numbers) + { + var total = await Sum(numbers); + var word = await UploadWord(letters); + + return string.Format("You sent over <{0}> <{1}s>", total, word); + } + + public async Task Sum(ChannelReader source) + { + var total = 0; + while (await source.WaitToReadAsync()) + { + while (source.TryRead(out var item)) + { + total += item; + } + } + return total; + } + + public async Task LocalSum(ChannelReader source) + { + var total = 0; + while (await source.WaitToReadAsync()) + { + while (source.TryRead(out var item)) + { + total += item; + } + } + Debug.WriteLine(String.Format("Complete, your total is <{0}>.", total)); + } + + public async Task UploadWord(ChannelReader source) + { + var sb = new StringBuilder(); + + // receiving a StreamCompleteMessage should cause this WaitToRead to return false + while (await source.WaitToReadAsync()) + { + while (source.TryRead(out var item)) + { + Debug.WriteLine($"received: {item}"); + Console.WriteLine($"received: {item}"); + sb.Append(item); + } + } + + // method returns, somewhere else returns a CompletionMessage with any errors + return sb.ToString(); + } + + public async Task UploadWithSuffix(ChannelReader source, string suffix) + { + var sb = new StringBuilder(); + + while (await source.WaitToReadAsync()) + { + while (source.TryRead(out var item)) + { + await Task.Delay(50); + Debug.WriteLine($"received: {item}"); + sb.Append(item); + } + } + + sb.Append(suffix); + + return sb.ToString(); + } + + public async Task UploadFile(ChannelReader source, string filepath) + { + var result = Enumerable.Empty(); + int chunk = 1; + + while (await source.WaitToReadAsync()) + { + while (source.TryRead(out var item)) + { + Debug.WriteLine($"received chunk #{chunk++}"); + result = result.Concat(item); // atrocious + await Task.Delay(50); + } + } + + File.WriteAllBytes(filepath, result.ToArray()); + + Debug.WriteLine("returning status code"); + return $"file written to '{filepath}'"; + } + } +} diff --git a/src/SignalR/samples/SignalRSamples/SignalRSamples.csproj b/src/SignalR/samples/SignalRSamples/SignalRSamples.csproj index 76733a4496..15d52e1209 100644 --- a/src/SignalR/samples/SignalRSamples/SignalRSamples.csproj +++ b/src/SignalR/samples/SignalRSamples/SignalRSamples.csproj @@ -1,7 +1,7 @@  - netcoreapp2.2;net461 + netcoreapp3.0 false @@ -10,7 +10,7 @@ - + @@ -21,8 +21,8 @@ - + diff --git a/src/SignalR/samples/SignalRSamples/Startup.cs b/src/SignalR/samples/SignalRSamples/Startup.cs index e2cbd56b6a..f8516fe9ae 100644 --- a/src/SignalR/samples/SignalRSamples/Startup.cs +++ b/src/SignalR/samples/SignalRSamples/Startup.cs @@ -29,7 +29,7 @@ namespace SignalRSamples options.KeepAliveInterval = TimeSpan.FromSeconds(5); }) .AddMessagePackProtocol(); - //.AddRedis(); + //.AddStackExchangeRedis(); services.AddCors(o => { @@ -60,6 +60,7 @@ namespace SignalRSamples routes.MapHub("/dynamic"); routes.MapHub("/default"); routes.MapHub("/streaming"); + routes.MapHub("/uploading"); routes.MapHub("/hubT"); }); diff --git a/src/SignalR/samples/SocialWeather/SocialWeather.csproj b/src/SignalR/samples/SocialWeather/SocialWeather.csproj index e96e794705..b1113d58bb 100644 --- a/src/SignalR/samples/SocialWeather/SocialWeather.csproj +++ b/src/SignalR/samples/SocialWeather/SocialWeather.csproj @@ -1,7 +1,7 @@  - netcoreapp2.2;net461 + netcoreapp3.0 false diff --git a/src/SignalR/samples/WebSocketSample/WebSocketSample.csproj b/src/SignalR/samples/WebSocketSample/WebSocketSample.csproj index 8227ddf127..d63209836c 100644 --- a/src/SignalR/samples/WebSocketSample/WebSocketSample.csproj +++ b/src/SignalR/samples/WebSocketSample/WebSocketSample.csproj @@ -1,7 +1,7 @@  - netcoreapp2.2;net461 + netcoreapp3.0 Exe false diff --git a/src/SignalR/src/Common/MemoryBufferWriter.cs b/src/SignalR/src/Common/MemoryBufferWriter.cs index e13f2be3f7..95d3e2f375 100644 --- a/src/SignalR/src/Common/MemoryBufferWriter.cs +++ b/src/SignalR/src/Common/MemoryBufferWriter.cs @@ -293,7 +293,7 @@ namespace Microsoft.AspNetCore.Internal } } -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 public override void Write(ReadOnlySpan span) { if (_currentSegment != null && span.TryCopyTo(_currentSegment.AsSpan(_position))) diff --git a/src/SignalR/src/Common/PipeWriterStream.cs b/src/SignalR/src/Common/PipeWriterStream.cs index 8c39ca46fb..10bfa18b96 100644 --- a/src/SignalR/src/Common/PipeWriterStream.cs +++ b/src/SignalR/src/Common/PipeWriterStream.cs @@ -60,7 +60,7 @@ namespace System.IO.Pipelines return WriteCoreAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); } -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 public override ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken = default) { return WriteCoreAsync(source, cancellationToken); diff --git a/src/SignalR/src/Common/ReflectionHelper.cs b/src/SignalR/src/Common/ReflectionHelper.cs new file mode 100644 index 0000000000..eb11f635c1 --- /dev/null +++ b/src/SignalR/src/Common/ReflectionHelper.cs @@ -0,0 +1,39 @@ +// 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 System.Threading.Channels; + +namespace Microsoft.AspNetCore.SignalR +{ + internal static class ReflectionHelper + { + public static bool IsStreamingType(Type type) + { + // IMPORTANT !! + // All valid types must be generic + // because HubConnectionContext gets the generic argument and uses it to determine the expected item type of the stream + // The long-term solution is making a (streaming type => expected item type) method. + + if (!type.IsGenericType) + { + return false; + } + + // walk up inheritance chain, until parent is either null or a ChannelReader + // TODO #2594 - add Streams here, to make sending files easy + while (type != null) + { + if (type.GetGenericTypeDefinition() == typeof(ChannelReader<>)) + { + return true; + } + + type = type.BaseType; + } + return false; + } + } +} diff --git a/src/SignalR/src/Common/StreamExtensions.cs b/src/SignalR/src/Common/StreamExtensions.cs index 8c971ede0d..9601fc2add 100644 --- a/src/SignalR/src/Common/StreamExtensions.cs +++ b/src/SignalR/src/Common/StreamExtensions.cs @@ -15,7 +15,7 @@ namespace System.IO { if (buffer.IsSingleSegment) { -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 return stream.WriteAsync(buffer.First, cancellationToken); #else var isArray = MemoryMarshal.TryGetArray(buffer.First, out var arraySegment); @@ -33,7 +33,7 @@ namespace System.IO var position = buffer.Start; while (buffer.TryGet(ref position, out var segment)) { -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 await stream.WriteAsync(segment, cancellationToken); #else var isArray = MemoryMarshal.TryGetArray(segment, out var arraySegment); diff --git a/src/SignalR/src/Common/Utf8BufferTextReader.cs b/src/SignalR/src/Common/Utf8BufferTextReader.cs index f78c878b1d..66af65c237 100644 --- a/src/SignalR/src/Common/Utf8BufferTextReader.cs +++ b/src/SignalR/src/Common/Utf8BufferTextReader.cs @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal var source = _utf8Buffer.First.Span; var bytesUsed = 0; var charsUsed = 0; -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 var destination = new Span(buffer, index, count); _decoder.Convert(source, destination, false, out bytesUsed, out charsUsed, out var completed); #else diff --git a/src/SignalR/src/Common/Utf8BufferTextWriter.cs b/src/SignalR/src/Common/Utf8BufferTextWriter.cs index eb57dc2e36..d60caf3bda 100644 --- a/src/SignalR/src/Common/Utf8BufferTextWriter.cs +++ b/src/SignalR/src/Common/Utf8BufferTextWriter.cs @@ -111,7 +111,7 @@ namespace Microsoft.AspNetCore.Internal // this should be an exceptional case var bytesUsed = 0; var charsUsed = 0; -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 _encoder.Convert(new Span(&value, 1), destination, false, out charsUsed, out bytesUsed, out _); #else fixed (byte* destinationBytes = &MemoryMarshal.GetReference(destination)) @@ -167,7 +167,7 @@ namespace Microsoft.AspNetCore.Internal var bytesUsed = 0; var charsUsed = 0; -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 _encoder.Convert(buffer, destination, false, out charsUsed, out bytesUsed, out _); #else unsafe diff --git a/src/SignalR/src/Common/WebSocketExtensions.cs b/src/SignalR/src/Common/WebSocketExtensions.cs index 63eb7e62b3..fedb954296 100644 --- a/src/SignalR/src/Common/WebSocketExtensions.cs +++ b/src/SignalR/src/Common/WebSocketExtensions.cs @@ -13,7 +13,7 @@ namespace System.Net.WebSockets { public static ValueTask SendAsync(this WebSocket webSocket, ReadOnlySequence buffer, WebSocketMessageType webSocketMessageType, CancellationToken cancellationToken = default) { -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 if (buffer.IsSingleSegment) { return webSocket.SendAsync(buffer.First, webSocketMessageType, endOfMessage: true, cancellationToken); @@ -39,22 +39,28 @@ namespace System.Net.WebSockets private static async ValueTask SendMultiSegmentAsync(WebSocket webSocket, ReadOnlySequence buffer, WebSocketMessageType webSocketMessageType, CancellationToken cancellationToken = default) { var position = buffer.Start; + // Get a segment before the loop so we can be one segment behind while writing + // This allows us to do a non-zero byte write for the endOfMessage = true send + buffer.TryGet(ref position, out var prevSegment); while (buffer.TryGet(ref position, out var segment)) { -#if NETCOREAPP2_2 - await webSocket.SendAsync(segment, webSocketMessageType, endOfMessage: false, cancellationToken); +#if NETCOREAPP3_0 + await webSocket.SendAsync(prevSegment, webSocketMessageType, endOfMessage: false, cancellationToken); #else - var isArray = MemoryMarshal.TryGetArray(segment, out var arraySegment); + var isArray = MemoryMarshal.TryGetArray(prevSegment, out var arraySegment); Debug.Assert(isArray); await webSocket.SendAsync(arraySegment, webSocketMessageType, endOfMessage: false, cancellationToken); #endif + prevSegment = segment; } - // Empty end of message frame -#if NETCOREAPP2_2 - await webSocket.SendAsync(Memory.Empty, webSocketMessageType, endOfMessage: true, cancellationToken); + // End of message frame +#if NETCOREAPP3_0 + await webSocket.SendAsync(prevSegment, webSocketMessageType, endOfMessage: true, cancellationToken); #else - await webSocket.SendAsync(new ArraySegment(Array.Empty()), webSocketMessageType, endOfMessage: true, cancellationToken); + var isArrayEnd = MemoryMarshal.TryGetArray(prevSegment, out var arraySegmentEnd); + Debug.Assert(isArrayEnd); + await webSocket.SendAsync(arraySegmentEnd, webSocketMessageType, endOfMessage: true, cancellationToken); #endif } } diff --git a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/HttpConnection.cs b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/HttpConnection.cs index 80f9c88d54..bd9010f9b7 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/HttpConnection.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/HttpConnection.cs @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client private static readonly Task _noAccessToken = Task.FromResult(null); private static readonly TimeSpan HttpClientTimeout = TimeSpan.FromSeconds(120); -#if !NETCOREAPP2_2 +#if !NETCOREAPP3_0 private static readonly Version Windows8Version = new Version(6, 2); #endif @@ -573,7 +573,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client private static bool IsWebSocketsSupported() { -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 // .NET Core 2.1 and above has a managed implementation return true; #else diff --git a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/Internal/WebSocketsTransport.cs b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/Internal/WebSocketsTransport.cs index 054e32c5bc..4fe1e61b73 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/Internal/WebSocketsTransport.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/Internal/WebSocketsTransport.cs @@ -119,7 +119,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal Log.StartTransport(_logger, transferFormat, resolvedUrl); - await _webSocket.ConnectAsync(resolvedUrl, CancellationToken.None); + try + { + await _webSocket.ConnectAsync(resolvedUrl, CancellationToken.None); + } + catch + { + _webSocket.Dispose(); + throw; + } Log.StartedTransport(_logger); @@ -196,7 +204,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal { while (true) { -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 var result = await socket.ReceiveAsync(Memory.Empty, CancellationToken.None); if (result.MessageType == WebSocketMessageType.Close) @@ -212,7 +220,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal } #endif var memory = _application.Output.GetMemory(); -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 // Because we checked the CloseStatus from the 0 byte read above, we don't need to check again after reading var receiveResult = await socket.ReceiveAsync(memory, CancellationToken.None); #else @@ -222,7 +230,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal // Exceptions are handled above where the send and receive tasks are being run. var receiveResult = await socket.ReceiveAsync(arraySegment, CancellationToken.None); #endif - // Need to check again for NetCoreApp2.2 because a close can happen between a 0-byte read and the actual read + // Need to check again for netcoreapp3.0 because a close can happen between a 0-byte read and the actual read if (receiveResult.MessageType == WebSocketMessageType.Close) { Log.WebSocketClosed(_logger, _webSocket.CloseStatus); diff --git a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/Microsoft.AspNetCore.Http.Connections.Client.csproj b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/Microsoft.AspNetCore.Http.Connections.Client.csproj index 2c330f24b5..8a018490d4 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/Microsoft.AspNetCore.Http.Connections.Client.csproj +++ b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections.Client/Microsoft.AspNetCore.Http.Connections.Client.csproj @@ -2,7 +2,7 @@ Client for ASP.NET Core Connection Handlers - netstandard2.0;netcoreapp2.2 + netstandard2.0;netcoreapp3.0 diff --git a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections/Internal/Transports/WebSocketsTransport.cs b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections/Internal/Transports/WebSocketsTransport.cs index f7700701e9..8a14478831 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections/Internal/Transports/WebSocketsTransport.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections/Internal/Transports/WebSocketsTransport.cs @@ -144,7 +144,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal.Transports { while (!token.IsCancellationRequested) { -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 // Do a 0 byte read so that idle connections don't allocate a buffer when waiting for a read var result = await socket.ReceiveAsync(Memory.Empty, token); @@ -155,7 +155,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal.Transports #endif var memory = _application.Output.GetMemory(); -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 var receiveResult = await socket.ReceiveAsync(memory, token); #else var isArray = MemoryMarshal.TryGetArray(memory, out var arraySegment); @@ -164,7 +164,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal.Transports // Exceptions are handled above where the send and receive tasks are being run. var receiveResult = await socket.ReceiveAsync(arraySegment, token); #endif - // Need to check again for NetCoreApp2.2 because a close can happen between a 0-byte read and the actual read + // Need to check again for netcoreapp3.0 because a close can happen between a 0-byte read and the actual read if (receiveResult.MessageType == WebSocketMessageType.Close) { return; diff --git a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections/Microsoft.AspNetCore.Http.Connections.csproj b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections/Microsoft.AspNetCore.Http.Connections.csproj index 1cc7e47c44..e72fe999b4 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.Http.Connections/Microsoft.AspNetCore.Http.Connections.csproj +++ b/src/SignalR/src/Microsoft.AspNetCore.Http.Connections/Microsoft.AspNetCore.Http.Connections.csproj @@ -2,7 +2,7 @@ Components for providing real-time bi-directional communication across the Web. - netstandard2.0;netcoreapp2.2 + netcoreapp3.0 diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.Log.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.Log.cs index ed713cf001..8eafaedb20 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.Log.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.Log.cs @@ -186,6 +186,18 @@ namespace Microsoft.AspNetCore.SignalR.Client private static readonly Action _unableToAcquireConnectionLockForPing = LoggerMessage.Define(LogLevel.Trace, new EventId(62, "UnableToAcquireConnectionLockForPing"), "Skipping ping because a send is already in progress."); + private static readonly Action _startingStream = + LoggerMessage.Define(LogLevel.Trace, new EventId(63, "StartingStream"), "Initiating stream '{StreamId}'."); + + private static readonly Action _sendingStreamItem = + LoggerMessage.Define(LogLevel.Trace, new EventId(64, "StreamItemSent"), "Sending item for stream '{StreamId}'."); + + private static readonly Action _cancelingStream = + LoggerMessage.Define(LogLevel.Trace, new EventId(65, "CancelingStream"), "Stream '{StreamId}' has been canceled by client."); + + private static readonly Action _completingStream = + LoggerMessage.Define(LogLevel.Trace, new EventId(66, "CompletingStream"), "Sending completion message for stream '{StreamId}'."); + public static void PreparingNonBlockingInvocation(ILogger logger, string target, int count) { _preparingNonBlockingInvocation(logger, target, count, null); @@ -496,6 +508,26 @@ namespace Microsoft.AspNetCore.SignalR.Client { _unableToAcquireConnectionLockForPing(logger, null); } + + public static void StartingStream(ILogger logger, string streamId) + { + _startingStream(logger, streamId, null); + } + + public static void SendingStreamItem(ILogger logger, string streamId) + { + _sendingStreamItem(logger, streamId, null); + } + + public static void CancelingStream(ILogger logger, string streamId) + { + _cancelingStream(logger, streamId, null); + } + + public static void CompletingStream(ILogger logger, string streamId) + { + _completingStream(logger, streamId, null); + } } } } diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs index 743f810784..9ee9fabd63 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Client.Core/HubConnection.cs @@ -7,6 +7,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; +using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Channels; @@ -38,6 +40,8 @@ namespace Microsoft.AspNetCore.SignalR.Client // This lock protects the connection state. private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(1, 1); + private static readonly MethodInfo _sendStreamItemsMethod = typeof(HubConnection).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Single(m => m.Name.Equals("SendStreamItems")); + // Persistent across all connections private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; @@ -45,15 +49,19 @@ namespace Microsoft.AspNetCore.SignalR.Client private readonly IServiceProvider _serviceProvider; private readonly IConnectionFactory _connectionFactory; private readonly ConcurrentDictionary _handlers = new ConcurrentDictionary(StringComparer.Ordinal); + private long _nextActivationServerTimeout; private long _nextActivationSendPing; private bool _disposed; private bool _hasInherentKeepAlive; + private CancellationToken _uploadStreamToken; + private readonly ConnectionLogScope _logScope; // Transient state to a connection private ConnectionState _connectionState; + private int _serverProtocolMinorVersion; public event Func Closed; @@ -422,6 +430,7 @@ namespace Microsoft.AspNetCore.SignalR.Client CheckConnectionActive(nameof(StreamAsChannelCoreAsync)); cancellationToken.ThrowIfCancellationRequested(); + // I just want an excuse to use 'irq' as a variable name... var irq = InvocationRequest.Stream(cancellationToken, returnType, _connectionState.GetNextId(), _loggerFactory, this, out channel); await InvokeStreamCore(methodName, irq, args, cancellationToken); @@ -438,9 +447,84 @@ namespace Microsoft.AspNetCore.SignalR.Client return channel; } + private Dictionary PackageStreamingParams(object[] args) + { + // lazy initialized, to avoid allocation unecessary dictionaries + Dictionary readers = null; + + for (var i = 0; i < args.Length; i++) + { + if (ReflectionHelper.IsStreamingType(args[i].GetType())) + { + if (readers == null) + { + readers = new Dictionary(); + } + + var id = _connectionState.GetNextStreamId(); + readers[id] = args[i]; + args[i] = new StreamPlaceholder(id); + + Log.StartingStream(_logger, id); + } + } + + return readers; + } + + private void LaunchStreams(Dictionary readers, CancellationToken cancellationToken) + { + if (readers == null) + { + // if there were no streaming parameters then readers is never initialized + return; + } + foreach (var kvp in readers) + { + var reader = kvp.Value; + + // For each stream that needs to be sent, run a "send items" task in the background. + // This reads from the channel, attaches streamId, and sends to server. + // A single background thread here quickly gets messy. + _ = _sendStreamItemsMethod + .MakeGenericMethod(reader.GetType().GetGenericArguments()) + .Invoke(this, new object[] { kvp.Key.ToString(), reader, cancellationToken }); + } + } + + // this is called via reflection using the `_sendStreamItems` field + private async Task SendStreamItems(string streamId, ChannelReader reader, CancellationToken token) + { + Log.StartingStream(_logger, streamId); + + var combinedToken = CancellationTokenSource.CreateLinkedTokenSource(_uploadStreamToken, token).Token; + + string responseError = null; + try + { + while (await reader.WaitToReadAsync(combinedToken)) + { + while (!combinedToken.IsCancellationRequested && reader.TryRead(out var item)) + { + await SendWithLock(new StreamDataMessage(streamId, item)); + Log.SendingStreamItem(_logger, streamId); + } + } + } + catch (OperationCanceledException) + { + Log.CancelingStream(_logger, streamId); + responseError = $"Stream canceled by client."; + } + + Log.CompletingStream(_logger, streamId); + await SendWithLock(new StreamCompleteMessage(streamId, responseError)); + } private async Task InvokeCoreAsyncCore(string methodName, Type returnType, object[] args, CancellationToken cancellationToken) { + var readers = PackageStreamingParams(args); + CheckDisposed(); await WaitConnectionLockAsync(); @@ -458,21 +542,20 @@ namespace Microsoft.AspNetCore.SignalR.Client ReleaseConnectionLock(); } - // Wait for this outside the lock, because it won't complete until the server responds. + LaunchStreams(readers, cancellationToken); + + // Wait for this outside the lock, because it won't complete until the server responds return await invocationTask; } private async Task InvokeCore(string methodName, InvocationRequest irq, object[] args, CancellationToken cancellationToken) { - AssertConnectionValid(); - Log.PreparingBlockingInvocation(_logger, irq.InvocationId, methodName, irq.ResultType.FullName, args.Length); // Client invocations are always blocking var invocationMessage = new InvocationMessage(irq.InvocationId, methodName, args); Log.RegisteringInvocation(_logger, invocationMessage.InvocationId); - _connectionState.AddInvocation(irq); // Trace the full invocation @@ -498,7 +581,6 @@ namespace Microsoft.AspNetCore.SignalR.Client var invocationMessage = new StreamInvocationMessage(irq.InvocationId, methodName, args); - // I just want an excuse to use 'irq' as a variable name... Log.RegisteringInvocation(_logger, invocationMessage.InvocationId); _connectionState.AddInvocation(irq); @@ -528,28 +610,33 @@ namespace Microsoft.AspNetCore.SignalR.Client // REVIEW: If a token is passed in and is canceled during FlushAsync it seems to break .Complete()... await _connectionState.Connection.Transport.Output.FlushAsync(); + Log.MessageSent(_logger, hubMessage); // We've sent a message, so don't ping for a while ResetSendPing(); - - Log.MessageSent(_logger, hubMessage); } private async Task SendCoreAsyncCore(string methodName, object[] args, CancellationToken cancellationToken) { - CheckDisposed(); + var readers = PackageStreamingParams(args); + Log.PreparingNonBlockingInvocation(_logger, methodName, args.Length); + + var invocationMessage = new InvocationMessage(null, methodName, args); + await SendWithLock(invocationMessage, callerName: nameof(SendCoreAsync)); + + LaunchStreams(readers, cancellationToken); + } + + private async Task SendWithLock(HubMessage message, CancellationToken cancellationToken = default, [CallerMemberName] string callerName = "") + { + CheckDisposed(); await WaitConnectionLockAsync(); try { + CheckConnectionActive(callerName); CheckDisposed(); - CheckConnectionActive(nameof(SendCoreAsync)); - - Log.PreparingNonBlockingInvocation(_logger, methodName, args.Length); - - var invocationMessage = new InvocationMessage(null, methodName, args); - - await SendHubMessage(invocationMessage, cancellationToken); + await SendHubMessage(message, cancellationToken); } finally { @@ -578,15 +665,15 @@ namespace Microsoft.AspNetCore.SignalR.Client if (!connectionState.TryRemoveInvocation(completion.InvocationId, out irq)) { Log.DroppedCompletionMessage(_logger, completion.InvocationId); + break; } - else - { - DispatchInvocationCompletion(completion, irq); - irq.Dispose(); - } + + DispatchInvocationCompletion(completion, irq); + irq.Dispose(); + break; case StreamItemMessage streamItem: - // Complete the invocation with an error, we don't support streaming (yet) + // if there's no open StreamInvocation with the given id, then complete with an error if (!connectionState.TryGetInvocation(streamItem.InvocationId, out irq)) { Log.DroppedStreamMessage(_logger, streamItem.InvocationId); @@ -725,6 +812,8 @@ namespace Microsoft.AspNetCore.SignalR.Client $"Unable to complete handshake with the server due to an error: {message.Error}"); } + _serverProtocolMinorVersion = message.MinorVersion; + break; } } @@ -743,11 +832,12 @@ namespace Microsoft.AspNetCore.SignalR.Client } } } + + // shutdown if we're unable to read handshake // Ignore HubException because we throw it when we receive a handshake response with an error - // And we don't need to log that the handshake failed + // And because we already have the error, we don't need to log that the handshake failed catch (Exception ex) when (!(ex is HubException)) { - // shutdown if we're unable to read handshake Log.ErrorReceivingHandshakeResponse(_logger, ex); throw; } @@ -767,6 +857,9 @@ namespace Microsoft.AspNetCore.SignalR.Client var timer = new TimerAwaitable(TickRate, TickRate); _ = TimerLoop(timer); + var uploadStreamSource = new CancellationTokenSource(); + _uploadStreamToken = uploadStreamSource.Token; + try { while (true) @@ -820,7 +913,7 @@ namespace Microsoft.AspNetCore.SignalR.Client finally { // The buffer was sliced up to where it was consumed, so we can just advance to the start. - // We mark examined as buffer.End so that if we didn't receive a full frame, we'll wait for more data + // We mark examined as `buffer.End` so that if we didn't receive a full frame, we'll wait for more data // before yielding the read again. connectionState.Connection.Transport.Input.AdvanceTo(buffer.Start, buffer.End); } @@ -834,6 +927,7 @@ namespace Microsoft.AspNetCore.SignalR.Client finally { timer.Stop(); + uploadStreamSource.Cancel(); } // Clear the connectionState field @@ -869,7 +963,6 @@ namespace Microsoft.AspNetCore.SignalR.Client // There is no need to start a new task if there is no Closed event registered if (closed != null) { - // Fire-and-forget the closed event _ = RunClosedEvent(closed, connectionState.CloseException); } @@ -923,11 +1016,6 @@ namespace Microsoft.AspNetCore.SignalR.Client private void OnServerTimeout() { - if (Debugger.IsAttached) - { - return; - } - _connectionState.CloseException = new TimeoutException( $"Server timeout ({ServerTimeout.TotalMilliseconds:0.00}ms) elapsed without receiving a message from the server."); _connectionState.Connection.Transport.Input.CancelPendingRead(); @@ -1111,7 +1199,8 @@ namespace Microsoft.AspNetCore.SignalR.Client private TaskCompletionSource _stopTcs; private readonly object _lock = new object(); private readonly Dictionary _pendingCalls = new Dictionary(StringComparer.Ordinal); - private int _nextId; + private int _nextInvocationId; + private int _nextStreamId; public ConnectionContext Connection { get; } public Task ReceiveTask { get; set; } @@ -1132,7 +1221,8 @@ namespace Microsoft.AspNetCore.SignalR.Client Connection = connection; } - public string GetNextId() => Interlocked.Increment(ref _nextId).ToString(CultureInfo.InvariantCulture); + public string GetNextId() => Interlocked.Increment(ref _nextInvocationId).ToString(CultureInfo.InvariantCulture); + public string GetNextStreamId() => Interlocked.Increment(ref _nextStreamId).ToString(CultureInfo.InvariantCulture); public void AddInvocation(InvocationRequest irq) { @@ -1239,6 +1329,18 @@ namespace Microsoft.AspNetCore.SignalR.Client return irq.ResultType; } + Type IInvocationBinder.GetStreamItemType(string invocationId) + { + // previously, streaming was only server->client, and used GetReturnType for StreamItems + // literally the same code as the above method + if (!TryGetInvocation(invocationId, out var irq)) + { + Log.ReceivedUnexpectedResponse(_hubConnection._logger, invocationId); + return null; + } + return irq.ResultType; + } + IReadOnlyList IInvocationBinder.GetParameterTypes(string methodName) { if (!_hubConnection._handlers.TryGetValue(methodName, out var invocationHandlerList)) diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Client.Core/Microsoft.AspNetCore.SignalR.Client.Core.csproj b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Client.Core/Microsoft.AspNetCore.SignalR.Client.Core.csproj index 8cbe6845a4..bc9ad15161 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Client.Core/Microsoft.AspNetCore.SignalR.Client.Core.csproj +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Client.Core/Microsoft.AspNetCore.SignalR.Client.Core.csproj @@ -10,6 +10,7 @@ + diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/IInvocationBinder.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/IInvocationBinder.cs index 4e8fd1bcb5..2f1ba139c3 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/IInvocationBinder.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/IInvocationBinder.cs @@ -10,5 +10,6 @@ namespace Microsoft.AspNetCore.SignalR { Type GetReturnType(string invocationId); IReadOnlyList GetParameterTypes(string methodName); + Type GetStreamItemType(string streamId); } } diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Microsoft.AspNetCore.SignalR.Common.csproj b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Microsoft.AspNetCore.SignalR.Common.csproj index 8130586cf1..6e1fb9d892 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Microsoft.AspNetCore.SignalR.Common.csproj +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Microsoft.AspNetCore.SignalR.Common.csproj @@ -2,7 +2,7 @@ Common serialiation primitives for SignalR Clients Servers - netstandard2.0;netcoreapp2.2 + netstandard2.0;netcoreapp3.0 Microsoft.AspNetCore.SignalR true @@ -23,4 +23,4 @@ - \ No newline at end of file + diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/HandshakeProtocol.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/HandshakeProtocol.cs index d0c8260a9e..ed1965daed 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/HandshakeProtocol.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/HandshakeProtocol.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.Collections.Concurrent; using System.IO; using System.Text; using Microsoft.AspNetCore.Internal; @@ -18,26 +19,31 @@ namespace Microsoft.AspNetCore.SignalR.Protocol { private const string ProtocolPropertyName = "protocol"; private const string ProtocolVersionPropertyName = "version"; + private const string MinorVersionPropertyName = "minorVersion"; private const string ErrorPropertyName = "error"; private const string TypePropertyName = "type"; - /// - /// The serialized representation of a success handshake. - /// - public static ReadOnlyMemory SuccessHandshakeData; + private static ConcurrentDictionary> _messageCache = new ConcurrentDictionary>(); - static HandshakeProtocol() + public static ReadOnlySpan GetSuccessfulHandshake(IHubProtocol protocol) { - var memoryBufferWriter = MemoryBufferWriter.Get(); - try + ReadOnlyMemory result; + if(!_messageCache.TryGetValue(protocol, out result)) { - WriteResponseMessage(HandshakeResponseMessage.Empty, memoryBufferWriter); - SuccessHandshakeData = memoryBufferWriter.ToArray(); - } - finally - { - MemoryBufferWriter.Return(memoryBufferWriter); + var memoryBufferWriter = MemoryBufferWriter.Get(); + try + { + WriteResponseMessage(new HandshakeResponseMessage(protocol.MinorVersion), memoryBufferWriter); + result = memoryBufferWriter.ToArray(); + _messageCache.TryAdd(protocol, result); + } + finally + { + MemoryBufferWriter.Return(memoryBufferWriter); + } } + + return result.Span; } /// @@ -88,6 +94,9 @@ namespace Microsoft.AspNetCore.SignalR.Protocol writer.WriteValue(responseMessage.Error); } + writer.WritePropertyName(MinorVersionPropertyName); + writer.WriteValue(responseMessage.MinorVersion); + writer.WriteEndObject(); writer.Flush(); } @@ -123,6 +132,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol JsonUtils.CheckRead(reader); JsonUtils.EnsureObjectStart(reader); + int? minorVersion = null; string error = null; var completed = false; @@ -142,6 +152,9 @@ namespace Microsoft.AspNetCore.SignalR.Protocol case ErrorPropertyName: error = JsonUtils.ReadAsString(reader, ErrorPropertyName); break; + case MinorVersionPropertyName: + minorVersion = JsonUtils.ReadAsInt32(reader, MinorVersionPropertyName); + break; default: reader.Skip(); break; @@ -155,7 +168,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol } }; - responseMessage = (error != null) ? new HandshakeResponseMessage(error) : HandshakeResponseMessage.Empty; + responseMessage = new HandshakeResponseMessage(minorVersion, error); return true; } } diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/HandshakeResponseMessage.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/HandshakeResponseMessage.cs index 968da823e5..9e2454bbe9 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/HandshakeResponseMessage.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/HandshakeResponseMessage.cs @@ -11,20 +11,41 @@ namespace Microsoft.AspNetCore.SignalR.Protocol /// /// An empty response message with no error. /// - public static readonly HandshakeResponseMessage Empty = new HandshakeResponseMessage(null); + public static readonly HandshakeResponseMessage Empty = new HandshakeResponseMessage(error: null); /// /// Gets the optional error message. /// public string Error { get; } + /// + /// Highest minor protocol version that the server supports. + /// + public int MinorVersion { get; } + + /// + /// Initializes a new instance of the class. + /// An error response does need a minor version. Since the handshake has failed, any extra data will be ignored. + /// + /// Error encountered by the server, indicating why the handshake has failed. + public HandshakeResponseMessage(string error) : this(null, error) { } + + /// + /// Initializes a new instance of the class. + /// A reponse with a minor version indicates success, and doesn't require an error field. + /// + /// The highest protocol minor version that the server supports. + public HandshakeResponseMessage(int minorVersion) : this(minorVersion, null) { } + /// /// Initializes a new instance of the class. /// - /// An optional response error message. A null error message indicates a successful handshake. - public HandshakeResponseMessage(string error) + /// Error encountered by the server, indicating why the handshake has failed. + /// The highest protocol minor version that the server supports. + public HandshakeResponseMessage(int? minorVersion, string error) { - // Note that a response with an empty string for error in the JSON is considered an errored response + // MinorVersion defaults to 0, because old servers don't send a minor version + MinorVersion = minorVersion ?? 0; Error = error; } } diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/HubProtocolConstants.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/HubProtocolConstants.cs index ce1e3cbfd5..25fbf6dbbc 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/HubProtocolConstants.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/HubProtocolConstants.cs @@ -42,5 +42,15 @@ namespace Microsoft.AspNetCore.SignalR.Protocol /// Represents the close message type. /// public const int CloseMessageType = 7; + + /// + /// Represents the stream complete message type. + /// + public const int StreamCompleteMessageType = 8; + + /// + /// Same as StreamItemMessage, except + /// + public const int StreamDataMessageType = 9; } } diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/IHubProtocol.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/IHubProtocol.cs index 7aaedc65fa..99b7fe5d36 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/IHubProtocol.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/IHubProtocol.cs @@ -18,10 +18,15 @@ namespace Microsoft.AspNetCore.SignalR.Protocol string Name { get; } /// - /// Gets the version of the protocol. + /// Gets the major version of the protocol. /// int Version { get; } + /// + /// Gets the minor version of the protocol. + /// + int MinorVersion { get; } + /// /// Gets the transfer format of the protocol. /// diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamBindingFailureMessage.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamBindingFailureMessage.cs new file mode 100644 index 0000000000..571e1fdc39 --- /dev/null +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamBindingFailureMessage.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Runtime.ExceptionServices; +using System.Text; + +namespace Microsoft.AspNetCore.SignalR.Protocol +{ + /// + /// Represents a failure to bind arguments for a StreamDataMessage. This does not represent an actual + /// message that is sent on the wire, it is returned by + /// to indicate that a binding failure occurred when parsing a StreamDataMessage. The stream ID is associated + /// so that the error can be sent to the relevant hub method. + /// + public class StreamBindingFailureMessage : HubMessage + { + /// + /// Gets the id of the relevant stream + /// + public string Id { get; } + + /// + /// Gets the exception thrown during binding. + /// + public ExceptionDispatchInfo BindingFailure { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The stream ID. + /// The exception thrown during binding. + public StreamBindingFailureMessage(string id, ExceptionDispatchInfo bindingFailure) + { + Id = id; + BindingFailure = bindingFailure; + } + } +} diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamCompleteMessage.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamCompleteMessage.cs new file mode 100644 index 0000000000..587764aa72 --- /dev/null +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamCompleteMessage.cs @@ -0,0 +1,41 @@ +// 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; + +namespace Microsoft.AspNetCore.SignalR.Protocol +{ + /// + /// A message for indicating that a particular stream has ended. + /// + public class StreamCompleteMessage : HubMessage + { + /// + /// Gets the stream id. + /// + public string StreamId { get; } + + /// + /// Gets the error. Will be null if there is no error. + /// + public string Error { get; } + + /// + /// Whether the message has an error. + /// + public bool HasError { get => Error != null; } + + /// + /// Initializes a new instance of + /// + /// The streamId of the stream to complete. + /// An optional error field. + public StreamCompleteMessage(string streamId, string error = null) + { + StreamId = streamId; + Error = error; + } + } +} diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamDataMessage.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamDataMessage.cs new file mode 100644 index 0000000000..6862ed96a2 --- /dev/null +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamDataMessage.cs @@ -0,0 +1,33 @@ +// 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. + +namespace Microsoft.AspNetCore.SignalR.Protocol +{ + /// + /// Sent to parameter streams. + /// Similar to , except the data is sent to a parameter stream, rather than in response to an invocation. + /// + public class StreamDataMessage : HubMessage + { + /// + /// The piece of data this message carries. + /// + public object Item { get; } + + /// + /// The stream to which to deliver data. + /// + public string StreamId { get; } + + public StreamDataMessage(string streamId, object item) + { + StreamId = streamId; + Item = item; + } + + public override string ToString() + { + return $"StreamDataMessage {{ {nameof(StreamId)}: \"{StreamId}\", {nameof(Item)}: {Item ?? "<>"} }}"; + } + } +} diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamPlaceholder.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamPlaceholder.cs new file mode 100644 index 0000000000..f111e90cba --- /dev/null +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/Protocol/StreamPlaceholder.cs @@ -0,0 +1,25 @@ +// 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; + +namespace Microsoft.AspNetCore.SignalR.Protocol +{ + /// + /// Used by protocol serializers/deserializers to transfer information about streaming parameters. + /// Is packed as an argument in the form `{"streamId": "42"}`, and sent over wire. + /// Is then unpacked on the other side, and a new channel is created and saved under the streamId. + /// Then, each is routed to the appropiate channel based on streamId. + /// + public class StreamPlaceholder + { + public string StreamId { get; private set; } + + public StreamPlaceholder(string streamId) + { + StreamId = streamId; + } + } +} diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/breakingchanges.netcore.json b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/breakingchanges.netcore.json new file mode 100644 index 0000000000..ac9bc43e9f --- /dev/null +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Common/breakingchanges.netcore.json @@ -0,0 +1,17 @@ +[ + { + "TypeId": "public static class Microsoft.AspNetCore.SignalR.Protocol.HandshakeProtocol", + "MemberId": "public static System.ReadOnlyMemory SuccessHandshakeData", + "Kind": "Removal" + }, + { + "TypeId": "public interface Microsoft.AspNetCore.SignalR.Protocol.IHubProtocol", + "MemberId": "System.Int32 get_MinorVersion()", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.AspNetCore.SignalR.IInvocationBinder", + "MemberId": "System.Type GetStreamItemType(System.String streamId)", + "Kind": "Addition" + } +] \ No newline at end of file diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionContext.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionContext.cs index 6dc3a93620..895a673184 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionContext.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionContext.cs @@ -32,6 +32,7 @@ namespace Microsoft.AspNetCore.SignalR private readonly long _clientTimeoutInterval; private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1); + private StreamTracker _streamTracker; private long _lastSendTimeStamp = DateTime.UtcNow.Ticks; private long _lastReceivedTimeStamp = DateTime.UtcNow.Ticks; private bool _receivedMessageThisInterval = false; @@ -55,6 +56,18 @@ namespace Microsoft.AspNetCore.SignalR _clientTimeoutInterval = clientTimeoutInterval.Ticks; } + internal StreamTracker StreamTracker + { + get + { + // lazy for performance reasons + if (_streamTracker == null) + { + _streamTracker = new StreamTracker(); + } + return _streamTracker; + } + } /// /// Initializes a new instance of the class. /// @@ -302,10 +315,9 @@ namespace Microsoft.AspNetCore.SignalR try { - if (message == HandshakeResponseMessage.Empty) + if (message.Error == null) { - // success response is always an empty object so send cached data - _connectionContext.Transport.Output.Write(HandshakeProtocol.SuccessHandshakeData.Span); + _connectionContext.Transport.Output.Write(HandshakeProtocol.GetSuccessfulHandshake(Protocol)); } else { @@ -425,7 +437,8 @@ namespace Microsoft.AspNetCore.SignalR } Log.HandshakeComplete(_logger, Protocol.Name); - await WriteHandshakeResponseAsync(HandshakeResponseMessage.Empty); + + await WriteHandshakeResponseAsync(new HandshakeResponseMessage(Protocol.MinorVersion)); return true; } } diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionHandler.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionHandler.cs index 9211fda589..85e765571c 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionHandler.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/HubConnectionHandler.cs @@ -186,6 +186,9 @@ namespace Microsoft.AspNetCore.SignalR { var input = connection.Input; var protocol = connection.Protocol; + + var binder = new HubConnectionBinder(_dispatcher, connection); + while (true) { var result = await input.ReadAsync(); @@ -202,7 +205,7 @@ namespace Microsoft.AspNetCore.SignalR { connection.ResetClientTimeout(); - while (protocol.TryParseMessage(ref buffer, _dispatcher, out var message)) + while (protocol.TryParseMessage(ref buffer, binder, out var message)) { await _dispatcher.DispatchMessageAsync(connection, message); } diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/DefaultHubDispatcher.Log.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/DefaultHubDispatcher.Log.cs index 9e05de55d2..10fb4da955 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/DefaultHubDispatcher.Log.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/DefaultHubDispatcher.Log.cs @@ -57,6 +57,18 @@ namespace Microsoft.AspNetCore.SignalR.Internal private static readonly Action _invalidReturnValueFromStreamingMethod = LoggerMessage.Define(LogLevel.Error, new EventId(15, "InvalidReturnValueFromStreamingMethod"), "A streaming method returned a value that cannot be used to build enumerator {HubMethod}."); + private static readonly Action _receivedStreamItem = + LoggerMessage.Define(LogLevel.Trace, new EventId(16, "ReceivedStreamItem"), "Received item for stream '{StreamId}'."); + + private static readonly Action _startingParameterStream = + LoggerMessage.Define(LogLevel.Trace, new EventId(17, "StartingParameterStream"), "Creating streaming parameter channel '{StreamId}'."); + + private static readonly Action _completingStream = + LoggerMessage.Define(LogLevel.Trace, new EventId(18, "CompletingStream"), "Stream '{StreamId}' has been completed by client."); + + private static readonly Action _closingStreamWithBindingError = + LoggerMessage.Define(LogLevel.Warning, new EventId(19, "ClosingStreamWithBindingError"), "Stream '{StreamId}' closed with error '{Error}'."); + public static void ReceivedHubInvocation(ILogger logger, InvocationMessage invocationMessage) { _receivedHubInvocation(logger, invocationMessage, null); @@ -90,8 +102,11 @@ namespace Microsoft.AspNetCore.SignalR.Internal public static void SendingResult(ILogger logger, string invocationId, ObjectMethodExecutor objectMethodExecutor) { - var resultType = objectMethodExecutor.AsyncResultType == null ? objectMethodExecutor.MethodReturnType : objectMethodExecutor.AsyncResultType; - _sendingResult(logger, invocationId, resultType.FullName, null); + if (logger.IsEnabled(LogLevel.Trace)) + { + var resultType = objectMethodExecutor.AsyncResultType == null ? objectMethodExecutor.MethodReturnType : objectMethodExecutor.AsyncResultType; + _sendingResult(logger, invocationId, resultType.FullName, null); + } } public static void FailedInvokingHubMethod(ILogger logger, string hubMethod, Exception exception) @@ -133,6 +148,26 @@ namespace Microsoft.AspNetCore.SignalR.Internal { _invalidReturnValueFromStreamingMethod(logger, hubMethod, null); } + + public static void ReceivedStreamItem(ILogger logger, StreamDataMessage message) + { + _receivedStreamItem(logger, message.StreamId, null); + } + + public static void StartingParameterStream(ILogger logger, string streamId) + { + _startingParameterStream(logger, streamId, null); + } + + public static void CompletingStream(ILogger logger, StreamCompleteMessage message) + { + _completingStream(logger, message.StreamId, null); + } + + public static void ClosingStreamWithBindingError(ILogger logger, StreamCompleteMessage message) + { + _closingStreamWithBindingError(logger, message.StreamId, message.Error, null); + } } } } diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/DefaultHubDispatcher.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/DefaultHubDispatcher.cs index 70ef9734bd..6d78877e09 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/DefaultHubDispatcher.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/DefaultHubDispatcher.cs @@ -81,15 +81,18 @@ namespace Microsoft.AspNetCore.SignalR.Internal switch (hubMessage) { case InvocationBindingFailureMessage bindingFailureMessage: - return ProcessBindingFailure(connection, bindingFailureMessage); + return ProcessInvocationBindingFailure(connection, bindingFailureMessage); + + case StreamBindingFailureMessage bindingFailureMessage: + return ProcessStreamBindingFailure(connection, bindingFailureMessage); case InvocationMessage invocationMessage: Log.ReceivedHubInvocation(_logger, invocationMessage); - return ProcessInvocation(connection, invocationMessage, isStreamedInvocation: false); + return ProcessInvocation(connection, invocationMessage, isStreamResponse: false); case StreamInvocationMessage streamInvocationMessage: Log.ReceivedStreamHubInvocation(_logger, streamInvocationMessage); - return ProcessInvocation(connection, streamInvocationMessage, isStreamedInvocation: true); + return ProcessInvocation(connection, streamInvocationMessage, isStreamResponse: true); case CancelInvocationMessage cancelInvocationMessage: // Check if there is an associated active stream and cancel it if it exists. @@ -110,6 +113,17 @@ namespace Microsoft.AspNetCore.SignalR.Internal connection.StartClientTimeout(); break; + case StreamDataMessage streamItem: + Log.ReceivedStreamItem(_logger, streamItem); + return ProcessStreamItem(connection, streamItem); + + case StreamCompleteMessage streamCompleteMessage: + // closes channels, removes from Lookup dict + // user's method can see the channel is complete and begin wrapping up + Log.CompletingStream(_logger, streamCompleteMessage); + connection.StreamTracker.Complete(streamCompleteMessage); + break; + // Other kind of message we weren't expecting default: Log.UnsupportedMessageReceived(_logger, hubMessage.GetType().FullName); @@ -119,30 +133,38 @@ namespace Microsoft.AspNetCore.SignalR.Internal return Task.CompletedTask; } - private Task ProcessBindingFailure(HubConnectionContext connection, InvocationBindingFailureMessage bindingFailureMessage) + private Task ProcessInvocationBindingFailure(HubConnectionContext connection, InvocationBindingFailureMessage bindingFailureMessage) { Log.FailedInvokingHubMethod(_logger, bindingFailureMessage.Target, bindingFailureMessage.BindingFailure.SourceException); + + var errorMessage = ErrorMessageHelper.BuildErrorMessage($"Failed to invoke '{bindingFailureMessage.Target}' due to an error on the server.", bindingFailureMessage.BindingFailure.SourceException, _enableDetailedErrors); return SendInvocationError(bindingFailureMessage.InvocationId, connection, errorMessage); } - public override Type GetReturnType(string invocationId) + private Task ProcessStreamBindingFailure(HubConnectionContext connection, StreamBindingFailureMessage bindingFailureMessage) { - return typeof(object); + var errorString = ErrorMessageHelper.BuildErrorMessage( + $"Failed to bind Stream Item arguments to proper type.", + bindingFailureMessage.BindingFailure.SourceException, _enableDetailedErrors); + + var message = new StreamCompleteMessage(bindingFailureMessage.Id, errorString); + Log.ClosingStreamWithBindingError(_logger, message); + connection.StreamTracker.Complete(message); + + return Task.CompletedTask; } - public override IReadOnlyList GetParameterTypes(string methodName) + private Task ProcessStreamItem(HubConnectionContext connection, StreamDataMessage message) { - if (!_methods.TryGetValue(methodName, out var descriptor)) - { - throw new HubException("Method does not exist."); - } - return descriptor.ParameterTypes; + Log.ReceivedStreamItem(_logger, message); + return connection.StreamTracker.ProcessItem(message); + } private Task ProcessInvocation(HubConnectionContext connection, - HubMethodInvocationMessage hubMethodInvocationMessage, bool isStreamedInvocation) + HubMethodInvocationMessage hubMethodInvocationMessage, bool isStreamResponse) { if (!_methods.TryGetValue(hubMethodInvocationMessage.Target, out var descriptor)) { @@ -153,12 +175,17 @@ namespace Microsoft.AspNetCore.SignalR.Internal } else { - return Invoke(descriptor, connection, hubMethodInvocationMessage, isStreamedInvocation); + bool isStreamCall = descriptor.HasStreamingParameters; + if (isStreamResponse && isStreamCall) + { + throw new NotSupportedException("Streaming responses for streaming uploads are not supported."); + } + return Invoke(descriptor, connection, hubMethodInvocationMessage, isStreamResponse, isStreamCall); } } private async Task Invoke(HubMethodDescriptor descriptor, HubConnectionContext connection, - HubMethodInvocationMessage hubMethodInvocationMessage, bool isStreamedInvocation) + HubMethodInvocationMessage hubMethodInvocationMessage, bool isStreamResponse, bool isStreamCall) { var methodExecutor = descriptor.MethodExecutor; @@ -176,7 +203,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal return; } - if (!await ValidateInvocationMode(descriptor, isStreamedInvocation, hubMethodInvocationMessage, connection)) + if (!await ValidateInvocationMode(descriptor, isStreamResponse, hubMethodInvocationMessage, connection)) { return; } @@ -184,9 +211,28 @@ namespace Microsoft.AspNetCore.SignalR.Internal hubActivator = scope.ServiceProvider.GetRequiredService>(); hub = hubActivator.Create(); + if (isStreamCall) + { + // swap out placeholders for channels + var args = hubMethodInvocationMessage.Arguments; + for (int i = 0; i < args.Length; i++) + { + var placeholder = args[i] as StreamPlaceholder; + if (placeholder == null) + { + continue; + } + + Log.StartingParameterStream(_logger, placeholder.StreamId); + var itemType = methodExecutor.MethodParameters[i].ParameterType.GetGenericArguments()[0]; + args[i] = connection.StreamTracker.AddStream(placeholder.StreamId, itemType); + } + } + try { InitializeHub(hub, connection); + Task invocation = null; CancellationTokenSource cts = null; var arguments = hubMethodInvocationMessage.Arguments; @@ -222,29 +268,50 @@ namespace Microsoft.AspNetCore.SignalR.Internal } } - var result = await ExecuteHubMethod(methodExecutor, hub, arguments); - - if (isStreamedInvocation) + if (isStreamResponse) { + var result = await ExecuteHubMethod(methodExecutor, hub, arguments); + if (!TryGetStreamingEnumerator(connection, hubMethodInvocationMessage.InvocationId, descriptor, result, out var enumerator, ref cts)) { Log.InvalidReturnValueFromStreamingMethod(_logger, methodExecutor.MethodInfo.Name); - await SendInvocationError(hubMethodInvocationMessage.InvocationId, connection, $"The value returned by the streaming method '{methodExecutor.MethodInfo.Name}' is not a ChannelReader<>."); return; } - disposeScope = false; Log.StreamingResult(_logger, hubMethodInvocationMessage.InvocationId, methodExecutor); - // Fire-and-forget stream invocations, otherwise they would block other hub invocations from being able to run _ = StreamResultsAsync(hubMethodInvocationMessage.InvocationId, connection, enumerator, scope, hubActivator, hub, cts); } - // Non-empty/null InvocationId ==> Blocking invocation that needs a response - else if (!string.IsNullOrEmpty(hubMethodInvocationMessage.InvocationId)) + + else if (string.IsNullOrEmpty(hubMethodInvocationMessage.InvocationId)) { - Log.SendingResult(_logger, hubMethodInvocationMessage.InvocationId, methodExecutor); - await connection.WriteAsync(CompletionMessage.WithResult(hubMethodInvocationMessage.InvocationId, result)); + // Send Async, no response expected + invocation = ExecuteHubMethod(methodExecutor, hub, arguments); + } + + else + { + // Invoke Async, one reponse expected + async Task ExecuteInvocation() + { + var result = await ExecuteHubMethod(methodExecutor, hub, arguments); + Log.SendingResult(_logger, hubMethodInvocationMessage.InvocationId, methodExecutor); + await connection.WriteAsync(CompletionMessage.WithResult(hubMethodInvocationMessage.InvocationId, result)); + } + invocation = ExecuteInvocation(); + } + + if (isStreamCall || isStreamResponse) + { + // don't await streaming invocations + // leave them running in the background, allowing dispatcher to process other messages between streaming items + disposeScope = false; + } + else + { + // complete the non-streaming calls now + await invocation; } } catch (TargetInvocationException ex) @@ -270,7 +337,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal } } - private async Task StreamResultsAsync(string invocationId, HubConnectionContext connection, IAsyncEnumerator enumerator, IServiceScope scope, IHubActivator hubActivator, THub hub, CancellationTokenSource streamCts) + private async Task StreamResultsAsync(string invocationId, HubConnectionContext connection, IAsyncEnumerator enumerator, IServiceScope scope, + IHubActivator hubActivator, THub hub, CancellationTokenSource streamCts) { string error = null; @@ -453,5 +521,14 @@ namespace Microsoft.AspNetCore.SignalR.Internal Log.HubMethodBound(_logger, hubName, methodName); } } + + public override IReadOnlyList GetParameterTypes(string methodName) + { + if (!_methods.TryGetValue(methodName, out var descriptor)) + { + throw new HubException("Method does not exist."); + } + return descriptor.ParameterTypes; + } } } diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubConnectionBinder.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubConnectionBinder.cs new file mode 100644 index 0000000000..dcd4bd5c7d --- /dev/null +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubConnectionBinder.cs @@ -0,0 +1,36 @@ +// 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 Microsoft.AspNetCore.SignalR.Internal; + +namespace Microsoft.AspNetCore.SignalR.Internal +{ + internal class HubConnectionBinder : IInvocationBinder where THub : Hub + { + private HubDispatcher _dispatcher; + private HubConnectionContext _connection; + + public HubConnectionBinder(HubDispatcher dispatcher, HubConnectionContext connection) + { + _dispatcher = dispatcher; + _connection = connection; + } + + public IReadOnlyList GetParameterTypes(string methodName) + { + return _dispatcher.GetParameterTypes(methodName); + } + + public Type GetReturnType(string invocationId) + { + return typeof(object); + } + + public Type GetStreamItemType(string streamId) + { + return _connection.StreamTracker.GetStreamItemType(streamId); + } + } +} \ No newline at end of file diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubDispatcher.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubDispatcher.cs index 5d787fb476..9bd545da50 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubDispatcher.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubDispatcher.cs @@ -8,12 +8,11 @@ using Microsoft.AspNetCore.SignalR.Protocol; namespace Microsoft.AspNetCore.SignalR.Internal { - public abstract class HubDispatcher : IInvocationBinder where THub : Hub + public abstract class HubDispatcher where THub : Hub { public abstract Task OnConnectedAsync(HubConnectionContext connection); public abstract Task OnDisconnectedAsync(HubConnectionContext connection, Exception exception); public abstract Task DispatchMessageAsync(HubConnectionContext connection, HubMessage hubMessage); - public abstract IReadOnlyList GetParameterTypes(string methodName); - public abstract Type GetReturnType(string invocationId); + public abstract IReadOnlyList GetParameterTypes(string name); } } diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubMethodDescriptor.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubMethodDescriptor.cs index 6c7c895659..fe22be662a 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubMethodDescriptor.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Internal/HubMethodDescriptor.cs @@ -9,6 +9,7 @@ using System.Reflection; using System.Threading; using System.Threading.Channels; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.SignalR.Internal @@ -43,7 +44,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal return false; } return true; - }).Select(p => p.ParameterType).ToArray(); + }).Select(GetParameterType).ToArray(); if (HasSyntheticArguments) { @@ -53,6 +54,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal Policies = policies.ToArray(); } + public bool HasStreamingParameters { get; private set; } + private Func> _convertToEnumerator; public ObjectMethodExecutor MethodExecutor { get; } @@ -73,6 +76,17 @@ namespace Microsoft.AspNetCore.SignalR.Internal public bool HasSyntheticArguments { get; private set; } + private Type GetParameterType(ParameterInfo p) + { + var type = p.ParameterType; + if (ReflectionHelper.IsStreamingType(type)) + { + HasStreamingParameters = true; + return typeof(StreamPlaceholder); + } + return type; + } + private static bool IsChannelType(Type type, out Type payloadType) { var channelType = type.AllBaseTypes().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ChannelReader<>)); diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Microsoft.AspNetCore.SignalR.Core.csproj b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Microsoft.AspNetCore.SignalR.Core.csproj index d63b5aed42..98856f40d9 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Microsoft.AspNetCore.SignalR.Core.csproj +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/Microsoft.AspNetCore.SignalR.Core.csproj @@ -2,10 +2,14 @@ Real-time communication framework for ASP.NET Core. - netstandard2.0 + netcoreapp3.0 Microsoft.AspNetCore.SignalR + + + + diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/StreamTracker.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/StreamTracker.cs new file mode 100644 index 0000000000..3d36e38c0b --- /dev/null +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Core/StreamTracker.cs @@ -0,0 +1,105 @@ +// 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.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Protocol; + +namespace Microsoft.AspNetCore.SignalR +{ + internal class StreamTracker + { + private static readonly MethodInfo _buildConverterMethod = typeof(StreamTracker).GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Single(m => m.Name.Equals("BuildStream")); + private ConcurrentDictionary _lookup = new ConcurrentDictionary(); + + /// + /// Creates a new stream and returns the ChannelReader for it as an object. + /// + public object AddStream(string streamId, Type itemType) + { + var newConverter = (IStreamConverter)_buildConverterMethod.MakeGenericMethod(itemType).Invoke(null, Array.Empty()); + _lookup[streamId] = newConverter; + return newConverter.GetReaderAsObject(); + } + + private IStreamConverter TryGetConverter(string streamId) + { + if (_lookup.TryGetValue(streamId, out var converter)) + { + return converter; + } + else + { + throw new KeyNotFoundException($"No stream with id '{streamId}' could be found."); + } + } + + public Task ProcessItem(StreamDataMessage message) + { + return TryGetConverter(message.StreamId).WriteToStream(message.Item); + } + + public Type GetStreamItemType(string streamId) + { + return TryGetConverter(streamId).GetItemType(); + } + + public void Complete(StreamCompleteMessage message) + { + _lookup.TryRemove(message.StreamId, out var converter); + if (converter == null) + { + throw new KeyNotFoundException($"No stream with id '{message.StreamId}' could be found."); + } + converter.TryComplete(message.HasError ? new Exception(message.Error) : null); + } + + private static IStreamConverter BuildStream() + { + return new ChannelConverter(); + } + + private interface IStreamConverter + { + Type GetItemType(); + object GetReaderAsObject(); + Task WriteToStream(object item); + void TryComplete(Exception ex); + } + + private class ChannelConverter : IStreamConverter + { + private Channel _channel; + + public ChannelConverter() + { + _channel = Channel.CreateUnbounded(); + } + + public Type GetItemType() + { + return typeof(T); + } + + public object GetReaderAsObject() + { + return _channel.Reader; + } + + public Task WriteToStream(object o) + { + return _channel.Writer.WriteAsync((T)o).AsTask(); + } + + public void TryComplete(Exception ex) + { + _channel.Writer.TryComplete(ex); + } + } + } +} diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj index 47f37af697..7daf883097 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Microsoft.AspNetCore.SignalR.Protocols.Json.csproj @@ -1,4 +1,4 @@ - + Implements the SignalR Hub Protocol over JSON. diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Protocol/JsonHubProtocol.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Protocol/JsonHubProtocol.cs index e6ea079714..8436d0aa68 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Protocol/JsonHubProtocol.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Protocols.Json/Protocol/JsonHubProtocol.cs @@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol private const string ResultPropertyName = "result"; private const string ItemPropertyName = "item"; private const string InvocationIdPropertyName = "invocationId"; + private const string StreamIdPropertyName = "streamId"; private const string TypePropertyName = "type"; private const string ErrorPropertyName = "error"; private const string TargetPropertyName = "target"; @@ -32,6 +33,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol private static readonly string ProtocolName = "json"; private static readonly int ProtocolVersion = 1; + private static readonly int ProtocolMinorVersion = 0; /// /// Gets the serializer used to serialize invocation arguments and return values. @@ -60,6 +62,9 @@ namespace Microsoft.AspNetCore.SignalR.Protocol /// public int Version => ProtocolVersion; + /// + public int MinorVersion => ProtocolMinorVersion; + /// public TransferFormat TransferFormat => TransferFormat.Text; @@ -115,6 +120,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol int? type = null; string invocationId = null; + string streamId = null; string target = null; string error = null; var hasItem = false; @@ -161,6 +167,9 @@ namespace Microsoft.AspNetCore.SignalR.Protocol case InvocationIdPropertyName: invocationId = JsonUtils.ReadAsString(reader, InvocationIdPropertyName); break; + case StreamIdPropertyName: + streamId = JsonUtils.ReadAsString(reader, StreamIdPropertyName); + break; case TargetPropertyName: target = JsonUtils.ReadAsString(reader, TargetPropertyName); break; @@ -195,15 +204,32 @@ namespace Microsoft.AspNetCore.SignalR.Protocol hasItem = true; - if (string.IsNullOrEmpty(invocationId)) + + string id = null; + if (!string.IsNullOrEmpty(invocationId)) { - // If we don't have an invocation id then we need to store it as a JToken so we can parse it later - itemToken = JToken.Load(reader); + id = invocationId; + } + else if (!string.IsNullOrEmpty(streamId)) + { + id = streamId; } else { - var returnType = binder.GetReturnType(invocationId); - item = PayloadSerializer.Deserialize(reader, returnType); + // If we don't have an id yetmthen we need to store it as a JToken to parse later + itemToken = JToken.Load(reader); + break; + } + + Type itemType = binder.GetStreamItemType(id); + + try + { + item = PayloadSerializer.Deserialize(reader, itemType); + } + catch (JsonSerializationException ex) + { + return new StreamBindingFailureMessage(id, ExceptionDispatchInfo.Capture(ex)); } break; case ArgumentsPropertyName: @@ -309,11 +335,33 @@ namespace Microsoft.AspNetCore.SignalR.Protocol : BindStreamInvocationMessage(invocationId, target, arguments, hasArguments, binder); } break; + case HubProtocolConstants.StreamDataMessageType: + if (itemToken != null) + { + var itemType = binder.GetStreamItemType(streamId); + try + { + item = itemToken.ToObject(itemType, PayloadSerializer); + } + catch (JsonSerializationException ex) + { + return new StreamBindingFailureMessage(streamId, ExceptionDispatchInfo.Capture(ex)); + } + } + message = BindParamStreamMessage(streamId, item, hasItem, binder); + break; case HubProtocolConstants.StreamItemMessageType: if (itemToken != null) { - var returnType = binder.GetReturnType(invocationId); - item = itemToken.ToObject(returnType, PayloadSerializer); + var returnType = binder.GetStreamItemType(invocationId); + try + { + item = itemToken.ToObject(returnType, PayloadSerializer); + } + catch (JsonSerializationException ex) + { + return new StreamBindingFailureMessage(invocationId, ExceptionDispatchInfo.Capture(ex)); + }; } message = BindStreamItemMessage(invocationId, item, hasItem, binder); @@ -334,6 +382,9 @@ namespace Microsoft.AspNetCore.SignalR.Protocol return PingMessage.Instance; case HubProtocolConstants.CloseMessageType: return BindCloseMessage(error); + case HubProtocolConstants.StreamCompleteMessageType: + message = BindStreamCompleteMessage(streamId, error); + break; case null: throw new InvalidDataException($"Missing required property '{TypePropertyName}'."); default: @@ -404,6 +455,10 @@ namespace Microsoft.AspNetCore.SignalR.Protocol WriteHeaders(writer, m); WriteStreamInvocationMessage(m, writer); break; + case StreamDataMessage m: + WriteMessageType(writer, HubProtocolConstants.StreamDataMessageType); + WriteStreamDataMessage(m, writer); + break; case StreamItemMessage m: WriteMessageType(writer, HubProtocolConstants.StreamItemMessageType); WriteHeaders(writer, m); @@ -426,6 +481,10 @@ namespace Microsoft.AspNetCore.SignalR.Protocol WriteMessageType(writer, HubProtocolConstants.CloseMessageType); WriteCloseMessage(m, writer); break; + case StreamCompleteMessage m: + WriteMessageType(writer, HubProtocolConstants.StreamCompleteMessageType); + WriteStreamCompleteMessage(m, writer); + break; default: throw new InvalidOperationException($"Unsupported message type: {message.GetType().FullName}"); } @@ -474,6 +533,18 @@ namespace Microsoft.AspNetCore.SignalR.Protocol WriteInvocationId(message, writer); } + private void WriteStreamCompleteMessage(StreamCompleteMessage message, JsonTextWriter writer) + { + writer.WritePropertyName(StreamIdPropertyName); + writer.WriteValue(message.StreamId); + + if (message.Error != null) + { + writer.WritePropertyName(ErrorPropertyName); + writer.WriteValue(message.Error); + } + } + private void WriteStreamItemMessage(StreamItemMessage message, JsonTextWriter writer) { WriteInvocationId(message, writer); @@ -481,6 +552,14 @@ namespace Microsoft.AspNetCore.SignalR.Protocol PayloadSerializer.Serialize(writer, message.Item); } + private void WriteStreamDataMessage(StreamDataMessage message, JsonTextWriter writer) + { + writer.WritePropertyName(StreamIdPropertyName); + writer.WriteValue(message.StreamId); + writer.WritePropertyName(ItemPropertyName); + PayloadSerializer.Serialize(writer, message.Item); + } + private void WriteInvocationMessage(InvocationMessage message, JsonTextWriter writer) { WriteInvocationId(message, writer); @@ -544,6 +623,17 @@ namespace Microsoft.AspNetCore.SignalR.Protocol return new CancelInvocationMessage(invocationId); } + private HubMessage BindStreamCompleteMessage(string streamId, string error) + { + if (string.IsNullOrEmpty(streamId)) + { + throw new InvalidDataException($"Missing required property '{StreamIdPropertyName}'."); + } + + // note : if the stream completes normally, the error should be `null` + return new StreamCompleteMessage(streamId, error); + } + private HubMessage BindCompletionMessage(string invocationId, string error, object result, bool hasResult, IInvocationBinder binder) { if (string.IsNullOrEmpty(invocationId)) @@ -564,6 +654,20 @@ namespace Microsoft.AspNetCore.SignalR.Protocol return new CompletionMessage(invocationId, error, result: null, hasResult: false); } + private HubMessage BindParamStreamMessage(string streamId, object item, bool hasItem, IInvocationBinder binder) + { + if (string.IsNullOrEmpty(streamId)) + { + throw new InvalidDataException($"Missing required property '{StreamIdPropertyName}"); + } + if (!hasItem) + { + throw new InvalidDataException($"Missing required property '{ItemPropertyName}"); + } + + return new StreamDataMessage(streamId, item); + } + private HubMessage BindStreamItemMessage(string invocationId, object item, bool hasItem, IInvocationBinder binder) { if (string.IsNullOrEmpty(invocationId)) @@ -654,7 +758,6 @@ namespace Microsoft.AspNetCore.SignalR.Protocol { if (paramIndex < paramCount) { - // Set all known arguments arguments[paramIndex] = PayloadSerializer.Deserialize(reader, paramTypes[paramIndex]); } else diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Protocols.MessagePack/Protocol/MessagePackHubProtocol.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Protocols.MessagePack/Protocol/MessagePackHubProtocol.cs index d4e29baf92..f74cb7d1ff 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Protocols.MessagePack/Protocol/MessagePackHubProtocol.cs +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Protocols.MessagePack/Protocol/MessagePackHubProtocol.cs @@ -29,13 +29,17 @@ namespace Microsoft.AspNetCore.SignalR.Protocol private static readonly string ProtocolName = "messagepack"; private static readonly int ProtocolVersion = 1; - + private static readonly int ProtocolMinorVersion = 0; + /// public string Name => ProtocolName; /// public int Version => ProtocolVersion; + /// + public int MinorVersion => ProtocolMinorVersion; + /// public TransferFormat TransferFormat => TransferFormat.Binary; @@ -117,7 +121,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol private static HubMessage ParseMessage(byte[] input, int startOffset, IInvocationBinder binder, IFormatterResolver resolver) { - _ = MessagePackBinary.ReadArrayHeader(input, startOffset, out var readSize); + MessagePackBinary.ReadArrayHeader(input, startOffset, out var readSize); startOffset += readSize; var messageType = ReadInt32(input, ref startOffset, "messageType"); @@ -138,6 +142,8 @@ namespace Microsoft.AspNetCore.SignalR.Protocol return PingMessage.Instance; case HubProtocolConstants.CloseMessageType: return CreateCloseMessage(input, ref startOffset); + case HubProtocolConstants.StreamCompleteMessageType: + return CreateStreamCompleteMessage(input, ref startOffset); default: // Future protocol changes can add message types, old clients can ignore them return null; @@ -192,7 +198,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol { var headers = ReadHeaders(input, ref offset); var invocationId = ReadInvocationId(input, ref offset); - var itemType = binder.GetReturnType(invocationId); + var itemType = binder.GetStreamItemType(invocationId); var value = DeserializeObject(input, ref offset, itemType, "item", resolver); return ApplyHeaders(headers, new StreamItemMessage(invocationId, value)); } @@ -240,6 +246,17 @@ namespace Microsoft.AspNetCore.SignalR.Protocol return new CloseMessage(error); } + private static StreamCompleteMessage CreateStreamCompleteMessage(byte[] input, ref int offset) + { + var streamId = ReadString(input, ref offset, "streamId"); + var error = ReadString(input, ref offset, "error"); + if (string.IsNullOrEmpty(error)) + { + error = null; + } + return new StreamCompleteMessage(streamId, error); + } + private static Dictionary ReadHeaders(byte[] input, ref int offset) { var headerCount = ReadMapLength(input, ref offset, "headers"); @@ -372,6 +389,9 @@ namespace Microsoft.AspNetCore.SignalR.Protocol case CloseMessage closeMessage: WriteCloseMessage(closeMessage, packer); break; + case StreamCompleteMessage m: + WriteStreamCompleteMessage(m, packer); + break; default: throw new InvalidDataException($"Unexpected message type: {message.GetType().Name}"); } @@ -465,6 +485,21 @@ namespace Microsoft.AspNetCore.SignalR.Protocol MessagePackBinary.WriteString(packer, message.InvocationId); } + private void WriteStreamCompleteMessage(StreamCompleteMessage message, Stream packer) + { + MessagePackBinary.WriteArrayHeader(packer, 3); + MessagePackBinary.WriteInt16(packer, HubProtocolConstants.StreamCompleteMessageType); + MessagePackBinary.WriteString(packer, message.StreamId); + if (message.HasError) + { + MessagePackBinary.WriteString(packer, message.Error); + } + else + { + MessagePackBinary.WriteNil(packer); + } + } + private void WriteCloseMessage(CloseMessage message, Stream packer) { MessagePackBinary.WriteArrayHeader(packer, 2); diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/AckHandler.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/AckHandler.cs deleted file mode 100644 index c5502e28a2..0000000000 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/AckHandler.cs +++ /dev/null @@ -1,117 +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.Concurrent; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.SignalR.Redis.Internal -{ - internal class AckHandler : IDisposable - { - private readonly ConcurrentDictionary _acks = new ConcurrentDictionary(); - private readonly Timer _timer; - private readonly TimeSpan _ackThreshold = TimeSpan.FromSeconds(30); - private readonly TimeSpan _ackInterval = TimeSpan.FromSeconds(5); - private readonly object _lock = new object(); - private bool _disposed; - - public AckHandler() - { - // Don't capture the current ExecutionContext and its AsyncLocals onto the timer - bool restoreFlow = false; - try - { - if (!ExecutionContext.IsFlowSuppressed()) - { - ExecutionContext.SuppressFlow(); - restoreFlow = true; - } - - _timer = new Timer(state => ((AckHandler)state).CheckAcks(), state: this, dueTime: _ackInterval, period: _ackInterval); - } - finally - { - // Restore the current ExecutionContext - if (restoreFlow) - { - ExecutionContext.RestoreFlow(); - } - } - } - - public Task CreateAck(int id) - { - lock (_lock) - { - if (_disposed) - { - return Task.CompletedTask; - } - - return _acks.GetOrAdd(id, _ => new AckInfo()).Tcs.Task; - } - } - - public void TriggerAck(int id) - { - if (_acks.TryRemove(id, out var ack)) - { - ack.Tcs.TrySetResult(null); - } - } - - private void CheckAcks() - { - if (_disposed) - { - return; - } - - var utcNow = DateTime.UtcNow; - - foreach (var pair in _acks) - { - var elapsed = utcNow - pair.Value.Created; - if (elapsed > _ackThreshold) - { - if (_acks.TryRemove(pair.Key, out var ack)) - { - ack.Tcs.TrySetCanceled(); - } - } - } - } - - public void Dispose() - { - lock (_lock) - { - _disposed = true; - - _timer.Dispose(); - - foreach (var pair in _acks) - { - if (_acks.TryRemove(pair.Key, out var ack)) - { - ack.Tcs.TrySetCanceled(); - } - } - } - } - - private class AckInfo - { - public TaskCompletionSource Tcs { get; private set; } - public DateTime Created { get; private set; } - - public AckInfo() - { - Created = DateTime.UtcNow; - Tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - } - } - } -} \ No newline at end of file diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/GroupAction.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/GroupAction.cs deleted file mode 100644 index 874d190f84..0000000000 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/GroupAction.cs +++ /dev/null @@ -1,15 +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. - -namespace Microsoft.AspNetCore.SignalR.Redis.Internal -{ - // The size of the enum is defined by the protocol. Do not change it. If you need more than 255 items, - // add an additional enum. - public enum GroupAction : byte - { - // These numbers are used by the protocol, do not change them and always use explicit assignment - // when adding new items to this enum. 0 is intentionally omitted - Add = 1, - Remove = 2, - } -} diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/MessagePackUtil.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/MessagePackUtil.cs deleted file mode 100644 index b824d90394..0000000000 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/MessagePackUtil.cs +++ /dev/null @@ -1,68 +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.Diagnostics; -using System.Runtime.InteropServices; -using MessagePack; - -namespace Microsoft.AspNetCore.SignalR.Redis.Internal -{ - internal static class MessagePackUtil - { - public static int ReadArrayHeader(ref ReadOnlyMemory data) - { - var arr = GetArray(data); - var val = MessagePackBinary.ReadArrayHeader(arr.Array, arr.Offset, out var readSize); - data = data.Slice(readSize); - return val; - } - - public static int ReadMapHeader(ref ReadOnlyMemory data) - { - var arr = GetArray(data); - var val = MessagePackBinary.ReadMapHeader(arr.Array, arr.Offset, out var readSize); - data = data.Slice(readSize); - return val; - } - - public static string ReadString(ref ReadOnlyMemory data) - { - var arr = GetArray(data); - var val = MessagePackBinary.ReadString(arr.Array, arr.Offset, out var readSize); - data = data.Slice(readSize); - return val; - } - - public static byte[] ReadBytes(ref ReadOnlyMemory data) - { - var arr = GetArray(data); - var val = MessagePackBinary.ReadBytes(arr.Array, arr.Offset, out var readSize); - data = data.Slice(readSize); - return val; - } - - public static int ReadInt32(ref ReadOnlyMemory data) - { - var arr = GetArray(data); - var val = MessagePackBinary.ReadInt32(arr.Array, arr.Offset, out var readSize); - data = data.Slice(readSize); - return val; - } - - public static byte ReadByte(ref ReadOnlyMemory data) - { - var arr = GetArray(data); - var val = MessagePackBinary.ReadByte(arr.Array, arr.Offset, out var readSize); - data = data.Slice(readSize); - return val; - } - - private static ArraySegment GetArray(ReadOnlyMemory data) - { - var isArray = MemoryMarshal.TryGetArray(data, out var array); - Debug.Assert(isArray); - return array; - } - } -} diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisChannels.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisChannels.cs deleted file mode 100644 index 28393b2886..0000000000 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisChannels.cs +++ /dev/null @@ -1,75 +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.Runtime.CompilerServices; - -namespace Microsoft.AspNetCore.SignalR.Redis.Internal -{ - internal class RedisChannels - { - private readonly string _prefix; - - /// - /// Gets the name of the channel for sending to all connections. - /// - /// - /// The payload on this channel is objects containing - /// invocations to be sent to all connections - /// - public string All { get; } - - /// - /// Gets the name of the internal channel for group management messages. - /// - public string GroupManagement { get; } - - public RedisChannels(string prefix) - { - _prefix = prefix; - - All = prefix + ":all"; - GroupManagement = prefix + ":internal:groups"; - } - - /// - /// Gets the name of the channel for sending a message to a specific connection. - /// - /// The ID of the connection to get the channel for. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string Connection(string connectionId) - { - return _prefix + ":connection:" + connectionId; - } - - /// - /// Gets the name of the channel for sending a message to a named group of connections. - /// - /// The name of the group to get the channel for. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string Group(string groupName) - { - return _prefix + ":group:" + groupName; - } - - /// - /// Gets the name of the channel for sending a message to all collections associated with a user. - /// - /// The ID of the user to get the channel for. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string User(string userId) - { - return _prefix + ":user:" + userId; - } - - /// - /// Gets the name of the acknowledgement channel for the specified server. - /// - /// The name of the server to get the acknowledgement channel for. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string Ack(string serverName) - { - return _prefix + ":internal:ack:" + serverName; - } - } -} diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisGroupCommand.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisGroupCommand.cs deleted file mode 100644 index 3759da98ae..0000000000 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisGroupCommand.cs +++ /dev/null @@ -1,42 +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. - -namespace Microsoft.AspNetCore.SignalR.Redis.Internal -{ - public readonly struct RedisGroupCommand - { - /// - /// Gets the ID of the group command. - /// - public int Id { get; } - - /// - /// Gets the name of the server that sent the command. - /// - public string ServerName { get; } - - /// - /// Gets the action to be performed on the group. - /// - public GroupAction Action { get; } - - /// - /// Gets the group on which the action is performed. - /// - public string GroupName { get; } - - /// - /// Gets the ID of the connection to be added or removed from the group. - /// - public string ConnectionId { get; } - - public RedisGroupCommand(int id, string serverName, GroupAction action, string groupName, string connectionId) - { - Id = id; - ServerName = serverName; - Action = action; - GroupName = groupName; - ConnectionId = connectionId; - } - } -} diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisInvocation.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisInvocation.cs deleted file mode 100644 index a1a8a3ee07..0000000000 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisInvocation.cs +++ /dev/null @@ -1,35 +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.Collections.Generic; -using Microsoft.AspNetCore.SignalR.Protocol; - -namespace Microsoft.AspNetCore.SignalR.Redis.Internal -{ - public readonly struct RedisInvocation - { - /// - /// Gets a list of connections that should be excluded from this invocation. - /// May be null to indicate that no connections are to be excluded. - /// - public IReadOnlyList ExcludedConnectionIds { get; } - - /// - /// Gets the message serialization cache containing serialized payloads for the message. - /// - public SerializedHubMessage Message { get; } - - public RedisInvocation(SerializedHubMessage message, IReadOnlyList excludedConnectionIds) - { - Message = message; - ExcludedConnectionIds = excludedConnectionIds; - } - - public static RedisInvocation Create(string target, object[] arguments, IReadOnlyList excludedConnectionIds = null) - { - return new RedisInvocation( - new SerializedHubMessage(new InvocationMessage(target, null, arguments)), - excludedConnectionIds); - } - } -} diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisLog.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisLog.cs deleted file mode 100644 index ac0e5fb569..0000000000 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisLog.cs +++ /dev/null @@ -1,119 +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.Linq; -using Microsoft.Extensions.Logging; -using StackExchange.Redis; - -namespace Microsoft.AspNetCore.SignalR.Redis.Internal -{ - // We don't want to use our nested static class here because RedisHubLifetimeManager is generic. - // We'd end up creating separate instances of all the LoggerMessage.Define values for each Hub. - internal static class RedisLog - { - private static readonly Action _connectingToEndpoints = - LoggerMessage.Define(LogLevel.Information, new EventId(1, "ConnectingToEndpoints"), "Connecting to Redis endpoints: {Endpoints}. Using Server Name: {ServerName}"); - - private static readonly Action _connected = - LoggerMessage.Define(LogLevel.Information, new EventId(2, "Connected"), "Connected to Redis."); - - private static readonly Action _subscribing = - LoggerMessage.Define(LogLevel.Trace, new EventId(3, "Subscribing"), "Subscribing to channel: {Channel}."); - - private static readonly Action _receivedFromChannel = - LoggerMessage.Define(LogLevel.Trace, new EventId(4, "ReceivedFromChannel"), "Received message from Redis channel {Channel}."); - - private static readonly Action _publishToChannel = - LoggerMessage.Define(LogLevel.Trace, new EventId(5, "PublishToChannel"), "Publishing message to Redis channel {Channel}."); - - private static readonly Action _unsubscribe = - LoggerMessage.Define(LogLevel.Trace, new EventId(6, "Unsubscribe"), "Unsubscribing from channel: {Channel}."); - - private static readonly Action _notConnected = - LoggerMessage.Define(LogLevel.Error, new EventId(7, "Connected"), "Not connected to Redis."); - - private static readonly Action _connectionRestored = - LoggerMessage.Define(LogLevel.Information, new EventId(8, "ConnectionRestored"), "Connection to Redis restored."); - - private static readonly Action _connectionFailed = - LoggerMessage.Define(LogLevel.Error, new EventId(9, "ConnectionFailed"), "Connection to Redis failed."); - - private static readonly Action _failedWritingMessage = - LoggerMessage.Define(LogLevel.Warning, new EventId(10, "FailedWritingMessage"), "Failed writing message."); - - private static readonly Action _internalMessageFailed = - LoggerMessage.Define(LogLevel.Warning, new EventId(11, "InternalMessageFailed"), "Error processing message for internal server message."); - - public static void ConnectingToEndpoints(ILogger logger, EndPointCollection endpoints, string serverName) - { - if (logger.IsEnabled(LogLevel.Information)) - { - if (endpoints.Count > 0) - { - _connectingToEndpoints(logger, string.Join(", ", endpoints.Select(e => EndPointCollection.ToString(e))), serverName, null); - } - } - } - - public static void Connected(ILogger logger) - { - _connected(logger, null); - } - - public static void Subscribing(ILogger logger, string channelName) - { - _subscribing(logger, channelName, null); - } - - public static void ReceivedFromChannel(ILogger logger, string channelName) - { - _receivedFromChannel(logger, channelName, null); - } - - public static void PublishToChannel(ILogger logger, string channelName) - { - _publishToChannel(logger, channelName, null); - } - - public static void Unsubscribe(ILogger logger, string channelName) - { - _unsubscribe(logger, channelName, null); - } - - public static void NotConnected(ILogger logger) - { - _notConnected(logger, null); - } - - public static void ConnectionRestored(ILogger logger) - { - _connectionRestored(logger, null); - } - - public static void ConnectionFailed(ILogger logger, Exception exception) - { - _connectionFailed(logger, exception); - } - - public static void FailedWritingMessage(ILogger logger, Exception exception) - { - _failedWritingMessage(logger, exception); - } - - public static void InternalMessageFailed(ILogger logger, Exception exception) - { - _internalMessageFailed(logger, exception); - } - - // This isn't DefineMessage-based because it's just the simple TextWriter logging from ConnectionMultiplexer - public static void ConnectionMultiplexerMessage(ILogger logger, string message) - { - if (logger.IsEnabled(LogLevel.Debug)) - { - // We tag it with EventId 100 though so it can be pulled out of logs easily. - logger.LogDebug(new EventId(100, "RedisConnectionLog"), message); - } - } - } -} diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisProtocol.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisProtocol.cs deleted file mode 100644 index 6eaeb2ee79..0000000000 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisProtocol.cs +++ /dev/null @@ -1,208 +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.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using MessagePack; -using Microsoft.AspNetCore.Internal; -using Microsoft.AspNetCore.SignalR.Protocol; - -namespace Microsoft.AspNetCore.SignalR.Redis.Internal -{ - public class RedisProtocol - { - private readonly IReadOnlyList _protocols; - - public RedisProtocol(IReadOnlyList protocols) - { - _protocols = protocols; - } - - // The Redis Protocol: - // * The message type is known in advance because messages are sent to different channels based on type - // * Invocations are sent to the All, Group, Connection and User channels - // * Group Commands are sent to the GroupManagement channel - // * Acks are sent to the Acknowledgement channel. - // * See the Write[type] methods for a description of the protocol for each in-depth. - // * The "Variable length integer" is the length-prefixing format used by BinaryReader/BinaryWriter: - // * https://docs.microsoft.com/en-us/dotnet/api/system.io.binarywriter.write?view=netstandard-2.0 - // * The "Length prefixed string" is the string format used by BinaryReader/BinaryWriter: - // * A 7-bit variable length integer encodes the length in bytes, followed by the encoded string in UTF-8. - - public byte[] WriteInvocation(string methodName, object[] args) => - WriteInvocation(methodName, args, excludedConnectionIds: null); - - public byte[] WriteInvocation(string methodName, object[] args, IReadOnlyList excludedConnectionIds) - { - // Written as a MessagePack 'arr' containing at least these items: - // * A MessagePack 'arr' of 'str's representing the excluded ids - // * [The output of WriteSerializedHubMessage, which is an 'arr'] - // Any additional items are discarded. - - var writer = MemoryBufferWriter.Get(); - - try - { - MessagePackBinary.WriteArrayHeader(writer, 2); - if (excludedConnectionIds != null && excludedConnectionIds.Count > 0) - { - MessagePackBinary.WriteArrayHeader(writer, excludedConnectionIds.Count); - foreach (var id in excludedConnectionIds) - { - MessagePackBinary.WriteString(writer, id); - } - } - else - { - MessagePackBinary.WriteArrayHeader(writer, 0); - } - - WriteSerializedHubMessage(writer, - new SerializedHubMessage(new InvocationMessage(methodName, args))); - return writer.ToArray(); - } - finally - { - MemoryBufferWriter.Return(writer); - } - } - - public byte[] WriteGroupCommand(RedisGroupCommand command) - { - // Written as a MessagePack 'arr' containing at least these items: - // * An 'int': the Id of the command - // * A 'str': The server name - // * An 'int': The action (likely less than 0x7F and thus a single-byte fixnum) - // * A 'str': The group name - // * A 'str': The connection Id - // Any additional items are discarded. - - var writer = MemoryBufferWriter.Get(); - try - { - MessagePackBinary.WriteArrayHeader(writer, 5); - MessagePackBinary.WriteInt32(writer, command.Id); - MessagePackBinary.WriteString(writer, command.ServerName); - MessagePackBinary.WriteByte(writer, (byte)command.Action); - MessagePackBinary.WriteString(writer, command.GroupName); - MessagePackBinary.WriteString(writer, command.ConnectionId); - - return writer.ToArray(); - } - finally - { - MemoryBufferWriter.Return(writer); - } - } - - public byte[] WriteAck(int messageId) - { - // Written as a MessagePack 'arr' containing at least these items: - // * An 'int': The Id of the command being acknowledged. - // Any additional items are discarded. - - var writer = MemoryBufferWriter.Get(); - try - { - MessagePackBinary.WriteArrayHeader(writer, 1); - MessagePackBinary.WriteInt32(writer, messageId); - - return writer.ToArray(); - } - finally - { - MemoryBufferWriter.Return(writer); - } - } - - public RedisInvocation ReadInvocation(ReadOnlyMemory data) - { - // See WriteInvocation for the format - ValidateArraySize(ref data, 2, "Invocation"); - - // Read excluded Ids - IReadOnlyList excludedConnectionIds = null; - var idCount = MessagePackUtil.ReadArrayHeader(ref data); - if (idCount > 0) - { - var ids = new string[idCount]; - for (var i = 0; i < idCount; i++) - { - ids[i] = MessagePackUtil.ReadString(ref data); - } - - excludedConnectionIds = ids; - } - - // Read payload - var message = ReadSerializedHubMessage(ref data); - return new RedisInvocation(message, excludedConnectionIds); - } - - public RedisGroupCommand ReadGroupCommand(ReadOnlyMemory data) - { - // See WriteGroupCommand for format. - ValidateArraySize(ref data, 5, "GroupCommand"); - - var id = MessagePackUtil.ReadInt32(ref data); - var serverName = MessagePackUtil.ReadString(ref data); - var action = (GroupAction)MessagePackUtil.ReadByte(ref data); - var groupName = MessagePackUtil.ReadString(ref data); - var connectionId = MessagePackUtil.ReadString(ref data); - - return new RedisGroupCommand(id, serverName, action, groupName, connectionId); - } - - public int ReadAck(ReadOnlyMemory data) - { - // See WriteAck for format - ValidateArraySize(ref data, 1, "Ack"); - return MessagePackUtil.ReadInt32(ref data); - } - - private void WriteSerializedHubMessage(Stream stream, SerializedHubMessage message) - { - // Written as a MessagePack 'map' where the keys are the name of the protocol (as a MessagePack 'str') - // and the values are the serialized blob (as a MessagePack 'bin'). - - MessagePackBinary.WriteMapHeader(stream, _protocols.Count); - - foreach (var protocol in _protocols) - { - MessagePackBinary.WriteString(stream, protocol.Name); - - var serialized = message.GetSerializedMessage(protocol); - var isArray = MemoryMarshal.TryGetArray(serialized, out var array); - Debug.Assert(isArray); - MessagePackBinary.WriteBytes(stream, array.Array, array.Offset, array.Count); - } - } - - public static SerializedHubMessage ReadSerializedHubMessage(ref ReadOnlyMemory data) - { - var count = MessagePackUtil.ReadMapHeader(ref data); - var serializations = new SerializedMessage[count]; - for (var i = 0; i < count; i++) - { - var protocol = MessagePackUtil.ReadString(ref data); - var serialized = MessagePackUtil.ReadBytes(ref data); - serializations[i] = new SerializedMessage(protocol, serialized); - } - - return new SerializedHubMessage(serializations); - } - - private static void ValidateArraySize(ref ReadOnlyMemory data, int expectedLength, string messageType) - { - var length = MessagePackUtil.ReadArrayHeader(ref data); - - if (length < expectedLength) - { - throw new InvalidDataException($"Insufficient items in {messageType} array."); - } - } - } -} diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisSubscriptionManager.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisSubscriptionManager.cs deleted file mode 100644 index f42f555dc8..0000000000 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Internal/RedisSubscriptionManager.cs +++ /dev/null @@ -1,63 +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.Concurrent; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.SignalR.Redis.Internal -{ - internal class RedisSubscriptionManager - { - private readonly ConcurrentDictionary _subscriptions = new ConcurrentDictionary(StringComparer.Ordinal); - private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); - - public async Task AddSubscriptionAsync(string id, HubConnectionContext connection, Func subscribeMethod) - { - await _lock.WaitAsync(); - - try - { - var subscription = _subscriptions.GetOrAdd(id, _ => new HubConnectionStore()); - - subscription.Add(connection); - - // Subscribe once - if (subscription.Count == 1) - { - await subscribeMethod(id, subscription); - } - } - finally - { - _lock.Release(); - } - } - - public async Task RemoveSubscriptionAsync(string id, HubConnectionContext connection, Func unsubscribeMethod) - { - await _lock.WaitAsync(); - - try - { - if (!_subscriptions.TryGetValue(id, out var subscription)) - { - return; - } - - subscription.Remove(connection); - - if (subscription.Count == 0) - { - _subscriptions.TryRemove(id, out _); - await unsubscribeMethod(id); - } - } - finally - { - _lock.Release(); - } - } - } -} diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Microsoft.AspNetCore.SignalR.Redis.csproj b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Microsoft.AspNetCore.SignalR.Redis.csproj deleted file mode 100644 index 8862770b75..0000000000 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/Microsoft.AspNetCore.SignalR.Redis.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - Redis for ASP.NET Core SignalR. - netstandard2.0 - - - - - - - - - - - - - - - - - - diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/RedisDependencyInjectionExtensions.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/RedisDependencyInjectionExtensions.cs deleted file mode 100644 index 75318cde9a..0000000000 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/RedisDependencyInjectionExtensions.cs +++ /dev/null @@ -1,69 +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 Microsoft.AspNetCore.SignalR; -using Microsoft.AspNetCore.SignalR.Redis; -using StackExchange.Redis; - -namespace Microsoft.Extensions.DependencyInjection -{ - /// - /// Extension methods for configuring Redis-based scale-out for a SignalR Server in an . - /// - public static class RedisDependencyInjectionExtensions - { - /// - /// Adds scale-out to a , using a shared Redis server. - /// - /// The . - /// The same instance of the for chaining. - public static ISignalRServerBuilder AddRedis(this ISignalRServerBuilder signalrBuilder) - { - return AddRedis(signalrBuilder, o => { }); - } - - /// - /// Adds scale-out to a , using a shared Redis server. - /// - /// The . - /// The connection string used to connect to the Redis server. - /// The same instance of the for chaining. - public static ISignalRServerBuilder AddRedis(this ISignalRServerBuilder signalrBuilder, string redisConnectionString) - { - return AddRedis(signalrBuilder, o => - { - o.Configuration = ConfigurationOptions.Parse(redisConnectionString); - }); - } - - /// - /// Adds scale-out to a , using a shared Redis server. - /// - /// The . - /// A callback to configure the Redis options. - /// The same instance of the for chaining. - public static ISignalRServerBuilder AddRedis(this ISignalRServerBuilder signalrBuilder, Action configure) - { - signalrBuilder.Services.Configure(configure); - signalrBuilder.Services.AddSingleton(typeof(HubLifetimeManager<>), typeof(RedisHubLifetimeManager<>)); - return signalrBuilder; - } - - /// - /// Adds scale-out to a , using a shared Redis server. - /// - /// The . - /// The connection string used to connect to the Redis server. - /// A callback to configure the Redis options. - /// The same instance of the for chaining. - public static ISignalRServerBuilder AddRedis(this ISignalRServerBuilder signalrBuilder, string redisConnectionString, Action configure) - { - return AddRedis(signalrBuilder, o => - { - o.Configuration = ConfigurationOptions.Parse(redisConnectionString); - configure(o); - }); - } - } -} diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/RedisHubLifetimeManager.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/RedisHubLifetimeManager.cs deleted file mode 100644 index fc15bd7132..0000000000 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/RedisHubLifetimeManager.cs +++ /dev/null @@ -1,587 +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; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.SignalR.Protocol; -using Microsoft.AspNetCore.SignalR.Redis.Internal; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using StackExchange.Redis; - -namespace Microsoft.AspNetCore.SignalR.Redis -{ - public class RedisHubLifetimeManager : HubLifetimeManager, IDisposable where THub : Hub - { - private readonly HubConnectionStore _connections = new HubConnectionStore(); - private readonly RedisSubscriptionManager _groups = new RedisSubscriptionManager(); - private readonly RedisSubscriptionManager _users = new RedisSubscriptionManager(); - private IConnectionMultiplexer _redisServerConnection; - private ISubscriber _bus; - private readonly ILogger _logger; - private readonly RedisOptions _options; - private readonly RedisChannels _channels; - private readonly string _serverName = GenerateServerName(); - private readonly RedisProtocol _protocol; - private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(1); - - private readonly AckHandler _ackHandler; - private int _internalId; - - public RedisHubLifetimeManager(ILogger> logger, - IOptions options, - IHubProtocolResolver hubProtocolResolver) - { - _logger = logger; - _options = options.Value; - _ackHandler = new AckHandler(); - _channels = new RedisChannels(typeof(THub).FullName); - _protocol = new RedisProtocol(hubProtocolResolver.AllProtocols); - - RedisLog.ConnectingToEndpoints(_logger, options.Value.Configuration.EndPoints, _serverName); - _ = EnsureRedisServerConnection(); - } - - public override async Task OnConnectedAsync(HubConnectionContext connection) - { - await EnsureRedisServerConnection(); - var feature = new RedisFeature(); - connection.Features.Set(feature); - - var connectionTask = Task.CompletedTask; - var userTask = Task.CompletedTask; - - _connections.Add(connection); - - connectionTask = SubscribeToConnection(connection); - - if (!string.IsNullOrEmpty(connection.UserIdentifier)) - { - userTask = SubscribeToUser(connection); - } - - await Task.WhenAll(connectionTask, userTask); - } - - public override Task OnDisconnectedAsync(HubConnectionContext connection) - { - _connections.Remove(connection); - - var tasks = new List(); - - var connectionChannel = _channels.Connection(connection.ConnectionId); - RedisLog.Unsubscribe(_logger, connectionChannel); - tasks.Add(_bus.UnsubscribeAsync(connectionChannel)); - - var feature = connection.Features.Get(); - var groupNames = feature.Groups; - - if (groupNames != null) - { - // Copy the groups to an array here because they get removed from this collection - // in RemoveFromGroupAsync - foreach (var group in groupNames.ToArray()) - { - // Use RemoveGroupAsyncCore because the connection is local and we don't want to - // accidentally go to other servers with our remove request. - tasks.Add(RemoveGroupAsyncCore(connection, group)); - } - } - - if (!string.IsNullOrEmpty(connection.UserIdentifier)) - { - tasks.Add(RemoveUserAsync(connection)); - } - - return Task.WhenAll(tasks); - } - - public override Task SendAllAsync(string methodName, object[] args, CancellationToken cancellationToken = default) - { - var message = _protocol.WriteInvocation(methodName, args); - return PublishAsync(_channels.All, message); - } - - public override Task SendAllExceptAsync(string methodName, object[] args, IReadOnlyList excludedConnectionIds, CancellationToken cancellationToken = default) - { - var message = _protocol.WriteInvocation(methodName, args, excludedConnectionIds); - return PublishAsync(_channels.All, message); - } - - public override Task SendConnectionAsync(string connectionId, string methodName, object[] args, CancellationToken cancellationToken = default) - { - if (connectionId == null) - { - throw new ArgumentNullException(nameof(connectionId)); - } - - // If the connection is local we can skip sending the message through the bus since we require sticky connections. - // This also saves serializing and deserializing the message! - var connection = _connections[connectionId]; - if (connection != null) - { - return connection.WriteAsync(new InvocationMessage(methodName, args)).AsTask(); - } - - var message = _protocol.WriteInvocation(methodName, args); - return PublishAsync(_channels.Connection(connectionId), message); - } - - public override Task SendGroupAsync(string groupName, string methodName, object[] args, CancellationToken cancellationToken = default) - { - if (groupName == null) - { - throw new ArgumentNullException(nameof(groupName)); - } - - var message = _protocol.WriteInvocation(methodName, args); - return PublishAsync(_channels.Group(groupName), message); - } - - public override Task SendGroupExceptAsync(string groupName, string methodName, object[] args, IReadOnlyList excludedConnectionIds, CancellationToken cancellationToken = default) - { - if (groupName == null) - { - throw new ArgumentNullException(nameof(groupName)); - } - - var message = _protocol.WriteInvocation(methodName, args, excludedConnectionIds); - return PublishAsync(_channels.Group(groupName), message); - } - - 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); - } - - public override Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) - { - if (connectionId == null) - { - throw new ArgumentNullException(nameof(connectionId)); - } - - if (groupName == null) - { - throw new ArgumentNullException(nameof(groupName)); - } - - var connection = _connections[connectionId]; - if (connection != null) - { - // short circuit if connection is on this server - return AddGroupAsyncCore(connection, groupName); - } - - return SendGroupActionAndWaitForAck(connectionId, groupName, GroupAction.Add); - } - - public override Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) - { - if (connectionId == null) - { - throw new ArgumentNullException(nameof(connectionId)); - } - - if (groupName == null) - { - throw new ArgumentNullException(nameof(groupName)); - } - - var connection = _connections[connectionId]; - if (connection != null) - { - // short circuit if connection is on this server - return RemoveGroupAsyncCore(connection, groupName); - } - - return SendGroupActionAndWaitForAck(connectionId, groupName, GroupAction.Remove); - } - - public override Task SendConnectionsAsync(IReadOnlyList connectionIds, string methodName, object[] args, CancellationToken cancellationToken = default) - { - if (connectionIds == null) - { - throw new ArgumentNullException(nameof(connectionIds)); - } - - var publishTasks = new List(connectionIds.Count); - var payload = _protocol.WriteInvocation(methodName, args); - - foreach (var connectionId in connectionIds) - { - publishTasks.Add(PublishAsync(_channels.Connection(connectionId), payload)); - } - - return Task.WhenAll(publishTasks); - } - - public override Task SendGroupsAsync(IReadOnlyList groupNames, string methodName, object[] args, CancellationToken cancellationToken = default) - { - if (groupNames == null) - { - throw new ArgumentNullException(nameof(groupNames)); - } - var publishTasks = new List(groupNames.Count); - var payload = _protocol.WriteInvocation(methodName, args); - - foreach (var groupName in groupNames) - { - if (!string.IsNullOrEmpty(groupName)) - { - publishTasks.Add(PublishAsync(_channels.Group(groupName), payload)); - } - } - - return Task.WhenAll(publishTasks); - } - - public override Task SendUsersAsync(IReadOnlyList userIds, string methodName, object[] args, CancellationToken cancellationToken = default) - { - if (userIds.Count > 0) - { - var payload = _protocol.WriteInvocation(methodName, args); - var publishTasks = new List(userIds.Count); - foreach (var userId in userIds) - { - if (!string.IsNullOrEmpty(userId)) - { - publishTasks.Add(PublishAsync(_channels.User(userId), payload)); - } - } - - return Task.WhenAll(publishTasks); - } - - return Task.CompletedTask; - } - - private async Task PublishAsync(string channel, byte[] payload) - { - await EnsureRedisServerConnection(); - RedisLog.PublishToChannel(_logger, channel); - await _bus.PublishAsync(channel, payload); - } - - private Task AddGroupAsyncCore(HubConnectionContext connection, string groupName) - { - var feature = connection.Features.Get(); - var groupNames = feature.Groups; - - lock (groupNames) - { - // Connection already in group - if (!groupNames.Add(groupName)) - { - return Task.CompletedTask; - } - } - - var groupChannel = _channels.Group(groupName); - return _groups.AddSubscriptionAsync(groupChannel, connection, SubscribeToGroupAsync); - } - - /// - /// This takes because we want to remove the connection from the - /// _connections list in OnDisconnectedAsync and still be able to remove groups with this method. - /// - private async Task RemoveGroupAsyncCore(HubConnectionContext connection, string groupName) - { - var groupChannel = _channels.Group(groupName); - - await _groups.RemoveSubscriptionAsync(groupChannel, connection, channelName => - { - RedisLog.Unsubscribe(_logger, channelName); - return _bus.UnsubscribeAsync(channelName); - }); - - var feature = connection.Features.Get(); - var groupNames = feature.Groups; - if (groupNames != null) - { - lock (groupNames) - { - groupNames.Remove(groupName); - } - } - } - - private async Task SendGroupActionAndWaitForAck(string connectionId, string groupName, GroupAction action) - { - var id = Interlocked.Increment(ref _internalId); - var ack = _ackHandler.CreateAck(id); - // Send Add/Remove Group to other servers and wait for an ack or timeout - var message = _protocol.WriteGroupCommand(new RedisGroupCommand(id, _serverName, action, groupName, connectionId)); - await PublishAsync(_channels.GroupManagement, message); - - await ack; - } - - private Task RemoveUserAsync(HubConnectionContext connection) - { - var userChannel = _channels.User(connection.UserIdentifier); - - return _users.RemoveSubscriptionAsync(userChannel, connection, channelName => - { - RedisLog.Unsubscribe(_logger, channelName); - return _bus.UnsubscribeAsync(channelName); - }); - } - - public void Dispose() - { - _bus?.UnsubscribeAll(); - _redisServerConnection?.Dispose(); - _ackHandler.Dispose(); - } - - private Task SubscribeToAll() - { - RedisLog.Subscribing(_logger, _channels.All); - return _bus.SubscribeAsync(_channels.All, async (c, data) => - { - try - { - RedisLog.ReceivedFromChannel(_logger, _channels.All); - - var invocation = _protocol.ReadInvocation((byte[])data); - - var tasks = new List(_connections.Count); - - foreach (var connection in _connections) - { - if (invocation.ExcludedConnectionIds == null || !invocation.ExcludedConnectionIds.Contains(connection.ConnectionId)) - { - tasks.Add(connection.WriteAsync(invocation.Message).AsTask()); - } - } - - await Task.WhenAll(tasks); - } - catch (Exception ex) - { - RedisLog.FailedWritingMessage(_logger, ex); - } - }); - } - - private Task SubscribeToGroupManagementChannel() - { - return _bus.SubscribeAsync(_channels.GroupManagement, async (c, data) => - { - try - { - var groupMessage = _protocol.ReadGroupCommand((byte[])data); - - var connection = _connections[groupMessage.ConnectionId]; - if (connection == null) - { - // user not on this server - return; - } - - if (groupMessage.Action == GroupAction.Remove) - { - await RemoveGroupAsyncCore(connection, groupMessage.GroupName); - } - - if (groupMessage.Action == GroupAction.Add) - { - await AddGroupAsyncCore(connection, groupMessage.GroupName); - } - - // Send an ack to the server that sent the original command. - await PublishAsync(_channels.Ack(groupMessage.ServerName), _protocol.WriteAck(groupMessage.Id)); - } - catch (Exception ex) - { - RedisLog.InternalMessageFailed(_logger, ex); - } - }); - } - - private Task SubscribeToAckChannel() - { - // Create server specific channel in order to send an ack to a single server - return _bus.SubscribeAsync(_channels.Ack(_serverName), (c, data) => - { - var ackId = _protocol.ReadAck((byte[])data); - - _ackHandler.TriggerAck(ackId); - }); - } - - private Task SubscribeToConnection(HubConnectionContext connection) - { - var connectionChannel = _channels.Connection(connection.ConnectionId); - - RedisLog.Subscribing(_logger, connectionChannel); - return _bus.SubscribeAsync(connectionChannel, async (c, data) => - { - var invocation = _protocol.ReadInvocation((byte[])data); - await connection.WriteAsync(invocation.Message); - }); - } - - private Task SubscribeToUser(HubConnectionContext connection) - { - var userChannel = _channels.User(connection.UserIdentifier); - - return _users.AddSubscriptionAsync(userChannel, connection, (channelName, subscriptions) => - { - RedisLog.Subscribing(_logger, channelName); - return _bus.SubscribeAsync(channelName, async (c, data) => - { - try - { - var invocation = _protocol.ReadInvocation((byte[])data); - - var tasks = new List(); - foreach (var userConnection in subscriptions) - { - tasks.Add(userConnection.WriteAsync(invocation.Message).AsTask()); - } - - await Task.WhenAll(tasks); - } - catch (Exception ex) - { - RedisLog.FailedWritingMessage(_logger, ex); - } - }); - }); - } - - private Task SubscribeToGroupAsync(string groupChannel, HubConnectionStore groupConnections) - { - RedisLog.Subscribing(_logger, groupChannel); - return _bus.SubscribeAsync(groupChannel, async (c, data) => - { - try - { - var invocation = _protocol.ReadInvocation((byte[])data); - - var tasks = new List(); - foreach (var groupConnection in groupConnections) - { - if (invocation.ExcludedConnectionIds?.Contains(groupConnection.ConnectionId) == true) - { - continue; - } - - tasks.Add(groupConnection.WriteAsync(invocation.Message).AsTask()); - } - - await Task.WhenAll(tasks); - } - catch (Exception ex) - { - RedisLog.FailedWritingMessage(_logger, ex); - } - }); - } - - private async Task EnsureRedisServerConnection() - { - if (_redisServerConnection == null) - { - await _connectionLock.WaitAsync(); - try - { - if (_redisServerConnection == null) - { - var writer = new LoggerTextWriter(_logger); - _redisServerConnection = await _options.ConnectAsync(writer); - _bus = _redisServerConnection.GetSubscriber(); - - _redisServerConnection.ConnectionRestored += (_, e) => - { - // We use the subscription connection type - // Ignore messages from the interactive connection (avoids duplicates) - if (e.ConnectionType == ConnectionType.Interactive) - { - return; - } - - RedisLog.ConnectionRestored(_logger); - }; - - _redisServerConnection.ConnectionFailed += (_, e) => - { - // We use the subscription connection type - // Ignore messages from the interactive connection (avoids duplicates) - if (e.ConnectionType == ConnectionType.Interactive) - { - return; - } - - RedisLog.ConnectionFailed(_logger, e.Exception); - }; - - if (_redisServerConnection.IsConnected) - { - RedisLog.Connected(_logger); - } - else - { - RedisLog.NotConnected(_logger); - } - - await SubscribeToAll(); - await SubscribeToGroupManagementChannel(); - await SubscribeToAckChannel(); - } - } - finally - { - _connectionLock.Release(); - } - } - } - - private static string GenerateServerName() - { - // Use the machine name for convenient diagnostics, but add a guid to make it unique. - // Example: MyServerName_02db60e5fab243b890a847fa5c4dcb29 - return $"{Environment.MachineName}_{Guid.NewGuid():N}"; - } - - private class LoggerTextWriter : TextWriter - { - private readonly ILogger _logger; - - public LoggerTextWriter(ILogger logger) - { - _logger = logger; - } - - public override Encoding Encoding => Encoding.UTF8; - - public override void Write(char value) - { - - } - - public override void WriteLine(string value) - { - RedisLog.ConnectionMultiplexerMessage(_logger, value); - } - } - - private interface IRedisFeature - { - HashSet Groups { get; } - } - - private class RedisFeature : IRedisFeature - { - public HashSet Groups { get; } = new HashSet(StringComparer.OrdinalIgnoreCase); - } - } -} diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/RedisOptions.cs b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/RedisOptions.cs deleted file mode 100644 index 17d5890cf8..0000000000 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/RedisOptions.cs +++ /dev/null @@ -1,50 +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.IO; -using System.Net; -using System.Threading.Tasks; -using StackExchange.Redis; - -namespace Microsoft.AspNetCore.SignalR.Redis -{ - /// - /// Options used to configure . - /// - public class RedisOptions - { - /// - /// Gets or sets configuration options exposed by StackExchange.Redis. - /// - public ConfigurationOptions Configuration { get; set; } = new ConfigurationOptions - { - // Enable reconnecting by default - AbortOnConnectFail = false - }; - - /// - /// Gets or sets the Redis connection factory. - /// - public Func> ConnectionFactory { get; set; } - - internal async Task ConnectAsync(TextWriter log) - { - // Factory is publically settable. Assigning to a local variable before null check for thread safety. - var factory = ConnectionFactory; - if (factory == null) - { - // REVIEW: Should we do this? - if (Configuration.EndPoints.Count == 0) - { - Configuration.EndPoints.Add(IPAddress.Loopback, 0); - Configuration.SetDefaultPorts(); - } - - return await ConnectionMultiplexer.ConnectAsync(Configuration, log); - } - - return await factory(log); - } - } -} diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/baseline.netcore.json b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/baseline.netcore.json deleted file mode 100644 index 48c2646d0e..0000000000 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Redis/baseline.netcore.json +++ /dev/null @@ -1,532 +0,0 @@ -{ - "AssemblyIdentity": "Microsoft.AspNetCore.SignalR.Redis, Version=1.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", - "Types": [ - { - "Name": "Microsoft.Extensions.DependencyInjection.RedisDependencyInjectionExtensions", - "Visibility": "Public", - "Kind": "Class", - "Abstract": true, - "Static": true, - "Sealed": true, - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "AddRedis", - "Parameters": [ - { - "Name": "signalrBuilder", - "Type": "Microsoft.AspNetCore.SignalR.ISignalRServerBuilder" - } - ], - "ReturnType": "Microsoft.AspNetCore.SignalR.ISignalRServerBuilder", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "AddRedis", - "Parameters": [ - { - "Name": "signalrBuilder", - "Type": "Microsoft.AspNetCore.SignalR.ISignalRServerBuilder" - }, - { - "Name": "redisConnectionString", - "Type": "System.String" - } - ], - "ReturnType": "Microsoft.AspNetCore.SignalR.ISignalRServerBuilder", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "AddRedis", - "Parameters": [ - { - "Name": "signalrBuilder", - "Type": "Microsoft.AspNetCore.SignalR.ISignalRServerBuilder" - }, - { - "Name": "configure", - "Type": "System.Action" - } - ], - "ReturnType": "Microsoft.AspNetCore.SignalR.ISignalRServerBuilder", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "AddRedis", - "Parameters": [ - { - "Name": "signalrBuilder", - "Type": "Microsoft.AspNetCore.SignalR.ISignalRServerBuilder" - }, - { - "Name": "redisConnectionString", - "Type": "System.String" - }, - { - "Name": "configure", - "Type": "System.Action" - } - ], - "ReturnType": "Microsoft.AspNetCore.SignalR.ISignalRServerBuilder", - "Static": true, - "Extension": true, - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - }, - { - "Name": "Microsoft.AspNetCore.SignalR.Redis.RedisHubLifetimeManager", - "Visibility": "Public", - "Kind": "Class", - "BaseType": "Microsoft.AspNetCore.SignalR.HubLifetimeManager", - "ImplementedInterfaces": [ - "System.IDisposable" - ], - "Members": [ - { - "Kind": "Method", - "Name": "OnConnectedAsync", - "Parameters": [ - { - "Name": "connection", - "Type": "Microsoft.AspNetCore.SignalR.HubConnectionContext" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "OnDisconnectedAsync", - "Parameters": [ - { - "Name": "connection", - "Type": "Microsoft.AspNetCore.SignalR.HubConnectionContext" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "SendAllAsync", - "Parameters": [ - { - "Name": "methodName", - "Type": "System.String" - }, - { - "Name": "args", - "Type": "System.Object[]" - }, - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "SendAllExceptAsync", - "Parameters": [ - { - "Name": "methodName", - "Type": "System.String" - }, - { - "Name": "args", - "Type": "System.Object[]" - }, - { - "Name": "excludedConnectionIds", - "Type": "System.Collections.Generic.IReadOnlyList" - }, - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "SendConnectionAsync", - "Parameters": [ - { - "Name": "connectionId", - "Type": "System.String" - }, - { - "Name": "methodName", - "Type": "System.String" - }, - { - "Name": "args", - "Type": "System.Object[]" - }, - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "SendGroupAsync", - "Parameters": [ - { - "Name": "groupName", - "Type": "System.String" - }, - { - "Name": "methodName", - "Type": "System.String" - }, - { - "Name": "args", - "Type": "System.Object[]" - }, - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "SendGroupExceptAsync", - "Parameters": [ - { - "Name": "groupName", - "Type": "System.String" - }, - { - "Name": "methodName", - "Type": "System.String" - }, - { - "Name": "args", - "Type": "System.Object[]" - }, - { - "Name": "excludedConnectionIds", - "Type": "System.Collections.Generic.IReadOnlyList" - }, - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "SendUserAsync", - "Parameters": [ - { - "Name": "userId", - "Type": "System.String" - }, - { - "Name": "methodName", - "Type": "System.String" - }, - { - "Name": "args", - "Type": "System.Object[]" - }, - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "AddToGroupAsync", - "Parameters": [ - { - "Name": "connectionId", - "Type": "System.String" - }, - { - "Name": "groupName", - "Type": "System.String" - }, - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "RemoveFromGroupAsync", - "Parameters": [ - { - "Name": "connectionId", - "Type": "System.String" - }, - { - "Name": "groupName", - "Type": "System.String" - }, - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "SendConnectionsAsync", - "Parameters": [ - { - "Name": "connectionIds", - "Type": "System.Collections.Generic.IReadOnlyList" - }, - { - "Name": "methodName", - "Type": "System.String" - }, - { - "Name": "args", - "Type": "System.Object[]" - }, - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "SendGroupsAsync", - "Parameters": [ - { - "Name": "groupNames", - "Type": "System.Collections.Generic.IReadOnlyList" - }, - { - "Name": "methodName", - "Type": "System.String" - }, - { - "Name": "args", - "Type": "System.Object[]" - }, - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "SendUsersAsync", - "Parameters": [ - { - "Name": "userIds", - "Type": "System.Collections.Generic.IReadOnlyList" - }, - { - "Name": "methodName", - "Type": "System.String" - }, - { - "Name": "args", - "Type": "System.Object[]" - }, - { - "Name": "cancellationToken", - "Type": "System.Threading.CancellationToken", - "DefaultValue": "default(System.Threading.CancellationToken)" - } - ], - "ReturnType": "System.Threading.Tasks.Task", - "Virtual": true, - "Override": true, - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "Dispose", - "Parameters": [], - "ReturnType": "System.Void", - "Sealed": true, - "Virtual": true, - "ImplementedInterface": "System.IDisposable", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [ - { - "Name": "logger", - "Type": "Microsoft.Extensions.Logging.ILogger>" - }, - { - "Name": "options", - "Type": "Microsoft.Extensions.Options.IOptions" - }, - { - "Name": "hubProtocolResolver", - "Type": "Microsoft.AspNetCore.SignalR.IHubProtocolResolver" - } - ], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [ - { - "ParameterName": "THub", - "ParameterPosition": 0, - "BaseTypeOrInterfaces": [ - "Microsoft.AspNetCore.SignalR.Hub" - ] - } - ] - }, - { - "Name": "Microsoft.AspNetCore.SignalR.Redis.RedisOptions", - "Visibility": "Public", - "Kind": "Class", - "ImplementedInterfaces": [], - "Members": [ - { - "Kind": "Method", - "Name": "get_Configuration", - "Parameters": [], - "ReturnType": "StackExchange.Redis.ConfigurationOptions", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "set_Configuration", - "Parameters": [ - { - "Name": "value", - "Type": "StackExchange.Redis.ConfigurationOptions" - } - ], - "ReturnType": "System.Void", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "get_ConnectionFactory", - "Parameters": [], - "ReturnType": "System.Func>", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Method", - "Name": "set_ConnectionFactory", - "Parameters": [ - { - "Name": "value", - "Type": "System.Func>" - } - ], - "ReturnType": "System.Void", - "Visibility": "Public", - "GenericParameter": [] - }, - { - "Kind": "Constructor", - "Name": ".ctor", - "Parameters": [], - "Visibility": "Public", - "GenericParameter": [] - } - ], - "GenericParameters": [] - } - ] -} \ No newline at end of file diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Specification.Tests/Microsoft.AspNetCore.SignalR.Specification.Tests.csproj b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Specification.Tests/Microsoft.AspNetCore.SignalR.Specification.Tests.csproj index c5745d4d84..7cb0c7c64c 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.Specification.Tests/Microsoft.AspNetCore.SignalR.Specification.Tests.csproj +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.Specification.Tests/Microsoft.AspNetCore.SignalR.Specification.Tests.csproj @@ -2,9 +2,9 @@ Tests for users to verify their own implementations of SignalR types - netcoreapp2.2;net461 + netcoreapp3.0 - + @@ -15,7 +15,7 @@ - + diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR.StackExchangeRedis/Microsoft.AspNetCore.SignalR.StackExchangeRedis.csproj b/src/SignalR/src/Microsoft.AspNetCore.SignalR.StackExchangeRedis/Microsoft.AspNetCore.SignalR.StackExchangeRedis.csproj index f1fabc764b..45ff7d501b 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR.StackExchangeRedis/Microsoft.AspNetCore.SignalR.StackExchangeRedis.csproj +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR.StackExchangeRedis/Microsoft.AspNetCore.SignalR.StackExchangeRedis.csproj @@ -2,7 +2,7 @@ Provides scale-out support for ASP.NET Core SignalR using a Redis server and the StackExchange.Redis client. - netstandard2.0 + netcoreapp3.0 diff --git a/src/SignalR/src/Microsoft.AspNetCore.SignalR/Microsoft.AspNetCore.SignalR.csproj b/src/SignalR/src/Microsoft.AspNetCore.SignalR/Microsoft.AspNetCore.SignalR.csproj index 885b476430..3ca8671d7c 100644 --- a/src/SignalR/src/Microsoft.AspNetCore.SignalR/Microsoft.AspNetCore.SignalR.csproj +++ b/src/SignalR/src/Microsoft.AspNetCore.SignalR/Microsoft.AspNetCore.SignalR.csproj @@ -1,7 +1,7 @@  Components for providing real-time bi-directional communication across the Web. - netstandard2.0 + netcoreapp3.0 diff --git a/src/SignalR/test/Directory.Build.props b/src/SignalR/test/Directory.Build.props index dfd1b2c370..ae9c6b5322 100644 --- a/src/SignalR/test/Directory.Build.props +++ b/src/SignalR/test/Directory.Build.props @@ -2,9 +2,6 @@ - netcoreapp2.2 - $(DeveloperBuildTestTfms) - $(StandardTestTfms);net461 win7-x86 diff --git a/src/SignalR/test/Microsoft.AspNetCore.Http.Connections.Tests/Microsoft.AspNetCore.Http.Connections.Tests.csproj b/src/SignalR/test/Microsoft.AspNetCore.Http.Connections.Tests/Microsoft.AspNetCore.Http.Connections.Tests.csproj index b161cd535b..2b4c82d26b 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.Http.Connections.Tests/Microsoft.AspNetCore.Http.Connections.Tests.csproj +++ b/src/SignalR/test/Microsoft.AspNetCore.Http.Connections.Tests/Microsoft.AspNetCore.Http.Connections.Tests.csproj @@ -1,8 +1,7 @@  - $(StandardTestTfms) - win7-x86 + netcoreapp3.0 diff --git a/src/SignalR/test/Microsoft.AspNetCore.Http.Connections.Tests/WebSocketsTests.cs b/src/SignalR/test/Microsoft.AspNetCore.Http.Connections.Tests/WebSocketsTests.cs index 0af2f65812..8068853f17 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.Http.Connections.Tests/WebSocketsTests.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.Http.Connections.Tests/WebSocketsTests.cs @@ -396,5 +396,36 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests } } } + + [Fact] + public async Task MultiSegmentSendWillNotSendEmptyEndOfMessageFrame() + { + using (var feature = new TestWebSocketConnectionFeature()) + { + var serverSocket = await feature.AcceptAsync(); + var sequence = ReadOnlySequenceFactory.CreateSegments(new byte[] { 1 }, new byte[] { 15 }); + Assert.False(sequence.IsSingleSegment); + + await serverSocket.SendAsync(sequence, WebSocketMessageType.Text); + + // Run the client socket + var client = feature.Client.ExecuteAndCaptureFramesAsync(); + + await serverSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", default); + + var messages = await client.OrTimeout(); + Assert.Equal(2, messages.Received.Count); + + // First message: 1 byte, endOfMessage false + Assert.Single(messages.Received[0].Buffer); + Assert.Equal(1, messages.Received[0].Buffer[0]); + Assert.False(messages.Received[0].EndOfMessage); + + // Second message: 1 byte, endOfMessage true + Assert.Single(messages.Received[1].Buffer); + Assert.Equal(15, messages.Received[1].Buffer[0]); + Assert.True(messages.Received[1].EndOfMessage); + } + } } } diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/Microsoft.AspNetCore.SignalR.Client.FunctionalTests.csproj b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/Microsoft.AspNetCore.SignalR.Client.FunctionalTests.csproj index bc1254feac..50cbc55270 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/Microsoft.AspNetCore.SignalR.Client.FunctionalTests.csproj +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/Microsoft.AspNetCore.SignalR.Client.FunctionalTests.csproj @@ -1,7 +1,7 @@  - $(StandardTestTfms) + netcoreapp3.0 diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/VersionJsonHubProtocol.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/VersionJsonHubProtocol.cs index 86766b088b..8c0e761386 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/VersionJsonHubProtocol.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.FunctionalTests/VersionJsonHubProtocol.cs @@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests public string Name => _innerProtocol.Name; public int Version => _version; public TransferFormat TransferFormat => _innerProtocol.TransferFormat; + public int MinorVersion => 0; // not used in this test class, just for interface conformance public bool TryParseMessage(ref ReadOnlySequence input, IInvocationBinder binder, out HubMessage message) { diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.ConnectionLifecycle.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.ConnectionLifecycle.cs index 27e66cb814..ae73e83aec 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.ConnectionLifecycle.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HttpConnectionTests.ConnectionLifecycle.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.Http.Connections.Client; using Microsoft.AspNetCore.Http.Connections.Client.Internal; using Microsoft.AspNetCore.SignalR.Tests; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Xunit; @@ -321,23 +322,31 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests [Fact] public async Task SSEWontStartIfSuccessfulConnectionIsNotEstablished() { - // TODO: Add logging https://github.com/aspnet/SignalR/issues/2879 - var httpHandler = new TestHttpMessageHandler(); - - httpHandler.OnGet("/?id=00000000-0000-0000-0000-000000000000", (_, __) => + bool ExpectedErrors(WriteContext writeContext) { - return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError)); - }); + return writeContext.LoggerName == typeof(HttpConnection).FullName && + writeContext.EventId.Name == "ErrorStartingTransport"; + } - var sse = new ServerSentEventsTransport(new HttpClient(httpHandler)); + using (StartVerifiableLog(expectedErrorsFilter: ExpectedErrors)) + { + var httpHandler = new TestHttpMessageHandler(); - await WithConnectionAsync( - CreateConnection(httpHandler, transport: sse), - async (connection) => + httpHandler.OnGet("/?id=00000000-0000-0000-0000-000000000000", (_, __) => { - await Assert.ThrowsAsync( - () => connection.StartAsync(TransferFormat.Text).OrTimeout()); + return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError)); }); + + var sse = new ServerSentEventsTransport(new HttpClient(httpHandler), LoggerFactory); + + await WithConnectionAsync( + CreateConnection(httpHandler, loggerFactory: LoggerFactory, transport: sse), + async (connection) => + { + await Assert.ThrowsAsync( + () => connection.StartAsync(TransferFormat.Text).OrTimeout()); + }); + } } [Fact] @@ -354,7 +363,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests return ResponseUtils.CreateResponse(HttpStatusCode.Accepted); }); - var sse = new ServerSentEventsTransport(new HttpClient(httpHandler)); + var sse = new ServerSentEventsTransport(new HttpClient(httpHandler), LoggerFactory); await WithConnectionAsync( CreateConnection(httpHandler, loggerFactory: LoggerFactory, transport: sse), diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.Protocol.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.Protocol.cs index 642a897946..a24d6cf3ce 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.Protocol.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.Protocol.cs @@ -63,6 +63,28 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests } } + [Fact] + public async Task ClientIsOkayReceivingMinorVersionInHandshake() + { + // We're just testing that the client doesn't fail when a minor version is added to the handshake + // The client doesn't actually use that version anywhere yet so there's nothing else to test at this time + + var connection = new TestConnection(autoHandshake: false); + var hubConnection = CreateHubConnection(connection); + try + { + var startTask = hubConnection.StartAsync(); + var message = await connection.ReadHandshakeAndSendResponseAsync(56); + + await startTask; + } + finally + { + await hubConnection.DisposeAsync().OrTimeout(); + await connection.DisposeAsync().OrTimeout(); + } + } + [Fact] public async Task InvokeSendsAnInvocationMessage() { diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs index 677d39cbb0..66d711b2da 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/HubConnectionTests.cs @@ -3,6 +3,9 @@ using System; using System.Buffers; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; @@ -21,25 +24,36 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests [Fact] public async Task InvokeThrowsIfSerializingMessageFails() { - var exception = new InvalidOperationException(); - var hubConnection = CreateHubConnection(new TestConnection(), protocol: MockHubProtocol.Throw(exception)); - await hubConnection.StartAsync().OrTimeout(); + bool ExpectedErrors(WriteContext writeContext) + { + return writeContext.LoggerName == typeof(HubConnection).FullName && + writeContext.EventId.Name == "FailedToSendInvocation"; + } + using (StartVerifiableLog(ExpectedErrors)) + { + var exception = new InvalidOperationException(); + var hubConnection = CreateHubConnection(new TestConnection(), protocol: MockHubProtocol.Throw(exception), LoggerFactory); + await hubConnection.StartAsync().OrTimeout(); - var actualException = - await Assert.ThrowsAsync(async () => await hubConnection.InvokeAsync("test").OrTimeout()); - Assert.Same(exception, actualException); + var actualException = + await Assert.ThrowsAsync(async () => await hubConnection.InvokeAsync("test").OrTimeout()); + Assert.Same(exception, actualException); + } } [Fact] public async Task SendAsyncThrowsIfSerializingMessageFails() { - var exception = new InvalidOperationException(); - var hubConnection = CreateHubConnection(new TestConnection(), protocol: MockHubProtocol.Throw(exception)); - await hubConnection.StartAsync().OrTimeout(); + using (StartVerifiableLog()) + { + var exception = new InvalidOperationException(); + var hubConnection = CreateHubConnection(new TestConnection(), protocol: MockHubProtocol.Throw(exception), LoggerFactory); + await hubConnection.StartAsync().OrTimeout(); - var actualException = - await Assert.ThrowsAsync(async () => await hubConnection.SendAsync("test").OrTimeout()); - Assert.Same(exception, actualException); + var actualException = + await Assert.ThrowsAsync(async () => await hubConnection.SendAsync("test").OrTimeout()); + Assert.Same(exception, actualException); + } } [Fact] @@ -68,50 +82,70 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests [Fact] public async Task PendingInvocationsAreCanceledWhenConnectionClosesCleanly() { - var hubConnection = CreateHubConnection(new TestConnection()); + using (StartVerifiableLog()) + { + var hubConnection = CreateHubConnection(new TestConnection(), loggerFactory: LoggerFactory); - await hubConnection.StartAsync().OrTimeout(); - var invokeTask = hubConnection.InvokeAsync("testMethod").OrTimeout(); - await hubConnection.StopAsync().OrTimeout(); + await hubConnection.StartAsync().OrTimeout(); + var invokeTask = hubConnection.InvokeAsync("testMethod").OrTimeout(); + await hubConnection.StopAsync().OrTimeout(); - await Assert.ThrowsAsync(async () => await invokeTask); + await Assert.ThrowsAsync(async () => await invokeTask); + } } [Fact] public async Task PendingInvocationsAreTerminatedWithExceptionWhenTransportCompletesWithError() { - var connection = new TestConnection(); - var hubConnection = CreateHubConnection(connection, protocol: Mock.Of()); + bool ExpectedErrors(WriteContext writeContext) + { + return writeContext.LoggerName == typeof(HubConnection).FullName && + (writeContext.EventId.Name == "ShutdownWithError" || + writeContext.EventId.Name == "ServerDisconnectedWithError"); + } + using (StartVerifiableLog(ExpectedErrors)) + { + var connection = new TestConnection(); + var hubConnection = CreateHubConnection(connection, protocol: Mock.Of(), LoggerFactory); - await hubConnection.StartAsync().OrTimeout(); - var invokeTask = hubConnection.InvokeAsync("testMethod").OrTimeout(); + await hubConnection.StartAsync().OrTimeout(); + var invokeTask = hubConnection.InvokeAsync("testMethod").OrTimeout(); - var exception = new InvalidOperationException(); - connection.CompleteFromTransport(exception); + var exception = new InvalidOperationException(); + connection.CompleteFromTransport(exception); - var actualException = await Assert.ThrowsAsync(async () => await invokeTask); - Assert.Equal(exception, actualException); + var actualException = await Assert.ThrowsAsync(async () => await invokeTask); + Assert.Equal(exception, actualException); + } } [Fact] public async Task ConnectionTerminatedIfServerTimeoutIntervalElapsesWithNoMessages() { - var hubConnection = CreateHubConnection(new TestConnection()); - hubConnection.ServerTimeout = TimeSpan.FromMilliseconds(100); - - var closeTcs = new TaskCompletionSource(); - hubConnection.Closed += ex => + bool ExpectedErrors(WriteContext writeContext) { - closeTcs.TrySetResult(ex); - return Task.CompletedTask; - }; + return writeContext.LoggerName == typeof(HubConnection).FullName && + writeContext.EventId.Name == "ShutdownWithError"; + } + using (StartVerifiableLog(ExpectedErrors)) + { + var hubConnection = CreateHubConnection(new TestConnection(), loggerFactory: LoggerFactory); + hubConnection.ServerTimeout = TimeSpan.FromMilliseconds(100); - await hubConnection.StartAsync().OrTimeout(); + var closeTcs = new TaskCompletionSource(); + hubConnection.Closed += ex => + { + closeTcs.TrySetResult(ex); + return Task.CompletedTask; + }; - var exception = Assert.IsType(await closeTcs.Task.OrTimeout()); + await hubConnection.StartAsync().OrTimeout(); - // We use an interpolated string so the tests are accurate on non-US machines. - Assert.Equal($"Server timeout ({hubConnection.ServerTimeout.TotalMilliseconds:0.00}ms) elapsed without receiving a message from the server.", exception.Message); + var exception = Assert.IsType(await closeTcs.Task.OrTimeout()); + + // We use an interpolated string so the tests are accurate on non-US machines. + Assert.Equal($"Server timeout ({hubConnection.ServerTimeout.TotalMilliseconds:0.00}ms) elapsed without receiving a message from the server.", exception.Message); + } } [Fact] @@ -168,6 +202,232 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests } } + [Fact] + [LogLevel(LogLevel.Trace)] + public async Task StreamIntsToServer() + { + using (StartVerifiableLog()) + { + var connection = new TestConnection(); + var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory); + await hubConnection.StartAsync().OrTimeout(); + + var channel = Channel.CreateUnbounded(); + var invokeTask = hubConnection.InvokeAsync("SomeMethod", channel.Reader); + + var invocation = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.InvocationMessageType, invocation["type"]); + Assert.Equal("SomeMethod", invocation["target"]); + var streamId = invocation["arguments"][0]["streamId"]; + + foreach (var number in new[] { 42, 43, 322, 3145, -1234 }) + { + await channel.Writer.WriteAsync(number).AsTask().OrTimeout(); + + var item = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.StreamDataMessageType, item["type"]); + Assert.Equal(number, item["item"]); + Assert.Equal(streamId, item["streamId"]); + } + + channel.Writer.TryComplete(); + var completion = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.StreamCompleteMessageType, completion["type"]); + + await connection.ReceiveJsonMessage( + new { type = HubProtocolConstants.CompletionMessageType, invocationId = invocation["invocationId"], result = 42 } + ).OrTimeout(); + var result = await invokeTask.OrTimeout(); + Assert.Equal(42, result); + } + } + + [Fact] + [LogLevel(LogLevel.Trace)] + public async Task StreamIntsToServerViaSend() + { + using (StartVerifiableLog()) + { + var connection = new TestConnection(); + var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory); + await hubConnection.StartAsync().OrTimeout(); + + var channel = Channel.CreateUnbounded(); + var sendTask = hubConnection.SendAsync("SomeMethod", channel.Reader); + + var invocation = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.InvocationMessageType, invocation["type"]); + Assert.Equal("SomeMethod", invocation["target"]); + Assert.Null(invocation["invocationId"]); + var streamId = invocation["arguments"][0]["streamId"]; + + foreach (var item in new[] { 2, 3, 10, 5 }) + { + await channel.Writer.WriteAsync(item); + + var received = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.StreamDataMessageType, received["type"]); + Assert.Equal(item, received["item"]); + Assert.Equal(streamId, received["streamId"]); + } + } + } + + [Fact] + [LogLevel(LogLevel.Trace)] + public async Task StreamsObjectsToServer() + { + using (StartVerifiableLog()) + { + var connection = new TestConnection(); + var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory); + await hubConnection.StartAsync().OrTimeout(); + + var channel = Channel.CreateUnbounded(); + var invokeTask = hubConnection.InvokeAsync("UploadMethod", channel.Reader); + + var invocation = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.InvocationMessageType, invocation["type"]); + Assert.Equal("UploadMethod", invocation["target"]); + var id = invocation["invocationId"]; + + var items = new[] { new SampleObject("ab", 12), new SampleObject("ef", 23) }; + foreach (var item in items) + { + await channel.Writer.WriteAsync(item); + + var received = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.StreamDataMessageType, received["type"]); + Assert.Equal(item.Foo, received["item"]["foo"]); + Assert.Equal(item.Bar, received["item"]["bar"]); + } + + channel.Writer.TryComplete(); + var completion = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.StreamCompleteMessageType, completion["type"]); + + var expected = new SampleObject("oof", 14); + await connection.ReceiveJsonMessage( + new { type = HubProtocolConstants.CompletionMessageType, invocationId = id, result = expected } + ).OrTimeout(); + var result = await invokeTask.OrTimeout(); + + Assert.Equal(expected.Foo, result.Foo); + Assert.Equal(expected.Bar, result.Bar); + } + } + + [Fact] + [LogLevel(LogLevel.Trace)] + public async Task UploadStreamCancelationSendsStreamComplete() + { + using (StartVerifiableLog()) + { + var connection = new TestConnection(); + var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory); + await hubConnection.StartAsync().OrTimeout(); + + var cts = new CancellationTokenSource(); + var channel = Channel.CreateUnbounded(); + var invokeTask = hubConnection.InvokeAsync("UploadMethod", channel.Reader, cts.Token); + + var invokeMessage = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.InvocationMessageType, invokeMessage["type"]); + + cts.Cancel(); + + // after cancellation, don't send from the pipe + foreach (var number in new[] { 42, 43, 322, 3145, -1234 }) + { + + await channel.Writer.WriteAsync(number); + } + + // the next sent message should be a completion message + var complete = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.StreamCompleteMessageType, complete["type"]); + Assert.EndsWith("canceled by client.", ((string)complete["error"])); + } + } + + [Fact] + [LogLevel(LogLevel.Trace)] + public async Task InvocationCanCompleteBeforeStreamCompletes() + { + using (StartVerifiableLog()) + { + var connection = new TestConnection(); + var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory); + await hubConnection.StartAsync().OrTimeout(); + + var channel = Channel.CreateUnbounded(); + var invokeTask = hubConnection.InvokeAsync("UploadMethod", channel.Reader); + var invocation = await connection.ReadSentJsonAsync().OrTimeout(); + Assert.Equal(HubProtocolConstants.InvocationMessageType, invocation["type"]); + var id = invocation["invocationId"]; + + await connection.ReceiveJsonMessage(new { type = HubProtocolConstants.CompletionMessageType, invocationId = id, result = 10 }); + + var result = await invokeTask.OrTimeout(); + Assert.Equal(10L, result); + + // after the server returns, with whatever response + // the client's behavior is undefined, and the server is responsible for ignoring stray messages + } + } + + [Fact] + [LogLevel(LogLevel.Trace)] + public async Task WrongTypeOnServerResponse() + { + bool ExpectedErrors(WriteContext writeContext) + { + return writeContext.LoggerName == typeof(HubConnection).FullName && + (writeContext.EventId.Name == "ServerDisconnectedWithError" + || writeContext.EventId.Name == "ShutdownWithError"); + } + using (StartVerifiableLog(ExpectedErrors)) + { + var connection = new TestConnection(); + var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory); + await hubConnection.StartAsync().OrTimeout(); + + // we expect to get sent ints, and receive an int back + var channel = Channel.CreateUnbounded(); + var invokeTask = hubConnection.InvokeAsync("SumInts", channel.Reader); + + var invocation = await connection.ReadSentJsonAsync(); + Assert.Equal(HubProtocolConstants.InvocationMessageType, invocation["type"]); + var id = invocation["invocationId"]; + + await channel.Writer.WriteAsync(5); + await channel.Writer.WriteAsync(10); + + await connection.ReceiveJsonMessage(new { type = HubProtocolConstants.CompletionMessageType, invocationId = id, result = "humbug" }); + + try + { + await invokeTask; + } + catch (Exception ex) + { + Assert.Equal(typeof(Newtonsoft.Json.JsonSerializationException), ex.GetType()); + } + } + } + + private class SampleObject + { + public SampleObject(string foo, int bar) + { + Foo = foo; + Bar = bar; + } + + public string Foo { get; private set; } + public int Bar { get; private set; } + } + private struct TestKeepAliveFeature : IConnectionInherentKeepAliveFeature { public bool HasInherentKeepAlive { get; set; } @@ -197,6 +457,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests public string Name => "MockHubProtocol"; public int Version => 1; + public int MinorVersion => 1; public TransferFormat TransferFormat => TransferFormat.Binary; diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/Microsoft.AspNetCore.SignalR.Client.Tests.csproj b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/Microsoft.AspNetCore.SignalR.Client.Tests.csproj index 43229b1bea..b9e8663cf9 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/Microsoft.AspNetCore.SignalR.Client.Tests.csproj +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/Microsoft.AspNetCore.SignalR.Client.Tests.csproj @@ -1,7 +1,7 @@  - $(StandardTestTfms) + netcoreapp3.0 diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/ServerSentEventsTransportTests.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/ServerSentEventsTransportTests.cs index ab497f6ab3..421e1cef61 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/ServerSentEventsTransportTests.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/ServerSentEventsTransportTests.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Connections.Client.Internal; using Microsoft.AspNetCore.SignalR.Tests; +using Microsoft.Extensions.Logging.Testing; using Moq; using Moq.Protected; using Xunit; @@ -156,7 +157,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests [Fact] public async Task SSETransportStopsWithErrorIfSendingMessageFails() { - // TODO: Add logging https://github.com/aspnet/SignalR/issues/2879 + bool ExpectedErrors(WriteContext writeContext) + { + return writeContext.LoggerName == typeof(ServerSentEventsTransport).FullName && + writeContext.EventId.Name == "ErrorSending"; + } + var eventStreamTcs = new TaskCompletionSource(); var copyToAsyncTcs = new TaskCompletionSource(); @@ -185,8 +191,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests }); using (var httpClient = new HttpClient(mockHttpHandler.Object)) + using (StartVerifiableLog(expectedErrorsFilter: ExpectedErrors)) { - var sseTransport = new ServerSentEventsTransport(httpClient); + var sseTransport = new ServerSentEventsTransport(httpClient, LoggerFactory); await sseTransport.StartAsync( new Uri("http://fakeuri.org"), TransferFormat.Text).OrTimeout(); diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs index fe979ef5a8..432d4595a8 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestConnection.cs @@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests return this; } - public async Task ReadHandshakeAndSendResponseAsync() + public async Task ReadHandshakeAndSendResponseAsync(int minorVersion = 0) { var s = await ReadSentTextMessageAsync(); @@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests var output = MemoryBufferWriter.Get(); try { - HandshakeProtocol.WriteResponseMessage(HandshakeResponseMessage.Empty, output); + HandshakeProtocol.WriteResponseMessage(new HandshakeResponseMessage(minorVersion), output); response = output.ToArray(); } finally @@ -163,6 +163,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests } } + public async Task ReadSentJsonAsync() + { + return JObject.Parse(await ReadSentTextMessageAsync()); + } + public async Task> ReadAllSentMessagesAsync(bool ignorePings = true) { if (!Disposed.IsCompleted) diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestHttpMessageHandler.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestHttpMessageHandler.cs index 43251e4bc1..75472a4970 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestHttpMessageHandler.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Client.Tests/TestHttpMessageHandler.cs @@ -4,6 +4,8 @@ using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.AspNetCore.SignalR.Client.Tests { @@ -13,6 +15,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests { private List _receivedRequests = new List(); private RequestDelegate _app; + private readonly ILogger _logger; private List> _middleware = new List>(); @@ -29,8 +32,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests } } - public TestHttpMessageHandler(bool autoNegotiate = true, bool handleFirstPoll = true) + public TestHttpMessageHandler(ILoggerFactory loggerFactory, bool autoNegotiate = true, bool handleFirstPoll = true) { + _logger = loggerFactory?.CreateLogger() ?? NullLoggerFactory.Instance.CreateLogger(); + if (autoNegotiate) { OnNegotiate((_, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationContent())); @@ -54,6 +59,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests } } + public TestHttpMessageHandler(bool autoNegotiate = true, bool handleFirstPoll = true) + : this(NullLoggerFactory.Instance, autoNegotiate, handleFirstPoll) + { + } + protected override void Dispose(bool disposing) { Disposed = true; @@ -62,6 +72,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + _logger.LogDebug("Calling handlers for a '{Method}' going to '{Url}'.", request.Method, request.RequestUri); await Task.Yield(); lock (_receivedRequests) diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/CompositeTestBinder.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/CompositeTestBinder.cs index b1957dc5fa..06354680a4 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/CompositeTestBinder.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/CompositeTestBinder.cs @@ -37,5 +37,10 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol arg is StreamItemMessage || arg is StreamInvocationMessage; } + + public Type GetStreamItemType(string streamId) + { + throw new NotImplementedException(); + } } } diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/HandshakeProtocolTests.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/HandshakeProtocolTests.cs index 4529664bea..b2c9aaa65d 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/HandshakeProtocolTests.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/HandshakeProtocolTests.cs @@ -38,6 +38,17 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol Assert.Equal(error, response.Error); } + [Theory] + [InlineData("{\"error\":\"\",\"minorVersion\":34}\u001e", 34)] + [InlineData("{\"error\":\"flump flump flump\",\"minorVersion\":112}\u001e", 112)] + public void ParsingResponseMessageGivesMinorVersion(string json, int version) + { + var message = new ReadOnlySequence(Encoding.UTF8.GetBytes(json)); + + Assert.True(HandshakeProtocol.TryParseResponseMessage(ref message, out var response)); + Assert.Equal(version, response.MinorVersion); + } + [Fact] public void ParsingHandshakeRequestNotCompleteReturnsFalse() { diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs index 8229dac321..7bd1946100 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/JsonHubProtocolTests.cs @@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnore", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), true, NullValueHandling.Ignore, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"byteArrProp\":\"AQID\"}]}"), new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueIgnoreAndNoCamelCase", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), false, NullValueHandling.Include, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"StringProp\":\"SignalR!\",\"DoubleProp\":6.2831853071,\"IntProp\":42,\"DateTimeProp\":\"2017-04-11T00:00:00Z\",\"NullProp\":null,\"ByteArrProp\":\"AQID\"}]}"), new JsonProtocolTestData("InvocationMessage_HasCustomArgumentWithNullValueInclude", new InvocationMessage(null, "Target", new object[] { new CustomObject() }), true, NullValueHandling.Include, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}]}"), + new JsonProtocolTestData("InvocationMessage_HasStreamPlaceholder", new InvocationMessage(null, "Target", new object[] { new StreamPlaceholder("__test_id__")}), true, NullValueHandling.Ignore, "{\"type\":1,\"target\":\"Target\",\"arguments\":[{\"streamId\":\"__test_id__\"}]}"), new JsonProtocolTestData("InvocationMessage_HasHeaders", AddHeaders(TestHeaders, new InvocationMessage("123", "Target", new object[] { 1, "Foo", 2.0f })), true, NullValueHandling.Ignore, "{\"type\":1," + SerializedHeaders + ",\"invocationId\":\"123\",\"target\":\"Target\",\"arguments\":[1,\"Foo\",2.0]}"), new JsonProtocolTestData("InvocationMessage_StringIsoDateArgument", new InvocationMessage("Method", new object[] { "2016-05-10T13:51:20+12:34" }), true, NullValueHandling.Ignore, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20+12:34\"]}"), new JsonProtocolTestData("InvocationMessage_DateTimeOffsetArgument", new InvocationMessage("Method", new object[] { DateTimeOffset.Parse("2016-05-10T13:51:20+12:34") }), true, NullValueHandling.Ignore, "{\"type\":1,\"target\":\"Method\",\"arguments\":[\"2016-05-10T13:51:20+12:34\"]}"), @@ -55,7 +56,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol new JsonProtocolTestData("StreamItemMessage_HasCustomItemWithNullValueInclude", new StreamItemMessage("123", new CustomObject()), true, NullValueHandling.Include, "{\"type\":2,\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"), new JsonProtocolTestData("StreamItemMessage_HasHeaders", AddHeaders(TestHeaders, new StreamItemMessage("123", new CustomObject())), true, NullValueHandling.Include, "{\"type\":2," + SerializedHeaders + ",\"invocationId\":\"123\",\"item\":{\"stringProp\":\"SignalR!\",\"doubleProp\":6.2831853071,\"intProp\":42,\"dateTimeProp\":\"2017-04-11T00:00:00Z\",\"nullProp\":null,\"byteArrProp\":\"AQID\"}}"), - new JsonProtocolTestData("CompletionMessage_HasIntergerResult", CompletionMessage.WithResult("123", 1), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":1}"), + new JsonProtocolTestData("CompletionMessage_HasIntegerResult", CompletionMessage.WithResult("123", 1), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":1}"), new JsonProtocolTestData("CompletionMessage_HasStringResult", CompletionMessage.WithResult("123", "Foo"), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":\"Foo\"}"), new JsonProtocolTestData("CompletionMessage_HasFloatResult", CompletionMessage.WithResult("123", 2.0f), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":2.0}"), new JsonProtocolTestData("CompletionMessage_HasBoolResult", CompletionMessage.WithResult("123", true), true, NullValueHandling.Ignore, "{\"type\":3,\"invocationId\":\"123\",\"result\":true}"), @@ -88,7 +89,11 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol new JsonProtocolTestData("CloseMessage", CloseMessage.Empty, false, NullValueHandling.Ignore, "{\"type\":7}"), new JsonProtocolTestData("CloseMessage_HasError", new CloseMessage("Error!"), false, NullValueHandling.Ignore, "{\"type\":7,\"error\":\"Error!\"}"), new JsonProtocolTestData("CloseMessage_HasErrorWithCamelCase", new CloseMessage("Error!"), true, NullValueHandling.Ignore, "{\"type\":7,\"error\":\"Error!\"}"), - new JsonProtocolTestData("CloseMessage_HasErrorEmptyString", new CloseMessage(""), false, NullValueHandling.Ignore, "{\"type\":7,\"error\":\"\"}") + new JsonProtocolTestData("CloseMessage_HasErrorEmptyString", new CloseMessage(""), false, NullValueHandling.Ignore, "{\"type\":7,\"error\":\"\"}"), + + new JsonProtocolTestData("StreamCompleteMessage", new StreamCompleteMessage("123"), true, NullValueHandling.Ignore, "{\"type\":8,\"streamId\":\"123\"}"), + new JsonProtocolTestData("StreamCompleteMessageWithError", new StreamCompleteMessage("123", "zoinks"), true, NullValueHandling.Ignore, "{\"type\":8,\"streamId\":\"123\",\"error\":\"zoinks\"}"), + }.ToDictionary(t => t.Name); public static IEnumerable ProtocolTestDataNames => ProtocolTestData.Keys.Select(name => new object[] { name }); @@ -101,6 +106,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol new JsonProtocolTestData("StreamInvocationMessage_IntegerArrayArgumentFirst", new StreamInvocationMessage("3", "Method", new object[] { 1, 2 }), false, NullValueHandling.Ignore, "{ \"type\":4, \"arguments\": [1,2], \"target\": \"Method\", \"invocationId\": \"3\" }"), new JsonProtocolTestData("CompletionMessage_ResultFirst", new CompletionMessage("15", null, 10, hasResult: true), false, NullValueHandling.Ignore, "{ \"type\":3, \"result\": 10, \"invocationId\": \"15\" }"), new JsonProtocolTestData("StreamItemMessage_ItemFirst", new StreamItemMessage("1a", "foo"), false, NullValueHandling.Ignore, "{ \"item\": \"foo\", \"invocationId\": \"1a\", \"type\":2 }") + }.ToDictionary(t => t.Name); public static IEnumerable OutOfOrderJsonTestDataNames => OutOfOrderJsonTestData.Keys.Select(name => new object[] { name }); diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MemoryBufferWriterTests.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MemoryBufferWriterTests.cs index d8f51a80e3..7cd455a009 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MemoryBufferWriterTests.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MemoryBufferWriterTests.cs @@ -358,7 +358,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol } } -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 [Fact] public void WriteSpanWorksAtNonZeroOffset() { diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs index c0fbec1baf..1f8bf8c009 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/MessagePackHubProtocolTests.cs @@ -91,6 +91,11 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol name: "InvocationWithHeadersNoIdAndArrayOfCustomObjectArgs", message: AddHeaders(TestHeaders, new InvocationMessage("method", new object[] { new CustomObject(), new CustomObject() })), binary: "lQGDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmXApm1ldGhvZJKGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgOGqlN0cmluZ1Byb3CoU2lnbmFsUiGqRG91YmxlUHJvcMtAGSH7VELPEqdJbnRQcm9wKqxEYXRlVGltZVByb3DW/1jsHICoTnVsbFByb3DAq0J5dGVBcnJQcm9wxAMBAgM="), + new ProtocolTestData( + name: "InvocationWithStreamPlaceholderObject", + message: new InvocationMessage(null, "Target", new object[] { new StreamPlaceholder("__test_id__")}), + binary: "lQGAwKZUYXJnZXSRgahTdHJlYW1JZKtfX3Rlc3RfaWRfXw==" + ), // StreamItem Messages new ProtocolTestData( @@ -228,6 +233,16 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol message: AddHeaders(TestHeaders, new CancelInvocationMessage("xyz")), binary: "kwWDo0Zvb6NCYXKyS2V5V2l0aApOZXcNCkxpbmVzq1N0aWxsIFdvcmtzsVZhbHVlV2l0aE5ld0xpbmVzsEFsc28KV29ya3MNCkZpbmWjeHl6"), + // StreamComplete Messages + new ProtocolTestData( + name: "StreamComplete", + message: new StreamCompleteMessage("xyz"), + binary: "kwijeHl6wA=="), + new ProtocolTestData( + name: "StreamCompleteWithError", + message: new StreamCompleteMessage("xyz", "zoinks"), + binary: "kwijeHl6pnpvaW5rcw=="), + // Ping Messages new ProtocolTestData( name: "Ping", @@ -259,7 +274,14 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol var expectedMessage = new InvocationMessage("xyz", "method", Array.Empty()); // Verify that the input binary string decodes to the expected MsgPack primitives - var bytes = new byte[] { ArrayBytes(6), 1, 0x80, StringBytes(3), (byte)'x', (byte)'y', (byte)'z', StringBytes(6), (byte)'m', (byte)'e', (byte)'t', (byte)'h', (byte)'o', (byte)'d', ArrayBytes(0), StringBytes(2), (byte)'e', (byte)'x' }; + var bytes = new byte[] { ArrayBytes(8), + 1, + 0x80, + StringBytes(3), (byte)'x', (byte)'y', (byte)'z', + StringBytes(6), (byte)'m', (byte)'e', (byte)'t', (byte)'h', (byte)'o', (byte)'d', + ArrayBytes(0), + 0xc3, + StringBytes(2), (byte)'e', (byte)'x' }; // Parse the input fully now. bytes = Frame(bytes); diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestBinder.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestBinder.cs index 1855b19f81..d7282c42a1 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestBinder.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestBinder.cs @@ -58,5 +58,13 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol } throw new InvalidOperationException("Unexpected binder call"); } + + public Type GetStreamItemType(string streamId) + { + // In v1, stream items were only sent from server -> client + // and so they had items typed based on what the hub method returned. + // We just forward here for backwards compatibility. + return GetReturnType(streamId); + } } } diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestHubMessageEqualityComparer.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestHubMessageEqualityComparer.cs index a7c425be0a..0682c75640 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestHubMessageEqualityComparer.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/TestHubMessageEqualityComparer.cs @@ -34,11 +34,13 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol return StreamInvocationMessagesEqual(streamInvocationMessage, (StreamInvocationMessage)y); case CancelInvocationMessage cancelItemMessage: return string.Equals(cancelItemMessage.InvocationId, ((CancelInvocationMessage)y).InvocationId, StringComparison.Ordinal); - case PingMessage pingMessage: + case PingMessage _: // If the types are equal (above), then we're done. return true; case CloseMessage closeMessage: return string.Equals(closeMessage.Error, ((CloseMessage) y).Error); + case StreamCompleteMessage streamCompleteMessage: + return StreamCompleteMessagesEqual(streamCompleteMessage, (StreamCompleteMessage)y); default: throw new InvalidOperationException($"Unknown message type: {x.GetType().FullName}"); } @@ -46,34 +48,40 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol private bool CompletionMessagesEqual(CompletionMessage x, CompletionMessage y) { - return SequenceEqual(x.Headers, y.Headers) && - string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) && - string.Equals(x.Error, y.Error, StringComparison.Ordinal) && - x.HasResult == y.HasResult && - (Equals(x.Result, y.Result) || SequenceEqual(x.Result, y.Result)); + return SequenceEqual(x.Headers, y.Headers) + && string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) + && string.Equals(x.Error, y.Error, StringComparison.Ordinal) + && x.HasResult == y.HasResult + && (Equals(x.Result, y.Result) || SequenceEqual(x.Result, y.Result)); } private bool StreamItemMessagesEqual(StreamItemMessage x, StreamItemMessage y) { - return SequenceEqual(x.Headers, y.Headers) && - string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) && - (Equals(x.Item, y.Item) || SequenceEqual(x.Item, y.Item)); + return SequenceEqual(x.Headers, y.Headers) + && string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) + && (Equals(x.Item, y.Item) || SequenceEqual(x.Item, y.Item)); } private bool InvocationMessagesEqual(InvocationMessage x, InvocationMessage y) { - return SequenceEqual(x.Headers, y.Headers) && - string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) && - string.Equals(x.Target, y.Target, StringComparison.Ordinal) && - ArgumentListsEqual(x.Arguments, y.Arguments); + return SequenceEqual(x.Headers, y.Headers) + && string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) + && string.Equals(x.Target, y.Target, StringComparison.Ordinal) + && ArgumentListsEqual(x.Arguments, y.Arguments); } private bool StreamInvocationMessagesEqual(StreamInvocationMessage x, StreamInvocationMessage y) { - return SequenceEqual(x.Headers, y.Headers) && - string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) && - string.Equals(x.Target, y.Target, StringComparison.Ordinal) && - ArgumentListsEqual(x.Arguments, y.Arguments); + return SequenceEqual(x.Headers, y.Headers) + && string.Equals(x.InvocationId, y.InvocationId, StringComparison.Ordinal) + && string.Equals(x.Target, y.Target, StringComparison.Ordinal) + && ArgumentListsEqual(x.Arguments, y.Arguments); + } + + private bool StreamCompleteMessagesEqual(StreamCompleteMessage x, StreamCompleteMessage y) + { + return x.StreamId == y.StreamId + && y.Error == y.Error; } private bool ArgumentListsEqual(object[] left, object[] right) @@ -90,7 +98,7 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol for (var i = 0; i < left.Length; i++) { - if (!(Equals(left[i], right[i]) || SequenceEqual(left[i], right[i]))) + if (!(Equals(left[i], right[i]) || SequenceEqual(left[i], right[i]) || PlaceholdersEqual(left[i], right[i]))) { return false; } @@ -98,6 +106,21 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol return true; } + private bool PlaceholdersEqual(object left, object right) + { + if (left.GetType() != right.GetType()) + { + return false; + } + switch(left) + { + case StreamPlaceholder leftPlaceholder: + return leftPlaceholder.StreamId == (right as StreamPlaceholder).StreamId; + default: + return false; + } + } + private bool SequenceEqual(object left, object right) { if (left == null && right == null) diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Microsoft.AspNetCore.SignalR.Common.Tests.csproj b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Microsoft.AspNetCore.SignalR.Common.Tests.csproj index 39827c10d7..0a279ffb0d 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Microsoft.AspNetCore.SignalR.Common.Tests.csproj +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Common.Tests/Microsoft.AspNetCore.SignalR.Common.Tests.csproj @@ -1,7 +1,7 @@  - $(StandardTestTfms) + netcoreapp3.0 @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/Docker.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/Docker.cs deleted file mode 100644 index cf25fcc393..0000000000 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/Docker.cs +++ /dev/null @@ -1,213 +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.Concurrent; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using System.Threading; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Microsoft.AspNetCore.SignalR.Redis.Tests -{ - public class Docker - { - private static readonly string _exeSuffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty; - - private static readonly string _dockerContainerName = "redisTestContainer-1x"; - private static readonly string _dockerMonitorContainerName = _dockerContainerName + "Monitor-1x"; - private static readonly Lazy _instance = new Lazy(Create); - - public static Docker Default => _instance.Value; - - private readonly string _path; - - public Docker(string path) - { - _path = path; - } - - private static Docker Create() - { - var location = GetDockerLocation(); - if (location == null) - { - return null; - } - - var docker = new Docker(location); - - docker.RunCommand("info --format '{{.OSType}}'", "docker info", out var output); - - if (!string.Equals(output.Trim('\'', '"', '\r', '\n', ' '), "linux")) - { - Console.WriteLine($"'docker info' output: {output}"); - return null; - } - - return docker; - } - - private static string GetDockerLocation() - { - // OSX + Docker + Redis don't play well together for some reason. We already have these tests covered on Linux and Windows - // So we are happy ignoring them on OSX - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return null; - } - - foreach (var dir in Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator)) - { - var candidate = Path.Combine(dir, "docker" + _exeSuffix); - if (File.Exists(candidate)) - { - return candidate; - } - } - - return null; - } - - private void StartRedis(ILogger logger) - { - try - { - Run(); - } - catch (Exception ex) - { - logger.LogError(ex, "Error starting redis docker container, retrying."); - Thread.Sleep(1000); - Run(); - } - - void Run() - { - // create and run docker container, remove automatically when stopped, map 6379 from the container to 6379 localhost - // use static name 'redisTestContainer' so if the container doesn't get removed we don't keep adding more - // use redis base docker image - // 30 second timeout to allow redis image to be downloaded, should be a rare occurrence, only happening when a new version is released - RunProcessAndThrowIfFailed(_path, $"run --rm -p 6380:6379 --name {_dockerContainerName} -d redis", "redis", logger, TimeSpan.FromSeconds(30)); - } - } - - public void Start(ILogger logger) - { - logger.LogInformation("Starting docker container"); - - // stop container if there is one, could be from a previous test run, ignore failures - RunProcessAndWait(_path, $"stop {_dockerMonitorContainerName}", "docker stop", logger, TimeSpan.FromSeconds(15), out var _); - RunProcessAndWait(_path, $"stop {_dockerContainerName}", "docker stop", logger, TimeSpan.FromSeconds(15), out var output); - - StartRedis(logger); - - // inspect the redis docker image and extract the IPAddress. Necessary when running tests from inside a docker container, spinning up a new docker container for redis - // outside the current container requires linking the networks (difficult to automate) or using the IP:Port combo - RunProcessAndWait(_path, "inspect --format=\"{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}\" " + _dockerContainerName, "docker ipaddress", logger, TimeSpan.FromSeconds(5), out output); - output = output.Trim().Replace(Environment.NewLine, ""); - - // variable used by Startup.cs - Environment.SetEnvironmentVariable("REDIS_CONNECTION-PREV", $"{output}:6379"); - - var (monitorProcess, monitorOutput) = RunProcess(_path, $"run -i --name {_dockerMonitorContainerName} --link {_dockerContainerName}:redis --rm redis redis-cli -h redis -p 6379", "redis monitor", logger); - monitorProcess.StandardInput.WriteLine("MONITOR"); - monitorProcess.StandardInput.Flush(); - } - - public void Stop(ILogger logger) - { - // Get logs from Redis container before stopping the container - RunProcessAndThrowIfFailed(_path, $"logs {_dockerContainerName}", "docker logs", logger, TimeSpan.FromSeconds(5)); - - logger.LogInformation("Stopping docker container"); - RunProcessAndWait(_path, $"stop {_dockerMonitorContainerName}", "docker stop", logger, TimeSpan.FromSeconds(15), out var _); - RunProcessAndWait(_path, $"stop {_dockerContainerName}", "docker stop", logger, TimeSpan.FromSeconds(15), out var _); - } - - public int RunCommand(string commandAndArguments, string prefix, out string output) => - RunCommand(commandAndArguments, prefix, NullLogger.Instance, out output); - - public int RunCommand(string commandAndArguments, string prefix, ILogger logger, out string output) - { - return RunProcessAndWait(_path, commandAndArguments, prefix, logger, TimeSpan.FromSeconds(5), out output); - } - - private static void RunProcessAndThrowIfFailed(string fileName, string arguments, string prefix, ILogger logger, TimeSpan timeout) - { - var exitCode = RunProcessAndWait(fileName, arguments, prefix, logger, timeout, out var output); - - if (exitCode != 0) - { - throw new Exception($"Command '{fileName} {arguments}' failed with exit code '{exitCode}'. Output:{Environment.NewLine}{output}"); - } - } - - private static int RunProcessAndWait(string fileName, string arguments, string prefix, ILogger logger, TimeSpan timeout, out string output) - { - var (process, lines) = RunProcess(fileName, arguments, prefix, logger); - - using (process) - { - if (!process.WaitForExit((int)timeout.TotalMilliseconds)) - { - process.Close(); - logger.LogError("Closing process '{processName}' because it is running longer than the configured timeout.", fileName); - } - - // Need to WaitForExit without a timeout to guarantee the output stream has written everything - process.WaitForExit(); - - output = string.Join(Environment.NewLine, lines); - - return process.ExitCode; - } - } - - private static (Process, ConcurrentQueue) RunProcess(string fileName, string arguments, string prefix, ILogger logger) - { - var process = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = fileName, - Arguments = arguments, - UseShellExecute = false, - RedirectStandardError = true, - RedirectStandardOutput = true, - RedirectStandardInput = true - }, - EnableRaisingEvents = true - }; - - var lines = new ConcurrentQueue(); - process.OutputDataReceived += (_, a) => - { - LogIfNotNull(logger.LogInformation, $"'{prefix}' stdout: {{0}}", a.Data); - lines.Enqueue(a.Data); - }; - process.ErrorDataReceived += (_, a) => - { - LogIfNotNull(logger.LogError, $"'{prefix}' stderr: {{0}}", a.Data); - lines.Enqueue(a.Data); - }; - - process.Start(); - - process.BeginErrorReadLine(); - process.BeginOutputReadLine(); - - return (process, lines); - } - - private static void LogIfNotNull(Action logger, string message, string data) - { - if (!string.IsNullOrEmpty(data)) - { - logger(message, new[] { data }); - } - } - } -} diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/EchoHub.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/EchoHub.cs deleted file mode 100644 index e7747b50d3..0000000000 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/EchoHub.cs +++ /dev/null @@ -1,31 +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.Threading.Tasks; - -namespace Microsoft.AspNetCore.SignalR.Redis.Tests -{ - public class EchoHub : Hub - { - public string Echo(string message) - { - return message; - } - - public Task EchoGroup(string groupName, string message) - { - return Clients.Group(groupName).SendAsync("Echo", message); - } - - public Task EchoUser(string userName, string message) - { - return Clients.User(userName).SendAsync("Echo", message); - } - - public Task AddSelfToGroup(string groupName) - { - return Groups.AddToGroupAsync(Context.ConnectionId, groupName); - } - } -} diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/Microsoft.AspNetCore.SignalR.Redis.Tests.csproj b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/Microsoft.AspNetCore.SignalR.Redis.Tests.csproj deleted file mode 100644 index 24d6cebb93..0000000000 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/Microsoft.AspNetCore.SignalR.Redis.Tests.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - $(StandardTestTfms) - - - - - PreserveNewest - - - - - - - - - - - - - - - - - - diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisDependencyInjectionExtensionsTests.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisDependencyInjectionExtensionsTests.cs deleted file mode 100644 index 385bf345d9..0000000000 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisDependencyInjectionExtensionsTests.cs +++ /dev/null @@ -1,41 +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.Net; -using System.Text; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Xunit; - -namespace Microsoft.AspNetCore.SignalR.Redis.Tests -{ - public class RedisDependencyInjectionExtensionsTests - { - // No need to go too deep with these tests, or we're just testing StackExchange.Redis again :). It's the one doing the parsing. - [Theory] - [InlineData("testredis.example.com", "testredis.example.com", 0, null, false)] - [InlineData("testredis.example.com:6380,ssl=True", "testredis.example.com", 6380, null, true)] - [InlineData("testredis.example.com:6380,password=hunter2,ssl=True", "testredis.example.com", 6380, "hunter2", true)] - public void AddRedisWithConnectionStringProperlyParsesOptions(string connectionString, string host, int port, string password, bool useSsl) - { - var services = new ServiceCollection(); - services.AddSignalR().AddRedis(connectionString); - var provider = services.BuildServiceProvider(); - - var options = provider.GetService>(); - Assert.NotNull(options.Value); - Assert.NotNull(options.Value.Configuration); - Assert.Equal(password, options.Value.Configuration.Password); - Assert.Collection(options.Value.Configuration.EndPoints, - endpoint => - { - var dnsEndpoint = Assert.IsType(endpoint); - Assert.Equal(host, dnsEndpoint.Host); - Assert.Equal(port, dnsEndpoint.Port); - }); - Assert.Equal(useSsl, options.Value.Configuration.Ssl); - } - } -} diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisEndToEnd.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisEndToEnd.cs deleted file mode 100644 index a049ed8ccc..0000000000 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisEndToEnd.cs +++ /dev/null @@ -1,194 +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.Threading.Tasks; -using Microsoft.AspNetCore.Http.Connections; -using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.AspNetCore.SignalR.Protocol; -using Microsoft.AspNetCore.SignalR.Tests; -using Microsoft.AspNetCore.Testing.xunit; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.AspNetCore.SignalR.Redis.Tests -{ - // Disable running server tests in parallel so server logs can accurately be captured per test - [CollectionDefinition(Name, DisableParallelization = true)] - public class RedisEndToEndTestsCollection : ICollectionFixture> - { - public const string Name = nameof(RedisEndToEndTestsCollection); - } - - [Collection(RedisEndToEndTestsCollection.Name)] - public class RedisEndToEndTests : VerifiableLoggedTest - { - private readonly RedisServerFixture _serverFixture; - - public RedisEndToEndTests(RedisServerFixture serverFixture) - { - if (serverFixture == null) - { - throw new ArgumentNullException(nameof(serverFixture)); - } - - _serverFixture = serverFixture; - } - - [ConditionalTheory] - [SkipIfDockerNotPresent] - [MemberData(nameof(TransportTypesAndProtocolTypes))] - public async Task HubConnectionCanSendAndReceiveMessages(HttpTransportType transportType, string protocolName) - { - using (StartVerifiableLog()) - { - var protocol = HubProtocolHelpers.GetHubProtocol(protocolName); - - var connection = CreateConnection(_serverFixture.FirstServer.Url + "/echo", transportType, protocol, LoggerFactory); - - await connection.StartAsync().OrTimeout(); - var str = await connection.InvokeAsync("Echo", "Hello, World!").OrTimeout(); - - Assert.Equal("Hello, World!", str); - - await connection.DisposeAsync().OrTimeout(); - } - } - - [ConditionalTheory] - [SkipIfDockerNotPresent] - [MemberData(nameof(TransportTypesAndProtocolTypes))] - public async Task HubConnectionCanSendAndReceiveGroupMessages(HttpTransportType transportType, string protocolName) - { - using (StartVerifiableLog()) - { - var protocol = HubProtocolHelpers.GetHubProtocol(protocolName); - - var connection = CreateConnection(_serverFixture.FirstServer.Url + "/echo", transportType, protocol, LoggerFactory); - var secondConnection = CreateConnection(_serverFixture.SecondServer.Url + "/echo", transportType, protocol, LoggerFactory); - - var tcs = new TaskCompletionSource(); - connection.On("Echo", message => tcs.TrySetResult(message)); - var tcs2 = new TaskCompletionSource(); - secondConnection.On("Echo", message => tcs2.TrySetResult(message)); - - var groupName = $"TestGroup_{transportType}_{protocolName}_{Guid.NewGuid()}"; - - await secondConnection.StartAsync().OrTimeout(); - await connection.StartAsync().OrTimeout(); - await connection.InvokeAsync("AddSelfToGroup", groupName).OrTimeout(); - await secondConnection.InvokeAsync("AddSelfToGroup", groupName).OrTimeout(); - await connection.InvokeAsync("EchoGroup", groupName, "Hello, World!").OrTimeout(); - - Assert.Equal("Hello, World!", await tcs.Task.OrTimeout()); - Assert.Equal("Hello, World!", await tcs2.Task.OrTimeout()); - - await connection.DisposeAsync().OrTimeout(); - } - } - - [ConditionalTheory] - [SkipIfDockerNotPresent] - [MemberData(nameof(TransportTypesAndProtocolTypes))] - public async Task CanSendAndReceiveUserMessagesFromMultipleConnectionsWithSameUser(HttpTransportType transportType, string protocolName) - { - using (StartVerifiableLog()) - { - var protocol = HubProtocolHelpers.GetHubProtocol(protocolName); - - var connection = CreateConnection(_serverFixture.FirstServer.Url + "/echo", transportType, protocol, LoggerFactory, userName: "userA"); - var secondConnection = CreateConnection(_serverFixture.SecondServer.Url + "/echo", transportType, protocol, LoggerFactory, userName: "userA"); - - var tcs = new TaskCompletionSource(); - connection.On("Echo", message => tcs.TrySetResult(message)); - var tcs2 = new TaskCompletionSource(); - secondConnection.On("Echo", message => tcs2.TrySetResult(message)); - - await secondConnection.StartAsync().OrTimeout(); - await connection.StartAsync().OrTimeout(); - await connection.InvokeAsync("EchoUser", "userA", "Hello, World!").OrTimeout(); - - Assert.Equal("Hello, World!", await tcs.Task.OrTimeout()); - Assert.Equal("Hello, World!", await tcs2.Task.OrTimeout()); - - await connection.DisposeAsync().OrTimeout(); - await secondConnection.DisposeAsync().OrTimeout(); - } - } - - [ConditionalTheory] - [SkipIfDockerNotPresent] - [MemberData(nameof(TransportTypesAndProtocolTypes))] - public async Task CanSendAndReceiveUserMessagesWhenOneConnectionWithUserDisconnects(HttpTransportType transportType, string protocolName) - { - // Regression test: - // When multiple connections from the same user were connected and one left, it used to unsubscribe from the user channel - // Now we keep track of users connections and only unsubscribe when no users are listening - using (StartVerifiableLog()) - { - var protocol = HubProtocolHelpers.GetHubProtocol(protocolName); - - var firstConnection = CreateConnection(_serverFixture.FirstServer.Url + "/echo", transportType, protocol, LoggerFactory, userName: "userA"); - var secondConnection = CreateConnection(_serverFixture.SecondServer.Url + "/echo", transportType, protocol, LoggerFactory, userName: "userA"); - - var tcs = new TaskCompletionSource(); - firstConnection.On("Echo", message => tcs.TrySetResult(message)); - - await secondConnection.StartAsync().OrTimeout(); - await firstConnection.StartAsync().OrTimeout(); - await secondConnection.DisposeAsync().OrTimeout(); - await firstConnection.InvokeAsync("EchoUser", "userA", "Hello, World!").OrTimeout(); - - Assert.Equal("Hello, World!", await tcs.Task.OrTimeout()); - - await firstConnection.DisposeAsync().OrTimeout(); - } - } - - private static HubConnection CreateConnection(string url, HttpTransportType transportType, IHubProtocol protocol, ILoggerFactory loggerFactory, string userName = null) - { - var hubConnectionBuilder = new HubConnectionBuilder() - .WithLoggerFactory(loggerFactory) - .WithUrl(url, transportType, httpConnectionOptions => - { - if (!string.IsNullOrEmpty(userName)) - { - httpConnectionOptions.Headers["UserName"] = userName; - } - }); - - hubConnectionBuilder.Services.AddSingleton(protocol); - - return hubConnectionBuilder.Build(); - } - - private static IEnumerable TransportTypes() - { - if (TestHelpers.IsWebSocketsSupported()) - { - yield return HttpTransportType.WebSockets; - } - yield return HttpTransportType.ServerSentEvents; - yield return HttpTransportType.LongPolling; - } - - public static IEnumerable TransportTypesAndProtocolTypes - { - get - { - foreach (var transport in TransportTypes()) - { - yield return new object[] { transport, "json" }; - - if (transport != HttpTransportType.ServerSentEvents) - { - yield return new object[] { transport, "messagepack" }; - } - } - } - } - } -} diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisHubLifetimeManagerTests.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisHubLifetimeManagerTests.cs deleted file mode 100644 index 39dc6acb66..0000000000 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisHubLifetimeManagerTests.cs +++ /dev/null @@ -1,102 +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.Threading.Tasks; -using Microsoft.AspNetCore.SignalR.Internal; -using Microsoft.AspNetCore.SignalR.Protocol; -using Microsoft.AspNetCore.SignalR.Tests; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Microsoft.AspNetCore.SignalR.Specification.Tests; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Serialization; -using Xunit; - -namespace Microsoft.AspNetCore.SignalR.Redis.Tests -{ - public class RedisHubLifetimeManagerTests : ScaleoutHubLifetimeManagerTests - { - private TestRedisServer _server; - - public override HubLifetimeManager CreateNewHubLifetimeManager(TestRedisServer backplane) - { - return CreateLifetimeManager(backplane); - } - - public override TestRedisServer CreateBackplane() - { - return new TestRedisServer(); - } - - public override HubLifetimeManager CreateNewHubLifetimeManager() - { - _server = new TestRedisServer(); - - return CreateLifetimeManager(_server); - } - - public class TestObject - { - public string TestProperty { get; set; } - } - - private RedisHubLifetimeManager CreateLifetimeManager(TestRedisServer server, MessagePackHubProtocolOptions messagePackOptions = null, JsonHubProtocolOptions jsonOptions = null) - { - var options = new RedisOptions() { ConnectionFactory = async (t) => await Task.FromResult(new TestConnectionMultiplexer(server)) }; - messagePackOptions = messagePackOptions ?? new MessagePackHubProtocolOptions(); - jsonOptions = jsonOptions ?? new JsonHubProtocolOptions(); - return new RedisHubLifetimeManager( - NullLogger>.Instance, - Options.Create(options), - new DefaultHubProtocolResolver(new IHubProtocol[] - { - new JsonHubProtocol(Options.Create(jsonOptions)), - new MessagePackHubProtocol(Options.Create(messagePackOptions)), - }, NullLogger.Instance)); - } - - [Fact] - public async Task CamelCasedJsonIsPreservedAcrossRedisBoundary() - { - var server = new TestRedisServer(); - - var messagePackOptions = new MessagePackHubProtocolOptions(); - - var jsonOptions = new JsonHubProtocolOptions(); - jsonOptions.PayloadSerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); - - using (var client1 = new TestClient()) - using (var client2 = new TestClient()) - { - // The sending manager has serializer settings - var manager1 = CreateLifetimeManager(server, messagePackOptions, jsonOptions); - - // The receiving one doesn't matter because of how we serialize! - var manager2 = CreateLifetimeManager(server); - - var connection1 = HubConnectionContextUtils.Create(client1.Connection); - var connection2 = HubConnectionContextUtils.Create(client2.Connection); - - await manager1.OnConnectedAsync(connection1).OrTimeout(); - await manager2.OnConnectedAsync(connection2).OrTimeout(); - - await manager1.SendAllAsync("Hello", new object[] { new TestObject { TestProperty = "Foo" } }); - - var message = Assert.IsType(await client2.ReadAsync().OrTimeout()); - Assert.Equal("Hello", message.Target); - Assert.Collection( - message.Arguments, - arg0 => - { - var dict = Assert.IsType(arg0); - Assert.Collection(dict.Properties(), - prop => - { - Assert.Equal("testProperty", prop.Name); - Assert.Equal("Foo", prop.Value.Value()); - }); - }); - } - } - } -} diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisProtocolTests.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisProtocolTests.cs deleted file mode 100644 index 89b960df71..0000000000 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisProtocolTests.cs +++ /dev/null @@ -1,202 +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.Buffers; -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.SignalR.Internal; -using Microsoft.AspNetCore.SignalR.Protocol; -using Microsoft.AspNetCore.SignalR.Redis.Internal; -using Microsoft.AspNetCore.SignalR.Tests; -using Xunit; - -namespace Microsoft.AspNetCore.SignalR.Redis.Tests -{ - public class RedisProtocolTests - { - private static Dictionary> _ackTestData = new[] - { - CreateTestData("Zero", 0, 0x91, 0x00), - CreateTestData("Fixnum", 42, 0x91, 0x2A), - CreateTestData("Uint8", 180, 0x91, 0xCC, 0xB4), - CreateTestData("Uint16", 384, 0x91, 0xCD, 0x01, 0x80), - CreateTestData("Uint32", 70_000, 0x91, 0xCE, 0x00, 0x01, 0x11, 0x70), - }.ToDictionary(t => t.Name); - - public static IEnumerable AckTestData = _ackTestData.Keys.Select(k => new object[] { k }); - - [Theory] - [MemberData(nameof(AckTestData))] - public void ParseAck(string testName) - { - var testData = _ackTestData[testName]; - var protocol = new RedisProtocol(Array.Empty()); - - var decoded = protocol.ReadAck(testData.Encoded); - - Assert.Equal(testData.Decoded, decoded); - } - - [Theory] - [MemberData(nameof(AckTestData))] - public void WriteAck(string testName) - { - var testData = _ackTestData[testName]; - var protocol = new RedisProtocol(Array.Empty()); - - var encoded = protocol.WriteAck(testData.Decoded); - - Assert.Equal(testData.Encoded, encoded); - } - - private static Dictionary> _groupCommandTestData = new[] - { - CreateTestData("GroupAdd", new RedisGroupCommand(42, "S", GroupAction.Add, "G", "C" ), 0x95, 0x2A, 0xA1, (byte)'S', 0x01, 0xA1, (byte)'G', 0xA1, (byte)'C'), - CreateTestData("GroupRemove", new RedisGroupCommand(42, "S", GroupAction.Remove, "G", "C" ), 0x95, 0x2A, 0xA1, (byte)'S', 0x02, 0xA1, (byte)'G', 0xA1, (byte)'C'), - }.ToDictionary(t => t.Name); - - public static IEnumerable GroupCommandTestData = _groupCommandTestData.Keys.Select(k => new object[] { k }); - - [Theory] - [MemberData(nameof(GroupCommandTestData))] - public void ParseGroupCommand(string testName) - { - var testData = _groupCommandTestData[testName]; - var protocol = new RedisProtocol(Array.Empty()); - - var decoded = protocol.ReadGroupCommand(testData.Encoded); - - Assert.Equal(testData.Decoded.Id, decoded.Id); - Assert.Equal(testData.Decoded.ServerName, decoded.ServerName); - Assert.Equal(testData.Decoded.Action, decoded.Action); - Assert.Equal(testData.Decoded.GroupName, decoded.GroupName); - Assert.Equal(testData.Decoded.ConnectionId, decoded.ConnectionId); - } - - [Theory] - [MemberData(nameof(GroupCommandTestData))] - public void WriteGroupCommand(string testName) - { - var testData = _groupCommandTestData[testName]; - var protocol = new RedisProtocol(Array.Empty()); - - var encoded = protocol.WriteGroupCommand(testData.Decoded); - - Assert.Equal(testData.Encoded, encoded); - } - - // The actual invocation message doesn't matter - private static InvocationMessage _testMessage = new InvocationMessage("target", Array.Empty()); - - // We use a func so we are guaranteed to get a new SerializedHubMessage for each test - private static Dictionary>> _invocationTestData = new[] - { - CreateTestData>( - "NoExcludedIds", - () => new RedisInvocation(new SerializedHubMessage(_testMessage), null), - 0x92, - 0x90, - 0x82, - 0xA2, (byte)'p', (byte)'1', - 0xC4, 0x01, 0x2A, - 0xA2, (byte)'p', (byte)'2', - 0xC4, 0x01, 0x2A), - CreateTestData>( - "OneExcludedId", - () => new RedisInvocation(new SerializedHubMessage(_testMessage), new [] { "a" }), - 0x92, - 0x91, - 0xA1, (byte)'a', - 0x82, - 0xA2, (byte)'p', (byte)'1', - 0xC4, 0x01, 0x2A, - 0xA2, (byte)'p', (byte)'2', - 0xC4, 0x01, 0x2A), - CreateTestData>( - "ManyExcludedIds", - () => new RedisInvocation(new SerializedHubMessage(_testMessage), new [] { "a", "b", "c", "d", "e", "f" }), - 0x92, - 0x96, - 0xA1, (byte)'a', - 0xA1, (byte)'b', - 0xA1, (byte)'c', - 0xA1, (byte)'d', - 0xA1, (byte)'e', - 0xA1, (byte)'f', - 0x82, - 0xA2, (byte)'p', (byte)'1', - 0xC4, 0x01, 0x2A, - 0xA2, (byte)'p', (byte)'2', - 0xC4, 0x01, 0x2A), - }.ToDictionary(t => t.Name); - - public static IEnumerable InvocationTestData = _invocationTestData.Keys.Select(k => new object[] { k }); - - [Theory] - [MemberData(nameof(InvocationTestData))] - public void ParseInvocation(string testName) - { - var testData = _invocationTestData[testName]; - var hubProtocols = new[] { new DummyHubProtocol("p1"), new DummyHubProtocol("p2") }; - var protocol = new RedisProtocol(hubProtocols); - - var expected = testData.Decoded(); - - var decoded = protocol.ReadInvocation(testData.Encoded); - - Assert.Equal(expected.ExcludedConnectionIds, decoded.ExcludedConnectionIds); - - // Verify the deserialized object has the necessary serialized forms - foreach (var hubProtocol in hubProtocols) - { - Assert.Equal( - expected.Message.GetSerializedMessage(hubProtocol).ToArray(), - decoded.Message.GetSerializedMessage(hubProtocol).ToArray()); - - var writtenMessages = hubProtocol.GetWrittenMessages(); - Assert.Collection(writtenMessages, - actualMessage => - { - var invocation = Assert.IsType(actualMessage); - Assert.Same(_testMessage.Target, invocation.Target); - Assert.Same(_testMessage.Arguments, invocation.Arguments); - }); - } - } - - [Theory] - [MemberData(nameof(InvocationTestData))] - public void WriteInvocation(string testName) - { - var testData = _invocationTestData[testName]; - var protocol = new RedisProtocol(new[] { new DummyHubProtocol("p1"), new DummyHubProtocol("p2") }); - - // Actual invocation doesn't matter because we're using a dummy hub protocol. - // But the dummy protocol will check that we gave it the test message to make sure everything flows through properly. - var expected = testData.Decoded(); - var encoded = protocol.WriteInvocation(_testMessage.Target, _testMessage.Arguments, expected.ExcludedConnectionIds); - - Assert.Equal(testData.Encoded, encoded); - } - - // Create ProtocolTestData using the Power of Type Inference(TM). - private static ProtocolTestData CreateTestData(string name, T decoded, params byte[] encoded) - => new ProtocolTestData(name, decoded, encoded); - - public class ProtocolTestData - { - public string Name { get; } - public T Decoded { get; } - public byte[] Encoded { get; } - - public ProtocolTestData(string name, T decoded, byte[] encoded) - { - Name = name; - Decoded = decoded; - Encoded = encoded; - } - } - } -} diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisServerFixture.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisServerFixture.cs deleted file mode 100644 index 37f510a6af..0000000000 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/RedisServerFixture.cs +++ /dev/null @@ -1,64 +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 Microsoft.AspNetCore.SignalR.Tests; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Testing; - -namespace Microsoft.AspNetCore.SignalR.Redis.Tests -{ - public class RedisServerFixture : IDisposable - where TStartup : class - { - public InProcessTestServer FirstServer { get; private set; } - public InProcessTestServer SecondServer { get; private set; } - - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IDisposable _logToken; - - public RedisServerFixture() - { - // Docker is not available on the machine, tests using this fixture - // should be using SkipIfDockerNotPresentAttribute and will be skipped. - if (Docker.Default == null) - { - return; - } - - var testLog = AssemblyTestLog.ForAssembly(typeof(RedisServerFixture).Assembly); - _logToken = testLog.StartTestLog(null, $"{nameof(RedisServerFixture)}_{typeof(TStartup).Name}", out _loggerFactory, LogLevel.Trace, "RedisServerFixture"); - _logger = _loggerFactory.CreateLogger>(); - - Docker.Default.Start(_logger); - - FirstServer = StartServer(); - SecondServer = StartServer(); - } - - private InProcessTestServer StartServer() - { - try - { - return new InProcessTestServer(_loggerFactory); - } - catch (Exception ex) - { - _logger.LogError(ex, "Server failed to start."); - throw; - } - } - - public void Dispose() - { - if (Docker.Default != null) - { - FirstServer.Dispose(); - SecondServer.Dispose(); - Docker.Default.Stop(_logger); - _logToken.Dispose(); - } - } - } -} diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/SkipIfDockerNotPresentAttribute.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/SkipIfDockerNotPresentAttribute.cs deleted file mode 100644 index 6bdba837bd..0000000000 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/SkipIfDockerNotPresentAttribute.cs +++ /dev/null @@ -1,39 +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 Microsoft.AspNetCore.Testing.xunit; - -namespace Microsoft.AspNetCore.SignalR.Redis.Tests -{ - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class SkipIfDockerNotPresentAttribute : Attribute, ITestCondition - { - public bool IsMet => CheckDocker(); - public string SkipReason { get; private set; } = "Docker is not available"; - - private bool CheckDocker() - { - if(Docker.Default != null) - { - // Docker is present, but is it working? - if (Docker.Default.RunCommand("ps", "docker ps", out var output) != 0) - { - SkipReason = $"Failed to invoke test command 'docker ps'. Output: {output}"; - } - else - { - // We have a docker - return true; - } - } - else - { - SkipReason = "Docker is not installed on the host machine."; - } - - // If we get here, we don't have a docker - return false; - } - } -} diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/Startup.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/Startup.cs deleted file mode 100644 index 3259ab7f40..0000000000 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/Startup.cs +++ /dev/null @@ -1,49 +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 Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.SignalR.Redis.Tests -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - services.AddSignalR(options => - { - options.EnableDetailedErrors = true; - }) - .AddMessagePackProtocol() - .AddRedis(options => - { - options.Configuration.EndPoints.Add(Environment.GetEnvironmentVariable("REDIS_CONNECTION-PREV")); - }); - - services.AddSingleton(); - } - - public void Configure(IApplicationBuilder app, IHostingEnvironment env) - { - app.UseSignalR(options => options.MapHub("/echo")); - } - - private class UserNameIdProvider : IUserIdProvider - { - public string GetUserId(HubConnectionContext connection) - { - // This is an AWFUL way to authenticate users! We're just using it for test purposes. - var userNameHeader = connection.GetHttpContext().Request.Headers["UserName"]; - if (!StringValues.IsNullOrEmpty(userNameHeader)) - { - return userNameHeader; - } - - return null; - } - } - } -} diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/TestConnectionMultiplexer.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/TestConnectionMultiplexer.cs deleted file mode 100644 index 2852f9c64c..0000000000 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Redis.Tests/TestConnectionMultiplexer.cs +++ /dev/null @@ -1,350 +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.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Threading.Tasks; -using StackExchange.Redis; - -namespace Microsoft.AspNetCore.SignalR.Tests -{ - public class TestConnectionMultiplexer : IConnectionMultiplexer - { - public string ClientName => throw new NotImplementedException(); - - public string Configuration => throw new NotImplementedException(); - - public int TimeoutMilliseconds => throw new NotImplementedException(); - - public long OperationCount => throw new NotImplementedException(); - - public bool PreserveAsyncOrder { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - - public bool IsConnected => true; - - public bool IncludeDetailInExceptions { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public int StormLogThreshold { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - - public event EventHandler ErrorMessage - { - add { } - remove { } - } - - public event EventHandler ConnectionFailed - { - add { } - remove { } - } - - public event EventHandler InternalError - { - add { } - remove { } - } - - public event EventHandler ConnectionRestored - { - add { } - remove { } - } - - public event EventHandler ConfigurationChanged - { - add { } - remove { } - } - - public event EventHandler ConfigurationChangedBroadcast - { - add { } - remove { } - } - - public event EventHandler HashSlotMoved - { - add { } - remove { } - } - - private readonly ISubscriber _subscriber; - - public TestConnectionMultiplexer(TestRedisServer server) - { - _subscriber = new TestSubscriber(server); - } - - public void BeginProfiling(object forContext) - { - throw new NotImplementedException(); - } - - public void Close(bool allowCommandsToComplete = true) - { - throw new NotImplementedException(); - } - - public Task CloseAsync(bool allowCommandsToComplete = true) - { - throw new NotImplementedException(); - } - - public bool Configure(TextWriter log = null) - { - throw new NotImplementedException(); - } - - public Task ConfigureAsync(TextWriter log = null) - { - throw new NotImplementedException(); - } - - public void Dispose() - { - throw new NotImplementedException(); - } - - public ProfiledCommandEnumerable FinishProfiling(object forContext, bool allowCleanupSweep = true) - { - throw new NotImplementedException(); - } - - public ServerCounters GetCounters() - { - throw new NotImplementedException(); - } - - public IDatabase GetDatabase(int db = -1, object asyncState = null) - { - throw new NotImplementedException(); - } - - public EndPoint[] GetEndPoints(bool configuredOnly = false) - { - throw new NotImplementedException(); - } - - public IServer GetServer(string host, int port, object asyncState = null) - { - throw new NotImplementedException(); - } - - public IServer GetServer(string hostAndPort, object asyncState = null) - { - throw new NotImplementedException(); - } - - public IServer GetServer(IPAddress host, int port) - { - throw new NotImplementedException(); - } - - public IServer GetServer(EndPoint endpoint, object asyncState = null) - { - throw new NotImplementedException(); - } - - public string GetStatus() - { - throw new NotImplementedException(); - } - - public void GetStatus(TextWriter log) - { - throw new NotImplementedException(); - } - - public string GetStormLog() - { - throw new NotImplementedException(); - } - - public ISubscriber GetSubscriber(object asyncState = null) - { - return _subscriber; - } - - public int HashSlot(RedisKey key) - { - throw new NotImplementedException(); - } - - public long PublishReconfigure(CommandFlags flags = CommandFlags.None) - { - throw new NotImplementedException(); - } - - public Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None) - { - throw new NotImplementedException(); - } - - public void RegisterProfiler(IProfiler profiler) - { - throw new NotImplementedException(); - } - - public void ResetStormLog() - { - throw new NotImplementedException(); - } - - public void Wait(Task task) - { - throw new NotImplementedException(); - } - - public T Wait(Task task) - { - throw new NotImplementedException(); - } - - public void WaitAll(params Task[] tasks) - { - throw new NotImplementedException(); - } - } - - public class TestRedisServer - { - private readonly ConcurrentDictionary>> _subscriptions = - new ConcurrentDictionary>>(); - - public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) - { - if (_subscriptions.TryGetValue(channel, out var handlers)) - { - foreach (var handler in handlers) - { - handler(channel, message); - } - } - - return handlers != null ? handlers.Count : 0; - } - - public void Subscribe(RedisChannel channel, Action handler, CommandFlags flags = CommandFlags.None) - { - _subscriptions.AddOrUpdate(channel, _ => new List> { handler }, (_, list) => - { - list.Add(handler); - return list; - }); - } - - public void Unsubscribe(RedisChannel channel, Action handler = null, CommandFlags flags = CommandFlags.None) - { - if (_subscriptions.TryGetValue(channel, out var list)) - { - list.Remove(handler); - } - } - } - - public class TestSubscriber : ISubscriber - { - private readonly TestRedisServer _server; - public ConnectionMultiplexer Multiplexer => throw new NotImplementedException(); - - public TestSubscriber(TestRedisServer server) - { - _server = server; - } - - public EndPoint IdentifyEndpoint(RedisChannel channel, CommandFlags flags = CommandFlags.None) - { - throw new NotImplementedException(); - } - - public Task IdentifyEndpointAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None) - { - throw new NotImplementedException(); - } - - public bool IsConnected(RedisChannel channel = default) - { - throw new NotImplementedException(); - } - - public TimeSpan Ping(CommandFlags flags = CommandFlags.None) - { - throw new NotImplementedException(); - } - - public Task PingAsync(CommandFlags flags = CommandFlags.None) - { - throw new NotImplementedException(); - } - - public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) - { - return _server.Publish(channel, message, flags); - } - - public async Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) - { - await Task.Yield(); - return Publish(channel, message, flags); - } - - public void Subscribe(RedisChannel channel, Action handler, CommandFlags flags = CommandFlags.None) - { - _server.Subscribe(channel, handler, flags); - } - - public Task SubscribeAsync(RedisChannel channel, Action handler, CommandFlags flags = CommandFlags.None) - { - Subscribe(channel, handler, flags); - return Task.CompletedTask; - } - - public EndPoint SubscribedEndpoint(RedisChannel channel) - { - throw new NotImplementedException(); - } - - public bool TryWait(Task task) - { - throw new NotImplementedException(); - } - - public void Unsubscribe(RedisChannel channel, Action handler = null, CommandFlags flags = CommandFlags.None) - { - _server.Unsubscribe(channel, handler, flags); - } - - public void UnsubscribeAll(CommandFlags flags = CommandFlags.None) - { - throw new NotImplementedException(); - } - - public Task UnsubscribeAllAsync(CommandFlags flags = CommandFlags.None) - { - throw new NotImplementedException(); - } - - public Task UnsubscribeAsync(RedisChannel channel, Action handler = null, CommandFlags flags = CommandFlags.None) - { - Unsubscribe(channel, handler, flags); - return Task.CompletedTask; - } - - public void Wait(Task task) - { - throw new NotImplementedException(); - } - - public T Wait(Task task) - { - throw new NotImplementedException(); - } - - public void WaitAll(params Task[] tasks) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests/Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests.csproj b/src/SignalR/test/Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests/Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests.csproj index ef017f8ba8..ddb63a904f 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests/Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests.csproj +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests/Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests.csproj @@ -1,7 +1,7 @@  - $(StandardTestTfms) + netcoreapp3.0 diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.Utils/DummyHubProtocol.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.Utils/DummyHubProtocol.cs index 74ca4eaf04..8ce5aa6029 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.Utils/DummyHubProtocol.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.Utils/DummyHubProtocol.cs @@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests public string Name { get; } public int Version => 1; + public int MinorVersion => 0; public TransferFormat TransferFormat => TransferFormat.Text; public DummyHubProtocol(string name, Action onWrite = null) diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.Utils/Microsoft.AspNetCore.SignalR.Tests.Utils.csproj b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.Utils/Microsoft.AspNetCore.SignalR.Tests.Utils.csproj index 976041ab7c..211e6824e7 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.Utils/Microsoft.AspNetCore.SignalR.Tests.Utils.csproj +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.Utils/Microsoft.AspNetCore.SignalR.Tests.Utils.csproj @@ -1,7 +1,7 @@  - $(StandardTestTfms) + netcoreapp3.0 Microsoft.AspNetCore.SignalR.Tests $(DefineConstants);TESTUTILS diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs index 371307737f..947a323b55 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestClient.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests private List<(Action handler, object state)> _heartbeatHandlers; private static int _id; - private readonly IHubProtocol _protocol; + private IHubProtocol _protocol; private readonly IInvocationBinder _invocationBinder; private readonly CancellationTokenSource _cts; @@ -178,6 +178,12 @@ namespace Microsoft.AspNetCore.SignalR.Tests return SendHubMessageAsync(new StreamInvocationMessage(invocationId, methodName, args)); } + public Task BeginUploadStreamAsync(string invocationId, string methodName, params object[] args) + { + var message = new InvocationMessage(invocationId, methodName, args); + return SendHubMessageAsync(message); + } + public async Task SendHubMessageAsync(HubMessage message) { var payload = _protocol.GetMessageBytes(message); @@ -295,6 +301,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } } + private class DefaultInvocationBinder : IInvocationBinder { public IReadOnlyList GetParameterTypes(string methodName) @@ -307,6 +314,11 @@ namespace Microsoft.AspNetCore.SignalR.Tests { return typeof(object); } + + public Type GetStreamItemType(string streamId) + { + throw new NotImplementedException(); + } } } } diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestHelpers.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestHelpers.cs index be8f1e90c5..10473ad9ab 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestHelpers.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.Utils/TestHelpers.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { public static bool IsWebSocketsSupported() { -#if NETCOREAPP2_2 +#if NETCOREAPP3_0 // .NET Core 2.1 and greater has sockets return true; #else diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Hubs.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Hubs.cs index 37fc164c14..5e08a5f501 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Hubs.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Hubs.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Text; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; @@ -179,6 +180,78 @@ namespace Microsoft.AspNetCore.SignalR.Tests public SelfRef Self; } + + public async Task StreamingConcat(ChannelReader source) + { + var sb = new StringBuilder(); + + while (await source.WaitToReadAsync()) + { + while (source.TryRead(out var item)) + { + sb.Append(item); + } + } + + return sb.ToString(); + } + + public async Task StreamingSum(ChannelReader source) + { + var total = 0; + while (await source.WaitToReadAsync()) + { + while (source.TryRead(out var item)) + { + total += item; + } + } + return total; + } + + public async Task> UploadArray(ChannelReader source) + { + var results = new List(); + + while (await source.WaitToReadAsync()) + { + while (source.TryRead(out var item)) + { + results.Add(item); + } + } + + return results; + } + + public async Task TestTypeCastingErrors(ChannelReader source) + { + try + { + await source.WaitToReadAsync(); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + return "error identified and caught"; + } + + return "wrong type accepted, this is bad"; + } + + public async Task TestCustomErrorPassing(ChannelReader source) + { + try + { + await source.WaitToReadAsync(); + } + catch (Exception ex) + { + return ex.Message == HubConnectionHandlerTests.CustomErrorMessage; + } + + return false; + } } public abstract class TestHub : Hub diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Utils.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Utils.cs index daa97acbcd..fed151dd6d 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Utils.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTestUtils/Utils.cs @@ -71,9 +71,9 @@ namespace Microsoft.AspNetCore.SignalR.Tests return services.BuildServiceProvider(); } - public static Connections.ConnectionHandler GetHubConnectionHandler(Type hubType, ILoggerFactory loggerFactory = null) + public static Connections.ConnectionHandler GetHubConnectionHandler(Type hubType, Action addServices = null, ILoggerFactory loggerFactory = null) { - var serviceProvider = CreateServiceProvider(null, loggerFactory); + var serviceProvider = CreateServiceProvider(addServices, loggerFactory); return (Connections.ConnectionHandler)serviceProvider.GetService(GetConnectionHandlerType(hubType)); } } diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.cs b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.cs index efd3df8fbc..1147b2e4e4 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.cs +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Security.Claims; using System.Text; @@ -227,7 +228,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests var hubProtocolMock = new Mock(); hubProtocolMock.Setup(m => m.Name).Returns("CustomProtocol"); - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), loggerFactory: LoggerFactory); using (var client = new TestClient(protocol: hubProtocolMock.Object)) { @@ -257,7 +258,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests var hubProtocolMock = new Mock(); hubProtocolMock.Setup(m => m.Name).Returns("CustomProtocol"); - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), loggerFactory: LoggerFactory); using (var client = new TestClient(protocol: new MessagePackHubProtocol())) { @@ -280,7 +281,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), loggerFactory: LoggerFactory); var part1 = Encoding.UTF8.GetBytes("{\"protocol\": \"json\""); var part2 = Encoding.UTF8.GetBytes(",\"version\": 1}"); var part3 = Encoding.UTF8.GetBytes("\u001e"); @@ -323,7 +324,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), loggerFactory: LoggerFactory); var part1 = Encoding.UTF8.GetBytes("{\"type\":1, \"invocationId\":\"1\", "); var part2 = Encoding.UTF8.GetBytes("\"target\": \"Echo\", \"arguments\""); var part3 = Encoding.UTF8.GetBytes(":[\"hello\"]}\u001e"); @@ -366,7 +367,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), loggerFactory: LoggerFactory); var payload = Encoding.UTF8.GetBytes("{\"protocol\": \"json\",\"version\": 1}\u001e{\"type\":1, \"invocationId\":\"1\", \"target\": \"Echo\", \"arguments\":[\"hello\"]}\u001e"); using (var client = new TestClient()) @@ -404,7 +405,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), loggerFactory: LoggerFactory); using (var client = new TestClient()) { @@ -429,7 +430,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests hubProtocolMock.Setup(m => m.Name).Returns("json"); hubProtocolMock.Setup(m => m.Version).Returns(9001); - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), loggerFactory: LoggerFactory); using (var client = new TestClient(protocol: hubProtocolMock.Object)) { @@ -456,7 +457,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests using (StartVerifiableLog(ExpectedErrors)) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), loggerFactory: LoggerFactory); using (var client = new TestClient()) { @@ -618,7 +619,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, loggerFactory: LoggerFactory); using (var client = new TestClient()) { @@ -1095,7 +1096,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, loggerFactory: LoggerFactory); using (var firstClient = new TestClient()) using (var secondClient = new TestClient()) @@ -1171,7 +1172,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, loggerFactory: LoggerFactory); using (var firstClient = new TestClient()) using (var secondClient = new TestClient()) @@ -1212,7 +1213,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, loggerFactory: LoggerFactory); using (var firstClient = new TestClient()) using (var secondClient = new TestClient()) @@ -1255,7 +1256,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests using (StartVerifiableLog(ExpectedErrors)) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(SimpleVoidReturningTypedHub), LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(SimpleVoidReturningTypedHub), loggerFactory: LoggerFactory); using (var firstClient = new TestClient()) { @@ -1275,7 +1276,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, loggerFactory: LoggerFactory); using (var firstClient = new TestClient()) using (var secondClient = new TestClient()) @@ -1321,7 +1322,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, loggerFactory: LoggerFactory); using (var firstClient = new TestClient()) using (var secondClient = new TestClient()) @@ -1369,7 +1370,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, loggerFactory: LoggerFactory); using (var firstClient = new TestClient(userIdentifier: "userA")) using (var secondClient = new TestClient(userIdentifier: "userB")) @@ -1414,7 +1415,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, loggerFactory: LoggerFactory); using (var firstClient = new TestClient()) using (var secondClient = new TestClient()) @@ -1458,7 +1459,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, loggerFactory: LoggerFactory); using (var firstClient = new TestClient()) using (var secondClient = new TestClient()) @@ -1511,7 +1512,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, loggerFactory: LoggerFactory); using (var firstClient = new TestClient()) using (var secondClient = new TestClient()) @@ -1562,7 +1563,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, loggerFactory: LoggerFactory); using (var firstClient = new TestClient()) using (var secondClient = new TestClient()) @@ -1628,7 +1629,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, loggerFactory: LoggerFactory); using (var firstClient = new TestClient(userIdentifier: "userA")) using (var secondClient = new TestClient(userIdentifier: "userB")) @@ -1662,7 +1663,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(hubType, loggerFactory: LoggerFactory); using (var firstClient = new TestClient()) using (var secondClient = new TestClient()) @@ -1695,7 +1696,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests { using (StartVerifiableLog()) { - var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), LoggerFactory); + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), loggerFactory: LoggerFactory); using (var firstClient = new TestClient()) using (var secondClient = new TestClient()) @@ -1731,10 +1732,10 @@ namespace Microsoft.AspNetCore.SignalR.Tests { var protocol = HubProtocolHelpers.GetHubProtocol(protocolName); - var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(null, LoggerFactory); - var connectionHandler = serviceProvider.GetService>(); - var invocationBinder = new Mock(); - invocationBinder.Setup(b => b.GetReturnType(It.IsAny())).Returns(typeof(string)); + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(null, 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: protocol, invocationBinder: invocationBinder.Object)) { @@ -2688,6 +2689,93 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } + [Fact] + public async Task UploadStringsToConcat() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); + await client.BeginUploadStreamAsync("invocation", nameof(MethodHub.StreamingConcat), new StreamPlaceholder("id")); + + foreach (var letter in new[] { "B", "E", "A", "N", "E", "D" }) + { + await client.SendHubMessageAsync(new StreamDataMessage("id", letter)).OrTimeout(); + } + + await client.SendHubMessageAsync(new StreamCompleteMessage("id")).OrTimeout(); + var result = (CompletionMessage)await client.ReadAsync().OrTimeout(); + + Assert.Equal("BEANED", result.Result); + } + } + + [Fact] + public async Task UploadStreamedObjects() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); + await client.BeginUploadStreamAsync("invocation", nameof(MethodHub.UploadArray), new StreamPlaceholder("id")); + + var objects = new[] { new SampleObject("solo", 322), new SampleObject("ggez", 3145) }; + foreach (var thing in objects) + { + await client.SendHubMessageAsync(new StreamDataMessage("id", thing)).OrTimeout(); + } + + await client.SendHubMessageAsync(new StreamCompleteMessage("id")).OrTimeout(); + var response = (CompletionMessage)await client.ReadAsync().OrTimeout(); + var result = ((JArray)response.Result).ToArray(); + + Assert.Equal(objects[0].Foo, ((JContainer)result[0])["foo"]); + Assert.Equal(objects[0].Bar, ((JContainer)result[0])["bar"]); + Assert.Equal(objects[1].Foo, ((JContainer)result[1])["foo"]); + Assert.Equal(objects[1].Bar, ((JContainer)result[1])["bar"]); + } + } + + [Fact] + public async Task UploadManyStreams() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); + var ids = new[] { "0", "1", "2" }; + + foreach (string id in ids) + { + await client.BeginUploadStreamAsync("invocation_"+id, nameof(MethodHub.StreamingConcat), new StreamPlaceholder(id)); + } + + var words = new[] { "zygapophyses", "qwerty", "abcd" }; + var pos = new[] { 0, 0, 0 }; + var order = new[] { 2, 2, 0, 2, 1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1 }; + + foreach (var spot in order) + { + await client.SendHubMessageAsync(new StreamDataMessage(spot.ToString(), words[spot][pos[spot]])).OrTimeout(); + pos[spot] += 1; + } + + foreach (string id in new[] { "0", "2", "1" }) + { + await client.SendHubMessageAsync(new StreamCompleteMessage(id)).OrTimeout(); + var response = await client.ReadAsync().OrTimeout(); + Debug.Write(response); + Assert.Equal(words[int.Parse(id)], ((CompletionMessage)response).Result); + } + } + } + [Fact] public async Task ConnectionAbortedIfSendFailsWithProtocolError() { @@ -2718,6 +2806,137 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } + [Fact] + public async Task UploadStreamItemInvalidTypeAutoCasts() + { + // NOTE -- json.net is flexible here, and casts for us + + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); + await client.BeginUploadStreamAsync("invocation", nameof(MethodHub.StreamingConcat), new StreamPlaceholder("id")).OrTimeout(); + + // send integers that are then cast to strings + await client.SendHubMessageAsync(new StreamDataMessage("id", 5)).OrTimeout(); + await client.SendHubMessageAsync(new StreamDataMessage("id", 10)).OrTimeout(); + + await client.SendHubMessageAsync(new StreamCompleteMessage("id")).OrTimeout(); + var response = (CompletionMessage)await client.ReadAsync().OrTimeout(); + + Assert.Equal("510", response.Result); + } + } + + [Fact] + public async Task ServerReportsProtocolMinorVersion() + { + var testProtocol = new Mock(); + testProtocol.Setup(m => m.Name).Returns("CustomProtocol"); + testProtocol.Setup(m => m.MinorVersion).Returns(112); + testProtocol.Setup(m => m.IsVersionSupported(It.IsAny())).Returns(true); + testProtocol.Setup(m => m.TransferFormat).Returns(TransferFormat.Binary); + + var connectionHandler = HubConnectionHandlerTestUtils.GetHubConnectionHandler(typeof(HubT), + (services) => services.AddSingleton(testProtocol.Object)); + + using (var client = new TestClient(protocol: testProtocol.Object)) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); + + Assert.NotNull(client.HandshakeResponseMessage); + Assert.Equal(112, client.HandshakeResponseMessage.MinorVersion); + + client.Dispose(); + await connectionHandlerTask.OrTimeout(); + } + } + + [Fact] + public async Task UploadStreamItemInvalidType() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); + await client.BeginUploadStreamAsync("invocationId", nameof(MethodHub.TestTypeCastingErrors), new StreamPlaceholder("channelId")).OrTimeout(); + + // client is running wild, sending strings not ints. + // this error should be propogated to the user's HubMethod code + await client.SendHubMessageAsync(new StreamItemMessage("channelId", "not a number")).OrTimeout(); + var response = await client.ReadAsync().OrTimeout(); + + Assert.Equal(typeof(CompletionMessage), response.GetType()); + Assert.Equal("error identified and caught", (string)((CompletionMessage)response).Result); + } + } + + [Fact] + public async Task UploadStreamItemInvalidId() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => + { + services.AddSignalR(options => options.EnableDetailedErrors = true); + }); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); + await client.SendHubMessageAsync(new StreamItemMessage("fake_id", "not a number")).OrTimeout(); + + // Client is breaking protocol by sending an invalid id, and should be closed. + var message = client.TryRead(); + Assert.IsType(message); + Assert.Equal("Connection closed with an error. KeyNotFoundException: No stream with id 'fake_id' could be found.", ((CloseMessage)message).Error); + } + } + + [Fact] + public async Task UploadStreamCompleteInvalidId() + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => + { + services.AddSignalR(options => options.EnableDetailedErrors = true); + }); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); + await client.SendHubMessageAsync(new StreamCompleteMessage("fake_id")).OrTimeout(); + + // Client is breaking protocol by sending an invalid id, and should be closed. + var message = client.TryRead(); + Assert.IsType(message); + Assert.Equal("Connection closed with an error. KeyNotFoundException: No stream with id 'fake_id' could be found.", ((CloseMessage)message).Error); + } + } + + public static string CustomErrorMessage = "custom error for testing ::::)"; + + [Fact] + public async Task UploadStreamCompleteWithError() + { + + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(); + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler).OrTimeout(); + await client.BeginUploadStreamAsync("invocation", nameof(MethodHub.TestCustomErrorPassing), new StreamPlaceholder("id")).OrTimeout(); + await client.SendHubMessageAsync(new StreamCompleteMessage("id", CustomErrorMessage)).OrTimeout(); + + var response = (CompletionMessage)await client.ReadAsync().OrTimeout(); + Assert.True((bool)response.Result); + } + } + [Theory] [InlineData(nameof(LongRunningHub.CancelableStream))] [InlineData(nameof(LongRunningHub.CancelableStream2), 1, 2)] @@ -2867,5 +3086,16 @@ namespace Microsoft.AspNetCore.SignalR.Tests public string GetUserId(HubConnectionContext connection) => _getUserId(connection); } + + private class SampleObject + { + public SampleObject(string foo, int bar) + { + Bar = bar; + Foo = foo; + } + public int Bar { get; } + public string Foo { get; } + } } } diff --git a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Microsoft.AspNetCore.SignalR.Tests.csproj b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Microsoft.AspNetCore.SignalR.Tests.csproj index 0796e5430a..fcb8415080 100644 --- a/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Microsoft.AspNetCore.SignalR.Tests.csproj +++ b/src/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/Microsoft.AspNetCore.SignalR.Tests.csproj @@ -1,20 +1,9 @@  - $(StandardTestTfms) - - - true - win7-x86 + netcoreapp3.0 - - - - PreserveNewest diff --git a/src/SignalR/version.props b/src/SignalR/version.props index dc9ebfc939..32c7a64b55 100644 --- a/src/SignalR/version.props +++ b/src/SignalR/version.props @@ -1,16 +1,7 @@  - 1.1.0 - 1.0.0 - rtm - $(VersionPrefix) - $(VersionPrefix)-$(VersionSuffix)-final - t000 - $(JavaVersionPrefix)-$(VersionSuffix)-$(BuildNumber) - $(JavaVersionPrefix)-$(VersionSuffix) - $(JavaVersionPrefix)-$(VersionSuffix)-final - a- - $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) - $(VersionSuffix)-$(BuildNumber) + 3.0.0 + dev + $(VersionPrefix)-$(VersionSuffix)