Merge remote-tracking branch 'SignalR/rybrande/masterToSrc' into rybrande/MondoMaster

This commit is contained in:
Ryan Brandenburg 2018-11-27 15:47:41 -08:00
commit f31ad5f9bf
142 changed files with 2717 additions and 3743 deletions

View File

@ -1,7 +1,5 @@
<Project>
<PropertyGroup>
<RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.2' ">$(MicrosoftNETCoreApp22PackageVersion)</RuntimeFrameworkVersion>
<!-- aspnet/BuildTools#662 Don't police what version of NetCoreApp we use -->
<NETCoreAppMaximumVersion>99.9</NETCoreAppMaximumVersion>
<RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp3.0' ">$(MicrosoftNETCoreAppPackageVersion)</RuntimeFrameworkVersion>
</PropertyGroup>
</Project>

View File

@ -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}

View File

@ -1,9 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<!-- SignalR is versioned 1.0 alongside the 2.1 version of AspNetCore.All, this converts the .All version to the SignalR version -->
<MessagePackPackageVersion Condition=" '$(BenchmarksTargetFramework)' != '' ">$([System.String]::Copy($(MicrosoftAspNetCoreAllPackageVersion)).Replace('2.2', '1.1'))</MessagePackPackageVersion>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
@ -14,7 +12,7 @@
<ItemGroup Condition="'$(BenchmarksTargetFramework)' == ''">
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR\Microsoft.AspNetCore.SignalR.csproj" />
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Protocols.MessagePack\Microsoft.AspNetCore.SignalR.Protocols.MessagePack.csproj" />
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Redis\Microsoft.AspNetCore.SignalR.Redis.csproj" />
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.StackExchangeRedis\Microsoft.AspNetCore.SignalR.StackExchangeRedis.csproj" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="$(MicrosoftExtensionsConfigurationCommandLinePackageVersion)" />
@ -23,8 +21,8 @@
<!-- These references are used when running on the Benchmarks Server -->
<ItemGroup Condition="'$(BenchmarksTargetFramework)' != ''">
<PackageReference Include="Microsoft.AspNetCore.All" Version="$(MicrosoftAspNetCoreAllPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="$(MessagePackPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="$(MicrosoftAspNetCoreAppPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="$(MicrosoftAspNetCoreAppPackageVersion)" />
</ItemGroup>
</Project>

View File

@ -28,7 +28,7 @@ namespace BenchmarkServer
var redisConnectionString = _config["SignalRRedis"];
if (!string.IsNullOrEmpty(redisConnectionString))
{
signalrBuilder.AddRedis(redisConnectionString);
signalrBuilder.AddStackExchangeRedis(redisConnectionString);
}
}

View File

@ -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,

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetFramework>netcoreapp3.0</TargetFramework>
<RootNamespace>Microsoft.AspNetCore.SignalR.CranksRevenge</RootNamespace>
</PropertyGroup>

View File

@ -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)

View File

@ -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<byte> _requestMessage1;
ReadOnlySequence<byte> _requestMessage2;
ReadOnlySequence<byte> _requestMessage3;
ReadOnlySequence<byte> _requestMessage4;
ReadOnlySequence<byte> _responseMessage1;
ReadOnlySequence<byte> _responseMessage2;
ReadOnlySequence<byte> _responseMessage3;
ReadOnlySequence<byte> _responseMessage4;
ReadOnlySequence<byte> _responseMessage5;
ReadOnlySequence<byte> _responseMessage6;
[GlobalSetup]
public void GlobalSetup()
{
_requestMessage1 = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes("{\"protocol\":\"dummy\",\"version\":1}\u001e"));
_requestMessage2 = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes("{\"protocol\":\"\",\"version\":10}\u001e"));
_requestMessage3 = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes("{\"protocol\":\"\",\"version\":10,\"unknown\":null}\u001e"));
_requestMessage4 = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes("42"));
_responseMessage1 = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes("{\"error\":\"dummy\"}\u001e"));
_responseMessage2 = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes("{\"error\":\"\"}\u001e"));
_responseMessage3 = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes("{}\u001e"));
_responseMessage4 = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes("{\"unknown\":null}\u001e"));
_responseMessage5 = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes("{\"error\":\"\",\"minorVersion\":34}\u001e"));
_responseMessage6 = new ReadOnlySequence<byte>(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<byte> 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);
}
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@ -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));
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -59,5 +59,10 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
}
throw new InvalidOperationException("Unexpected binder call");
}
public Type GetStreamItemType(string streamId)
{
throw new NotImplementedException();
}
}
}

View File

@ -5,74 +5,73 @@
<PropertyGroup Label="Package Versions">
<BenchmarkDotNetPackageVersion>0.10.13</BenchmarkDotNetPackageVersion>
<GoogleProtobufPackageVersion>3.1.0</GoogleProtobufPackageVersion>
<InternalAspNetCoreAnalyzersPackageVersion>2.2.0-rtm-181106-13</InternalAspNetCoreAnalyzersPackageVersion>
<InternalAspNetCoreSdkPackageVersion>2.2.0-preview2-20181011.2</InternalAspNetCoreSdkPackageVersion>
<InternalAspNetCoreAnalyzersPackageVersion>3.0.0-preview-181113-11</InternalAspNetCoreAnalyzersPackageVersion>
<InternalAspNetCoreSdkPackageVersion>3.0.0-build-20181114.5</InternalAspNetCoreSdkPackageVersion>
<MessagePackPackageVersion>1.7.3.4</MessagePackPackageVersion>
<MicrosoftAspNetCoreAllPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreAllPackageVersion>
<MicrosoftAspNetCoreAuthenticationCookiesPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreAuthenticationCookiesPackageVersion>
<MicrosoftAspNetCoreAuthenticationCorePackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreAuthenticationCorePackageVersion>
<MicrosoftAspNetCoreAuthenticationJwtBearerPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreAuthenticationJwtBearerPackageVersion>
<MicrosoftAspNetCoreAuthorizationPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreAuthorizationPackageVersion>
<MicrosoftAspNetCoreAuthorizationPolicyPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreAuthorizationPolicyPackageVersion>
<MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>2.2.0-rtm-181106-13</MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>
<MicrosoftAspNetCoreConnectionsAbstractionsPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreConnectionsAbstractionsPackageVersion>
<MicrosoftAspNetCoreCorsPackageVersion>2.2.0-preview3-35457</MicrosoftAspNetCoreCorsPackageVersion>
<MicrosoftAspNetCoreDiagnosticsEntityFrameworkCorePackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreDiagnosticsEntityFrameworkCorePackageVersion>
<MicrosoftAspNetCoreDiagnosticsPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreDiagnosticsPackageVersion>
<MicrosoftAspNetCoreHostingAbstractionsPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
<MicrosoftAspNetCoreHostingPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreHostingPackageVersion>
<MicrosoftAspNetCoreHttpAbstractionsPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreHttpAbstractionsPackageVersion>
<MicrosoftAspNetCoreHttpFeaturesPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreHttpFeaturesPackageVersion>
<MicrosoftAspNetCoreHttpPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreHttpPackageVersion>
<MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>
<MicrosoftAspNetCoreMvcPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreMvcPackageVersion>
<MicrosoftAspNetCoreRoutingPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreRoutingPackageVersion>
<MicrosoftAspNetCoreServerIISIntegrationPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
<MicrosoftAspNetCoreServerKestrelPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreServerKestrelPackageVersion>
<MicrosoftAspNetCoreStaticFilesPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreStaticFilesPackageVersion>
<MicrosoftAspNetCoreTestHostPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreTestHostPackageVersion>
<MicrosoftAspNetCoreTestingPackageVersion>2.2.0-rtm-181106-13</MicrosoftAspNetCoreTestingPackageVersion>
<MicrosoftAspNetCoreWebSocketsPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreWebSocketsPackageVersion>
<MicrosoftCSharpPackageVersion>4.5.0</MicrosoftCSharpPackageVersion>
<MicrosoftEntityFrameworkCoreDesignPackageVersion>2.2.0-rtm-35661</MicrosoftEntityFrameworkCoreDesignPackageVersion>
<MicrosoftEntityFrameworkCoreSqlServerPackageVersion>2.2.0-rtm-35661</MicrosoftEntityFrameworkCoreSqlServerPackageVersion>
<MicrosoftEntityFrameworkCoreToolsPackageVersion>2.2.0-rtm-35661</MicrosoftEntityFrameworkCoreToolsPackageVersion>
<MicrosoftExtensionsBuffersTestingSourcesPackageVersion>2.2.0-rtm-35661</MicrosoftExtensionsBuffersTestingSourcesPackageVersion>
<MicrosoftExtensionsClosedGenericMatcherSourcesPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsClosedGenericMatcherSourcesPackageVersion>
<MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>
<MicrosoftExtensionsConfigurationCommandLinePackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsConfigurationCommandLinePackageVersion>
<MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>
<MicrosoftExtensionsConfigurationUserSecretsPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsConfigurationUserSecretsPackageVersion>
<MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>
<MicrosoftExtensionsDependencyInjectionPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsDependencyInjectionPackageVersion>
<MicrosoftExtensionsLoggingAbstractionsPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
<MicrosoftExtensionsLoggingConfigurationPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsLoggingConfigurationPackageVersion>
<MicrosoftExtensionsLoggingConsolePackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsLoggingConsolePackageVersion>
<MicrosoftExtensionsLoggingDebugPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsLoggingDebugPackageVersion>
<MicrosoftExtensionsLoggingPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsLoggingPackageVersion>
<MicrosoftExtensionsLoggingTestingPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsLoggingTestingPackageVersion>
<MicrosoftExtensionsObjectMethodExecutorSourcesPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsObjectMethodExecutorSourcesPackageVersion>
<MicrosoftExtensionsOptionsPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsOptionsPackageVersion>
<MicrosoftExtensionsSecurityHelperSourcesPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsSecurityHelperSourcesPackageVersion>
<MicrosoftExtensionsValueStopwatchSourcesPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsValueStopwatchSourcesPackageVersion>
<MicrosoftExtensionsWebEncodersSourcesPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsWebEncodersSourcesPackageVersion>
<MicrosoftNETCoreApp22PackageVersion>2.2.0-rtm-27105-02</MicrosoftNETCoreApp22PackageVersion>
<MicrosoftAspNetCoreAppPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreAppPackageVersion>
<MicrosoftAspNetCoreAuthenticationCookiesPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreAuthenticationCookiesPackageVersion>
<MicrosoftAspNetCoreAuthenticationCorePackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreAuthenticationCorePackageVersion>
<MicrosoftAspNetCoreAuthenticationJwtBearerPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreAuthenticationJwtBearerPackageVersion>
<MicrosoftAspNetCoreAuthorizationPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreAuthorizationPackageVersion>
<MicrosoftAspNetCoreAuthorizationPolicyPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreAuthorizationPolicyPackageVersion>
<MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>
<MicrosoftAspNetCoreConnectionsAbstractionsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreConnectionsAbstractionsPackageVersion>
<MicrosoftAspNetCoreCorsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreCorsPackageVersion>
<MicrosoftAspNetCoreDiagnosticsEntityFrameworkCorePackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreDiagnosticsEntityFrameworkCorePackageVersion>
<MicrosoftAspNetCoreDiagnosticsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreDiagnosticsPackageVersion>
<MicrosoftAspNetCoreHostingAbstractionsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
<MicrosoftAspNetCoreHostingPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHostingPackageVersion>
<MicrosoftAspNetCoreHttpAbstractionsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHttpAbstractionsPackageVersion>
<MicrosoftAspNetCoreHttpFeaturesPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHttpFeaturesPackageVersion>
<MicrosoftAspNetCoreHttpPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHttpPackageVersion>
<MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>
<MicrosoftAspNetCoreMvcPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreMvcPackageVersion>
<MicrosoftAspNetCoreRoutingPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreRoutingPackageVersion>
<MicrosoftAspNetCoreServerIISIntegrationPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
<MicrosoftAspNetCoreServerKestrelPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreServerKestrelPackageVersion>
<MicrosoftAspNetCoreStaticFilesPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreStaticFilesPackageVersion>
<MicrosoftAspNetCoreTestHostPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreTestHostPackageVersion>
<MicrosoftAspNetCoreTestingPackageVersion>3.0.0-preview-181113-11</MicrosoftAspNetCoreTestingPackageVersion>
<MicrosoftAspNetCoreWebSocketsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreWebSocketsPackageVersion>
<MicrosoftCSharpPackageVersion>4.6.0-preview1-26907-04</MicrosoftCSharpPackageVersion>
<MicrosoftEntityFrameworkCoreDesignPackageVersion>3.0.0-preview-181109-02</MicrosoftEntityFrameworkCoreDesignPackageVersion>
<MicrosoftEntityFrameworkCoreSqlServerPackageVersion>3.0.0-preview-181109-02</MicrosoftEntityFrameworkCoreSqlServerPackageVersion>
<MicrosoftEntityFrameworkCoreToolsPackageVersion>3.0.0-preview-181109-02</MicrosoftEntityFrameworkCoreToolsPackageVersion>
<MicrosoftExtensionsBuffersTestingSourcesPackageVersion>3.0.0-alpha1-10727</MicrosoftExtensionsBuffersTestingSourcesPackageVersion>
<MicrosoftExtensionsClosedGenericMatcherSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsClosedGenericMatcherSourcesPackageVersion>
<MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>
<MicrosoftExtensionsConfigurationCommandLinePackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsConfigurationCommandLinePackageVersion>
<MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>
<MicrosoftExtensionsConfigurationUserSecretsPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsConfigurationUserSecretsPackageVersion>
<MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>
<MicrosoftExtensionsDependencyInjectionPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsDependencyInjectionPackageVersion>
<MicrosoftExtensionsLoggingAbstractionsPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
<MicrosoftExtensionsLoggingConfigurationPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingConfigurationPackageVersion>
<MicrosoftExtensionsLoggingConsolePackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingConsolePackageVersion>
<MicrosoftExtensionsLoggingDebugPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingDebugPackageVersion>
<MicrosoftExtensionsLoggingPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingPackageVersion>
<MicrosoftExtensionsLoggingTestingPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingTestingPackageVersion>
<MicrosoftExtensionsObjectMethodExecutorSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsObjectMethodExecutorSourcesPackageVersion>
<MicrosoftExtensionsOptionsPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsOptionsPackageVersion>
<MicrosoftExtensionsSecurityHelperSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsSecurityHelperSourcesPackageVersion>
<MicrosoftExtensionsValueStopwatchSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsValueStopwatchSourcesPackageVersion>
<MicrosoftExtensionsWebEncodersSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsWebEncodersSourcesPackageVersion>
<MicrosoftNETCoreAppPackageVersion>3.0.0-preview1-26907-05</MicrosoftNETCoreAppPackageVersion>
<MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
<MoqPackageVersion>4.10.0</MoqPackageVersion>
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
<NewtonsoftJsonPackageVersion>11.0.2</NewtonsoftJsonPackageVersion>
<StackExchangeRedisPackageVersion>2.0.513</StackExchangeRedisPackageVersion>
<StackExchangeRedisStrongNamePackageVersion>1.2.6</StackExchangeRedisStrongNamePackageVersion>
<SystemBuffersPackageVersion>4.5.0</SystemBuffersPackageVersion>
<SystemIOPipelinesPackageVersion>4.5.2</SystemIOPipelinesPackageVersion>
<SystemMemoryPackageVersion>4.5.1</SystemMemoryPackageVersion>
<SystemNumericsVectorsPackageVersion>4.5.0</SystemNumericsVectorsPackageVersion>
<SystemBuffersPackageVersion>4.6.0-preview1-26907-04</SystemBuffersPackageVersion>
<SystemIOPipelinesPackageVersion>4.6.0-preview1-26907-04</SystemIOPipelinesPackageVersion>
<SystemMemoryPackageVersion>4.6.0-preview1-26717-04</SystemMemoryPackageVersion>
<SystemNumericsVectorsPackageVersion>4.6.0-preview1-26907-04</SystemNumericsVectorsPackageVersion>
<SystemReactiveLinqPackageVersion>3.1.1</SystemReactiveLinqPackageVersion>
<SystemReflectionEmitPackageVersion>4.3.0</SystemReflectionEmitPackageVersion>
<SystemRuntimeCompilerServicesUnsafePackageVersion>4.5.1</SystemRuntimeCompilerServicesUnsafePackageVersion>
<SystemSecurityPrincipalWindowsPackageVersion>4.5.0</SystemSecurityPrincipalWindowsPackageVersion>
<SystemThreadingChannelsPackageVersion>4.5.0</SystemThreadingChannelsPackageVersion>
<SystemThreadingTasksExtensionsPackageVersion>4.5.1</SystemThreadingTasksExtensionsPackageVersion>
<SystemRuntimeCompilerServicesUnsafePackageVersion>4.6.0-preview1-26907-04</SystemRuntimeCompilerServicesUnsafePackageVersion>
<SystemSecurityPrincipalWindowsPackageVersion>4.6.0-preview1-26907-04</SystemSecurityPrincipalWindowsPackageVersion>
<SystemThreadingChannelsPackageVersion>4.6.0-preview1-26907-04</SystemThreadingChannelsPackageVersion>
<SystemThreadingTasksExtensionsPackageVersion>4.6.0-preview1-26907-04</SystemThreadingTasksExtensionsPackageVersion>
<XunitAssertPackageVersion>2.3.1</XunitAssertPackageVersion>
<XunitExtensibilityCorePackageVersion>2.3.1</XunitExtensibilityCorePackageVersion>
<XunitPackageVersion>2.3.1</XunitPackageVersion>

View File

@ -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
}
}
}

View File

@ -15,11 +15,10 @@
<PropertyGroup>
<!-- These properties are use by the automation that updates dependencies.props -->
<LineupPackageId>Internal.AspNetCore.Universe.Lineup</LineupPackageId>
<LineupPackageVersion>2.2.0-*</LineupPackageVersion>
<LineupPackageRestoreSource>https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json</LineupPackageRestoreSource>
</PropertyGroup>
<ItemGroup>
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp22PackageVersion)" />
<DotNetCoreRuntime Include="$(MicrosoftNETCoreAppPackageVersion)" />
</ItemGroup>
</Project>

View File

@ -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'
}

View File

@ -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;
}
}

View File

@ -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<Object> pendingCall = irq.getPendingCall();
Subject<Object> 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 <T> The expected return type.
* @return An observable that yields the streaming results from the server.
*/
@SuppressWarnings("unchecked")
public <T> Observable<T> stream(Class<T> 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<T> subject = ReplaySubject.create();
Subject<Object> 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<T> 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());
}

View File

@ -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<Object> pendingCall = SingleSubject.create();
private final Subject<Object> 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<Object> getPendingCall() {
public Subject<Object> getPendingCall() {
return pendingCall;
}

View File

@ -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:

View File

@ -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

View File

@ -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;
}
}

View File

@ -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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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();

View File

@ -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";

View File

@ -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");

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetFramework>netcoreapp3.0</TargetFramework>
<TypeScriptCompileBlocked>True</TypeScriptCompileBlocked>
<TypeScriptToolsVersion>2.8</TypeScriptToolsVersion>
</PropertyGroup>

View File

@ -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",

View File

@ -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";

View File

@ -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
}
}
},

View File

@ -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": {

View File

@ -1,6 +1,6 @@
{
"name": "@aspnet/signalr",
"version": "1.1.0-rtm-t000",
"version": "3.0.0-alpha1-t000",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -13,6 +13,7 @@ export interface HandshakeRequestMessage {
/** @private */
export interface HandshakeResponseMessage {
readonly error: string;
readonly minorVersion: number;
}
/** @private */

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
<TargetFrameworks>netcoreapp3.0;net461</TargetFrameworks>
<!-- Don't create a NuGet package -->
<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>

View File

@ -26,6 +26,8 @@ namespace ClientSample
RawSample.Register(app);
HubSample.Register(app);
StreamingSample.Register(app);
UploadSample.Register(app);
app.Command("help", cmd =>
{

View File

@ -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("<BASEURL>", "The URL to the Chat Hub to test");
cmd.OnExecute(() => ExecuteAsync(baseUrlArgument.Value));
});
}
public static async Task<int> ExecuteAsync(string baseUrl)
{
var connection = new HubConnectionBuilder()
.WithUrl(baseUrl)
.Build();
await connection.StartAsync();
var reader = await connection.StreamAsChannelAsync<int>("ChannelCounter", 10, 2000);
while (await reader.WaitToReadAsync())
{
while (reader.TryRead(out var item))
{
Console.WriteLine($"received: {item}");
}
}
return 0;
}
}
}

View File

