Merge remote-tracking branch 'SignalR/rybrande/masterToSrc' into rybrande/MondoMaster
This commit is contained in:
commit
f31ad5f9bf
|
|
@ -1,7 +1,5 @@
|
||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.2' ">$(MicrosoftNETCoreApp22PackageVersion)</RuntimeFrameworkVersion>
|
<RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp3.0' ">$(MicrosoftNETCoreAppPackageVersion)</RuntimeFrameworkVersion>
|
||||||
<!-- aspnet/BuildTools#662 Don't police what version of NetCoreApp we use -->
|
|
||||||
<NETCoreAppMaximumVersion>99.9</NETCoreAppMaximumVersion>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.C
|
||||||
EndProject
|
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}"
|
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
|
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}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SocialWeather", "samples\SocialWeather\SocialWeather.csproj", "{8D789F94-CB74-45FD-ACE7-92AF6E55042E}"
|
||||||
EndProject
|
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}"
|
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
|
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}"
|
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
|
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}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JwtSample", "samples\JwtSample\JwtSample.csproj", "{6A7491D3-3C97-49BD-A71C-433AED657F30}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JwtClientSample", "samples\JwtClientSample\JwtClientSample.csproj", "{1A953296-E869-4DE2-A693-FD5FCDE27057}"
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{42E76F87-92B6-45AB-BF07-6B811C0F2CAC}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
|
||||||
{8D789F94-CB74-45FD-ACE7-92AF6E55042E}.Debug|Any CPU.Build.0 = 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
|
{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}.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.ActiveCfg = Release|Any CPU
|
||||||
{BE982591-F4BB-42D9-ABD4-A5D44C65971E}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
|
||||||
{6A7491D3-3C97-49BD-A71C-433AED657F30}.Debug|Any CPU.Build.0 = 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
|
{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}
|
{C4AEAB04-F341-4539-B6C0-52368FB4BF9E} = {C4BC9889-B49F-41B6-806B-F84941B2549B}
|
||||||
{AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9} = {6A35B453-52EC-48AF-89CA-D4A69800F131}
|
{AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9} = {6A35B453-52EC-48AF-89CA-D4A69800F131}
|
||||||
{42E76F87-92B6-45AB-BF07-6B811C0F2CAC} = {DA69F624-5398-4884-87E4-B816698CDE65}
|
{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}
|
{8D789F94-CB74-45FD-ACE7-92AF6E55042E} = {C4BC9889-B49F-41B6-806B-F84941B2549B}
|
||||||
{1CE2B3BE-056C-41E3-A5F5-6A1EF1D288BA} = {6A35B453-52EC-48AF-89CA-D4A69800F131}
|
{1CE2B3BE-056C-41E3-A5F5-6A1EF1D288BA} = {6A35B453-52EC-48AF-89CA-D4A69800F131}
|
||||||
{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9} = {C4BC9889-B49F-41B6-806B-F84941B2549B}
|
{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}
|
{B0243F99-2D3F-4CC6-AD71-E3F891B64724} = {DA69F624-5398-4884-87E4-B816698CDE65}
|
||||||
{E081EE41-D95F-4AD2-BC0B-4B562C0A2A47} = {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}
|
{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}
|
{6A7491D3-3C97-49BD-A71C-433AED657F30} = {C4BC9889-B49F-41B6-806B-F84941B2549B}
|
||||||
{1A953296-E869-4DE2-A693-FD5FCDE27057} = {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}
|
{0A0A6135-EA24-4307-95C2-CE1B7E164A5E} = {6A35B453-52EC-48AF-89CA-D4A69800F131}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
<TargetFramework>netcoreapp3.0</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>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
@ -14,7 +12,7 @@
|
||||||
<ItemGroup Condition="'$(BenchmarksTargetFramework)' == ''">
|
<ItemGroup Condition="'$(BenchmarksTargetFramework)' == ''">
|
||||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR\Microsoft.AspNetCore.SignalR.csproj" />
|
<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.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.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="$(MicrosoftExtensionsConfigurationCommandLinePackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="$(MicrosoftExtensionsConfigurationCommandLinePackageVersion)" />
|
||||||
|
|
@ -23,8 +21,8 @@
|
||||||
|
|
||||||
<!-- These references are used when running on the Benchmarks Server -->
|
<!-- These references are used when running on the Benchmarks Server -->
|
||||||
<ItemGroup Condition="'$(BenchmarksTargetFramework)' != ''">
|
<ItemGroup Condition="'$(BenchmarksTargetFramework)' != ''">
|
||||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="$(MicrosoftAspNetCoreAllPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.App" Version="$(MicrosoftAspNetCoreAppPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="$(MessagePackPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="$(MicrosoftAspNetCoreAppPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ namespace BenchmarkServer
|
||||||
var redisConnectionString = _config["SignalRRedis"];
|
var redisConnectionString = _config["SignalRRedis"];
|
||||||
if (!string.IsNullOrEmpty(redisConnectionString))
|
if (!string.IsNullOrEmpty(redisConnectionString))
|
||||||
{
|
{
|
||||||
signalrBuilder.AddRedis(redisConnectionString);
|
signalrBuilder.AddStackExchangeRedis(redisConnectionString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"Client": "SignalR",
|
"Client": "SignalR",
|
||||||
"Source": {
|
"Source": {
|
||||||
"Repository": "https://github.com/aspnet/SignalR.git",
|
"Repository": "https://github.com/aspnet/SignalR.git",
|
||||||
"BranchOrCommit": "release/2.2",
|
"BranchOrCommit": "dev",
|
||||||
"Project": "benchmarkapps/BenchmarkServer/BenchmarkServer.csproj"
|
"Project": "benchmarkapps/BenchmarkServer/BenchmarkServer.csproj"
|
||||||
},
|
},
|
||||||
"Connections": 10,
|
"Connections": 10,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
<RootNamespace>Microsoft.AspNetCore.SignalR.CranksRevenge</RootNamespace>
|
<RootNamespace>Microsoft.AspNetCore.SignalR.CranksRevenge</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
||||||
{
|
{
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
public int Version => 1;
|
public int Version => 1;
|
||||||
|
public int MinorVersion => 0;
|
||||||
public TransferFormat TransferFormat { get; }
|
public TransferFormat TransferFormat { get; }
|
||||||
|
|
||||||
public bool IsVersionSupported(int version)
|
public bool IsVersionSupported(int version)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
using Microsoft.AspNetCore.Http.Connections;
|
using Microsoft.AspNetCore.Http.Connections;
|
||||||
|
|
@ -15,6 +16,12 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
||||||
private NegotiationResponse _negotiateResponse;
|
private NegotiationResponse _negotiateResponse;
|
||||||
private Stream _stream;
|
private Stream _stream;
|
||||||
|
|
||||||
|
private byte[] _responseData1;
|
||||||
|
private byte[] _responseData2;
|
||||||
|
private byte[] _responseData3;
|
||||||
|
private byte[] _responseData4;
|
||||||
|
private byte[] _responseData5;
|
||||||
|
|
||||||
[GlobalSetup]
|
[GlobalSetup]
|
||||||
public void GlobalSetup()
|
public void GlobalSetup()
|
||||||
{
|
{
|
||||||
|
|
@ -35,6 +42,15 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
_stream = Stream.Null;
|
_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]
|
[Benchmark]
|
||||||
|
|
@ -51,5 +67,25 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
||||||
writer.Reset();
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -178,6 +178,7 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
||||||
public string Name => _name;
|
public string Name => _name;
|
||||||
|
|
||||||
public int Version => _innerProtocol.Version;
|
public int Version => _innerProtocol.Version;
|
||||||
|
public int MinorVersion => _innerProtocol.MinorVersion;
|
||||||
|
|
||||||
public TransferFormat TransferFormat => _innerProtocol.TransferFormat;
|
public TransferFormat TransferFormat => _innerProtocol.TransferFormat;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,7 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
public int Version => 1;
|
public int Version => 1;
|
||||||
|
public int MinorVersion => 0;
|
||||||
|
|
||||||
public TransferFormat TransferFormat => TransferFormat.Text;
|
public TransferFormat TransferFormat => TransferFormat.Text;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,5 +59,10 @@ namespace Microsoft.AspNetCore.SignalR.Microbenchmarks
|
||||||
}
|
}
|
||||||
throw new InvalidOperationException("Unexpected binder call");
|
throw new InvalidOperationException("Unexpected binder call");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Type GetStreamItemType(string streamId)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,74 +5,73 @@
|
||||||
<PropertyGroup Label="Package Versions">
|
<PropertyGroup Label="Package Versions">
|
||||||
<BenchmarkDotNetPackageVersion>0.10.13</BenchmarkDotNetPackageVersion>
|
<BenchmarkDotNetPackageVersion>0.10.13</BenchmarkDotNetPackageVersion>
|
||||||
<GoogleProtobufPackageVersion>3.1.0</GoogleProtobufPackageVersion>
|
<GoogleProtobufPackageVersion>3.1.0</GoogleProtobufPackageVersion>
|
||||||
<InternalAspNetCoreAnalyzersPackageVersion>2.2.0-rtm-181106-13</InternalAspNetCoreAnalyzersPackageVersion>
|
<InternalAspNetCoreAnalyzersPackageVersion>3.0.0-preview-181113-11</InternalAspNetCoreAnalyzersPackageVersion>
|
||||||
<InternalAspNetCoreSdkPackageVersion>2.2.0-preview2-20181011.2</InternalAspNetCoreSdkPackageVersion>
|
<InternalAspNetCoreSdkPackageVersion>3.0.0-build-20181114.5</InternalAspNetCoreSdkPackageVersion>
|
||||||
<MessagePackPackageVersion>1.7.3.4</MessagePackPackageVersion>
|
<MessagePackPackageVersion>1.7.3.4</MessagePackPackageVersion>
|
||||||
<MicrosoftAspNetCoreAllPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreAllPackageVersion>
|
<MicrosoftAspNetCoreAppPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreAppPackageVersion>
|
||||||
<MicrosoftAspNetCoreAuthenticationCookiesPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreAuthenticationCookiesPackageVersion>
|
<MicrosoftAspNetCoreAuthenticationCookiesPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreAuthenticationCookiesPackageVersion>
|
||||||
<MicrosoftAspNetCoreAuthenticationCorePackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreAuthenticationCorePackageVersion>
|
<MicrosoftAspNetCoreAuthenticationCorePackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreAuthenticationCorePackageVersion>
|
||||||
<MicrosoftAspNetCoreAuthenticationJwtBearerPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreAuthenticationJwtBearerPackageVersion>
|
<MicrosoftAspNetCoreAuthenticationJwtBearerPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreAuthenticationJwtBearerPackageVersion>
|
||||||
<MicrosoftAspNetCoreAuthorizationPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreAuthorizationPackageVersion>
|
<MicrosoftAspNetCoreAuthorizationPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreAuthorizationPackageVersion>
|
||||||
<MicrosoftAspNetCoreAuthorizationPolicyPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreAuthorizationPolicyPackageVersion>
|
<MicrosoftAspNetCoreAuthorizationPolicyPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreAuthorizationPolicyPackageVersion>
|
||||||
<MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>2.2.0-rtm-181106-13</MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>
|
<MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>
|
||||||
<MicrosoftAspNetCoreConnectionsAbstractionsPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreConnectionsAbstractionsPackageVersion>
|
<MicrosoftAspNetCoreConnectionsAbstractionsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreConnectionsAbstractionsPackageVersion>
|
||||||
<MicrosoftAspNetCoreCorsPackageVersion>2.2.0-preview3-35457</MicrosoftAspNetCoreCorsPackageVersion>
|
<MicrosoftAspNetCoreCorsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreCorsPackageVersion>
|
||||||
<MicrosoftAspNetCoreDiagnosticsEntityFrameworkCorePackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreDiagnosticsEntityFrameworkCorePackageVersion>
|
<MicrosoftAspNetCoreDiagnosticsEntityFrameworkCorePackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreDiagnosticsEntityFrameworkCorePackageVersion>
|
||||||
<MicrosoftAspNetCoreDiagnosticsPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreDiagnosticsPackageVersion>
|
<MicrosoftAspNetCoreDiagnosticsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreDiagnosticsPackageVersion>
|
||||||
<MicrosoftAspNetCoreHostingAbstractionsPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
|
<MicrosoftAspNetCoreHostingAbstractionsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
|
||||||
<MicrosoftAspNetCoreHostingPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreHostingPackageVersion>
|
<MicrosoftAspNetCoreHostingPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHostingPackageVersion>
|
||||||
<MicrosoftAspNetCoreHttpAbstractionsPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreHttpAbstractionsPackageVersion>
|
<MicrosoftAspNetCoreHttpAbstractionsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHttpAbstractionsPackageVersion>
|
||||||
<MicrosoftAspNetCoreHttpFeaturesPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreHttpFeaturesPackageVersion>
|
<MicrosoftAspNetCoreHttpFeaturesPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHttpFeaturesPackageVersion>
|
||||||
<MicrosoftAspNetCoreHttpPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreHttpPackageVersion>
|
<MicrosoftAspNetCoreHttpPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreHttpPackageVersion>
|
||||||
<MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>
|
<MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>
|
||||||
<MicrosoftAspNetCoreMvcPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreMvcPackageVersion>
|
<MicrosoftAspNetCoreMvcPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreMvcPackageVersion>
|
||||||
<MicrosoftAspNetCoreRoutingPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreRoutingPackageVersion>
|
<MicrosoftAspNetCoreRoutingPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreRoutingPackageVersion>
|
||||||
<MicrosoftAspNetCoreServerIISIntegrationPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
|
<MicrosoftAspNetCoreServerIISIntegrationPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
|
||||||
<MicrosoftAspNetCoreServerKestrelPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreServerKestrelPackageVersion>
|
<MicrosoftAspNetCoreServerKestrelPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreServerKestrelPackageVersion>
|
||||||
<MicrosoftAspNetCoreStaticFilesPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreStaticFilesPackageVersion>
|
<MicrosoftAspNetCoreStaticFilesPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreStaticFilesPackageVersion>
|
||||||
<MicrosoftAspNetCoreTestHostPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreTestHostPackageVersion>
|
<MicrosoftAspNetCoreTestHostPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreTestHostPackageVersion>
|
||||||
<MicrosoftAspNetCoreTestingPackageVersion>2.2.0-rtm-181106-13</MicrosoftAspNetCoreTestingPackageVersion>
|
<MicrosoftAspNetCoreTestingPackageVersion>3.0.0-preview-181113-11</MicrosoftAspNetCoreTestingPackageVersion>
|
||||||
<MicrosoftAspNetCoreWebSocketsPackageVersion>2.2.0-rtm-35661</MicrosoftAspNetCoreWebSocketsPackageVersion>
|
<MicrosoftAspNetCoreWebSocketsPackageVersion>3.0.0-alpha1-10742</MicrosoftAspNetCoreWebSocketsPackageVersion>
|
||||||
<MicrosoftCSharpPackageVersion>4.5.0</MicrosoftCSharpPackageVersion>
|
<MicrosoftCSharpPackageVersion>4.6.0-preview1-26907-04</MicrosoftCSharpPackageVersion>
|
||||||
<MicrosoftEntityFrameworkCoreDesignPackageVersion>2.2.0-rtm-35661</MicrosoftEntityFrameworkCoreDesignPackageVersion>
|
<MicrosoftEntityFrameworkCoreDesignPackageVersion>3.0.0-preview-181109-02</MicrosoftEntityFrameworkCoreDesignPackageVersion>
|
||||||
<MicrosoftEntityFrameworkCoreSqlServerPackageVersion>2.2.0-rtm-35661</MicrosoftEntityFrameworkCoreSqlServerPackageVersion>
|
<MicrosoftEntityFrameworkCoreSqlServerPackageVersion>3.0.0-preview-181109-02</MicrosoftEntityFrameworkCoreSqlServerPackageVersion>
|
||||||
<MicrosoftEntityFrameworkCoreToolsPackageVersion>2.2.0-rtm-35661</MicrosoftEntityFrameworkCoreToolsPackageVersion>
|
<MicrosoftEntityFrameworkCoreToolsPackageVersion>3.0.0-preview-181109-02</MicrosoftEntityFrameworkCoreToolsPackageVersion>
|
||||||
<MicrosoftExtensionsBuffersTestingSourcesPackageVersion>2.2.0-rtm-35661</MicrosoftExtensionsBuffersTestingSourcesPackageVersion>
|
<MicrosoftExtensionsBuffersTestingSourcesPackageVersion>3.0.0-alpha1-10727</MicrosoftExtensionsBuffersTestingSourcesPackageVersion>
|
||||||
<MicrosoftExtensionsClosedGenericMatcherSourcesPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsClosedGenericMatcherSourcesPackageVersion>
|
<MicrosoftExtensionsClosedGenericMatcherSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsClosedGenericMatcherSourcesPackageVersion>
|
||||||
<MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>
|
<MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>
|
||||||
<MicrosoftExtensionsConfigurationCommandLinePackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsConfigurationCommandLinePackageVersion>
|
<MicrosoftExtensionsConfigurationCommandLinePackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsConfigurationCommandLinePackageVersion>
|
||||||
<MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>
|
<MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>
|
||||||
<MicrosoftExtensionsConfigurationUserSecretsPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsConfigurationUserSecretsPackageVersion>
|
<MicrosoftExtensionsConfigurationUserSecretsPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsConfigurationUserSecretsPackageVersion>
|
||||||
<MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>
|
<MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsDependencyInjectionAbstractionsPackageVersion>
|
||||||
<MicrosoftExtensionsDependencyInjectionPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsDependencyInjectionPackageVersion>
|
<MicrosoftExtensionsDependencyInjectionPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsDependencyInjectionPackageVersion>
|
||||||
<MicrosoftExtensionsLoggingAbstractionsPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
|
<MicrosoftExtensionsLoggingAbstractionsPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
|
||||||
<MicrosoftExtensionsLoggingConfigurationPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsLoggingConfigurationPackageVersion>
|
<MicrosoftExtensionsLoggingConfigurationPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingConfigurationPackageVersion>
|
||||||
<MicrosoftExtensionsLoggingConsolePackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsLoggingConsolePackageVersion>
|
<MicrosoftExtensionsLoggingConsolePackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingConsolePackageVersion>
|
||||||
<MicrosoftExtensionsLoggingDebugPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsLoggingDebugPackageVersion>
|
<MicrosoftExtensionsLoggingDebugPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingDebugPackageVersion>
|
||||||
<MicrosoftExtensionsLoggingPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsLoggingPackageVersion>
|
<MicrosoftExtensionsLoggingPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingPackageVersion>
|
||||||
<MicrosoftExtensionsLoggingTestingPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsLoggingTestingPackageVersion>
|
<MicrosoftExtensionsLoggingTestingPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsLoggingTestingPackageVersion>
|
||||||
<MicrosoftExtensionsObjectMethodExecutorSourcesPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsObjectMethodExecutorSourcesPackageVersion>
|
<MicrosoftExtensionsObjectMethodExecutorSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsObjectMethodExecutorSourcesPackageVersion>
|
||||||
<MicrosoftExtensionsOptionsPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsOptionsPackageVersion>
|
<MicrosoftExtensionsOptionsPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsOptionsPackageVersion>
|
||||||
<MicrosoftExtensionsSecurityHelperSourcesPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsSecurityHelperSourcesPackageVersion>
|
<MicrosoftExtensionsSecurityHelperSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsSecurityHelperSourcesPackageVersion>
|
||||||
<MicrosoftExtensionsValueStopwatchSourcesPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsValueStopwatchSourcesPackageVersion>
|
<MicrosoftExtensionsValueStopwatchSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsValueStopwatchSourcesPackageVersion>
|
||||||
<MicrosoftExtensionsWebEncodersSourcesPackageVersion>2.2.0-rtm-181106-13</MicrosoftExtensionsWebEncodersSourcesPackageVersion>
|
<MicrosoftExtensionsWebEncodersSourcesPackageVersion>3.0.0-preview-181113-11</MicrosoftExtensionsWebEncodersSourcesPackageVersion>
|
||||||
<MicrosoftNETCoreApp22PackageVersion>2.2.0-rtm-27105-02</MicrosoftNETCoreApp22PackageVersion>
|
<MicrosoftNETCoreAppPackageVersion>3.0.0-preview1-26907-05</MicrosoftNETCoreAppPackageVersion>
|
||||||
<MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
|
<MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
|
||||||
<MoqPackageVersion>4.10.0</MoqPackageVersion>
|
<MoqPackageVersion>4.10.0</MoqPackageVersion>
|
||||||
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
|
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
|
||||||
<NewtonsoftJsonPackageVersion>11.0.2</NewtonsoftJsonPackageVersion>
|
<NewtonsoftJsonPackageVersion>11.0.2</NewtonsoftJsonPackageVersion>
|
||||||
<StackExchangeRedisPackageVersion>2.0.513</StackExchangeRedisPackageVersion>
|
<StackExchangeRedisPackageVersion>2.0.513</StackExchangeRedisPackageVersion>
|
||||||
<StackExchangeRedisStrongNamePackageVersion>1.2.6</StackExchangeRedisStrongNamePackageVersion>
|
<SystemBuffersPackageVersion>4.6.0-preview1-26907-04</SystemBuffersPackageVersion>
|
||||||
<SystemBuffersPackageVersion>4.5.0</SystemBuffersPackageVersion>
|
<SystemIOPipelinesPackageVersion>4.6.0-preview1-26907-04</SystemIOPipelinesPackageVersion>
|
||||||
<SystemIOPipelinesPackageVersion>4.5.2</SystemIOPipelinesPackageVersion>
|
<SystemMemoryPackageVersion>4.6.0-preview1-26717-04</SystemMemoryPackageVersion>
|
||||||
<SystemMemoryPackageVersion>4.5.1</SystemMemoryPackageVersion>
|
<SystemNumericsVectorsPackageVersion>4.6.0-preview1-26907-04</SystemNumericsVectorsPackageVersion>
|
||||||
<SystemNumericsVectorsPackageVersion>4.5.0</SystemNumericsVectorsPackageVersion>
|
|
||||||
<SystemReactiveLinqPackageVersion>3.1.1</SystemReactiveLinqPackageVersion>
|
<SystemReactiveLinqPackageVersion>3.1.1</SystemReactiveLinqPackageVersion>
|
||||||
<SystemReflectionEmitPackageVersion>4.3.0</SystemReflectionEmitPackageVersion>
|
<SystemReflectionEmitPackageVersion>4.3.0</SystemReflectionEmitPackageVersion>
|
||||||
<SystemRuntimeCompilerServicesUnsafePackageVersion>4.5.1</SystemRuntimeCompilerServicesUnsafePackageVersion>
|
<SystemRuntimeCompilerServicesUnsafePackageVersion>4.6.0-preview1-26907-04</SystemRuntimeCompilerServicesUnsafePackageVersion>
|
||||||
<SystemSecurityPrincipalWindowsPackageVersion>4.5.0</SystemSecurityPrincipalWindowsPackageVersion>
|
<SystemSecurityPrincipalWindowsPackageVersion>4.6.0-preview1-26907-04</SystemSecurityPrincipalWindowsPackageVersion>
|
||||||
<SystemThreadingChannelsPackageVersion>4.5.0</SystemThreadingChannelsPackageVersion>
|
<SystemThreadingChannelsPackageVersion>4.6.0-preview1-26907-04</SystemThreadingChannelsPackageVersion>
|
||||||
<SystemThreadingTasksExtensionsPackageVersion>4.5.1</SystemThreadingTasksExtensionsPackageVersion>
|
<SystemThreadingTasksExtensionsPackageVersion>4.6.0-preview1-26907-04</SystemThreadingTasksExtensionsPackageVersion>
|
||||||
<XunitAssertPackageVersion>2.3.1</XunitAssertPackageVersion>
|
<XunitAssertPackageVersion>2.3.1</XunitAssertPackageVersion>
|
||||||
<XunitExtensibilityCorePackageVersion>2.3.1</XunitExtensibilityCorePackageVersion>
|
<XunitExtensibilityCorePackageVersion>2.3.1</XunitExtensibilityCorePackageVersion>
|
||||||
<XunitPackageVersion>2.3.1</XunitPackageVersion>
|
<XunitPackageVersion>2.3.1</XunitPackageVersion>
|
||||||
|
|
|
||||||
|
|
@ -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
|
# De-Powershell the path
|
||||||
$RootDirectory = (Convert-Path $RootDirectory)
|
$RootDirectory = (Convert-Path $RootDirectory)
|
||||||
|
|
@ -45,4 +45,4 @@ $Apps.Keys | ForEach-Object {
|
||||||
} finally {
|
} finally {
|
||||||
Remove-Item $MetadataPath
|
Remove-Item $MetadataPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,10 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- These properties are use by the automation that updates dependencies.props -->
|
<!-- These properties are use by the automation that updates dependencies.props -->
|
||||||
<LineupPackageId>Internal.AspNetCore.Universe.Lineup</LineupPackageId>
|
<LineupPackageId>Internal.AspNetCore.Universe.Lineup</LineupPackageId>
|
||||||
<LineupPackageVersion>2.2.0-*</LineupPackageVersion>
|
|
||||||
<LineupPackageRestoreSource>https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json</LineupPackageRestoreSource>
|
<LineupPackageRestoreSource>https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json</LineupPackageRestoreSource>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp22PackageVersion)" />
|
<DotNetCoreRuntime Include="$(MicrosoftNETCoreAppPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ dependencies {
|
||||||
testCompile 'org.slf4j:slf4j-jdk14:1.7.25'
|
testCompile 'org.slf4j:slf4j-jdk14:1.7.25'
|
||||||
implementation 'com.google.code.gson:gson:2.8.5'
|
implementation 'com.google.code.gson:gson:2.8.5'
|
||||||
implementation 'com.squareup.okhttp3:okhttp:3.11.0'
|
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'
|
implementation 'org.slf4j:slf4j-api:1.7.25'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,9 +21,9 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import io.reactivex.Completable;
|
import io.reactivex.Completable;
|
||||||
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
import io.reactivex.subjects.CompletableSubject;
|
import io.reactivex.subjects.*;
|
||||||
import io.reactivex.subjects.SingleSubject;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A connection used to invoke hub methods on a SignalR Server.
|
* A connection used to invoke hub methods on a SignalR Server.
|
||||||
|
|
@ -58,7 +58,6 @@ public class HubConnection {
|
||||||
private long handshakeResponseTimeout = 15*1000;
|
private long handshakeResponseTimeout = 15*1000;
|
||||||
private final Logger logger = LoggerFactory.getLogger(HubConnection.class);
|
private final Logger logger = LoggerFactory.getLogger(HubConnection.class);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the server timeout interval for the connection.
|
* Sets the server timeout interval for the connection.
|
||||||
*
|
*
|
||||||
|
|
@ -202,8 +201,17 @@ public class HubConnection {
|
||||||
}
|
}
|
||||||
irq.complete(completionMessage);
|
irq.complete(completionMessage);
|
||||||
break;
|
break;
|
||||||
case STREAM_INVOCATION:
|
|
||||||
case STREAM_ITEM:
|
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:
|
case CANCEL_INVOCATION:
|
||||||
logger.error("This client does not support {} messages.", message.getMessageType());
|
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
|
// forward the invocation result or error to the user
|
||||||
// run continuations on a separate thread
|
// run continuations on a separate thread
|
||||||
Single<Object> pendingCall = irq.getPendingCall();
|
Subject<Object> pendingCall = irq.getPendingCall();
|
||||||
pendingCall.subscribe(result -> {
|
pendingCall.subscribe(result -> {
|
||||||
// Primitive types can't be cast with the Class cast function
|
// Primitive types can't be cast with the Class cast function
|
||||||
if (returnType.isPrimitive()) {
|
if (returnType.isPrimitive()) {
|
||||||
|
|
@ -498,10 +506,54 @@ public class HubConnection {
|
||||||
return subject;
|
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) {
|
private void sendHubMessage(HubMessage message) {
|
||||||
String serializedMessage = protocol.writeMessage(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());
|
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 {
|
} else {
|
||||||
logger.debug("Sending {} message.", message.getMessageType().name());
|
logger.debug("Sending {} message.", message.getMessageType().name());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,12 @@ package com.microsoft.signalr;
|
||||||
|
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
|
|
||||||
import io.reactivex.Single;
|
import io.reactivex.subjects.ReplaySubject;
|
||||||
import io.reactivex.subjects.SingleSubject;
|
import io.reactivex.subjects.Subject;
|
||||||
|
|
||||||
class InvocationRequest {
|
class InvocationRequest {
|
||||||
private final Class<?> returnType;
|
private final Class<?> returnType;
|
||||||
private final SingleSubject<Object> pendingCall = SingleSubject.create();
|
private final Subject<Object> pendingCall = ReplaySubject.create();
|
||||||
private final String invocationId;
|
private final String invocationId;
|
||||||
|
|
||||||
InvocationRequest(Class<?> returnType, String invocationId) {
|
InvocationRequest(Class<?> returnType, String invocationId) {
|
||||||
|
|
@ -19,13 +19,22 @@ class InvocationRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void complete(CompletionMessage completion) {
|
public void complete(CompletionMessage completion) {
|
||||||
if (completion.getResult() != null) {
|
if (completion.getError() == null) {
|
||||||
pendingCall.onSuccess(completion.getResult());
|
if (completion.getResult() != null) {
|
||||||
|
pendingCall.onNext(completion.getResult());
|
||||||
|
}
|
||||||
|
pendingCall.onComplete();
|
||||||
} else {
|
} else {
|
||||||
pendingCall.onError(new HubException(completion.getError()));
|
pendingCall.onError(new HubException(completion.getError()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addItem(StreamItem streamItem) {
|
||||||
|
if (streamItem.getItem() != null) {
|
||||||
|
pendingCall.onNext(streamItem.getItem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void fail(Exception ex) {
|
public void fail(Exception ex) {
|
||||||
pendingCall.onError(ex);
|
pendingCall.onError(ex);
|
||||||
}
|
}
|
||||||
|
|
@ -34,7 +43,7 @@ class InvocationRequest {
|
||||||
pendingCall.onError(new CancellationException("Invocation was canceled."));
|
pendingCall.onError(new CancellationException("Invocation was canceled."));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Single<Object> getPendingCall() {
|
public Subject<Object> getPendingCall() {
|
||||||
return pendingCall;
|
return pendingCall;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,15 +73,13 @@ class JsonHubProtocol implements HubProtocol {
|
||||||
error = reader.nextString();
|
error = reader.nextString();
|
||||||
break;
|
break;
|
||||||
case "result":
|
case "result":
|
||||||
if (invocationId == null) {
|
case "item":
|
||||||
|
if (invocationId == null || binder.getReturnType(invocationId) == null) {
|
||||||
resultToken = jsonParser.parse(reader);
|
resultToken = jsonParser.parse(reader);
|
||||||
} else {
|
} else {
|
||||||
result = gson.fromJson(reader, binder.getReturnType(invocationId));
|
result = gson.fromJson(reader, binder.getReturnType(invocationId));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "item":
|
|
||||||
reader.skipValue();
|
|
||||||
break;
|
|
||||||
case "arguments":
|
case "arguments":
|
||||||
if (target != null) {
|
if (target != null) {
|
||||||
boolean startedArray = false;
|
boolean startedArray = false;
|
||||||
|
|
@ -142,12 +140,19 @@ class JsonHubProtocol implements HubProtocol {
|
||||||
break;
|
break;
|
||||||
case COMPLETION:
|
case COMPLETION:
|
||||||
if (resultToken != null) {
|
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));
|
hubMessages.add(new CompletionMessage(invocationId, result, error));
|
||||||
break;
|
break;
|
||||||
case STREAM_INVOCATION:
|
|
||||||
case STREAM_ITEM:
|
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:
|
case CANCEL_INVOCATION:
|
||||||
throw new UnsupportedOperationException(String.format("The message type %s is not supported yet.", messageType));
|
throw new UnsupportedOperationException(String.format("The message type %s is not supported yet.", messageType));
|
||||||
case PING:
|
case PING:
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,28 @@
|
||||||
|
|
||||||
package com.microsoft.signalr;
|
package com.microsoft.signalr;
|
||||||
|
|
||||||
class StreamInvocationMessage extends InvocationMessage {
|
final class StreamInvocationMessage extends HubMessage {
|
||||||
private final int type = HubMessageType.STREAM_INVOCATION.value;
|
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) {
|
public StreamInvocationMessage(String invocationId, String target, Object[] args) {
|
||||||
super(invocationId, target, arguments);
|
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
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ package com.microsoft.signalr;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
@ -15,7 +16,9 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
|
import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.subjects.SingleSubject;
|
import io.reactivex.subjects.SingleSubject;
|
||||||
|
|
||||||
class HubConnectionTest {
|
class HubConnectionTest {
|
||||||
|
|
@ -367,6 +370,222 @@ class HubConnectionTest {
|
||||||
assertEquals(Double.valueOf(24), value.get());
|
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
|
@Test
|
||||||
public void invokeWaitsForCompletionMessage() {
|
public void invokeWaitsForCompletionMessage() {
|
||||||
MockTransport mockTransport = new MockTransport();
|
MockTransport mockTransport = new MockTransport();
|
||||||
|
|
|
||||||
|
|
@ -108,15 +108,6 @@ class JsonHubProtocolTest {
|
||||||
assertEquals(42, messageResult);
|
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
|
@Test
|
||||||
public void parseSingleUnsupportedStreamInvocationMessage() {
|
public void parseSingleUnsupportedStreamInvocationMessage() {
|
||||||
String stringifiedMessage = "{\"type\":4,\"Id\":1,\"target\":\"test\",\"arguments\":[42]}\u001E";
|
String stringifiedMessage = "{\"type\":4,\"Id\":1,\"target\":\"test\",\"arguments\":[42]}\u001E";
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import java.util.Scanner;
|
||||||
import com.microsoft.signalr.HubConnection;
|
import com.microsoft.signalr.HubConnection;
|
||||||
import com.microsoft.signalr.HubConnectionBuilder;
|
import com.microsoft.signalr.HubConnectionBuilder;
|
||||||
|
|
||||||
|
|
||||||
public class Chat {
|
public class Chat {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
System.out.println("Enter the URL of the SignalR Chat you want to join");
|
System.out.println("Enter the URL of the SignalR Chat you want to join");
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
<TypeScriptCompileBlocked>True</TypeScriptCompileBlocked>
|
<TypeScriptCompileBlocked>True</TypeScriptCompileBlocked>
|
||||||
<TypeScriptToolsVersion>2.8</TypeScriptToolsVersion>
|
<TypeScriptToolsVersion>2.8</TypeScriptToolsVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -1827,12 +1827,14 @@
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
|
|
@ -1847,17 +1849,20 @@
|
||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
|
@ -1974,7 +1979,8 @@
|
||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
|
|
@ -1986,6 +1992,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -2000,6 +2007,7 @@
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
|
|
@ -2007,12 +2015,14 @@
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.1",
|
"safe-buffer": "^5.1.1",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
|
|
@ -2031,6 +2041,7 @@
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
|
|
@ -2111,7 +2122,8 @@
|
||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
|
|
@ -2123,6 +2135,7 @@
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
|
|
@ -2244,6 +2257,7 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ function runJest(httpsUrl: string, httpUrl: string) {
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
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}`);
|
debug(`Launching Functional Test Server: ${serverPath}`);
|
||||||
let desiredServerUrl = "https://127.0.0.1:0;http://127.0.0.1:0";
|
let desiredServerUrl = "https://127.0.0.1:0;http://127.0.0.1:0";
|
||||||
|
|
|
||||||
|
|
@ -2601,7 +2601,8 @@
|
||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"aproba": {
|
"aproba": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
|
@ -3016,7 +3017,8 @@
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
|
|
@ -3072,6 +3074,7 @@
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-regex": "^2.0.0"
|
"ansi-regex": "^2.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -3115,12 +3118,14 @@
|
||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"yallist": {
|
"yallist": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@aspnet/signalr-protocol-msgpack",
|
"name": "@aspnet/signalr-protocol-msgpack",
|
||||||
"version": "1.1.0-rtm-t000",
|
"version": "3.0.0-alpha1-t000",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@aspnet/signalr",
|
"name": "@aspnet/signalr",
|
||||||
"version": "1.1.0-rtm-t000",
|
"version": "3.0.0-alpha1-t000",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export interface HandshakeRequestMessage {
|
||||||
/** @private */
|
/** @private */
|
||||||
export interface HandshakeResponseMessage {
|
export interface HandshakeResponseMessage {
|
||||||
readonly error: string;
|
readonly error: string;
|
||||||
|
readonly minorVersion: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
<TargetFrameworks>netcoreapp3.0;net461</TargetFrameworks>
|
||||||
<!-- Don't create a NuGet package -->
|
<!-- Don't create a NuGet package -->
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ namespace ClientSample
|
||||||
|
|
||||||
RawSample.Register(app);
|
RawSample.Register(app);
|
||||||
HubSample.Register(app);
|
HubSample.Register(app);
|
||||||
|
StreamingSample.Register(app);
|
||||||
|
UploadSample.Register(app);
|
||||||
|
|
||||||
app.Command("help", cmd =>
|
app.Command("help", cmd =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,7 @@ namespace ClientSample
|
||||||
|
|
||||||
public SocketAwaitable ReceiveAsync(Memory<byte> buffer)
|
public SocketAwaitable ReceiveAsync(Memory<byte> buffer)
|
||||||
{
|
{
|
||||||
#if NETCOREAPP2_2
|
#if NETCOREAPP3_0
|
||||||
_eventArgs.SetBuffer(buffer);
|
_eventArgs.SetBuffer(buffer);
|
||||||
#else
|
#else
|
||||||
var segment = buffer.GetArray();
|
var segment = buffer.GetArray();
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ namespace ClientSample
|
||||||
return SendAsync(buffers.First);
|
return SendAsync(buffers.First);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if NETCOREAPP2_2
|
#if NETCOREAPP3_0
|
||||||
if (!_eventArgs.MemoryBuffer.Equals(Memory<byte>.Empty))
|
if (!_eventArgs.MemoryBuffer.Equals(Memory<byte>.Empty))
|
||||||
#else
|
#else
|
||||||
if (_eventArgs.Buffer != null)
|
if (_eventArgs.Buffer != null)
|
||||||
|
|
@ -59,7 +59,7 @@ namespace ClientSample
|
||||||
_eventArgs.BufferList = null;
|
_eventArgs.BufferList = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if NETCOREAPP2_2
|
#if NETCOREAPP3_0
|
||||||
_eventArgs.SetBuffer(MemoryMarshal.AsMemory(memory));
|
_eventArgs.SetBuffer(MemoryMarshal.AsMemory(memory));
|
||||||
#else
|
#else
|
||||||
var segment = memory.GetArray();
|
var segment = memory.GetArray();
|
||||||
|
|
|
||||||
|
|
@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.2</TargetFrameworks>
|
<TargetFrameworks>netcoreapp3.0</TargetFrameworks>
|
||||||
<!-- Don't create a NuGet package -->
|
<!-- Don't create a NuGet package -->
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -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}'";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
<!-- Don't create a NuGet package -->
|
<!-- Don't create a NuGet package -->
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</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.Protocols.MessagePack\Microsoft.AspNetCore.SignalR.Protocols.MessagePack.csproj" />
|
||||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR\Microsoft.AspNetCore.SignalR.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.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="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(MicrosoftAspNetCoreDiagnosticsPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(MicrosoftAspNetCoreDiagnosticsPackageVersion)" />
|
||||||
|
|
@ -21,8 +21,8 @@
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Cors" Version="$(MicrosoftAspNetCoreCorsPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Cors" Version="$(MicrosoftAspNetCoreCorsPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="$(MicrosoftExtensionsConfigurationCommandLinePackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="$(MicrosoftExtensionsConfigurationCommandLinePackageVersion)" />
|
||||||
<PackageReference Include="Google.Protobuf" Version="$(GoogleProtobufPackageVersion)" />
|
|
||||||
<PackageReference Include="System.Reactive.Linq" Version="$(SystemReactiveLinqPackageVersion)" />
|
<PackageReference Include="System.Reactive.Linq" Version="$(SystemReactiveLinqPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.CSharp" Version="$(MicrosoftCSharpPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="CopyTSClient" BeforeTargets="AfterBuild">
|
<Target Name="CopyTSClient" BeforeTargets="AfterBuild">
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ namespace SignalRSamples
|
||||||
options.KeepAliveInterval = TimeSpan.FromSeconds(5);
|
options.KeepAliveInterval = TimeSpan.FromSeconds(5);
|
||||||
})
|
})
|
||||||
.AddMessagePackProtocol();
|
.AddMessagePackProtocol();
|
||||||
//.AddRedis();
|
//.AddStackExchangeRedis();
|
||||||
|
|
||||||
services.AddCors(o =>
|
services.AddCors(o =>
|
||||||
{
|
{
|
||||||
|
|
@ -60,6 +60,7 @@ namespace SignalRSamples
|
||||||
routes.MapHub<DynamicChat>("/dynamic");
|
routes.MapHub<DynamicChat>("/dynamic");
|
||||||
routes.MapHub<Chat>("/default");
|
routes.MapHub<Chat>("/default");
|
||||||
routes.MapHub<Streaming>("/streaming");
|
routes.MapHub<Streaming>("/streaming");
|
||||||
|
routes.MapHub<UploadHub>("/uploading");
|
||||||
routes.MapHub<HubTChat>("/hubT");
|
routes.MapHub<HubTChat>("/hubT");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
<!-- Don't create a NuGet package -->
|
<!-- Don't create a NuGet package -->
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<!-- Don't create a NuGet package -->
|
<!-- Don't create a NuGet package -->
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
|
|
||||||
|
|
@ -293,7 +293,7 @@ namespace Microsoft.AspNetCore.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if NETCOREAPP2_2
|
#if NETCOREAPP3_0
|
||||||
public override void Write(ReadOnlySpan<byte> span)
|
public override void Write(ReadOnlySpan<byte> span)
|
||||||
{
|
{
|
||||||
if (_currentSegment != null && span.TryCopyTo(_currentSegment.AsSpan(_position)))
|
if (_currentSegment != null && span.TryCopyTo(_currentSegment.AsSpan(_position)))
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ namespace System.IO.Pipelines
|
||||||
return WriteCoreAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
|
return WriteCoreAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
#if NETCOREAPP2_2
|
#if NETCOREAPP3_0
|
||||||
public override ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
|
public override ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return WriteCoreAsync(source, cancellationToken);
|
return WriteCoreAsync(source, cancellationToken);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ namespace System.IO
|
||||||
{
|
{
|
||||||
if (buffer.IsSingleSegment)
|
if (buffer.IsSingleSegment)
|
||||||
{
|
{
|
||||||
#if NETCOREAPP2_2
|
#if NETCOREAPP3_0
|
||||||
return stream.WriteAsync(buffer.First, cancellationToken);
|
return stream.WriteAsync(buffer.First, cancellationToken);
|
||||||
#else
|
#else
|
||||||
var isArray = MemoryMarshal.TryGetArray(buffer.First, out var arraySegment);
|
var isArray = MemoryMarshal.TryGetArray(buffer.First, out var arraySegment);
|
||||||
|
|
@ -33,7 +33,7 @@ namespace System.IO
|
||||||
var position = buffer.Start;
|
var position = buffer.Start;
|
||||||
while (buffer.TryGet(ref position, out var segment))
|
while (buffer.TryGet(ref position, out var segment))
|
||||||
{
|
{
|
||||||
#if NETCOREAPP2_2
|
#if NETCOREAPP3_0
|
||||||
await stream.WriteAsync(segment, cancellationToken);
|
await stream.WriteAsync(segment, cancellationToken);
|
||||||
#else
|
#else
|
||||||
var isArray = MemoryMarshal.TryGetArray(segment, out var arraySegment);
|
var isArray = MemoryMarshal.TryGetArray(segment, out var arraySegment);
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
var source = _utf8Buffer.First.Span;
|
var source = _utf8Buffer.First.Span;
|
||||||
var bytesUsed = 0;
|
var bytesUsed = 0;
|
||||||
var charsUsed = 0;
|
var charsUsed = 0;
|
||||||
#if NETCOREAPP2_2
|
#if NETCOREAPP3_0
|
||||||
var destination = new Span<char>(buffer, index, count);
|
var destination = new Span<char>(buffer, index, count);
|
||||||
_decoder.Convert(source, destination, false, out bytesUsed, out charsUsed, out var completed);
|
_decoder.Convert(source, destination, false, out bytesUsed, out charsUsed, out var completed);
|
||||||
#else
|
#else
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ namespace Microsoft.AspNetCore.Internal
|
||||||
// this should be an exceptional case
|
// this should be an exceptional case
|
||||||
var bytesUsed = 0;
|
var bytesUsed = 0;
|
||||||
var charsUsed = 0;
|
var charsUsed = 0;
|
||||||
#if NETCOREAPP2_2
|
#if NETCOREAPP3_0
|
||||||
_encoder.Convert(new Span<char>(&value, 1), destination, false, out charsUsed, out bytesUsed, out _);
|
_encoder.Convert(new Span<char>(&value, 1), destination, false, out charsUsed, out bytesUsed, out _);
|
||||||
#else
|
#else
|
||||||
fixed (byte* destinationBytes = &MemoryMarshal.GetReference(destination))
|
fixed (byte* destinationBytes = &MemoryMarshal.GetReference(destination))
|
||||||
|
|
@ -167,7 +167,7 @@ namespace Microsoft.AspNetCore.Internal
|
||||||
|
|
||||||
var bytesUsed = 0;
|
var bytesUsed = 0;
|
||||||
var charsUsed = 0;
|
var charsUsed = 0;
|
||||||
#if NETCOREAPP2_2
|
#if NETCOREAPP3_0
|
||||||
_encoder.Convert(buffer, destination, false, out charsUsed, out bytesUsed, out _);
|
_encoder.Convert(buffer, destination, false, out charsUsed, out bytesUsed, out _);
|
||||||
#else
|
#else
|
||||||
unsafe
|
unsafe
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ namespace System.Net.WebSockets
|
||||||
{
|
{
|
||||||
public static ValueTask SendAsync(this WebSocket webSocket, ReadOnlySequence<byte> buffer, WebSocketMessageType webSocketMessageType, CancellationToken cancellationToken = default)
|
public static ValueTask SendAsync(this WebSocket webSocket, ReadOnlySequence<byte> buffer, WebSocketMessageType webSocketMessageType, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
#if NETCOREAPP2_2
|
#if NETCOREAPP3_0
|
||||||
if (buffer.IsSingleSegment)
|
if (buffer.IsSingleSegment)
|
||||||
{
|
{
|
||||||
return webSocket.SendAsync(buffer.First, webSocketMessageType, endOfMessage: true, cancellationToken);
|
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)
|
private static async ValueTask SendMultiSegmentAsync(WebSocket webSocket, ReadOnlySequence<byte> buffer, WebSocketMessageType webSocketMessageType, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var position = buffer.Start;
|
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))
|
while (buffer.TryGet(ref position, out var segment))
|
||||||
{
|
{
|
||||||
#if NETCOREAPP2_2
|
#if NETCOREAPP3_0
|
||||||
await webSocket.SendAsync(segment, webSocketMessageType, endOfMessage: false, cancellationToken);
|
await webSocket.SendAsync(prevSegment, webSocketMessageType, endOfMessage: false, cancellationToken);
|
||||||
#else
|
#else
|
||||||
var isArray = MemoryMarshal.TryGetArray(segment, out var arraySegment);
|
var isArray = MemoryMarshal.TryGetArray(prevSegment, out var arraySegment);
|
||||||
Debug.Assert(isArray);
|
Debug.Assert(isArray);
|
||||||
await webSocket.SendAsync(arraySegment, webSocketMessageType, endOfMessage: false, cancellationToken);
|
await webSocket.SendAsync(arraySegment, webSocketMessageType, endOfMessage: false, cancellationToken);
|
||||||
#endif
|
#endif
|
||||||
|
prevSegment = segment;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty end of message frame
|
// End of message frame
|
||||||
#if NETCOREAPP2_2
|
#if NETCOREAPP3_0
|
||||||
await webSocket.SendAsync(Memory<byte>.Empty, webSocketMessageType, endOfMessage: true, cancellationToken);
|
await webSocket.SendAsync(prevSegment, webSocketMessageType, endOfMessage: true, cancellationToken);
|
||||||
#else
|
#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
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
|
||||||
private static readonly Task<string> _noAccessToken = Task.FromResult<string>(null);
|
private static readonly Task<string> _noAccessToken = Task.FromResult<string>(null);
|
||||||
|
|
||||||
private static readonly TimeSpan HttpClientTimeout = TimeSpan.FromSeconds(120);
|
private static readonly TimeSpan HttpClientTimeout = TimeSpan.FromSeconds(120);
|
||||||
#if !NETCOREAPP2_2
|
#if !NETCOREAPP3_0
|
||||||
private static readonly Version Windows8Version = new Version(6, 2);
|
private static readonly Version Windows8Version = new Version(6, 2);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
@ -573,7 +573,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
|
||||||
|
|
||||||
private static bool IsWebSocketsSupported()
|
private static bool IsWebSocketsSupported()
|
||||||
{
|
{
|
||||||
#if NETCOREAPP2_2
|
#if NETCOREAPP3_0
|
||||||
// .NET Core 2.1 and above has a managed implementation
|
// .NET Core 2.1 and above has a managed implementation
|
||||||
return true;
|
return true;
|
||||||
#else
|
#else
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal
|
||||||
|
|
||||||
Log.StartTransport(_logger, transferFormat, resolvedUrl);
|
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);
|
Log.StartedTransport(_logger);
|
||||||
|
|
||||||
|
|
@ -196,7 +204,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
#if NETCOREAPP2_2
|
#if NETCOREAPP3_0
|
||||||
var result = await socket.ReceiveAsync(Memory<byte>.Empty, CancellationToken.None);
|
var result = await socket.ReceiveAsync(Memory<byte>.Empty, CancellationToken.None);
|
||||||
|
|
||||||
if (result.MessageType == WebSocketMessageType.Close)
|
if (result.MessageType == WebSocketMessageType.Close)
|
||||||
|
|
@ -212,7 +220,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
var memory = _application.Output.GetMemory();
|
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
|
// 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);
|
var receiveResult = await socket.ReceiveAsync(memory, CancellationToken.None);
|
||||||
#else
|
#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.
|
// Exceptions are handled above where the send and receive tasks are being run.
|
||||||
var receiveResult = await socket.ReceiveAsync(arraySegment, CancellationToken.None);
|
var receiveResult = await socket.ReceiveAsync(arraySegment, CancellationToken.None);
|
||||||
#endif
|
#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)
|
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
||||||
{
|
{
|
||||||
Log.WebSocketClosed(_logger, _webSocket.CloseStatus);
|
Log.WebSocketClosed(_logger, _webSocket.CloseStatus);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Description>Client for ASP.NET Core Connection Handlers</Description>
|
<Description>Client for ASP.NET Core Connection Handlers</Description>
|
||||||
<TargetFrameworks>netstandard2.0;netcoreapp2.2</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;netcoreapp3.0</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal.Transports
|
||||||
{
|
{
|
||||||
while (!token.IsCancellationRequested)
|
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
|
// 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);
|
var result = await socket.ReceiveAsync(Memory<byte>.Empty, token);
|
||||||
|
|
||||||
|
|
@ -155,7 +155,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal.Transports
|
||||||
#endif
|
#endif
|
||||||
var memory = _application.Output.GetMemory();
|
var memory = _application.Output.GetMemory();
|
||||||
|
|
||||||
#if NETCOREAPP2_2
|
#if NETCOREAPP3_0
|
||||||
var receiveResult = await socket.ReceiveAsync(memory, token);
|
var receiveResult = await socket.ReceiveAsync(memory, token);
|
||||||
#else
|
#else
|
||||||
var isArray = MemoryMarshal.TryGetArray<byte>(memory, out var arraySegment);
|
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.
|
// Exceptions are handled above where the send and receive tasks are being run.
|
||||||
var receiveResult = await socket.ReceiveAsync(arraySegment, token);
|
var receiveResult = await socket.ReceiveAsync(arraySegment, token);
|
||||||
#endif
|
#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)
|
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Description>Components for providing real-time bi-directional communication across the Web.</Description>
|
<Description>Components for providing real-time bi-directional communication across the Web.</Description>
|
||||||
<TargetFrameworks>netstandard2.0;netcoreapp2.2</TargetFrameworks>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,18 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
private static readonly Action<ILogger, Exception> _unableToAcquireConnectionLockForPing =
|
private static readonly Action<ILogger, Exception> _unableToAcquireConnectionLockForPing =
|
||||||
LoggerMessage.Define(LogLevel.Trace, new EventId(62, "UnableToAcquireConnectionLockForPing"), "Skipping ping because a send is already in progress.");
|
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)
|
public static void PreparingNonBlockingInvocation(ILogger logger, string target, int count)
|
||||||
{
|
{
|
||||||
_preparingNonBlockingInvocation(logger, target, count, null);
|
_preparingNonBlockingInvocation(logger, target, count, null);
|
||||||
|
|
@ -496,6 +508,26 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
{
|
{
|
||||||
_unableToAcquireConnectionLockForPing(logger, null);
|
_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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
|
|
@ -38,6 +40,8 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
// This lock protects the connection state.
|
// This lock protects the connection state.
|
||||||
private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(1, 1);
|
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
|
// Persistent across all connections
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
@ -45,15 +49,19 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly IConnectionFactory _connectionFactory;
|
private readonly IConnectionFactory _connectionFactory;
|
||||||
private readonly ConcurrentDictionary<string, InvocationHandlerList> _handlers = new ConcurrentDictionary<string, InvocationHandlerList>(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, InvocationHandlerList> _handlers = new ConcurrentDictionary<string, InvocationHandlerList>(StringComparer.Ordinal);
|
||||||
|
|
||||||
private long _nextActivationServerTimeout;
|
private long _nextActivationServerTimeout;
|
||||||
private long _nextActivationSendPing;
|
private long _nextActivationSendPing;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
private bool _hasInherentKeepAlive;
|
private bool _hasInherentKeepAlive;
|
||||||
|
|
||||||
|
private CancellationToken _uploadStreamToken;
|
||||||
|
|
||||||
private readonly ConnectionLogScope _logScope;
|
private readonly ConnectionLogScope _logScope;
|
||||||
|
|
||||||
// Transient state to a connection
|
// Transient state to a connection
|
||||||
private ConnectionState _connectionState;
|
private ConnectionState _connectionState;
|
||||||
|
private int _serverProtocolMinorVersion;
|
||||||
|
|
||||||
public event Func<Exception, Task> Closed;
|
public event Func<Exception, Task> Closed;
|
||||||
|
|
||||||
|
|
@ -422,6 +430,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
CheckConnectionActive(nameof(StreamAsChannelCoreAsync));
|
CheckConnectionActive(nameof(StreamAsChannelCoreAsync));
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
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);
|
var irq = InvocationRequest.Stream(cancellationToken, returnType, _connectionState.GetNextId(), _loggerFactory, this, out channel);
|
||||||
await InvokeStreamCore(methodName, irq, args, cancellationToken);
|
await InvokeStreamCore(methodName, irq, args, cancellationToken);
|
||||||
|
|
||||||
|
|
@ -438,9 +447,84 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
return channel;
|
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)
|
private async Task<object> InvokeCoreAsyncCore(string methodName, Type returnType, object[] args, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
var readers = PackageStreamingParams(args);
|
||||||
|
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
await WaitConnectionLockAsync();
|
await WaitConnectionLockAsync();
|
||||||
|
|
||||||
|
|
@ -458,21 +542,20 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
ReleaseConnectionLock();
|
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;
|
return await invocationTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InvokeCore(string methodName, InvocationRequest irq, object[] args, CancellationToken cancellationToken)
|
private async Task InvokeCore(string methodName, InvocationRequest irq, object[] args, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
AssertConnectionValid();
|
|
||||||
|
|
||||||
Log.PreparingBlockingInvocation(_logger, irq.InvocationId, methodName, irq.ResultType.FullName, args.Length);
|
Log.PreparingBlockingInvocation(_logger, irq.InvocationId, methodName, irq.ResultType.FullName, args.Length);
|
||||||
|
|
||||||
// Client invocations are always blocking
|
// Client invocations are always blocking
|
||||||
var invocationMessage = new InvocationMessage(irq.InvocationId, methodName, args);
|
var invocationMessage = new InvocationMessage(irq.InvocationId, methodName, args);
|
||||||
|
|
||||||
Log.RegisteringInvocation(_logger, invocationMessage.InvocationId);
|
Log.RegisteringInvocation(_logger, invocationMessage.InvocationId);
|
||||||
|
|
||||||
_connectionState.AddInvocation(irq);
|
_connectionState.AddInvocation(irq);
|
||||||
|
|
||||||
// Trace the full invocation
|
// Trace the full invocation
|
||||||
|
|
@ -498,7 +581,6 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
|
|
||||||
var invocationMessage = new StreamInvocationMessage(irq.InvocationId, methodName, args);
|
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);
|
Log.RegisteringInvocation(_logger, invocationMessage.InvocationId);
|
||||||
|
|
||||||
_connectionState.AddInvocation(irq);
|
_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()...
|
// REVIEW: If a token is passed in and is canceled during FlushAsync it seems to break .Complete()...
|
||||||
await _connectionState.Connection.Transport.Output.FlushAsync();
|
await _connectionState.Connection.Transport.Output.FlushAsync();
|
||||||
|
Log.MessageSent(_logger, hubMessage);
|
||||||
|
|
||||||
// We've sent a message, so don't ping for a while
|
// We've sent a message, so don't ping for a while
|
||||||
ResetSendPing();
|
ResetSendPing();
|
||||||
|
|
||||||
Log.MessageSent(_logger, hubMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendCoreAsyncCore(string methodName, object[] args, CancellationToken cancellationToken)
|
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();
|
await WaitConnectionLockAsync();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
CheckConnectionActive(callerName);
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
CheckConnectionActive(nameof(SendCoreAsync));
|
await SendHubMessage(message, cancellationToken);
|
||||||
|
|
||||||
Log.PreparingNonBlockingInvocation(_logger, methodName, args.Length);
|
|
||||||
|
|
||||||
var invocationMessage = new InvocationMessage(null, methodName, args);
|
|
||||||
|
|
||||||
await SendHubMessage(invocationMessage, cancellationToken);
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -578,15 +665,15 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
if (!connectionState.TryRemoveInvocation(completion.InvocationId, out irq))
|
if (!connectionState.TryRemoveInvocation(completion.InvocationId, out irq))
|
||||||
{
|
{
|
||||||
Log.DroppedCompletionMessage(_logger, completion.InvocationId);
|
Log.DroppedCompletionMessage(_logger, completion.InvocationId);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
DispatchInvocationCompletion(completion, irq);
|
||||||
DispatchInvocationCompletion(completion, irq);
|
irq.Dispose();
|
||||||
irq.Dispose();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case StreamItemMessage streamItem:
|
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))
|
if (!connectionState.TryGetInvocation(streamItem.InvocationId, out irq))
|
||||||
{
|
{
|
||||||
Log.DroppedStreamMessage(_logger, streamItem.InvocationId);
|
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}");
|
$"Unable to complete handshake with the server due to an error: {message.Error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_serverProtocolMinorVersion = message.MinorVersion;
|
||||||
|
|
||||||
break;
|
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
|
// 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))
|
catch (Exception ex) when (!(ex is HubException))
|
||||||
{
|
{
|
||||||
// shutdown if we're unable to read handshake
|
|
||||||
Log.ErrorReceivingHandshakeResponse(_logger, ex);
|
Log.ErrorReceivingHandshakeResponse(_logger, ex);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
@ -767,6 +857,9 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
var timer = new TimerAwaitable(TickRate, TickRate);
|
var timer = new TimerAwaitable(TickRate, TickRate);
|
||||||
_ = TimerLoop(timer);
|
_ = TimerLoop(timer);
|
||||||
|
|
||||||
|
var uploadStreamSource = new CancellationTokenSource();
|
||||||
|
_uploadStreamToken = uploadStreamSource.Token;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
|
|
@ -820,7 +913,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// The buffer was sliced up to where it was consumed, so we can just advance to the start.
|
// 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.
|
// before yielding the read again.
|
||||||
connectionState.Connection.Transport.Input.AdvanceTo(buffer.Start, buffer.End);
|
connectionState.Connection.Transport.Input.AdvanceTo(buffer.Start, buffer.End);
|
||||||
}
|
}
|
||||||
|
|
@ -834,6 +927,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
timer.Stop();
|
timer.Stop();
|
||||||
|
uploadStreamSource.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the connectionState field
|
// 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
|
// There is no need to start a new task if there is no Closed event registered
|
||||||
if (closed != null)
|
if (closed != null)
|
||||||
{
|
{
|
||||||
|
|
||||||
// Fire-and-forget the closed event
|
// Fire-and-forget the closed event
|
||||||
_ = RunClosedEvent(closed, connectionState.CloseException);
|
_ = RunClosedEvent(closed, connectionState.CloseException);
|
||||||
}
|
}
|
||||||
|
|
@ -923,11 +1016,6 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
|
|
||||||
private void OnServerTimeout()
|
private void OnServerTimeout()
|
||||||
{
|
{
|
||||||
if (Debugger.IsAttached)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_connectionState.CloseException = new TimeoutException(
|
_connectionState.CloseException = new TimeoutException(
|
||||||
$"Server timeout ({ServerTimeout.TotalMilliseconds:0.00}ms) elapsed without receiving a message from the server.");
|
$"Server timeout ({ServerTimeout.TotalMilliseconds:0.00}ms) elapsed without receiving a message from the server.");
|
||||||
_connectionState.Connection.Transport.Input.CancelPendingRead();
|
_connectionState.Connection.Transport.Input.CancelPendingRead();
|
||||||
|
|
@ -1111,7 +1199,8 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
private TaskCompletionSource<object> _stopTcs;
|
private TaskCompletionSource<object> _stopTcs;
|
||||||
private readonly object _lock = new object();
|
private readonly object _lock = new object();
|
||||||
private readonly Dictionary<string, InvocationRequest> _pendingCalls = new Dictionary<string, InvocationRequest>(StringComparer.Ordinal);
|
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 ConnectionContext Connection { get; }
|
||||||
public Task ReceiveTask { get; set; }
|
public Task ReceiveTask { get; set; }
|
||||||
|
|
@ -1132,7 +1221,8 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
Connection = connection;
|
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)
|
public void AddInvocation(InvocationRequest irq)
|
||||||
{
|
{
|
||||||
|
|
@ -1239,6 +1329,18 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
return irq.ResultType;
|
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)
|
IReadOnlyList<Type> IInvocationBinder.GetParameterTypes(string methodName)
|
||||||
{
|
{
|
||||||
if (!_hubConnection._handlers.TryGetValue(methodName, out var invocationHandlerList))
|
if (!_hubConnection._handlers.TryGetValue(methodName, out var invocationHandlerList))
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<Compile Include="..\Common\AwaitableThreadPool.cs" Link="AwaitableThreadPool.cs" />
|
<Compile Include="..\Common\AwaitableThreadPool.cs" Link="AwaitableThreadPool.cs" />
|
||||||
<Compile Include="..\Common\ForceAsyncAwaiter.cs" Link="ForceAsyncAwaiter.cs" />
|
<Compile Include="..\Common\ForceAsyncAwaiter.cs" Link="ForceAsyncAwaiter.cs" />
|
||||||
<Compile Include="..\Common\PipeWriterStream.cs" Link="PipeWriterStream.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" />
|
<Compile Include="..\Common\TimerAwaitable.cs" Link="Internal\TimerAwaitable.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,6 @@ namespace Microsoft.AspNetCore.SignalR
|
||||||
{
|
{
|
||||||
Type GetReturnType(string invocationId);
|
Type GetReturnType(string invocationId);
|
||||||
IReadOnlyList<Type> GetParameterTypes(string methodName);
|
IReadOnlyList<Type> GetParameterTypes(string methodName);
|
||||||
|
Type GetStreamItemType(string streamId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Description>Common serialiation primitives for SignalR Clients Servers</Description>
|
<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>
|
<RootNamespace>Microsoft.AspNetCore.SignalR</RootNamespace>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
@ -23,4 +23,4 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.AspNetCore.Internal;
|
using Microsoft.AspNetCore.Internal;
|
||||||
|
|
@ -18,26 +19,31 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
{
|
{
|
||||||
private const string ProtocolPropertyName = "protocol";
|
private const string ProtocolPropertyName = "protocol";
|
||||||
private const string ProtocolVersionPropertyName = "version";
|
private const string ProtocolVersionPropertyName = "version";
|
||||||
|
private const string MinorVersionPropertyName = "minorVersion";
|
||||||
private const string ErrorPropertyName = "error";
|
private const string ErrorPropertyName = "error";
|
||||||
private const string TypePropertyName = "type";
|
private const string TypePropertyName = "type";
|
||||||
|
|
||||||
/// <summary>
|
private static ConcurrentDictionary<IHubProtocol, ReadOnlyMemory<byte>> _messageCache = new ConcurrentDictionary<IHubProtocol, ReadOnlyMemory<byte>>();
|
||||||
/// The serialized representation of a success handshake.
|
|
||||||
/// </summary>
|
|
||||||
public static ReadOnlyMemory<byte> SuccessHandshakeData;
|
|
||||||
|
|
||||||
static HandshakeProtocol()
|
public static ReadOnlySpan<byte> GetSuccessfulHandshake(IHubProtocol protocol)
|
||||||
{
|
{
|
||||||
var memoryBufferWriter = MemoryBufferWriter.Get();
|
ReadOnlyMemory<byte> result;
|
||||||
try
|
if(!_messageCache.TryGetValue(protocol, out result))
|
||||||
{
|
{
|
||||||
WriteResponseMessage(HandshakeResponseMessage.Empty, memoryBufferWriter);
|
var memoryBufferWriter = MemoryBufferWriter.Get();
|
||||||
SuccessHandshakeData = memoryBufferWriter.ToArray();
|
try
|
||||||
}
|
{
|
||||||
finally
|
WriteResponseMessage(new HandshakeResponseMessage(protocol.MinorVersion), memoryBufferWriter);
|
||||||
{
|
result = memoryBufferWriter.ToArray();
|
||||||
MemoryBufferWriter.Return(memoryBufferWriter);
|
_messageCache.TryAdd(protocol, result);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
MemoryBufferWriter.Return(memoryBufferWriter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result.Span;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -88,6 +94,9 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
writer.WriteValue(responseMessage.Error);
|
writer.WriteValue(responseMessage.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writer.WritePropertyName(MinorVersionPropertyName);
|
||||||
|
writer.WriteValue(responseMessage.MinorVersion);
|
||||||
|
|
||||||
writer.WriteEndObject();
|
writer.WriteEndObject();
|
||||||
writer.Flush();
|
writer.Flush();
|
||||||
}
|
}
|
||||||
|
|
@ -123,6 +132,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
JsonUtils.CheckRead(reader);
|
JsonUtils.CheckRead(reader);
|
||||||
JsonUtils.EnsureObjectStart(reader);
|
JsonUtils.EnsureObjectStart(reader);
|
||||||
|
|
||||||
|
int? minorVersion = null;
|
||||||
string error = null;
|
string error = null;
|
||||||
|
|
||||||
var completed = false;
|
var completed = false;
|
||||||
|
|
@ -142,6 +152,9 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
case ErrorPropertyName:
|
case ErrorPropertyName:
|
||||||
error = JsonUtils.ReadAsString(reader, ErrorPropertyName);
|
error = JsonUtils.ReadAsString(reader, ErrorPropertyName);
|
||||||
break;
|
break;
|
||||||
|
case MinorVersionPropertyName:
|
||||||
|
minorVersion = JsonUtils.ReadAsInt32(reader, MinorVersionPropertyName);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
reader.Skip();
|
reader.Skip();
|
||||||
break;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,41 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An empty response message with no error.
|
/// An empty response message with no error.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly HandshakeResponseMessage Empty = new HandshakeResponseMessage(null);
|
public static readonly HandshakeResponseMessage Empty = new HandshakeResponseMessage(error: null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the optional error message.
|
/// Gets the optional error message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Error { get; }
|
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>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="HandshakeResponseMessage"/> class.
|
/// Initializes a new instance of the <see cref="HandshakeResponseMessage"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="error">An optional response error message. A <c>null</c> error message indicates a successful handshake.</param>
|
/// <param name="error">Error encountered by the server, indicating why the handshake has failed.</param>
|
||||||
public HandshakeResponseMessage(string error)
|
/// <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;
|
Error = error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,5 +42,15 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
/// Represents the close message type.
|
/// Represents the close message type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const int CloseMessageType = 7;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,15 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
string Name { get; }
|
string Name { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the version of the protocol.
|
/// Gets the major version of the protocol.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
int Version { get; }
|
int Version { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the minor version of the protocol.
|
||||||
|
/// </summary>
|
||||||
|
int MinorVersion { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the transfer format of the protocol.
|
/// Gets the transfer format of the protocol.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>>"} }}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -32,6 +32,7 @@ namespace Microsoft.AspNetCore.SignalR
|
||||||
private readonly long _clientTimeoutInterval;
|
private readonly long _clientTimeoutInterval;
|
||||||
private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1);
|
private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1);
|
||||||
|
|
||||||
|
private StreamTracker _streamTracker;
|
||||||
private long _lastSendTimeStamp = DateTime.UtcNow.Ticks;
|
private long _lastSendTimeStamp = DateTime.UtcNow.Ticks;
|
||||||
private long _lastReceivedTimeStamp = DateTime.UtcNow.Ticks;
|
private long _lastReceivedTimeStamp = DateTime.UtcNow.Ticks;
|
||||||
private bool _receivedMessageThisInterval = false;
|
private bool _receivedMessageThisInterval = false;
|
||||||
|
|
@ -55,6 +56,18 @@ namespace Microsoft.AspNetCore.SignalR
|
||||||
_clientTimeoutInterval = clientTimeoutInterval.Ticks;
|
_clientTimeoutInterval = clientTimeoutInterval.Ticks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal StreamTracker StreamTracker
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// lazy for performance reasons
|
||||||
|
if (_streamTracker == null)
|
||||||
|
{
|
||||||
|
_streamTracker = new StreamTracker();
|
||||||
|
}
|
||||||
|
return _streamTracker;
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="HubConnectionContext"/> class.
|
/// Initializes a new instance of the <see cref="HubConnectionContext"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -302,10 +315,9 @@ namespace Microsoft.AspNetCore.SignalR
|
||||||
|
|
||||||
try
|
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.GetSuccessfulHandshake(Protocol));
|
||||||
_connectionContext.Transport.Output.Write(HandshakeProtocol.SuccessHandshakeData.Span);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -425,7 +437,8 @@ namespace Microsoft.AspNetCore.SignalR
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.HandshakeComplete(_logger, Protocol.Name);
|
Log.HandshakeComplete(_logger, Protocol.Name);
|
||||||
await WriteHandshakeResponseAsync(HandshakeResponseMessage.Empty);
|
|
||||||
|
await WriteHandshakeResponseAsync(new HandshakeResponseMessage(Protocol.MinorVersion));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,9 @@ namespace Microsoft.AspNetCore.SignalR
|
||||||
{
|
{
|
||||||
var input = connection.Input;
|
var input = connection.Input;
|
||||||
var protocol = connection.Protocol;
|
var protocol = connection.Protocol;
|
||||||
|
|
||||||
|
var binder = new HubConnectionBinder<THub>(_dispatcher, connection);
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var result = await input.ReadAsync();
|
var result = await input.ReadAsync();
|
||||||
|
|
@ -202,7 +205,7 @@ namespace Microsoft.AspNetCore.SignalR
|
||||||
{
|
{
|
||||||
connection.ResetClientTimeout();
|
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);
|
await _dispatcher.DispatchMessageAsync(connection, message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,18 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
private static readonly Action<ILogger, string, Exception> _invalidReturnValueFromStreamingMethod =
|
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}.");
|
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)
|
public static void ReceivedHubInvocation(ILogger logger, InvocationMessage invocationMessage)
|
||||||
{
|
{
|
||||||
_receivedHubInvocation(logger, invocationMessage, null);
|
_receivedHubInvocation(logger, invocationMessage, null);
|
||||||
|
|
@ -90,8 +102,11 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
|
|
||||||
public static void SendingResult(ILogger logger, string invocationId, ObjectMethodExecutor objectMethodExecutor)
|
public static void SendingResult(ILogger logger, string invocationId, ObjectMethodExecutor objectMethodExecutor)
|
||||||
{
|
{
|
||||||
var resultType = objectMethodExecutor.AsyncResultType == null ? objectMethodExecutor.MethodReturnType : objectMethodExecutor.AsyncResultType;
|
if (logger.IsEnabled(LogLevel.Trace))
|
||||||
_sendingResult(logger, invocationId, resultType.FullName, null);
|
{
|
||||||
|
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)
|
public static void FailedInvokingHubMethod(ILogger logger, string hubMethod, Exception exception)
|
||||||
|
|
@ -133,6 +148,26 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
{
|
{
|
||||||
_invalidReturnValueFromStreamingMethod(logger, hubMethod, null);
|
_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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,15 +81,18 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
switch (hubMessage)
|
switch (hubMessage)
|
||||||
{
|
{
|
||||||
case InvocationBindingFailureMessage bindingFailureMessage:
|
case InvocationBindingFailureMessage bindingFailureMessage:
|
||||||
return ProcessBindingFailure(connection, bindingFailureMessage);
|
return ProcessInvocationBindingFailure(connection, bindingFailureMessage);
|
||||||
|
|
||||||
|
case StreamBindingFailureMessage bindingFailureMessage:
|
||||||
|
return ProcessStreamBindingFailure(connection, bindingFailureMessage);
|
||||||
|
|
||||||
case InvocationMessage invocationMessage:
|
case InvocationMessage invocationMessage:
|
||||||
Log.ReceivedHubInvocation(_logger, invocationMessage);
|
Log.ReceivedHubInvocation(_logger, invocationMessage);
|
||||||
return ProcessInvocation(connection, invocationMessage, isStreamedInvocation: false);
|
return ProcessInvocation(connection, invocationMessage, isStreamResponse: false);
|
||||||
|
|
||||||
case StreamInvocationMessage streamInvocationMessage:
|
case StreamInvocationMessage streamInvocationMessage:
|
||||||
Log.ReceivedStreamHubInvocation(_logger, streamInvocationMessage);
|
Log.ReceivedStreamHubInvocation(_logger, streamInvocationMessage);
|
||||||
return ProcessInvocation(connection, streamInvocationMessage, isStreamedInvocation: true);
|
return ProcessInvocation(connection, streamInvocationMessage, isStreamResponse: true);
|
||||||
|
|
||||||
case CancelInvocationMessage cancelInvocationMessage:
|
case CancelInvocationMessage cancelInvocationMessage:
|
||||||
// Check if there is an associated active stream and cancel it if it exists.
|
// 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();
|
connection.StartClientTimeout();
|
||||||
break;
|
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
|
// Other kind of message we weren't expecting
|
||||||
default:
|
default:
|
||||||
Log.UnsupportedMessageReceived(_logger, hubMessage.GetType().FullName);
|
Log.UnsupportedMessageReceived(_logger, hubMessage.GetType().FullName);
|
||||||
|
|
@ -119,30 +133,38 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
return Task.CompletedTask;
|
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);
|
Log.FailedInvokingHubMethod(_logger, bindingFailureMessage.Target, bindingFailureMessage.BindingFailure.SourceException);
|
||||||
|
|
||||||
|
|
||||||
var errorMessage = ErrorMessageHelper.BuildErrorMessage($"Failed to invoke '{bindingFailureMessage.Target}' due to an error on the server.",
|
var errorMessage = ErrorMessageHelper.BuildErrorMessage($"Failed to invoke '{bindingFailureMessage.Target}' due to an error on the server.",
|
||||||
bindingFailureMessage.BindingFailure.SourceException, _enableDetailedErrors);
|
bindingFailureMessage.BindingFailure.SourceException, _enableDetailedErrors);
|
||||||
return SendInvocationError(bindingFailureMessage.InvocationId, connection, errorMessage);
|
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))
|
Log.ReceivedStreamItem(_logger, message);
|
||||||
{
|
return connection.StreamTracker.ProcessItem(message);
|
||||||
throw new HubException("Method does not exist.");
|
|
||||||
}
|
|
||||||
return descriptor.ParameterTypes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task ProcessInvocation(HubConnectionContext connection,
|
private Task ProcessInvocation(HubConnectionContext connection,
|
||||||
HubMethodInvocationMessage hubMethodInvocationMessage, bool isStreamedInvocation)
|
HubMethodInvocationMessage hubMethodInvocationMessage, bool isStreamResponse)
|
||||||
{
|
{
|
||||||
if (!_methods.TryGetValue(hubMethodInvocationMessage.Target, out var descriptor))
|
if (!_methods.TryGetValue(hubMethodInvocationMessage.Target, out var descriptor))
|
||||||
{
|
{
|
||||||
|
|
@ -153,12 +175,17 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
}
|
}
|
||||||
else
|
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,
|
private async Task Invoke(HubMethodDescriptor descriptor, HubConnectionContext connection,
|
||||||
HubMethodInvocationMessage hubMethodInvocationMessage, bool isStreamedInvocation)
|
HubMethodInvocationMessage hubMethodInvocationMessage, bool isStreamResponse, bool isStreamCall)
|
||||||
{
|
{
|
||||||
var methodExecutor = descriptor.MethodExecutor;
|
var methodExecutor = descriptor.MethodExecutor;
|
||||||
|
|
||||||
|
|
@ -176,7 +203,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await ValidateInvocationMode(descriptor, isStreamedInvocation, hubMethodInvocationMessage, connection))
|
if (!await ValidateInvocationMode(descriptor, isStreamResponse, hubMethodInvocationMessage, connection))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -184,9 +211,28 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
hubActivator = scope.ServiceProvider.GetRequiredService<IHubActivator<THub>>();
|
hubActivator = scope.ServiceProvider.GetRequiredService<IHubActivator<THub>>();
|
||||||
hub = hubActivator.Create();
|
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
|
try
|
||||||
{
|
{
|
||||||
InitializeHub(hub, connection);
|
InitializeHub(hub, connection);
|
||||||
|
Task invocation = null;
|
||||||
|
|
||||||
CancellationTokenSource cts = null;
|
CancellationTokenSource cts = null;
|
||||||
var arguments = hubMethodInvocationMessage.Arguments;
|
var arguments = hubMethodInvocationMessage.Arguments;
|
||||||
|
|
@ -222,29 +268,50 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await ExecuteHubMethod(methodExecutor, hub, arguments);
|
if (isStreamResponse)
|
||||||
|
|
||||||
if (isStreamedInvocation)
|
|
||||||
{
|
{
|
||||||
|
var result = await ExecuteHubMethod(methodExecutor, hub, arguments);
|
||||||
|
|
||||||
if (!TryGetStreamingEnumerator(connection, hubMethodInvocationMessage.InvocationId, descriptor, result, out var enumerator, ref cts))
|
if (!TryGetStreamingEnumerator(connection, hubMethodInvocationMessage.InvocationId, descriptor, result, out var enumerator, ref cts))
|
||||||
{
|
{
|
||||||
Log.InvalidReturnValueFromStreamingMethod(_logger, methodExecutor.MethodInfo.Name);
|
Log.InvalidReturnValueFromStreamingMethod(_logger, methodExecutor.MethodInfo.Name);
|
||||||
|
|
||||||
await SendInvocationError(hubMethodInvocationMessage.InvocationId, connection,
|
await SendInvocationError(hubMethodInvocationMessage.InvocationId, connection,
|
||||||
$"The value returned by the streaming method '{methodExecutor.MethodInfo.Name}' is not a ChannelReader<>.");
|
$"The value returned by the streaming method '{methodExecutor.MethodInfo.Name}' is not a ChannelReader<>.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
disposeScope = false;
|
|
||||||
Log.StreamingResult(_logger, hubMethodInvocationMessage.InvocationId, methodExecutor);
|
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);
|
_ = 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);
|
// Send Async, no response expected
|
||||||
await connection.WriteAsync(CompletionMessage.WithResult(hubMethodInvocationMessage.InvocationId, result));
|
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)
|
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;
|
string error = null;
|
||||||
|
|
||||||
|
|
@ -453,5 +521,14 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
Log.HubMethodBound(_logger, hubName, methodName);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,12 +8,11 @@ using Microsoft.AspNetCore.SignalR.Protocol;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.SignalR.Internal
|
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 OnConnectedAsync(HubConnectionContext connection);
|
||||||
public abstract Task OnDisconnectedAsync(HubConnectionContext connection, Exception exception);
|
public abstract Task OnDisconnectedAsync(HubConnectionContext connection, Exception exception);
|
||||||
public abstract Task DispatchMessageAsync(HubConnectionContext connection, HubMessage hubMessage);
|
public abstract Task DispatchMessageAsync(HubConnectionContext connection, HubMessage hubMessage);
|
||||||
public abstract IReadOnlyList<Type> GetParameterTypes(string methodName);
|
public abstract IReadOnlyList<Type> GetParameterTypes(string name);
|
||||||
public abstract Type GetReturnType(string invocationId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||||
using Microsoft.Extensions.Internal;
|
using Microsoft.Extensions.Internal;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.SignalR.Internal
|
namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
|
|
@ -43,7 +44,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}).Select(p => p.ParameterType).ToArray();
|
}).Select(GetParameterType).ToArray();
|
||||||
|
|
||||||
if (HasSyntheticArguments)
|
if (HasSyntheticArguments)
|
||||||
{
|
{
|
||||||
|
|
@ -53,6 +54,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
Policies = policies.ToArray();
|
Policies = policies.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool HasStreamingParameters { get; private set; }
|
||||||
|
|
||||||
private Func<object, CancellationToken, IAsyncEnumerator<object>> _convertToEnumerator;
|
private Func<object, CancellationToken, IAsyncEnumerator<object>> _convertToEnumerator;
|
||||||
|
|
||||||
public ObjectMethodExecutor MethodExecutor { get; }
|
public ObjectMethodExecutor MethodExecutor { get; }
|
||||||
|
|
@ -73,6 +76,17 @@ namespace Microsoft.AspNetCore.SignalR.Internal
|
||||||
|
|
||||||
public bool HasSyntheticArguments { get; private set; }
|
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)
|
private static bool IsChannelType(Type type, out Type payloadType)
|
||||||
{
|
{
|
||||||
var channelType = type.AllBaseTypes().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ChannelReader<>));
|
var channelType = type.AllBaseTypes().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ChannelReader<>));
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,14 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Description>Real-time communication framework for ASP.NET Core.</Description>
|
<Description>Real-time communication framework for ASP.NET Core.</Description>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
<RootNamespace>Microsoft.AspNetCore.SignalR</RootNamespace>
|
<RootNamespace>Microsoft.AspNetCore.SignalR</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\Common\ReflectionHelper.cs" Link="ReflectionHelper.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Microsoft.AspNetCore.SignalR.Common\Microsoft.AspNetCore.SignalR.Common.csproj" />
|
<ProjectReference Include="..\Microsoft.AspNetCore.SignalR.Common\Microsoft.AspNetCore.SignalR.Common.csproj" />
|
||||||
<ProjectReference Include="..\Microsoft.AspNetCore.SignalR.Protocols.Json\Microsoft.AspNetCore.SignalR.Protocols.Json.csproj" />
|
<ProjectReference Include="..\Microsoft.AspNetCore.SignalR.Protocols.Json\Microsoft.AspNetCore.SignalR.Protocols.Json.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Description>Implements the SignalR Hub Protocol over JSON.</Description>
|
<Description>Implements the SignalR Hub Protocol over JSON.</Description>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
private const string ResultPropertyName = "result";
|
private const string ResultPropertyName = "result";
|
||||||
private const string ItemPropertyName = "item";
|
private const string ItemPropertyName = "item";
|
||||||
private const string InvocationIdPropertyName = "invocationId";
|
private const string InvocationIdPropertyName = "invocationId";
|
||||||
|
private const string StreamIdPropertyName = "streamId";
|
||||||
private const string TypePropertyName = "type";
|
private const string TypePropertyName = "type";
|
||||||
private const string ErrorPropertyName = "error";
|
private const string ErrorPropertyName = "error";
|
||||||
private const string TargetPropertyName = "target";
|
private const string TargetPropertyName = "target";
|
||||||
|
|
@ -32,6 +33,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
|
|
||||||
private static readonly string ProtocolName = "json";
|
private static readonly string ProtocolName = "json";
|
||||||
private static readonly int ProtocolVersion = 1;
|
private static readonly int ProtocolVersion = 1;
|
||||||
|
private static readonly int ProtocolMinorVersion = 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the serializer used to serialize invocation arguments and return values.
|
/// Gets the serializer used to serialize invocation arguments and return values.
|
||||||
|
|
@ -60,6 +62,9 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int Version => ProtocolVersion;
|
public int Version => ProtocolVersion;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int MinorVersion => ProtocolMinorVersion;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public TransferFormat TransferFormat => TransferFormat.Text;
|
public TransferFormat TransferFormat => TransferFormat.Text;
|
||||||
|
|
||||||
|
|
@ -115,6 +120,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
|
|
||||||
int? type = null;
|
int? type = null;
|
||||||
string invocationId = null;
|
string invocationId = null;
|
||||||
|
string streamId = null;
|
||||||
string target = null;
|
string target = null;
|
||||||
string error = null;
|
string error = null;
|
||||||
var hasItem = false;
|
var hasItem = false;
|
||||||
|
|
@ -161,6 +167,9 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
case InvocationIdPropertyName:
|
case InvocationIdPropertyName:
|
||||||
invocationId = JsonUtils.ReadAsString(reader, InvocationIdPropertyName);
|
invocationId = JsonUtils.ReadAsString(reader, InvocationIdPropertyName);
|
||||||
break;
|
break;
|
||||||
|
case StreamIdPropertyName:
|
||||||
|
streamId = JsonUtils.ReadAsString(reader, StreamIdPropertyName);
|
||||||
|
break;
|
||||||
case TargetPropertyName:
|
case TargetPropertyName:
|
||||||
target = JsonUtils.ReadAsString(reader, TargetPropertyName);
|
target = JsonUtils.ReadAsString(reader, TargetPropertyName);
|
||||||
break;
|
break;
|
||||||
|
|
@ -195,15 +204,32 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
|
|
||||||
hasItem = true;
|
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
|
id = invocationId;
|
||||||
itemToken = JToken.Load(reader);
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(streamId))
|
||||||
|
{
|
||||||
|
id = streamId;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var returnType = binder.GetReturnType(invocationId);
|
// If we don't have an id yetmthen we need to store it as a JToken to parse later
|
||||||
item = PayloadSerializer.Deserialize(reader, returnType);
|
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;
|
break;
|
||||||
case ArgumentsPropertyName:
|
case ArgumentsPropertyName:
|
||||||
|
|
@ -309,11 +335,33 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
: BindStreamInvocationMessage(invocationId, target, arguments, hasArguments, binder);
|
: BindStreamInvocationMessage(invocationId, target, arguments, hasArguments, binder);
|
||||||
}
|
}
|
||||||
break;
|
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:
|
case HubProtocolConstants.StreamItemMessageType:
|
||||||
if (itemToken != null)
|
if (itemToken != null)
|
||||||
{
|
{
|
||||||
var returnType = binder.GetReturnType(invocationId);
|
var returnType = binder.GetStreamItemType(invocationId);
|
||||||
item = itemToken.ToObject(returnType, PayloadSerializer);
|
try
|
||||||
|
{
|
||||||
|
item = itemToken.ToObject(returnType, PayloadSerializer);
|
||||||
|
}
|
||||||
|
catch (JsonSerializationException ex)
|
||||||
|
{
|
||||||
|
return new StreamBindingFailureMessage(invocationId, ExceptionDispatchInfo.Capture(ex));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
message = BindStreamItemMessage(invocationId, item, hasItem, binder);
|
message = BindStreamItemMessage(invocationId, item, hasItem, binder);
|
||||||
|
|
@ -334,6 +382,9 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
return PingMessage.Instance;
|
return PingMessage.Instance;
|
||||||
case HubProtocolConstants.CloseMessageType:
|
case HubProtocolConstants.CloseMessageType:
|
||||||
return BindCloseMessage(error);
|
return BindCloseMessage(error);
|
||||||
|
case HubProtocolConstants.StreamCompleteMessageType:
|
||||||
|
message = BindStreamCompleteMessage(streamId, error);
|
||||||
|
break;
|
||||||
case null:
|
case null:
|
||||||
throw new InvalidDataException($"Missing required property '{TypePropertyName}'.");
|
throw new InvalidDataException($"Missing required property '{TypePropertyName}'.");
|
||||||
default:
|
default:
|
||||||
|
|
@ -404,6 +455,10 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
WriteHeaders(writer, m);
|
WriteHeaders(writer, m);
|
||||||
WriteStreamInvocationMessage(m, writer);
|
WriteStreamInvocationMessage(m, writer);
|
||||||
break;
|
break;
|
||||||
|
case StreamDataMessage m:
|
||||||
|
WriteMessageType(writer, HubProtocolConstants.StreamDataMessageType);
|
||||||
|
WriteStreamDataMessage(m, writer);
|
||||||
|
break;
|
||||||
case StreamItemMessage m:
|
case StreamItemMessage m:
|
||||||
WriteMessageType(writer, HubProtocolConstants.StreamItemMessageType);
|
WriteMessageType(writer, HubProtocolConstants.StreamItemMessageType);
|
||||||
WriteHeaders(writer, m);
|
WriteHeaders(writer, m);
|
||||||
|
|
@ -426,6 +481,10 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
WriteMessageType(writer, HubProtocolConstants.CloseMessageType);
|
WriteMessageType(writer, HubProtocolConstants.CloseMessageType);
|
||||||
WriteCloseMessage(m, writer);
|
WriteCloseMessage(m, writer);
|
||||||
break;
|
break;
|
||||||
|
case StreamCompleteMessage m:
|
||||||
|
WriteMessageType(writer, HubProtocolConstants.StreamCompleteMessageType);
|
||||||
|
WriteStreamCompleteMessage(m, writer);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new InvalidOperationException($"Unsupported message type: {message.GetType().FullName}");
|
throw new InvalidOperationException($"Unsupported message type: {message.GetType().FullName}");
|
||||||
}
|
}
|
||||||
|
|
@ -474,6 +533,18 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
WriteInvocationId(message, writer);
|
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)
|
private void WriteStreamItemMessage(StreamItemMessage message, JsonTextWriter writer)
|
||||||
{
|
{
|
||||||
WriteInvocationId(message, writer);
|
WriteInvocationId(message, writer);
|
||||||
|
|
@ -481,6 +552,14 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
PayloadSerializer.Serialize(writer, message.Item);
|
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)
|
private void WriteInvocationMessage(InvocationMessage message, JsonTextWriter writer)
|
||||||
{
|
{
|
||||||
WriteInvocationId(message, writer);
|
WriteInvocationId(message, writer);
|
||||||
|
|
@ -544,6 +623,17 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
return new CancelInvocationMessage(invocationId);
|
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)
|
private HubMessage BindCompletionMessage(string invocationId, string error, object result, bool hasResult, IInvocationBinder binder)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(invocationId))
|
if (string.IsNullOrEmpty(invocationId))
|
||||||
|
|
@ -564,6 +654,20 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
return new CompletionMessage(invocationId, error, result: null, hasResult: false);
|
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)
|
private HubMessage BindStreamItemMessage(string invocationId, object item, bool hasItem, IInvocationBinder binder)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(invocationId))
|
if (string.IsNullOrEmpty(invocationId))
|
||||||
|
|
@ -654,7 +758,6 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
{
|
{
|
||||||
if (paramIndex < paramCount)
|
if (paramIndex < paramCount)
|
||||||
{
|
{
|
||||||
// Set all known arguments
|
|
||||||
arguments[paramIndex] = PayloadSerializer.Deserialize(reader, paramTypes[paramIndex]);
|
arguments[paramIndex] = PayloadSerializer.Deserialize(reader, paramTypes[paramIndex]);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -29,13 +29,17 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
|
|
||||||
private static readonly string ProtocolName = "messagepack";
|
private static readonly string ProtocolName = "messagepack";
|
||||||
private static readonly int ProtocolVersion = 1;
|
private static readonly int ProtocolVersion = 1;
|
||||||
|
private static readonly int ProtocolMinorVersion = 0;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Name => ProtocolName;
|
public string Name => ProtocolName;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int Version => ProtocolVersion;
|
public int Version => ProtocolVersion;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int MinorVersion => ProtocolMinorVersion;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public TransferFormat TransferFormat => TransferFormat.Binary;
|
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)
|
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;
|
startOffset += readSize;
|
||||||
|
|
||||||
var messageType = ReadInt32(input, ref startOffset, "messageType");
|
var messageType = ReadInt32(input, ref startOffset, "messageType");
|
||||||
|
|
@ -138,6 +142,8 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
return PingMessage.Instance;
|
return PingMessage.Instance;
|
||||||
case HubProtocolConstants.CloseMessageType:
|
case HubProtocolConstants.CloseMessageType:
|
||||||
return CreateCloseMessage(input, ref startOffset);
|
return CreateCloseMessage(input, ref startOffset);
|
||||||
|
case HubProtocolConstants.StreamCompleteMessageType:
|
||||||
|
return CreateStreamCompleteMessage(input, ref startOffset);
|
||||||
default:
|
default:
|
||||||
// Future protocol changes can add message types, old clients can ignore them
|
// Future protocol changes can add message types, old clients can ignore them
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -192,7 +198,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
{
|
{
|
||||||
var headers = ReadHeaders(input, ref offset);
|
var headers = ReadHeaders(input, ref offset);
|
||||||
var invocationId = ReadInvocationId(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);
|
var value = DeserializeObject(input, ref offset, itemType, "item", resolver);
|
||||||
return ApplyHeaders(headers, new StreamItemMessage(invocationId, value));
|
return ApplyHeaders(headers, new StreamItemMessage(invocationId, value));
|
||||||
}
|
}
|
||||||
|
|
@ -240,6 +246,17 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
return new CloseMessage(error);
|
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)
|
private static Dictionary<string, string> ReadHeaders(byte[] input, ref int offset)
|
||||||
{
|
{
|
||||||
var headerCount = ReadMapLength(input, ref offset, "headers");
|
var headerCount = ReadMapLength(input, ref offset, "headers");
|
||||||
|
|
@ -372,6 +389,9 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
case CloseMessage closeMessage:
|
case CloseMessage closeMessage:
|
||||||
WriteCloseMessage(closeMessage, packer);
|
WriteCloseMessage(closeMessage, packer);
|
||||||
break;
|
break;
|
||||||
|
case StreamCompleteMessage m:
|
||||||
|
WriteStreamCompleteMessage(m, packer);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new InvalidDataException($"Unexpected message type: {message.GetType().Name}");
|
throw new InvalidDataException($"Unexpected message type: {message.GetType().Name}");
|
||||||
}
|
}
|
||||||
|
|
@ -465,6 +485,21 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
|
||||||
MessagePackBinary.WriteString(packer, message.InvocationId);
|
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)
|
private void WriteCloseMessage(CloseMessage message, Stream packer)
|
||||||
{
|
{
|
||||||
MessagePackBinary.WriteArrayHeader(packer, 2);
|
MessagePackBinary.WriteArrayHeader(packer, 2);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Description>Tests for users to verify their own implementations of SignalR types</Description>
|
<Description>Tests for users to verify their own implementations of SignalR types</Description>
|
||||||
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="..\Common\DuplexPipe.cs" Link="DuplexPipe.cs" />
|
<Compile Include="..\Common\DuplexPipe.cs" Link="DuplexPipe.cs" />
|
||||||
<Compile Include="..\Common\MemoryBufferWriter.cs" Link="Internal\MemoryBufferWriter.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.Common\Microsoft.AspNetCore.SignalR.Common.csproj" />
|
||||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Protocols.MessagePack\Microsoft.AspNetCore.SignalR.Protocols.MessagePack.csproj" />
|
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Protocols.MessagePack\Microsoft.AspNetCore.SignalR.Protocols.MessagePack.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="xunit.extensibility.core" Version="$(XunitExtensibilityCorePackageVersion)" />
|
<PackageReference Include="xunit.extensibility.core" Version="$(XunitExtensibilityCorePackageVersion)" />
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Description>Provides scale-out support for ASP.NET Core SignalR using a Redis server and the StackExchange.Redis client.</Description>
|
<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>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue