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