@ -22,7 +22,7 @@ namespace ClientSample
public SocketAwaitable ReceiveAsync(Memory<byte> buffer)
{
#if NETCOREAPP2_2
#if NETCOREAPP3_0
_eventArgs.SetBuffer(buffer);
#else
var segment = buffer.GetArray();

View File

@ -32,7 +32,7 @@ namespace ClientSample
return SendAsync(buffers.First);
}
#if NETCOREAPP2_2
#if NETCOREAPP3_0
if (!_eventArgs.MemoryBuffer.Equals(Memory<byte>.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();

View File

@ -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("<BASEURL>", "The URL to the Chat Hub to test");
cmd.OnExecute(() => ExecuteAsync(baseUrlArgument.Value));
});
}
public static async Task<int> 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<string>();
var invokeTask = connection.InvokeAsync<string>("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<T>(IEnumerable<T> sequence, ChannelWriter<T> 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<string>();
var numbers = Channel.CreateUnbounded<int>();
_ = WriteStreamAsync(new[] { "h", "i", "!" }, letters.Writer);
_ = WriteStreamAsync(new[] { 1, 2, 3, 4, 5 }, numbers.Writer);
var result = await connection.InvokeAsync<string>("DoubleStreamUpload", letters.Reader, numbers.Reader);
Debug.WriteLine(result);
}
public static async Task AdditionalArgs(HubConnection connection)
{
var channel = Channel.CreateUnbounded<char>();
_ = WriteStreamAsync<char>("main message".ToCharArray(), channel.Writer);
var result = await connection.InvokeAsync<string>("UploadWithSuffix", channel.Reader, " + wooh I'm a suffix");
Debug.WriteLine($"Your message was: {result}");
}
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.2</TargetFrameworks>
<TargetFrameworks>netcoreapp3.0</TargetFrameworks>
<!-- Don't create a NuGet package -->
<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@ -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<string> DoubleStreamUpload(ChannelReader<string> letters, ChannelReader<int> 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<int> Sum(ChannelReader<int> source)
{
var total = 0;
while (await source.WaitToReadAsync())
{
while (source.TryRead(out var item))
{
total += item;
}
}
return total;
}
public async Task LocalSum(ChannelReader<int> 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<string> UploadWord(ChannelReader<string> 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<string> UploadWithSuffix(ChannelReader<string> 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<string> UploadFile(ChannelReader<byte[]> source, string filepath)
{
var result = Enumerable.Empty<byte>();
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}'";
}
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
<TargetFramework>netcoreapp3.0</TargetFramework>
<!-- Don't create a NuGet package -->
<IsPackable>false</IsPackable>
</PropertyGroup>
@ -10,7 +10,7 @@
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Protocols.MessagePack\Microsoft.AspNetCore.SignalR.Protocols.MessagePack.csproj" />
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR\Microsoft.AspNetCore.SignalR.csproj" />
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Http.Connections\Microsoft.AspNetCore.Http.Connections.csproj" />
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Redis\Microsoft.AspNetCore.SignalR.Redis.csproj" />
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.StackExchangeRedis\Microsoft.AspNetCore.SignalR.StackExchangeRedis.csproj" />
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(MicrosoftAspNetCoreDiagnosticsPackageVersion)" />
@ -21,8 +21,8 @@
<PackageReference Include="Microsoft.AspNetCore.Cors" Version="$(MicrosoftAspNetCoreCorsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="$(MicrosoftExtensionsConfigurationCommandLinePackageVersion)" />
<PackageReference Include="Google.Protobuf" Version="$(GoogleProtobufPackageVersion)" />
<PackageReference Include="System.Reactive.Linq" Version="$(SystemReactiveLinqPackageVersion)" />
<PackageReference Include="Microsoft.CSharp" Version="$(MicrosoftCSharpPackageVersion)" />
</ItemGroup>
<Target Name="CopyTSClient" BeforeTargets="AfterBuild">

View File

@ -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<DynamicChat>("/dynamic");
routes.MapHub<Chat>("/default");
routes.MapHub<Streaming>("/streaming");
routes.MapHub<UploadHub>("/uploading");
routes.MapHub<HubTChat>("/hubT");
});

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
<TargetFramework>netcoreapp3.0</TargetFramework>
<!-- Don't create a NuGet package -->
<IsPackable>false</IsPackable>
</PropertyGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
<TargetFramework>netcoreapp3.0</TargetFramework>
<OutputType>Exe</OutputType>
<!-- Don't create a NuGet package -->
<IsPackable>false</IsPackable>

View File

@ -293,7 +293,7 @@ namespace Microsoft.AspNetCore.Internal
}
}
#if NETCOREAPP2_2
#if NETCOREAPP3_0
public override void Write(ReadOnlySpan<byte> span)
{
if (_currentSegment != null && span.TryCopyTo(_currentSegment.AsSpan(_position)))

View File

@ -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<byte> source, CancellationToken cancellationToken = default)
{
return WriteCoreAsync(source, cancellationToken);

View File

@ -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<T>
// 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;
}
}
}

View File

@ -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);

View File

@ -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<char>(buffer, index, count);
_decoder.Convert(source, destination, false, out bytesUsed, out charsUsed, out var completed);
#else

View File

@ -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<char>(&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

View File

@ -13,7 +13,7 @@ namespace System.Net.WebSockets
{
public static ValueTask SendAsync(this WebSocket webSocket, ReadOnlySequence<byte> 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<byte> 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<byte>.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<byte>(Array.Empty<byte>()), webSocketMessageType, endOfMessage: true, cancellationToken);
var isArrayEnd = MemoryMarshal.TryGetArray(prevSegment, out var arraySegmentEnd);
Debug.Assert(isArrayEnd);
await webSocket.SendAsync(arraySegmentEnd, webSocketMessageType, endOfMessage: true, cancellationToken);
#endif
}
}

View File

@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
private static readonly Task<string> _noAccessToken = Task.FromResult<string>(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

View File

@ -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<byte>.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);

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<Description>Client for ASP.NET Core Connection Handlers</Description>
<TargetFrameworks>netstandard2.0;netcoreapp2.2</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netcoreapp3.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

View File

@ -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<byte>.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<byte>(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;

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<Description>Components for providing real-time bi-directional communication across the Web.</Description>
<TargetFrameworks>netstandard2.0;netcoreapp2.2</TargetFrameworks>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@ -186,6 +186,18 @@ namespace Microsoft.AspNetCore.SignalR.Client
private static readonly Action<ILogger, Exception> _unableToAcquireConnectionLockForPing =
LoggerMessage.Define(LogLevel.Trace, new EventId(62, "UnableToAcquireConnectionLockForPing"), "Skipping ping because a send is already in progress.");
private static readonly Action<ILogger, string, Exception> _startingStream =
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(63, "StartingStream"), "Initiating stream '{StreamId}'.");
private static readonly Action<ILogger, string, Exception> _sendingStreamItem =
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(64, "StreamItemSent"), "Sending item for stream '{StreamId}'.");
private static readonly Action<ILogger, string, Exception> _cancelingStream =
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(65, "CancelingStream"), "Stream '{StreamId}' has been canceled by client.");
private static readonly Action<ILogger, string, Exception> _completingStream =
LoggerMessage.Define<string>(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);
}
}
}
}

View File

@ -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<string, InvocationHandlerList> _handlers = new ConcurrentDictionary<string, InvocationHandlerList>(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<Exception, Task> 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<string, object> PackageStreamingParams(object[] args)
{
// lazy initialized, to avoid allocation unecessary dictionaries
Dictionary<string, object> readers = null;
for (var i = 0; i < args.Length; i++)
{
if (ReflectionHelper.IsStreamingType(args[i].GetType()))
{
if (readers == null)
{
readers = new Dictionary<string, object>();
}
var id = _connectionState.GetNextStreamId();
readers[id] = args[i];
args[i] = new StreamPlaceholder(id);
Log.StartingStream(_logger, id);
}
}
return readers;
}
private void LaunchStreams(Dictionary<string, object> 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<T>(string streamId, ChannelReader<T> 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<object> 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<object> _stopTcs;
private readonly object _lock = new object();
private readonly Dictionary<string, InvocationRequest> _pendingCalls = new Dictionary<string, InvocationRequest>(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<Type> IInvocationBinder.GetParameterTypes(string methodName)
{
if (!_hubConnection._handlers.TryGetValue(methodName, out var invocationHandlerList))

View File

@ -10,6 +10,7 @@
<Compile Include="..\Common\AwaitableThreadPool.cs" Link="AwaitableThreadPool.cs" />
<Compile Include="..\Common\ForceAsyncAwaiter.cs" Link="ForceAsyncAwaiter.cs" />
<Compile Include="..\Common\PipeWriterStream.cs" Link="PipeWriterStream.cs" />
<Compile Include="..\Common\ReflectionHelper.cs" Link="ReflectionHelper.cs" />
<Compile Include="..\Common\TimerAwaitable.cs" Link="Internal\TimerAwaitable.cs" />
</ItemGroup>

View File

@ -10,5 +10,6 @@ namespace Microsoft.AspNetCore.SignalR
{
Type GetReturnType(string invocationId);
IReadOnlyList<Type> GetParameterTypes(string methodName);
Type GetStreamItemType(string streamId);
}
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<Description>Common serialiation primitives for SignalR Clients Servers</Description>
<TargetFrameworks>netstandard2.0;netcoreapp2.2</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netcoreapp3.0</TargetFrameworks>
<RootNamespace>Microsoft.AspNetCore.SignalR</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
@ -23,4 +23,4 @@
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
</ItemGroup>
</Project>
</Project>

View File

@ -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";
/// <summary>
/// The serialized representation of a success handshake.
/// </summary>
public static ReadOnlyMemory<byte> SuccessHandshakeData;
private static ConcurrentDictionary<IHubProtocol, ReadOnlyMemory<byte>> _messageCache = new ConcurrentDictionary<IHubProtocol, ReadOnlyMemory<byte>>();
static HandshakeProtocol()
public static ReadOnlySpan<byte> GetSuccessfulHandshake(IHubProtocol protocol)
{
var memoryBufferWriter = MemoryBufferWriter.Get();
try
ReadOnlyMemory<byte> 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;
}
/// <summary>
@ -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;
}
}

View File

@ -11,20 +11,41 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
/// <summary>
/// An empty response message with no error.
/// </summary>
public static readonly HandshakeResponseMessage Empty = new HandshakeResponseMessage(null);
public static readonly HandshakeResponseMessage Empty = new HandshakeResponseMessage(error: null);
/// <summary>
/// Gets the optional error message.
/// </summary>
public string Error { get; }
/// <summary>
/// Highest minor protocol version that the server supports.
/// </summary>
public int MinorVersion { get; }
/// <summary>
/// Initializes a new instance of the <see cref="HandshakeResponseMessage"/> class.
/// An error response does need a minor version. Since the handshake has failed, any extra data will be ignored.
/// </summary>
/// <param name="error">Error encountered by the server, indicating why the handshake has failed.</param>
public HandshakeResponseMessage(string error) : this(null, error) { }
/// <summary>
/// Initializes a new instance of the <see cref="HandshakeResponseMessage"/> class.
/// A reponse with a minor version indicates success, and doesn't require an error field.
/// </summary>
/// <param name="minorVersion">The highest protocol minor version that the server supports.</param>
public HandshakeResponseMessage(int minorVersion) : this(minorVersion, null) { }
/// <summary>
/// Initializes a new instance of the <see cref="HandshakeResponseMessage"/> class.
/// </summary>
/// <param name="error">An optional response error message. A <c>null</c> error message indicates a successful handshake.</param>
public HandshakeResponseMessage(string error)
/// <param name="error">Error encountered by the server, indicating why the handshake has failed.</param>
/// <param name="minorVersion">The highest protocol minor version that the server supports.</param>
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;
}
}

View File

@ -42,5 +42,15 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
/// Represents the close message type.
/// </summary>
public const int CloseMessageType = 7;
/// <summary>
/// Represents the stream complete message type.
/// </summary>
public const int StreamCompleteMessageType = 8;
/// <summary>
/// Same as StreamItemMessage, except
/// </summary>
public const int StreamDataMessageType = 9;
}
}

View File

@ -18,10 +18,15 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
string Name { get; }
/// <summary>
/// Gets the version of the protocol.
/// Gets the major version of the protocol.
/// </summary>
int Version { get; }
/// <summary>
/// Gets the minor version of the protocol.
/// </summary>
int MinorVersion { get; }
/// <summary>
/// Gets the transfer format of the protocol.
/// </summary>

View File

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Runtime.ExceptionServices;
using System.Text;
namespace Microsoft.AspNetCore.SignalR.Protocol
{
/// <summary>
/// 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 <see cref="IHubProtocol.TryParseMessage"/>
/// 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.
/// </summary>
public class StreamBindingFailureMessage : HubMessage
{
/// <summary>
/// Gets the id of the relevant stream
/// </summary>
public string Id { get; }
/// <summary>
/// Gets the exception thrown during binding.
/// </summary>
public ExceptionDispatchInfo BindingFailure { get; }
/// <summary>
/// Initializes a new instance of the <see cref="InvocationBindingFailureMessage"/> class.
/// </summary>
/// <param name="id">The stream ID.</param>
/// <param name="bindingFailure">The exception thrown during binding.</param>
public StreamBindingFailureMessage(string id, ExceptionDispatchInfo bindingFailure)
{
Id = id;
BindingFailure = bindingFailure;
}
}
}

View File

@ -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
{
/// <summary>
/// A message for indicating that a particular stream has ended.
/// </summary>
public class StreamCompleteMessage : HubMessage
{
/// <summary>
/// Gets the stream id.
/// </summary>
public string StreamId { get; }
/// <summary>
/// Gets the error. Will be null if there is no error.
/// </summary>
public string Error { get; }
/// <summary>
/// Whether the message has an error.
/// </summary>
public bool HasError { get => Error != null; }
/// <summary>
/// Initializes a new instance of <see cref="StreamCompleteMessage"/>
/// </summary>
/// <param name="streamId">The streamId of the stream to complete.</param>
/// <param name="error">An optional error field.</param>
public StreamCompleteMessage(string streamId, string error = null)
{
StreamId = streamId;
Error = error;
}
}
}

View File

@ -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
{
/// <summary>
/// Sent to parameter streams.
/// Similar to <see cref="StreamItemMessage"/>, except the data is sent to a parameter stream, rather than in response to an invocation.
/// </summary>
public class StreamDataMessage : HubMessage
{
/// <summary>
/// The piece of data this message carries.
/// </summary>
public object Item { get; }
/// <summary>
/// The stream to which to deliver data.
/// </summary>
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 ?? "<<null>>"} }}";
}
}
}

View File

@ -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
{
/// <summary>
/// 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 <see cref="StreamDataMessage"/> is routed to the appropiate channel based on streamId.
/// </summary>
public class StreamPlaceholder
{
public string StreamId { get; private set; }
public StreamPlaceholder(string streamId)
{
StreamId = streamId;
}
}
}

View File

@ -0,0 +1,17 @@
[
{
"TypeId": "public static class Microsoft.AspNetCore.SignalR.Protocol.HandshakeProtocol",
"MemberId": "public static System.ReadOnlyMemory<System.Byte> 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"
}
]

View File

@ -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;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="HubConnectionContext"/> class.
/// </summary>
@ -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;
}
}

View File

@ -186,6 +186,9 @@ namespace Microsoft.AspNetCore.SignalR
{
var input = connection.Input;
var protocol = connection.Protocol;
var binder = new HubConnectionBinder<THub>(_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);
}

View File

@ -57,6 +57,18 @@ namespace Microsoft.AspNetCore.SignalR.Internal
private static readonly Action<ILogger, string, Exception> _invalidReturnValueFromStreamingMethod =
LoggerMessage.Define<string>(LogLevel.Error, new EventId(15, "InvalidReturnValueFromStreamingMethod"), "A streaming method returned a value that cannot be used to build enumerator {HubMethod}.");
private static readonly Action<ILogger, string, Exception> _receivedStreamItem =
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(16, "ReceivedStreamItem"), "Received item for stream '{StreamId}'.");
private static readonly Action<ILogger, string, Exception> _startingParameterStream =
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(17, "StartingParameterStream"), "Creating streaming parameter channel '{StreamId}'.");
private static readonly Action<ILogger, string, Exception> _completingStream =
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(18, "CompletingStream"), "Stream '{StreamId}' has been completed by client.");
private static readonly Action<ILogger, string, string, Exception> _closingStreamWithBindingError =
LoggerMessage.Define<string, string>(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);
}
}
}
}

View File

@ -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<Type> 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<IHubActivator<THub>>();
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<object> enumerator, IServiceScope scope, IHubActivator<THub> hubActivator, THub hub, CancellationTokenSource streamCts)
private async Task StreamResultsAsync(string invocationId, HubConnectionContext connection, IAsyncEnumerator<object> enumerator, IServiceScope scope,
IHubActivator<THub> 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<Type> GetParameterTypes(string methodName)
{
if (!_methods.TryGetValue(methodName, out var descriptor))
{
throw new HubException("Method does not exist.");
}
return descriptor.ParameterTypes;
}
}
}

View File

@ -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<THub> : IInvocationBinder where THub : Hub
{
private HubDispatcher<THub> _dispatcher;
private HubConnectionContext _connection;
public HubConnectionBinder(HubDispatcher<THub> dispatcher, HubConnectionContext connection)
{
_dispatcher = dispatcher;
_connection = connection;
}
public IReadOnlyList<Type> 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);
}
}
}

View File

@ -8,12 +8,11 @@ using Microsoft.AspNetCore.SignalR.Protocol;
namespace Microsoft.AspNetCore.SignalR.Internal
{
public abstract class HubDispatcher<THub> : IInvocationBinder where THub : Hub
public abstract class HubDispatcher<THub> 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<Type> GetParameterTypes(string methodName);
public abstract Type GetReturnType(string invocationId);
public abstract IReadOnlyList<Type> GetParameterTypes(string name);
}
}

View File

@ -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<object, CancellationToken, IAsyncEnumerator<object>> _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<>));

View File

@ -2,10 +2,14 @@
<PropertyGroup>
<Description>Real-time communication framework for ASP.NET Core.</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netcoreapp3.0</TargetFramework>
<RootNamespace>Microsoft.AspNetCore.SignalR</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Common\ReflectionHelper.cs" Link="ReflectionHelper.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.AspNetCore.SignalR.Common\Microsoft.AspNetCore.SignalR.Common.csproj" />
<ProjectReference Include="..\Microsoft.AspNetCore.SignalR.Protocols.Json\Microsoft.AspNetCore.SignalR.Protocols.Json.csproj" />

View File

@ -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<string, IStreamConverter> _lookup = new ConcurrentDictionary<string, IStreamConverter>();
/// <summary>
/// Creates a new stream and returns the ChannelReader for it as an object.
/// </summary>
public object AddStream(string streamId, Type itemType)
{
var newConverter = (IStreamConverter)_buildConverterMethod.MakeGenericMethod(itemType).Invoke(null, Array.Empty<object>());
_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<T>()
{
return new ChannelConverter<T>();
}
private interface IStreamConverter
{
Type GetItemType();
object GetReaderAsObject();
Task WriteToStream(object item);
void TryComplete(Exception ex);
}
private class ChannelConverter<T> : IStreamConverter
{
private Channel<T> _channel;
public ChannelConverter()
{
_channel = Channel.CreateUnbounded<T>();
}
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);
}
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Implements the SignalR Hub Protocol over JSON.</Description>

View File

@ -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;
/// <summary>
/// Gets the serializer used to serialize invocation arguments and return values.
@ -60,6 +62,9 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
/// <inheritdoc />
public int Version => ProtocolVersion;
/// <inheritdoc />
public int MinorVersion => ProtocolMinorVersion;
/// <inheritdoc />
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

View File

@ -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;
/// <inheritdoc />
public string Name => ProtocolName;
/// <inheritdoc />
public int Version => ProtocolVersion;
/// <inheritdoc />
public int MinorVersion => ProtocolMinorVersion;
/// <inheritdoc />
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<string, string> 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);

View File

@ -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<int, AckInfo> _acks = new ConcurrentDictionary<int, AckInfo>();
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<object> Tcs { get; private set; }
public DateTime Created { get; private set; }
public AckInfo()
{
Created = DateTime.UtcNow;
Tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
}
}
}
}

View File

@ -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,
}
}

View File

@ -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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> GetArray(ReadOnlyMemory<byte> data)
{
var isArray = MemoryMarshal.TryGetArray(data, out var array);
Debug.Assert(isArray);
return array;
}
}
}

View File

@ -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;
/// <summary>
/// Gets the name of the channel for sending to all connections.
/// </summary>
/// <remarks>
/// The payload on this channel is <see cref="RedisInvocation"/> objects containing
/// invocations to be sent to all connections
/// </remarks>
public string All { get; }
/// <summary>
/// Gets the name of the internal channel for group management messages.
/// </summary>
public string GroupManagement { get; }
public RedisChannels(string prefix)
{
_prefix = prefix;
All = prefix + ":all";
GroupManagement = prefix + ":internal:groups";
}
/// <summary>
/// Gets the name of the channel for sending a message to a specific connection.
/// </summary>
/// <param name="connectionId">The ID of the connection to get the channel for.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string Connection(string connectionId)
{
return _prefix + ":connection:" + connectionId;
}
/// <summary>
/// Gets the name of the channel for sending a message to a named group of connections.
/// </summary>
/// <param name="groupName">The name of the group to get the channel for.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string Group(string groupName)
{
return _prefix + ":group:" + groupName;
}
/// <summary>
/// Gets the name of the channel for sending a message to all collections associated with a user.
/// </summary>
/// <param name="userId">The ID of the user to get the channel for.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string User(string userId)
{
return _prefix + ":user:" + userId;
}
/// <summary>
/// Gets the name of the acknowledgement channel for the specified server.
/// </summary>
/// <param name="serverName">The name of the server to get the acknowledgement channel for.</param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string Ack(string serverName)
{
return _prefix + ":internal:ack:" + serverName;
}
}
}

View File

@ -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
{
/// <summary>
/// Gets the ID of the group command.
/// </summary>
public int Id { get; }
/// <summary>
/// Gets the name of the server that sent the command.
/// </summary>
public string ServerName { get; }
/// <summary>
/// Gets the action to be performed on the group.
/// </summary>
public GroupAction Action { get; }
/// <summary>
/// Gets the group on which the action is performed.
/// </summary>
public string GroupName { get; }
/// <summary>
/// Gets the ID of the connection to be added or removed from the group.
/// </summary>
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;
}
}
}

View File

@ -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
{
/// <summary>
/// Gets a list of connections that should be excluded from this invocation.
/// May be null to indicate that no connections are to be excluded.
/// </summary>
public IReadOnlyList<string> ExcludedConnectionIds { get; }
/// <summary>
/// Gets the message serialization cache containing serialized payloads for the message.
/// </summary>
public SerializedHubMessage Message { get; }
public RedisInvocation(SerializedHubMessage message, IReadOnlyList<string> excludedConnectionIds)
{
Message = message;
ExcludedConnectionIds = excludedConnectionIds;
}
public static RedisInvocation Create(string target, object[] arguments, IReadOnlyList<string> excludedConnectionIds = null)
{
return new RedisInvocation(
new SerializedHubMessage(new InvocationMessage(target, null, arguments)),
excludedConnectionIds);
}
}
}

View File

@ -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<ILogger, string, string, Exception> _connectingToEndpoints =
LoggerMessage.Define<string, string>(LogLevel.Information, new EventId(1, "ConnectingToEndpoints"), "Connecting to Redis endpoints: {Endpoints}. Using Server Name: {ServerName}");
private static readonly Action<ILogger, Exception> _connected =
LoggerMessage.Define(LogLevel.Information, new EventId(2, "Connected"), "Connected to Redis.");
private static readonly Action<ILogger, string, Exception> _subscribing =
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(3, "Subscribing"), "Subscribing to channel: {Channel}.");
private static readonly Action<ILogger, string, Exception> _receivedFromChannel =
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(4, "ReceivedFromChannel"), "Received message from Redis channel {Channel}.");
private static readonly Action<ILogger, string, Exception> _publishToChannel =
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(5, "PublishToChannel"), "Publishing message to Redis channel {Channel}.");
private static readonly Action<ILogger, string, Exception> _unsubscribe =
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(6, "Unsubscribe"), "Unsubscribing from channel: {Channel}.");
private static readonly Action<ILogger, Exception> _notConnected =
LoggerMessage.Define(LogLevel.Error, new EventId(7, "Connected"), "Not connected to Redis.");
private static readonly Action<ILogger, Exception> _connectionRestored =
LoggerMessage.Define(LogLevel.Information, new EventId(8, "ConnectionRestored"), "Connection to Redis restored.");
private static readonly Action<ILogger, Exception> _connectionFailed =
LoggerMessage.Define(LogLevel.Error, new EventId(9, "ConnectionFailed"), "Connection to Redis failed.");
private static readonly Action<ILogger, Exception> _failedWritingMessage =
LoggerMessage.Define(LogLevel.Warning, new EventId(10, "FailedWritingMessage"), "Failed writing message.");
private static readonly Action<ILogger, Exception> _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);
}
}
}
}

View File

@ -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<IHubProtocol> _protocols;
public RedisProtocol(IReadOnlyList<IHubProtocol> 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<string> 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<byte> data)
{
// See WriteInvocation for the format
ValidateArraySize(ref data, 2, "Invocation");
// Read excluded Ids
IReadOnlyList<string> 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<byte> 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<byte> 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<byte> 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<byte> data, int expectedLength, string messageType)
{
var length = MessagePackUtil.ReadArrayHeader(ref data);
if (length < expectedLength)
{
throw new InvalidDataException($"Insufficient items in {messageType} array.");
}
}
}
}

View File

@ -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<string, HubConnectionStore> _subscriptions = new ConcurrentDictionary<string, HubConnectionStore>(StringComparer.Ordinal);
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
public async Task AddSubscriptionAsync(string id, HubConnectionContext connection, Func<string, HubConnectionStore, Task> 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<string, Task> 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();
}
}
}
}

View File

@ -1,23 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Redis for ASP.NET Core SignalR.</Description>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Common\JsonUtils.cs" Link="Internal\JsonUtils.cs" />
<Compile Include="..\Common\MemoryBufferWriter.cs" Link="Internal\MemoryBufferWriter.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
<PackageReference Include="StackExchange.Redis.StrongName" Version="$(StackExchangeRedisStrongNamePackageVersion)" />
<PackageReference Include="MessagePack" Version="$(MessagePackPackageVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.AspNetCore.SignalR.Core\Microsoft.AspNetCore.SignalR.Core.csproj" />
</ItemGroup>
</Project>

View File

@ -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
{
/// <summary>
/// Extension methods for configuring Redis-based scale-out for a SignalR Server in an <see cref="ISignalRServerBuilder" />.
/// </summary>
public static class RedisDependencyInjectionExtensions
{
/// <summary>
/// Adds scale-out to a <see cref="ISignalRServerBuilder"/>, using a shared Redis server.
/// </summary>
/// <param name="signalrBuilder">The <see cref="ISignalRServerBuilder"/>.</param>
/// <returns>The same instance of the <see cref="ISignalRServerBuilder"/> for chaining.</returns>
public static ISignalRServerBuilder AddRedis(this ISignalRServerBuilder signalrBuilder)
{
return AddRedis(signalrBuilder, o => { });
}
/// <summary>
/// Adds scale-out to a <see cref="ISignalRServerBuilder"/>, using a shared Redis server.
/// </summary>
/// <param name="signalrBuilder">The <see cref="ISignalRServerBuilder"/>.</param>
/// <param name="redisConnectionString">The connection string used to connect to the Redis server.</param>
/// <returns>The same instance of the <see cref="ISignalRServerBuilder"/> for chaining.</returns>
public static ISignalRServerBuilder AddRedis(this ISignalRServerBuilder signalrBuilder, string redisConnectionString)
{
return AddRedis(signalrBuilder, o =>
{
o.Configuration = ConfigurationOptions.Parse(redisConnectionString);
});
}
/// <summary>
/// Adds scale-out to a <see cref="ISignalRServerBuilder"/>, using a shared Redis server.
/// </summary>
/// <param name="signalrBuilder">The <see cref="ISignalRServerBuilder"/>.</param>
/// <param name="configure">A callback to configure the Redis options.</param>
/// <returns>The same instance of the <see cref="ISignalRServerBuilder"/> for chaining.</returns>
public static ISignalRServerBuilder AddRedis(this ISignalRServerBuilder signalrBuilder, Action<RedisOptions> configure)
{
signalrBuilder.Services.Configure(configure);
signalrBuilder.Services.AddSingleton(typeof(HubLifetimeManager<>), typeof(RedisHubLifetimeManager<>));
return signalrBuilder;
}
/// <summary>
/// Adds scale-out to a <see cref="ISignalRServerBuilder"/>, using a shared Redis server.
/// </summary>
/// <param name="signalrBuilder">The <see cref="ISignalRServerBuilder"/>.</param>
/// <param name="redisConnectionString">The connection string used to connect to the Redis server.</param>
/// <param name="configure">A callback to configure the Redis options.</param>
/// <returns>The same instance of the <see cref="ISignalRServerBuilder"/> for chaining.</returns>
public static ISignalRServerBuilder AddRedis(this ISignalRServerBuilder signalrBuilder, string redisConnectionString, Action<RedisOptions> configure)
{
return AddRedis(signalrBuilder, o =>
{
o.Configuration = ConfigurationOptions.Parse(redisConnectionString);
configure(o);
});
}
}
}

View File

@ -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<THub> : HubLifetimeManager<THub>, 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<RedisHubLifetimeManager<THub>> logger,
IOptions<RedisOptions> 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<IRedisFeature>(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<Task>();
var connectionChannel = _channels.Connection(connection.ConnectionId);
RedisLog.Unsubscribe(_logger, connectionChannel);
tasks.Add(_bus.UnsubscribeAsync(connectionChannel));
var feature = connection.Features.Get<IRedisFeature>();
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<string> 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<string> 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<string> connectionIds, string methodName, object[] args, CancellationToken cancellationToken = default)
{
if (connectionIds == null)
{
throw new ArgumentNullException(nameof(connectionIds));
}
var publishTasks = new List<Task>(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<string> groupNames, string methodName, object[] args, CancellationToken cancellationToken = default)
{
if (groupNames == null)
{
throw new ArgumentNullException(nameof(groupNames));
}
var publishTasks = new List<Task>(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<string> userIds, string methodName, object[] args, CancellationToken cancellationToken = default)
{
if (userIds.Count > 0)
{
var payload = _protocol.WriteInvocation(methodName, args);
var publishTasks = new List<Task>(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<IRedisFeature>();
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);
}
/// <summary>
/// This takes <see cref="HubConnectionContext"/> because we want to remove the connection from the
/// _connections list in OnDisconnectedAsync and still be able to remove groups with this method.
/// </summary>
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<IRedisFeature>();
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<Task>(_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<Task>();
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<Task>();
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<string> Groups { get; }
}
private class RedisFeature : IRedisFeature
{
public HashSet<string> Groups { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
}
}

View File

@ -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
{
/// <summary>
/// Options used to configure <see cref="RedisHubLifetimeManager{THub}"/>.
/// </summary>
public class RedisOptions
{
/// <summary>
/// Gets or sets configuration options exposed by <c>StackExchange.Redis</c>.
/// </summary>
public ConfigurationOptions Configuration { get; set; } = new ConfigurationOptions
{
// Enable reconnecting by default
AbortOnConnectFail = false
};
/// <summary>
/// Gets or sets the Redis connection factory.
/// </summary>
public Func<TextWriter, Task<IConnectionMultiplexer>> ConnectionFactory { get; set; }
internal async Task<IConnectionMultiplexer> 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);
}
}
}

View File

@ -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<Microsoft.AspNetCore.SignalR.Redis.RedisOptions>"
}
],
"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<Microsoft.AspNetCore.SignalR.Redis.RedisOptions>"
}
],
"ReturnType": "Microsoft.AspNetCore.SignalR.ISignalRServerBuilder",
"Static": true,
"Extension": true,
"Visibility": "Public",
"GenericParameter": []
}
],
"GenericParameters": []
},
{
"Name": "Microsoft.AspNetCore.SignalR.Redis.RedisHubLifetimeManager<T0>",
"Visibility": "Public",
"Kind": "Class",
"BaseType": "Microsoft.AspNetCore.SignalR.HubLifetimeManager<T0>",
"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<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": "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<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": "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<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": "SendGroupsAsync",
"Parameters": [
{
"Name": "groupNames",
"Type": "System.Collections.Generic.IReadOnlyList<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": "SendUsersAsync",
"Parameters": [
{
"Name": "userIds",
"Type": "System.Collections.Generic.IReadOnlyList<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": "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<Microsoft.AspNetCore.SignalR.Redis.RedisHubLifetimeManager<T0>>"
},
{
"Name": "options",
"Type": "Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.SignalR.Redis.RedisOptions>"
},
{
"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<System.IO.TextWriter, System.Threading.Tasks.Task<StackExchange.Redis.IConnectionMultiplexer>>",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "set_ConnectionFactory",
"Parameters": [
{
"Name": "value",
"Type": "System.Func<System.IO.TextWriter, System.Threading.Tasks.Task<StackExchange.Redis.IConnectionMultiplexer>>"
}
],
"ReturnType": "System.Void",
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Constructor",
"Name": ".ctor",
"Parameters": [],
"Visibility": "Public",
"GenericParameter": []
}
],
"GenericParameters": []
}
]
}

View File

@ -2,9 +2,9 @@
<PropertyGroup>
<Description>Tests for users to verify their own implementations of SignalR types</Description>
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Common\DuplexPipe.cs" Link="DuplexPipe.cs" />
<Compile Include="..\Common\MemoryBufferWriter.cs" Link="Internal\MemoryBufferWriter.cs" />
@ -15,7 +15,7 @@
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Common\Microsoft.AspNetCore.SignalR.Common.csproj" />
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Protocols.MessagePack\Microsoft.AspNetCore.SignalR.Protocols.MessagePack.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="xunit.extensibility.core" Version="$(XunitExtensibilityCorePackageVersion)" />

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<Description>Provides scale-out support for ASP.NET Core SignalR using a Redis server and the StackExchange.Redis client.</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

Some files were not shown because too many files have changed in this diff Show More