├── .gitattributes ├── .gitignore ├── AspNetCore.SignalR.Orleans.sln ├── README.md ├── samples ├── Playground │ ├── BuilderConfigurators │ │ ├── SampleClientBuilderConfigurator.cs │ │ └── SampleSiloBuilderConfigurator.cs │ ├── LineReadingService.cs │ ├── Playground.csproj │ ├── Program.cs │ ├── appsettings.Development.json │ └── appsettings.json ├── Sample.Abstractions │ ├── HubTypeIds.cs │ └── Sample.Abstractions.csproj ├── Sample.OrleansApp │ ├── Program.cs │ └── Sample.OrleansApp.csproj └── Sample.SignalRApp │ ├── Program.cs │ └── Sample.SignalRApp.csproj ├── src ├── AspNetCore.SignalR.Orleans.TestUtils │ ├── AspNetCore.SignalR.Orleans.TestUtils.csproj │ └── Microsoft │ │ ├── DuplexPipe.cs │ │ ├── HubConnectionContextUtils.cs │ │ ├── MemoryBufferWriter.cs │ │ ├── TaskExtensions.cs │ │ └── TestClient.cs ├── AspNetCore.SignalR.Orleans │ ├── AspNetCore.SignalR.Orleans.csproj │ ├── HubProxy`T.cs │ ├── IHubProxy`T.cs │ ├── Internal │ │ └── OrleansLog.cs │ ├── OrleansDependencyInjectionExtensions.cs │ ├── OrleansHubLifetimeManager.cs │ ├── OrleansOptions.cs │ └── SignalRClientBuilderExtensions.cs ├── Orleans.Messaging.SignalR.Common │ ├── HubInvocationMessage.cs │ ├── HubProxy.cs │ ├── HubProxyGrainFactoryExtensions.cs │ ├── IHubProxy.cs │ ├── Internal │ │ ├── GrainInterfaces │ │ │ ├── IClientGrain.cs │ │ │ ├── IClientsGrain.cs │ │ │ ├── IGrainWithHubTypedStringKey.cs │ │ │ ├── IGroupGrain.cs │ │ │ └── IUserGrain.cs │ │ ├── HubTypedKeyUtils.cs │ │ ├── InternalGrainFactoryExtensions.cs │ │ └── InternalSignalRConstants.cs │ ├── Orleans.Messaging.SignalR.Common.csproj │ ├── SendAllInvocationMessage.cs │ ├── SendClientInvocationMessage.cs │ └── SignalRConstants.cs └── Orleans.Messaging.SignalR │ ├── GrainWithHubTypedKeyExtensions.cs │ ├── Grains │ ├── ClientGrain.cs │ ├── ClientsGrain.cs │ ├── GroupGrain.cs │ └── UserGrain.cs │ ├── Orleans.Messaging.SignalR.csproj │ └── SignalRSiloHostBuilderExtensions.cs └── test └── AspNetCore.SignalR.Orleans.Tests ├── AspNetCore.SignalR.Orleans.Tests.csproj ├── BuilderConfigurators ├── TestClientBuilderConfigurator.cs └── TestSiloBuilderConfigurator.cs ├── OrleansHubLifetimeManagerTests.Base.cs ├── OrleansHubLifetimeManagerTests.Scaleout.cs ├── OrleansHubLifetimeManagerTests.cs ├── TestClusterFixture.cs └── TestHubs.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /AspNetCore.SignalR.Orleans.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.106 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{340A0C86-E0BA-4D29-9B50-8A1CDAF0B825}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{21E0A42D-945F-4E31-8F70-2D6308653439}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{70BF92C9-C435-4739-8A19-4679FDD7D963}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore.SignalR.Orleans.Tests", "test\AspNetCore.SignalR.Orleans.Tests\AspNetCore.SignalR.Orleans.Tests.csproj", "{071FE4FD-EF82-4337-8964-EFF22D48CC82}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore.SignalR.Orleans.TestUtils", "src\AspNetCore.SignalR.Orleans.TestUtils\AspNetCore.SignalR.Orleans.TestUtils.csproj", "{C3798B6A-2D21-4F60-B8F0-30DBF2648F53}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Playground", "samples\Playground\Playground.csproj", "{01A5BAAB-26B1-4715-91F2-88F80AF67765}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore.SignalR.Orleans", "src\AspNetCore.SignalR.Orleans\AspNetCore.SignalR.Orleans.csproj", "{4D89CA80-1E2B-40B8-80EC-D5167C07159D}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Orleans.Messaging.SignalR", "src\Orleans.Messaging.SignalR\Orleans.Messaging.SignalR.csproj", "{BFBC58D3-AEC7-46D7-BABC-20642E7BDA9D}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Orleans.Messaging.SignalR.Common", "src\Orleans.Messaging.SignalR.Common\Orleans.Messaging.SignalR.Common.csproj", "{3C083F86-3D23-47C0-8CAB-697A93F3491D}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Abstractions", "samples\Sample.Abstractions\Sample.Abstractions.csproj", "{E4FAE164-4BE0-4320-8E41-FB5D98E21234}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.OrleansApp", "samples\Sample.OrleansApp\Sample.OrleansApp.csproj", "{2D8F0830-A9D8-4BE8-8844-54C4469D62CD}" 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.SignalRApp", "samples\Sample.SignalRApp\Sample.SignalRApp.csproj", "{3F0C2DC7-BEC0-4012-8755-72B33EDDA962}" 29 | EndProject 30 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D7AE5151-226C-4AE0-9D8E-A7242158DCB2}" 31 | ProjectSection(SolutionItems) = preProject 32 | README.md = README.md 33 | EndProjectSection 34 | EndProject 35 | Global 36 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 37 | Debug|Any CPU = Debug|Any CPU 38 | Release|Any CPU = Release|Any CPU 39 | EndGlobalSection 40 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 41 | {071FE4FD-EF82-4337-8964-EFF22D48CC82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {071FE4FD-EF82-4337-8964-EFF22D48CC82}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {071FE4FD-EF82-4337-8964-EFF22D48CC82}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {071FE4FD-EF82-4337-8964-EFF22D48CC82}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {C3798B6A-2D21-4F60-B8F0-30DBF2648F53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {C3798B6A-2D21-4F60-B8F0-30DBF2648F53}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {C3798B6A-2D21-4F60-B8F0-30DBF2648F53}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {C3798B6A-2D21-4F60-B8F0-30DBF2648F53}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {01A5BAAB-26B1-4715-91F2-88F80AF67765}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {01A5BAAB-26B1-4715-91F2-88F80AF67765}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {01A5BAAB-26B1-4715-91F2-88F80AF67765}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {01A5BAAB-26B1-4715-91F2-88F80AF67765}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {4D89CA80-1E2B-40B8-80EC-D5167C07159D}.Debug|Any CPU.ActiveCfg = Release|Any CPU 54 | {4D89CA80-1E2B-40B8-80EC-D5167C07159D}.Debug|Any CPU.Build.0 = Release|Any CPU 55 | {4D89CA80-1E2B-40B8-80EC-D5167C07159D}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {4D89CA80-1E2B-40B8-80EC-D5167C07159D}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {BFBC58D3-AEC7-46D7-BABC-20642E7BDA9D}.Debug|Any CPU.ActiveCfg = Release|Any CPU 58 | {BFBC58D3-AEC7-46D7-BABC-20642E7BDA9D}.Debug|Any CPU.Build.0 = Release|Any CPU 59 | {BFBC58D3-AEC7-46D7-BABC-20642E7BDA9D}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {BFBC58D3-AEC7-46D7-BABC-20642E7BDA9D}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {3C083F86-3D23-47C0-8CAB-697A93F3491D}.Debug|Any CPU.ActiveCfg = Release|Any CPU 62 | {3C083F86-3D23-47C0-8CAB-697A93F3491D}.Debug|Any CPU.Build.0 = Release|Any CPU 63 | {3C083F86-3D23-47C0-8CAB-697A93F3491D}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {3C083F86-3D23-47C0-8CAB-697A93F3491D}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {E4FAE164-4BE0-4320-8E41-FB5D98E21234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {E4FAE164-4BE0-4320-8E41-FB5D98E21234}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {E4FAE164-4BE0-4320-8E41-FB5D98E21234}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {E4FAE164-4BE0-4320-8E41-FB5D98E21234}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {2D8F0830-A9D8-4BE8-8844-54C4469D62CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 70 | {2D8F0830-A9D8-4BE8-8844-54C4469D62CD}.Debug|Any CPU.Build.0 = Debug|Any CPU 71 | {2D8F0830-A9D8-4BE8-8844-54C4469D62CD}.Release|Any CPU.ActiveCfg = Release|Any CPU 72 | {2D8F0830-A9D8-4BE8-8844-54C4469D62CD}.Release|Any CPU.Build.0 = Release|Any CPU 73 | {3F0C2DC7-BEC0-4012-8755-72B33EDDA962}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 74 | {3F0C2DC7-BEC0-4012-8755-72B33EDDA962}.Debug|Any CPU.Build.0 = Debug|Any CPU 75 | {3F0C2DC7-BEC0-4012-8755-72B33EDDA962}.Release|Any CPU.ActiveCfg = Release|Any CPU 76 | {3F0C2DC7-BEC0-4012-8755-72B33EDDA962}.Release|Any CPU.Build.0 = Release|Any CPU 77 | EndGlobalSection 78 | GlobalSection(SolutionProperties) = preSolution 79 | HideSolutionNode = FALSE 80 | EndGlobalSection 81 | GlobalSection(NestedProjects) = preSolution 82 | {071FE4FD-EF82-4337-8964-EFF22D48CC82} = {21E0A42D-945F-4E31-8F70-2D6308653439} 83 | {C3798B6A-2D21-4F60-B8F0-30DBF2648F53} = {340A0C86-E0BA-4D29-9B50-8A1CDAF0B825} 84 | {01A5BAAB-26B1-4715-91F2-88F80AF67765} = {70BF92C9-C435-4739-8A19-4679FDD7D963} 85 | {4D89CA80-1E2B-40B8-80EC-D5167C07159D} = {340A0C86-E0BA-4D29-9B50-8A1CDAF0B825} 86 | {BFBC58D3-AEC7-46D7-BABC-20642E7BDA9D} = {340A0C86-E0BA-4D29-9B50-8A1CDAF0B825} 87 | {3C083F86-3D23-47C0-8CAB-697A93F3491D} = {340A0C86-E0BA-4D29-9B50-8A1CDAF0B825} 88 | {E4FAE164-4BE0-4320-8E41-FB5D98E21234} = {70BF92C9-C435-4739-8A19-4679FDD7D963} 89 | {2D8F0830-A9D8-4BE8-8844-54C4469D62CD} = {70BF92C9-C435-4739-8A19-4679FDD7D963} 90 | {3F0C2DC7-BEC0-4012-8755-72B33EDDA962} = {70BF92C9-C435-4739-8A19-4679FDD7D963} 91 | EndGlobalSection 92 | GlobalSection(ExtensibilityGlobals) = postSolution 93 | SolutionGuid = {652A62E6-802C-4CBC-AB76-7FC0AAAC7B0B} 94 | EndGlobalSection 95 | EndGlobal 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AspNetCore.SignalR.Orleans 2 | Orleans for ASP.NET Core SignalR. 3 | ## Installation 4 | In the client (SignalR) project via [NuGet](https://www.nuget.org/packages?q=aspnetcore.signalr.orleans): 5 | ``` 6 | PM> Install-Package AspNetCore.SignalR.Orleans 7 | ``` 8 | In the server (Orleans) project via [NuGet](https://www.nuget.org/packages?q=orleans.messaging.signalr): 9 | ``` 10 | PM> Install-Package Orleans.Messaging.SignalR 11 | ``` 12 | ## Configuration 13 | ### Silo 14 | ISiloHostBuilder: 15 | ```cs 16 | hostBuilder.UseSignalR(); 17 | ``` 18 | ### Client 19 | IClientBuilder: 20 | ```cs 21 | clientBuilder.UseSignalR(); 22 | ``` 23 | ### SignalR 24 | IServiceCollection: 25 | ```cs 26 | services.AddSignalR() 27 | .AddOrleans(options => options.ClusterClient = clusterClient) 28 | .AddOrleans(options => options.ClusterClient = clusterClient); 29 | ``` 30 | ## Samples 31 | ### Orleans Grains 32 | As shown in the example below, IHubProxy provides control over different channels on the Grain level. 33 | ```cs 34 | public class SampleGrainState 35 | { 36 | public ISet Members { get; set; } = new HashSet(); 37 | } 38 | 39 | public class SampleGrain : Grain, ISampleGrain 40 | { 41 | private IHubProxy _hubProxy; 42 | private IHubProxy _anotherHubProxy; 43 | 44 | public override Task OnActivateAsync() 45 | { 46 | var streamProvider = GetStreamProvider(SignalRConstants.STREAM_PROVIDER); 47 | _hubProxy = GrainFactory.GetHubProxy(streamProvider, Guid.Parse(HubTypeIds.SampleHub)); 48 | _anotherHubProxy = GrainFactory.GetHubProxy(streamProvider, Guid.Parse(HubTypeIds.AnotherSampleHub)); 49 | return base.OnActivateAsync(); 50 | } 51 | 52 | public async Task AddToGroupWithConditionAsync(string connectionId, string groupName) 53 | { 54 | if (State.Members.Count <= 4) 55 | { 56 | await _hubProxy.AddToGroupAsync(connectionId, groupName); 57 | State.Members.Add(connectionId); 58 | } 59 | } 60 | 61 | public Task NotifyAllExceptCurrentMembersAsync() 62 | { 63 | return _hubProxy.SendAllExceptAsync("OnReceived", new object[] { "Hello, Client!" }, State.Members.ToList()); 64 | } 65 | 66 | public Task NotifyUserFromAnotherHubAsync(string userId) 67 | { 68 | return _anotherHubProxy.SendUserAsync(userId, "OnReceived", new object[] { "Hello, User!" }); 69 | } 70 | } 71 | ``` 72 | ### SignalR Hubs 73 | The following example shows how to un/subscribe/publish to channels across Hubs, and to generate a Hub type ID that is consistent across any physical machine. 74 | ```cs 75 | // OrleansHubLifetimeManager uses typeof(THub).GUID to identify Hub types. 76 | // For small tests it returns consistent guids when the GuidAttribute is not associated, 77 | // but they should not be trusted to be stable over framework versions. 78 | // To be sure about consistency, explicitly decorating the Hub types with the GuidAttribute is recommend. 79 | // We can save them as constant in shared projects. 80 | [Guid(HubTypeIds.SampleHub)] 81 | public class SampleHub : Hub 82 | { 83 | private readonly IHubProxy _hubProxy; 84 | private readonly IHubProxy _anotherHubProxy; 85 | 86 | public SampleHub(IHubProxy hubProxy, IHubProxy anotherHubProxy) 87 | { 88 | _hubProxy = hubProxy; 89 | _anotherHubProxy = anotherHubProxy; 90 | } 91 | 92 | public Task NotifyUserFromAnotherHubAsync(string userId) 93 | { 94 | return _anotherHubProxy.SendUserAsync(userId, "OnReceived", new object[] { "Hello, user!" }); 95 | } 96 | } 97 | 98 | [Guid(HubTypeIds.AnotherSampleHub)] 99 | public class AnotherSampleHub : Hub 100 | { 101 | } 102 | ``` 103 | ### Shared 104 | ```cs 105 | public class HubTypeIds 106 | { 107 | public const string SampleHub = "85DE337C-0EBB-4DF5-9AA6-58E3503C5542"; 108 | 109 | public const string AnotherSampleHub = "2FC4CAD6-A545-47FB-9D21-AD1606F7115A"; 110 | } 111 | ``` 112 | ## IHubProxy, generic IHubProxy and Hub.Clients/Hub.Groups, what is the difference? 113 | The result of calling both interfaces (either from Hubs or Grains) and the own methods of SignalR Hub is identical. 114 | For convenience, it is recommended to use the pattern in the samples above to get the IHubProxy: in SignalR projects getting from the construction process, in Orleans projects during the grain activation. 115 | ```cs 116 | public interface IHubProxy 117 | { 118 | Task AddToGroupAsync(string connectionId, string groupName); 119 | 120 | Task RemoveFromGroupAsync(string connectionId, string groupName); 121 | 122 | Task SendAllAsync(string method, object[] args); 123 | 124 | Task SendAllExceptAsync(string method, object[] args, IReadOnlyList excludedConnectionIds); 125 | 126 | Task SendClientAsync(string connectionId, string method, object[] args); 127 | 128 | Task SendClientsAsync(IReadOnlyList connectionIds, string method, object[] args); 129 | 130 | Task SendClientsExceptAsync(IReadOnlyList connectionIds, string method, object[] args, IReadOnlyList excludedConnectionIds); 131 | 132 | Task SendGroupAsync(string groupName, string method, object[] args); 133 | 134 | Task SendGroupExceptAsync(string groupName, string method, object[] args, IReadOnlyList excludedConnectionIds); 135 | 136 | Task SendGroupsAsync(IReadOnlyList groupNames, string method, object[] args); 137 | 138 | Task SendUserAsync(string userId, string method, object[] args); 139 | 140 | Task SendUsersAsync(IReadOnlyList userIds, string method, object[] args); 141 | 142 | Task SendUsersExceptAsync(IReadOnlyList userIds, string method, object[] args, IReadOnlyList excludedConnectionIds); 143 | } 144 | 145 | // From Microsoft.AspNetCore.SignalR.Core 146 | public abstract class Hub : IDisposable 147 | { 148 | public IHubCallerClients Clients { get; set; } 149 | 150 | public IGroupManager Groups { get; set; } 151 | } 152 | ``` 153 | -------------------------------------------------------------------------------- /samples/Playground/BuilderConfigurators/SampleClientBuilderConfigurator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.Logging; 3 | using Orleans; 4 | using Orleans.Hosting; 5 | using Orleans.TestingHost; 6 | 7 | namespace Playground 8 | { 9 | public class SampleClientBuilderConfigurator : IClientBuilderConfigurator 10 | { 11 | public void Configure(IConfiguration configuration, IClientBuilder clientBuilder) 12 | { 13 | clientBuilder.UseSignalR() 14 | .ConfigureLogging(builder => 15 | { 16 | builder.SetMinimumLevel(LogLevel.Warning); 17 | builder.AddFilter("AspNetCore.SignalR.Orleans", LogLevel.Trace); 18 | builder.AddConsole(); 19 | }); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /samples/Playground/BuilderConfigurators/SampleSiloBuilderConfigurator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Orleans.Hosting; 3 | using Orleans.TestingHost; 4 | 5 | namespace Playground 6 | { 7 | public class SampleSiloBuilderConfigurator : ISiloBuilderConfigurator 8 | { 9 | public void Configure(ISiloHostBuilder hostBuilder) 10 | { 11 | hostBuilder.UseSignalR() 12 | .ConfigureLogging(builder => 13 | { 14 | builder.SetMinimumLevel(LogLevel.Warning); 15 | builder.AddFilter("AspNetCore.SignalR.Orleans", LogLevel.Trace); 16 | builder.AddConsole(); 17 | }); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/Playground/LineReadingService.cs: -------------------------------------------------------------------------------- 1 | using AspNetCore.SignalR.Orleans; 2 | using Microsoft.AspNetCore.SignalR; 3 | using Microsoft.AspNetCore.SignalR.Protocol; 4 | using Microsoft.AspNetCore.SignalR.Tests; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Options; 8 | using Newtonsoft.Json; 9 | using Orleans; 10 | using Orleans.Messaging.SignalR; 11 | using Orleans.TestingHost; 12 | using System; 13 | using System.Collections.Concurrent; 14 | using System.Linq; 15 | using System.Reactive.Concurrency; 16 | using System.Reactive.Linq; 17 | using System.Threading; 18 | using System.Threading.Tasks; 19 | 20 | namespace Playground 21 | { 22 | public class LineReadingService : BackgroundService 23 | { 24 | private readonly IServiceProvider _serviceProvider; 25 | private readonly IOptions> _options; 26 | private readonly TestCluster _testCluster; 27 | private readonly ILoggerFactory _loggerFactory; 28 | private readonly ILogger _logger; 29 | 30 | public LineReadingService(IServiceProvider serviceProvider, 31 | IOptions> options, 32 | TestCluster testCluster, 33 | ILoggerFactory loggerFactory, 34 | ILogger logger) 35 | { 36 | _serviceProvider = serviceProvider; 37 | _options = options; 38 | _testCluster = testCluster; 39 | _loggerFactory = loggerFactory; 40 | _logger = logger; 41 | } 42 | 43 | protected override Task ExecuteAsync(CancellationToken stoppingToken) 44 | { 45 | stoppingToken.Register(async () => 46 | { 47 | foreach (var item in testClients.Values) 48 | { 49 | item.Connection.Abort(); 50 | item.Reader.Dispose(); 51 | } 52 | foreach (var item in managers.Values) 53 | { 54 | item.Dispose(); 55 | } 56 | await _testCluster.Client.Close(); 57 | 58 | _logger.LogInformation(2, "stopped."); 59 | }); 60 | 61 | Observable.FromAsync(() => Console.In.ReadLineAsync()) 62 | .Repeat() 63 | .Where(value => !string.IsNullOrEmpty(value)) 64 | .Select(value => value.Split(' ')) 65 | .SubscribeOn(TaskPoolScheduler.Default) 66 | .Subscribe(async (string[] value) => 67 | { 68 | await Task.CompletedTask; 69 | 70 | try 71 | { 72 | await OnNextAsync(value); 73 | } 74 | catch (Exception e) 75 | { 76 | _logger.LogError(e, "Exception onNext."); 77 | } 78 | }, 79 | error => _logger.LogError(error, "OnError."), 80 | () => _logger.LogInformation("OnCompleted.")); 81 | 82 | return Task.CompletedTask; 83 | } 84 | 85 | private readonly ConcurrentDictionary> managers = new ConcurrentDictionary>(); 86 | 87 | public class ReadableClient 88 | { 89 | public HubConnectionContext Connection { get; set; } 90 | 91 | public IDisposable Reader { get; set; } 92 | } 93 | 94 | private readonly ConcurrentDictionary testClients = new ConcurrentDictionary(); 95 | 96 | private async Task CreateManagerAsync(string managerId) 97 | { 98 | var options = new OrleansOptions 99 | { 100 | ClusterClient = _testCluster.Client 101 | }; 102 | 103 | var manager = new OrleansHubLifetimeManager(Options.Create(options), 104 | new HubProxy(options.ClusterClient), 105 | _loggerFactory.CreateLogger>()); 106 | 107 | managers[managerId] = manager; 108 | 109 | await Task.CompletedTask; 110 | } 111 | 112 | private async Task CreateClientAsync(string managerId, string connectionId, string userId) 113 | { 114 | var client = new TestClient(connectionId: connectionId); 115 | 116 | var connection = HubConnectionContextUtils.Create(client.Connection, userIdentifier: userId); 117 | 118 | await managers[managerId].OnConnectedAsync(connection); 119 | 120 | var reader = Observable.Repeat(Observable.FromAsync(() => client.ReadAsync())) 121 | .ObserveOn(TaskPoolScheduler.Default) 122 | .Select(message => message as InvocationMessage) 123 | .Subscribe(message => 124 | { 125 | _logger.LogInformation($"Method: {message.Target}, Args: {JsonConvert.SerializeObject(message.Arguments)}"); 126 | }); 127 | 128 | testClients[connectionId] = new ReadableClient { Connection = connection, Reader = reader }; 129 | } 130 | 131 | private async Task OnNextAsync(string[] value) 132 | { 133 | switch (value[0]) 134 | { 135 | case "m": 136 | { 137 | var managerId = value[1]; 138 | await CreateManagerAsync(managerId); 139 | } 140 | break; 141 | case "c": 142 | { 143 | var managerId = value[1]; 144 | var connectionId = value[2]; 145 | var userId = value[3]; 146 | await CreateClientAsync(managerId, connectionId, userId); 147 | } 148 | break; 149 | case "ms": 150 | { 151 | Parallel.For(0, 3, async i => 152 | { 153 | await CreateManagerAsync($"{i}"); 154 | }); 155 | } 156 | break; 157 | case "cs": 158 | { 159 | var userId = value[1]; 160 | 161 | Parallel.ForEach(managers.Keys, async managerId => 162 | { 163 | var clientTasks = Enumerable.Range(0, 3).Select(i => $"{Guid.NewGuid()}").Select(id => CreateClientAsync(managerId, id, userId)); 164 | await Task.WhenAll(clientTasks); 165 | }); 166 | } 167 | break; 168 | case "atgs": 169 | { 170 | var groupName = value[1]; 171 | 172 | Parallel.ForEach(managers, manager => 173 | { 174 | Parallel.ForEach(testClients, client => 175 | { 176 | manager.Value.AddToGroupAsync(client.Value.Connection.ConnectionId, groupName); 177 | }); 178 | }); 179 | } 180 | break; 181 | case "dm": 182 | { 183 | var managerId = value[1]; 184 | 185 | managers[managerId].Dispose(); 186 | } 187 | break; 188 | case "dc": 189 | { 190 | var managerId = value[1]; 191 | var connectionId = value[2]; 192 | 193 | await managers[managerId].OnDisconnectedAsync(testClients[connectionId].Connection); 194 | } 195 | break; 196 | case "dac": 197 | { 198 | var managerId = value[1]; 199 | var connectionId = value[21]; 200 | 201 | await managers[managerId].HubProxy.DeactiveClientAsync(connectionId); 202 | } 203 | break; 204 | case "dag": 205 | { 206 | var managerId = value[1]; 207 | var groupName = value[2]; 208 | 209 | await managers[managerId].HubProxy.DeactiveGroupAsync(groupName); 210 | } 211 | break; 212 | case "dau": 213 | { 214 | var managerId = value[1]; 215 | var userId = value[2]; 216 | 217 | await managers[managerId].HubProxy.DeactiveUserAsync(userId); 218 | } 219 | break; 220 | case "sa": 221 | { 222 | var managerId = value[1]; 223 | 224 | await managers[managerId].SendAllAsync("Hello", new object[] { "All" }); 225 | _logger.LogInformation("Done"); 226 | } 227 | break; 228 | case "sg": 229 | { 230 | var managerId = value[1]; 231 | var groupName = value[2]; 232 | 233 | await managers[managerId].SendGroupAsync(groupName, "Hello", new object[] { "Group member" }); 234 | _logger.LogInformation("Done"); 235 | } 236 | break; 237 | case "su": 238 | { 239 | var managerId = value[1]; 240 | var userId = value[2]; 241 | 242 | await managers[managerId].SendUserAsync(userId, "Hello", new object[] { "User client" }); 243 | _logger.LogInformation("Done"); 244 | } 245 | break; 246 | case "atg": 247 | { 248 | var managerId = value[1]; 249 | var connectionId = value[2]; 250 | var groupName = value[3]; 251 | 252 | await managers[managerId].AddToGroupAsync(testClients[connectionId].Connection.ConnectionId, groupName); 253 | } 254 | break; 255 | case "rfg": 256 | { 257 | var managerId = value[1]; 258 | var connectionId = value[2]; 259 | var groupName = value[3]; 260 | 261 | await managers[managerId].RemoveFromGroupAsync(testClients[connectionId].Connection.ConnectionId, groupName); 262 | } 263 | break; 264 | default: 265 | break; 266 | } 267 | } 268 | } 269 | 270 | public static partial class TestClientExtensions 271 | { 272 | public static HubConnectionContext CreateConnection(this TestClient testClient, IHubProtocol protocol = null, string userIdentifier = null) 273 | { 274 | return HubConnectionContextUtils.Create(testClient.Connection, protocol, userIdentifier); 275 | } 276 | } 277 | 278 | public class SampleHub : Hub { } 279 | } 280 | 281 | 282 | // GetGrainIdentity() 283 | // *grn/DDCE39C2/00000000 284 | // + 285 | // GorfHye1QDl -t7eN7jufAg:THub // Key 286 | 287 | // IdentityString 288 | // *grn/DDCE39C2/0000000000000000000000000000000006ffffff 289 | // ddce39c2 290 | // + 291 | // GorfHye1QDl-t7eN7jufAg:THub // Key 292 | // - 293 | // 0xCB39546E // UniformHashCode 294 | 295 | // RuntimeIdentity 296 | // S127.0.0.1:11111:281586790 297 | 298 | // TypeCode 299 | // -573687358 300 | -------------------------------------------------------------------------------- /samples/Playground/Playground.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | latest 7 | 8 | 9 | 10 | 11 | PreserveNewest 12 | 13 | 14 | PreserveNewest 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | true 35 | true 36 | 37 | 38 | 39 | TRACE 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /samples/Playground/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.Extensions.Logging; 5 | using Orleans.TestingHost; 6 | using System; 7 | using System.IO; 8 | using System.Threading.Tasks; 9 | 10 | namespace Playground 11 | { 12 | partial class Program 13 | { 14 | public static TestCluster TestCluster { get; private set; } 15 | 16 | static async Task Main(string[] args) 17 | { 18 | TestCluster = BuildTestCluster(); 19 | await TestCluster.DeployAsync(); 20 | await BuildHost(args).RunAsync(); 21 | } 22 | 23 | public static IHost BuildHost(string[] args) 24 | { 25 | return new HostBuilder() 26 | .UseEnvironment(EnvironmentName.Development) 27 | .ConfigureHostConfiguration(builder => 28 | { 29 | }) 30 | .ConfigureAppConfiguration((context, builder) => 31 | { 32 | builder.SetBasePath(Directory.GetCurrentDirectory()); 33 | builder.AddJsonFile("appsettings.json"); 34 | builder.AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", true); 35 | builder.AddEnvironmentVariables(); 36 | }) 37 | .ConfigureServices((context, services) => 38 | { 39 | services.AddSingleton(TestCluster); 40 | 41 | services.AddHostedService(); 42 | 43 | services.AddLogging(builder => 44 | { 45 | builder.AddConfiguration(context.Configuration.GetSection("Logging")); 46 | builder.AddConsole(); 47 | }); 48 | }) 49 | .Build(); 50 | } 51 | 52 | public static TestCluster BuildTestCluster() 53 | { 54 | var builder = new TestClusterBuilder(3); 55 | builder.Options.ServiceId = Guid.NewGuid().ToString(); 56 | builder.AddSiloBuilderConfigurator(); 57 | builder.AddClientBuilderConfigurator(); 58 | return builder.Build(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /samples/Playground/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "AspNetCore.SignalR.Orleans": "Trace", 6 | "Playground": "Trace" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/Playground/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning", 5 | "AspNetCore.SignalR.Orleans": "Trace", 6 | "Orleans.Messaging.SignalR": "Trace", 7 | "Playground": "Trace" 8 | }, 9 | "Console": { 10 | "IncludeScopes": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /samples/Sample.Abstractions/HubTypeIds.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Abstractions 2 | { 3 | public class HubTypeIds 4 | { 5 | public const string SampleHub = "85DE337C-0EBB-4DF5-9AA6-58E3503C5542"; 6 | 7 | public const string AnotherSampleHub = "2FC4CAD6-A545-47FB-9D21-AD1606F7115A"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/Sample.Abstractions/Sample.Abstractions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | all 10 | runtime; build; native; contentfiles; analyzers 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /samples/Sample.OrleansApp/Program.cs: -------------------------------------------------------------------------------- 1 | using Orleans; 2 | using Orleans.Messaging.SignalR; 3 | using Sample.Abstractions; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace Sample.OrleansApp 10 | { 11 | class Program 12 | { 13 | static void Main(string[] args) 14 | { 15 | Console.WriteLine("Hello World!"); 16 | Console.ReadLine(); 17 | } 18 | } 19 | 20 | public interface ISampleGrain : IGrainWithStringKey 21 | { 22 | } 23 | 24 | public class SampleGrainState 25 | { 26 | public ISet Members { get; set; } = new HashSet(); 27 | } 28 | 29 | public class SampleGrain : Grain, ISampleGrain 30 | { 31 | private IHubProxy _hubProxy; 32 | private IHubProxy _anotherHubProxy; 33 | 34 | public override Task OnActivateAsync() 35 | { 36 | var streamProvider = GetStreamProvider(SignalRConstants.STREAM_PROVIDER); 37 | _hubProxy = GrainFactory.GetHubProxy(streamProvider, Guid.Parse(HubTypeIds.SampleHub)); 38 | _anotherHubProxy = GrainFactory.GetHubProxy(streamProvider, Guid.Parse(HubTypeIds.AnotherSampleHub)); 39 | return base.OnActivateAsync(); 40 | } 41 | 42 | public async Task AddToGroupWithConditionAsync(string connectionId, string groupName) 43 | { 44 | if (State.Members.Count <= 4) 45 | { 46 | await _hubProxy.AddToGroupAsync(connectionId, groupName); 47 | State.Members.Add(connectionId); 48 | } 49 | } 50 | 51 | public Task NotifyAllExceptCurrentMembersAsync() 52 | { 53 | return _hubProxy.SendAllExceptAsync("OnReceived", new object[] { "Hello, Client!" }, State.Members.ToList()); 54 | } 55 | 56 | public Task NotifyUserFromAnotherHubAsync(string userId) 57 | { 58 | return _anotherHubProxy.SendUserAsync(userId, "OnReceived", new object[] { "Hello, User!" }); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /samples/Sample.OrleansApp/Sample.OrleansApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | latest 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /samples/Sample.SignalRApp/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.SignalR; 2 | using Orleans.Messaging.SignalR; 3 | using Sample.Abstractions; 4 | using System; 5 | using System.Runtime.InteropServices; 6 | using System.Threading.Tasks; 7 | 8 | namespace SignalRSample.SignalRApp 9 | { 10 | class Program 11 | { 12 | static void Main(string[] args) 13 | { 14 | Console.WriteLine("Hello World!"); 15 | Console.ReadLine(); 16 | } 17 | } 18 | 19 | // OrleansHubLifetimeManager uses typeof(THub).GUID to identify Hub types. 20 | // For small tests it returns consistent guids when the GuidAttribute is not associated, 21 | // but they should not be trusted to be stable over framework versions. 22 | // To be sure about consistency, explicitly decorating the Hub types with the GuidAttribute is recommend. 23 | // We can save them as constant in shared projects. 24 | [Guid(HubTypeIds.SampleHub)] 25 | public class SampleHub : Hub 26 | { 27 | private readonly IHubProxy _hubProxy; 28 | private readonly IHubProxy _anotherHubProxy; 29 | 30 | public SampleHub(IHubProxy hubProxy, IHubProxy anotherHubProxy) 31 | { 32 | _hubProxy = hubProxy; 33 | _anotherHubProxy = anotherHubProxy; 34 | } 35 | 36 | public Task NotifyUserFromAnotherHubAsync(string userId) 37 | { 38 | return _anotherHubProxy.SendUserAsync(userId, "OnReceived", new object[] { "Hello, user!" }); 39 | } 40 | } 41 | 42 | [Guid(HubTypeIds.AnotherSampleHub)] 43 | public class AnotherSampleHub : Hub 44 | { 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /samples/Sample.SignalRApp/Sample.SignalRApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | latest 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/AspNetCore.SignalR.Orleans.TestUtils/AspNetCore.SignalR.Orleans.TestUtils.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | latest 6 | https://github.com/slango0513/AspNetCore.SignalR.Orleans#license 7 | https://github.com/slango0513/AspNetCore.SignalR.Orleans 8 | 9 | 10 | 11 | TRACE;TESTUTILS 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/AspNetCore.SignalR.Orleans.TestUtils/Microsoft/DuplexPipe.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | 3 | namespace System.IO.Pipelines 4 | { 5 | internal class DuplexPipe : IDuplexPipe 6 | { 7 | public DuplexPipe(PipeReader reader, PipeWriter writer) 8 | { 9 | Input = reader; 10 | Output = writer; 11 | } 12 | 13 | public PipeReader Input { get; } 14 | 15 | public PipeWriter Output { get; } 16 | 17 | public static DuplexPipePair CreateConnectionPair(PipeOptions inputOptions, PipeOptions outputOptions) 18 | { 19 | var input = new Pipe(inputOptions); 20 | var output = new Pipe(outputOptions); 21 | 22 | var transportToApplication = new DuplexPipe(output.Reader, input.Writer); 23 | var applicationToTransport = new DuplexPipe(input.Reader, output.Writer); 24 | 25 | return new DuplexPipePair(applicationToTransport, transportToApplication); 26 | } 27 | 28 | // This class exists to work around issues with value tuple on .NET Framework 29 | public readonly struct DuplexPipePair 30 | { 31 | public IDuplexPipe Transport { get; } 32 | public IDuplexPipe Application { get; } 33 | 34 | public DuplexPipePair(IDuplexPipe transport, IDuplexPipe application) 35 | { 36 | Transport = transport; 37 | Application = application; 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/AspNetCore.SignalR.Orleans.TestUtils/Microsoft/HubConnectionContextUtils.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Connections; 8 | using Microsoft.AspNetCore.SignalR.Protocol; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Extensions.Logging.Abstractions; 11 | 12 | namespace Microsoft.AspNetCore.SignalR.Tests 13 | { 14 | #if TESTUTILS 15 | public 16 | #else 17 | internal 18 | #endif 19 | static class HubConnectionContextUtils 20 | { 21 | public static HubConnectionContext Create(ConnectionContext connection, IHubProtocol protocol = null, string userIdentifier = null) 22 | { 23 | return new HubConnectionContext(connection, TimeSpan.FromSeconds(15), NullLoggerFactory.Instance) 24 | { 25 | Protocol = protocol ?? new JsonHubProtocol(), 26 | UserIdentifier = userIdentifier, 27 | }; 28 | } 29 | 30 | public static MockHubConnectionContext CreateMock(ConnectionContext connection) 31 | { 32 | return new MockHubConnectionContext(connection, TimeSpan.FromSeconds(15), NullLoggerFactory.Instance, TimeSpan.FromSeconds(15)); 33 | } 34 | 35 | public class MockHubConnectionContext : HubConnectionContext 36 | { 37 | public MockHubConnectionContext(ConnectionContext connectionContext, TimeSpan keepAliveInterval, ILoggerFactory loggerFactory, TimeSpan clientTimeoutInterval) 38 | : base(connectionContext, keepAliveInterval, loggerFactory, clientTimeoutInterval) 39 | { 40 | 41 | } 42 | 43 | public override ValueTask WriteAsync(HubMessage message, CancellationToken cancellationToken = default) 44 | { 45 | throw new Exception(); 46 | } 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/AspNetCore.SignalR.Orleans.TestUtils/Microsoft/MemoryBufferWriter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Buffers; 6 | using System.Collections.Generic; 7 | using System.Diagnostics; 8 | using System.IO; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace Microsoft.AspNetCore.Internal 13 | { 14 | internal sealed class MemoryBufferWriter : Stream, IBufferWriter 15 | { 16 | [ThreadStatic] 17 | private static MemoryBufferWriter _cachedInstance; 18 | 19 | #if DEBUG 20 | private bool _inUse; 21 | #endif 22 | 23 | private readonly int _minimumSegmentSize; 24 | private int _bytesWritten; 25 | 26 | private List _completedSegments; 27 | private byte[] _currentSegment; 28 | private int _position; 29 | 30 | public MemoryBufferWriter(int minimumSegmentSize = 4096) 31 | { 32 | _minimumSegmentSize = minimumSegmentSize; 33 | } 34 | 35 | public override long Length => _bytesWritten; 36 | public override bool CanRead => false; 37 | public override bool CanSeek => false; 38 | public override bool CanWrite => true; 39 | public override long Position 40 | { 41 | get => throw new NotSupportedException(); 42 | set => throw new NotSupportedException(); 43 | } 44 | 45 | public static MemoryBufferWriter Get() 46 | { 47 | var writer = _cachedInstance; 48 | if (writer == null) 49 | { 50 | writer = new MemoryBufferWriter(); 51 | } 52 | else 53 | { 54 | // Taken off the thread static 55 | _cachedInstance = null; 56 | } 57 | #if DEBUG 58 | if (writer._inUse) 59 | { 60 | throw new InvalidOperationException("The reader wasn't returned!"); 61 | } 62 | 63 | writer._inUse = true; 64 | #endif 65 | 66 | return writer; 67 | } 68 | 69 | public static void Return(MemoryBufferWriter writer) 70 | { 71 | _cachedInstance = writer; 72 | #if DEBUG 73 | writer._inUse = false; 74 | #endif 75 | writer.Reset(); 76 | } 77 | 78 | public void Reset() 79 | { 80 | if (_completedSegments != null) 81 | { 82 | for (var i = 0; i < _completedSegments.Count; i++) 83 | { 84 | _completedSegments[i].Return(); 85 | } 86 | 87 | _completedSegments.Clear(); 88 | } 89 | 90 | if (_currentSegment != null) 91 | { 92 | ArrayPool.Shared.Return(_currentSegment); 93 | _currentSegment = null; 94 | } 95 | 96 | _bytesWritten = 0; 97 | _position = 0; 98 | } 99 | 100 | public void Advance(int count) 101 | { 102 | _bytesWritten += count; 103 | _position += count; 104 | } 105 | 106 | public Memory GetMemory(int sizeHint = 0) 107 | { 108 | EnsureCapacity(sizeHint); 109 | 110 | return _currentSegment.AsMemory(_position, _currentSegment.Length - _position); 111 | } 112 | 113 | public Span GetSpan(int sizeHint = 0) 114 | { 115 | EnsureCapacity(sizeHint); 116 | 117 | return _currentSegment.AsSpan(_position, _currentSegment.Length - _position); 118 | } 119 | 120 | public void CopyTo(IBufferWriter destination) 121 | { 122 | if (_completedSegments != null) 123 | { 124 | // Copy completed segments 125 | var count = _completedSegments.Count; 126 | for (var i = 0; i < count; i++) 127 | { 128 | destination.Write(_completedSegments[i].Span); 129 | } 130 | } 131 | 132 | destination.Write(_currentSegment.AsSpan(0, _position)); 133 | } 134 | 135 | public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) 136 | { 137 | if (_completedSegments == null) 138 | { 139 | // There is only one segment so write without awaiting. 140 | return destination.WriteAsync(_currentSegment, 0, _position); 141 | } 142 | 143 | return CopyToSlowAsync(destination); 144 | } 145 | 146 | private void EnsureCapacity(int sizeHint) 147 | { 148 | // This does the Right Thing. It only subtracts _position from the current segment length if it's non-null. 149 | // If _currentSegment is null, it returns 0. 150 | var remainingSize = _currentSegment?.Length - _position ?? 0; 151 | 152 | // If the sizeHint is 0, any capacity will do 153 | // Otherwise, the buffer must have enough space for the entire size hint, or we need to add a segment. 154 | if ((sizeHint == 0 && remainingSize > 0) || (sizeHint > 0 && remainingSize >= sizeHint)) 155 | { 156 | // We have capacity in the current segment 157 | return; 158 | } 159 | 160 | AddSegment(sizeHint); 161 | } 162 | 163 | private void AddSegment(int sizeHint = 0) 164 | { 165 | if (_currentSegment != null) 166 | { 167 | // We're adding a segment to the list 168 | if (_completedSegments == null) 169 | { 170 | _completedSegments = new List(); 171 | } 172 | 173 | // Position might be less than the segment length if there wasn't enough space to satisfy the sizeHint when 174 | // GetMemory was called. In that case we'll take the current segment and call it "completed", but need to 175 | // ignore any empty space in it. 176 | _completedSegments.Add(new CompletedBuffer(_currentSegment, _position)); 177 | } 178 | 179 | // Get a new buffer using the minimum segment size, unless the size hint is larger than a single segment. 180 | _currentSegment = ArrayPool.Shared.Rent(Math.Max(_minimumSegmentSize, sizeHint)); 181 | _position = 0; 182 | } 183 | 184 | private async Task CopyToSlowAsync(Stream destination) 185 | { 186 | if (_completedSegments != null) 187 | { 188 | // Copy full segments 189 | var count = _completedSegments.Count; 190 | for (var i = 0; i < count; i++) 191 | { 192 | var segment = _completedSegments[i]; 193 | await destination.WriteAsync(segment.Buffer, 0, segment.Length); 194 | } 195 | } 196 | 197 | await destination.WriteAsync(_currentSegment, 0, _position); 198 | } 199 | 200 | public byte[] ToArray() 201 | { 202 | if (_currentSegment == null) 203 | { 204 | return Array.Empty(); 205 | } 206 | 207 | var result = new byte[_bytesWritten]; 208 | 209 | var totalWritten = 0; 210 | 211 | if (_completedSegments != null) 212 | { 213 | // Copy full segments 214 | var count = _completedSegments.Count; 215 | for (var i = 0; i < count; i++) 216 | { 217 | var segment = _completedSegments[i]; 218 | segment.Span.CopyTo(result.AsSpan(totalWritten)); 219 | totalWritten += segment.Span.Length; 220 | } 221 | } 222 | 223 | // Copy current incomplete segment 224 | _currentSegment.AsSpan(0, _position).CopyTo(result.AsSpan(totalWritten)); 225 | 226 | return result; 227 | } 228 | 229 | public void CopyTo(Span span) 230 | { 231 | Debug.Assert(span.Length >= _bytesWritten); 232 | 233 | if (_currentSegment == null) 234 | { 235 | return; 236 | } 237 | 238 | var totalWritten = 0; 239 | 240 | if (_completedSegments != null) 241 | { 242 | // Copy full segments 243 | var count = _completedSegments.Count; 244 | for (var i = 0; i < count; i++) 245 | { 246 | var segment = _completedSegments[i]; 247 | segment.Span.CopyTo(span.Slice(totalWritten)); 248 | totalWritten += segment.Span.Length; 249 | } 250 | } 251 | 252 | // Copy current incomplete segment 253 | _currentSegment.AsSpan(0, _position).CopyTo(span.Slice(totalWritten)); 254 | 255 | Debug.Assert(_bytesWritten == totalWritten + _position); 256 | } 257 | 258 | public override void Flush() { } 259 | public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; 260 | public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); 261 | public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); 262 | public override void SetLength(long value) => throw new NotSupportedException(); 263 | 264 | public override void WriteByte(byte value) 265 | { 266 | if (_currentSegment != null && (uint)_position < (uint)_currentSegment.Length) 267 | { 268 | _currentSegment[_position] = value; 269 | } 270 | else 271 | { 272 | AddSegment(); 273 | _currentSegment[0] = value; 274 | } 275 | 276 | _position++; 277 | _bytesWritten++; 278 | } 279 | 280 | public override void Write(byte[] buffer, int offset, int count) 281 | { 282 | var position = _position; 283 | if (_currentSegment != null && position < _currentSegment.Length - count) 284 | { 285 | Buffer.BlockCopy(buffer, offset, _currentSegment, position, count); 286 | 287 | _position = position + count; 288 | _bytesWritten += count; 289 | } 290 | else 291 | { 292 | BuffersExtensions.Write(this, buffer.AsSpan(offset, count)); 293 | } 294 | } 295 | 296 | #if NETCOREAPP2_2 297 | public override void Write(ReadOnlySpan span) 298 | { 299 | if (_currentSegment != null && span.TryCopyTo(_currentSegment.AsSpan(_position))) 300 | { 301 | _position += span.Length; 302 | _bytesWritten += span.Length; 303 | } 304 | else 305 | { 306 | BuffersExtensions.Write(this, span); 307 | } 308 | } 309 | #endif 310 | 311 | protected override void Dispose(bool disposing) 312 | { 313 | if (disposing) 314 | { 315 | Reset(); 316 | } 317 | } 318 | 319 | /// 320 | /// Holds a byte[] from the pool and a size value. Basically a Memory but guaranteed to be backed by an ArrayPool byte[], so that we know we can return it. 321 | /// 322 | private readonly struct CompletedBuffer 323 | { 324 | public byte[] Buffer { get; } 325 | public int Length { get; } 326 | 327 | public ReadOnlySpan Span => Buffer.AsSpan(0, Length); 328 | 329 | public CompletedBuffer(byte[] buffer, int length) 330 | { 331 | Buffer = buffer; 332 | Length = length; 333 | } 334 | 335 | public void Return() 336 | { 337 | ArrayPool.Shared.Return(Buffer); 338 | } 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/AspNetCore.SignalR.Orleans.TestUtils/Microsoft/TaskExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Diagnostics; 5 | using System.IO.Pipelines; 6 | using System.Runtime.CompilerServices; 7 | 8 | namespace System.Threading.Tasks 9 | { 10 | #if TESTUTILS 11 | public 12 | #else 13 | internal 14 | #endif 15 | static class TaskExtensions 16 | { 17 | private const int DefaultTimeout = 5000; 18 | 19 | public static Task OrTimeout(this Task task, int milliseconds = DefaultTimeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) 20 | { 21 | return OrTimeout(task, new TimeSpan(0, 0, 0, 0, milliseconds), memberName, filePath, lineNumber); 22 | } 23 | 24 | public static async Task OrTimeout(this Task task, TimeSpan timeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) 25 | { 26 | if (task.IsCompleted) 27 | { 28 | await task; 29 | return; 30 | } 31 | 32 | var cts = new CancellationTokenSource(); 33 | var completed = await Task.WhenAny(task, Task.Delay(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : timeout, cts.Token)); 34 | if (completed != task) 35 | { 36 | throw new TimeoutException(GetMessage(memberName, filePath, lineNumber)); 37 | } 38 | cts.Cancel(); 39 | 40 | await task; 41 | } 42 | 43 | public static Task OrTimeout(this ValueTask task, int milliseconds = DefaultTimeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) => 44 | OrTimeout(task, new TimeSpan(0, 0, 0, 0, milliseconds), memberName, filePath, lineNumber); 45 | 46 | public static Task OrTimeout(this ValueTask task, TimeSpan timeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) => 47 | task.AsTask().OrTimeout(timeout, memberName, filePath, lineNumber); 48 | 49 | public static Task OrTimeout(this Task task, int milliseconds = DefaultTimeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) 50 | { 51 | return OrTimeout(task, new TimeSpan(0, 0, 0, 0, milliseconds), memberName, filePath, lineNumber); 52 | } 53 | 54 | public static async Task OrTimeout(this Task task, TimeSpan timeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) 55 | { 56 | if (task.IsCompleted) 57 | { 58 | return await task; 59 | } 60 | 61 | var cts = new CancellationTokenSource(); 62 | var completed = await Task.WhenAny(task, Task.Delay(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : timeout, cts.Token)); 63 | if (completed != task) 64 | { 65 | throw new TimeoutException(GetMessage(memberName, filePath, lineNumber)); 66 | } 67 | cts.Cancel(); 68 | 69 | return await task; 70 | } 71 | 72 | public static async Task OrThrowIfOtherFails(this Task task, Task otherTask) 73 | { 74 | var completed = await Task.WhenAny(task, otherTask); 75 | if (completed == otherTask && otherTask.IsFaulted) 76 | { 77 | // Manifest the exception 78 | otherTask.GetAwaiter().GetResult(); 79 | throw new Exception("Unreachable code"); 80 | } 81 | else 82 | { 83 | // Await the task we were asked to await. Either it's finished, or the otherTask finished successfully, and it's not our job to check that 84 | await task; 85 | } 86 | } 87 | 88 | public static async Task OrThrowIfOtherFails(this Task task, Task otherTask) 89 | { 90 | await OrThrowIfOtherFails((Task)task, otherTask); 91 | 92 | // If we get here, 'task' is finished and succeeded. 93 | return task.GetAwaiter().GetResult(); 94 | } 95 | 96 | private static string GetMessage(string memberName, string filePath, int? lineNumber) 97 | { 98 | if (!string.IsNullOrEmpty(memberName)) 99 | { 100 | return $"Operation in {memberName} timed out at {filePath}:{lineNumber}"; 101 | } 102 | else 103 | { 104 | return "Operation timed out"; 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/AspNetCore.SignalR.Orleans.TestUtils/Microsoft/TestClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO.Pipelines; 7 | using System.Security.Claims; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Microsoft.AspNetCore.Connections; 11 | using Microsoft.AspNetCore.Connections.Features; 12 | using Microsoft.AspNetCore.Internal; 13 | using Microsoft.AspNetCore.SignalR.Protocol; 14 | 15 | namespace Microsoft.AspNetCore.SignalR.Tests 16 | { 17 | #if TESTUTILS 18 | public 19 | #else 20 | internal 21 | #endif 22 | class TestClient : ITransferFormatFeature, IConnectionHeartbeatFeature, IDisposable 23 | { 24 | private readonly object _heartbeatLock = new object(); 25 | private List<(Action handler, object state)> _heartbeatHandlers; 26 | 27 | private static int _id; 28 | private IHubProtocol _protocol; 29 | private readonly IInvocationBinder _invocationBinder; 30 | private readonly CancellationTokenSource _cts; 31 | 32 | public DefaultConnectionContext Connection { get; } 33 | public Task Connected => ((TaskCompletionSource)Connection.Items["ConnectedTask"]).Task; 34 | public HandshakeResponseMessage HandshakeResponseMessage { get; private set; } 35 | 36 | public TransferFormat SupportedFormats { get; set; } = TransferFormat.Text | TransferFormat.Binary; 37 | 38 | public TransferFormat ActiveFormat { get; set; } 39 | 40 | public TestClient(IHubProtocol protocol = null, IInvocationBinder invocationBinder = null, string userIdentifier = null, string connectionId = null) 41 | { 42 | var options = new PipeOptions(readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); 43 | var pair = DuplexPipe.CreateConnectionPair(options, options); 44 | Connection = new DefaultConnectionContext(connectionId ?? Guid.NewGuid().ToString(), pair.Transport, pair.Application); 45 | 46 | // Add features SignalR needs for testing 47 | Connection.Features.Set(this); 48 | Connection.Features.Set(this); 49 | 50 | var claimValue = Interlocked.Increment(ref _id).ToString(); 51 | var claims = new List { new Claim(ClaimTypes.Name, claimValue) }; 52 | if (userIdentifier != null) 53 | { 54 | claims.Add(new Claim(ClaimTypes.NameIdentifier, userIdentifier)); 55 | } 56 | 57 | Connection.User = new ClaimsPrincipal(new ClaimsIdentity(claims)); 58 | Connection.Items["ConnectedTask"] = new TaskCompletionSource(); 59 | 60 | _protocol = protocol ?? new JsonHubProtocol(); 61 | _invocationBinder = invocationBinder ?? new DefaultInvocationBinder(); 62 | 63 | _cts = new CancellationTokenSource(); 64 | } 65 | 66 | public async Task ConnectAsync( 67 | Connections.ConnectionHandler handler, 68 | bool sendHandshakeRequestMessage = true, 69 | bool expectedHandshakeResponseMessage = true) 70 | { 71 | if (sendHandshakeRequestMessage) 72 | { 73 | var memoryBufferWriter = MemoryBufferWriter.Get(); 74 | try 75 | { 76 | HandshakeProtocol.WriteRequestMessage(new HandshakeRequestMessage(_protocol.Name, _protocol.Version), memoryBufferWriter); 77 | await Connection.Application.Output.WriteAsync(memoryBufferWriter.ToArray()); 78 | } 79 | finally 80 | { 81 | MemoryBufferWriter.Return(memoryBufferWriter); 82 | } 83 | } 84 | 85 | var connection = handler.OnConnectedAsync(Connection); 86 | 87 | if (expectedHandshakeResponseMessage) 88 | { 89 | // note that the handshake response might not immediately be readable 90 | // e.g. server is waiting for request, times out after configured duration, 91 | // and sends response with timeout error 92 | HandshakeResponseMessage = (HandshakeResponseMessage)await ReadAsync(true).OrTimeout(); 93 | } 94 | 95 | return connection; 96 | } 97 | 98 | public async Task> StreamAsync(string methodName, params object[] args) 99 | { 100 | var invocationId = await SendStreamInvocationAsync(methodName, args); 101 | 102 | var messages = new List(); 103 | while (true) 104 | { 105 | var message = await ReadAsync(); 106 | 107 | if (message == null) 108 | { 109 | throw new InvalidOperationException("Connection aborted!"); 110 | } 111 | 112 | if (message is HubInvocationMessage hubInvocationMessage && !string.Equals(hubInvocationMessage.InvocationId, invocationId)) 113 | { 114 | throw new NotSupportedException("TestClient does not support multiple outgoing invocations!"); 115 | } 116 | 117 | switch (message) 118 | { 119 | case StreamItemMessage _: 120 | messages.Add(message); 121 | break; 122 | case CompletionMessage _: 123 | messages.Add(message); 124 | return messages; 125 | default: 126 | throw new NotSupportedException("TestClient does not support receiving invocations!"); 127 | } 128 | } 129 | } 130 | 131 | public async Task InvokeAsync(string methodName, params object[] args) 132 | { 133 | var invocationId = await SendInvocationAsync(methodName, nonBlocking: false, args: args); 134 | 135 | while (true) 136 | { 137 | var message = await ReadAsync(); 138 | 139 | if (message == null) 140 | { 141 | throw new InvalidOperationException("Connection aborted!"); 142 | } 143 | 144 | if (message is HubInvocationMessage hubInvocationMessage && !string.Equals(hubInvocationMessage.InvocationId, invocationId)) 145 | { 146 | throw new NotSupportedException("TestClient does not support multiple outgoing invocations!"); 147 | } 148 | 149 | switch (message) 150 | { 151 | case StreamItemMessage result: 152 | throw new NotSupportedException("Use 'StreamAsync' to call a streaming method"); 153 | case CompletionMessage completion: 154 | return completion; 155 | case PingMessage _: 156 | // Pings are ignored 157 | break; 158 | default: 159 | throw new NotSupportedException("TestClient does not support receiving invocations!"); 160 | } 161 | } 162 | } 163 | 164 | public Task SendInvocationAsync(string methodName, params object[] args) 165 | { 166 | return SendInvocationAsync(methodName, nonBlocking: false, args: args); 167 | } 168 | 169 | public Task SendInvocationAsync(string methodName, bool nonBlocking, params object[] args) 170 | { 171 | var invocationId = nonBlocking ? null : GetInvocationId(); 172 | return SendHubMessageAsync(new InvocationMessage(invocationId, methodName, args)); 173 | } 174 | 175 | public Task SendStreamInvocationAsync(string methodName, params object[] args) 176 | { 177 | var invocationId = GetInvocationId(); 178 | return SendHubMessageAsync(new StreamInvocationMessage(invocationId, methodName, args)); 179 | } 180 | 181 | public Task BeginUploadStreamAsync(string invocationId, string methodName, params object[] args) 182 | { 183 | var message = new InvocationMessage(invocationId, methodName, args); 184 | return SendHubMessageAsync(message); 185 | } 186 | 187 | public async Task SendHubMessageAsync(HubMessage message) 188 | { 189 | var payload = _protocol.GetMessageBytes(message); 190 | 191 | await Connection.Application.Output.WriteAsync(payload); 192 | return message is HubInvocationMessage hubMessage ? hubMessage.InvocationId : null; 193 | } 194 | 195 | public async Task ReadAsync(bool isHandshake = false) 196 | { 197 | while (true) 198 | { 199 | var message = TryRead(isHandshake); 200 | 201 | if (message == null) 202 | { 203 | var result = await Connection.Application.Input.ReadAsync(); 204 | var buffer = result.Buffer; 205 | 206 | try 207 | { 208 | if (!buffer.IsEmpty) 209 | { 210 | continue; 211 | } 212 | 213 | if (result.IsCompleted) 214 | { 215 | return null; 216 | } 217 | } 218 | finally 219 | { 220 | Connection.Application.Input.AdvanceTo(buffer.Start); 221 | } 222 | } 223 | else 224 | { 225 | return message; 226 | } 227 | } 228 | } 229 | 230 | public HubMessage TryRead(bool isHandshake = false) 231 | { 232 | if (!Connection.Application.Input.TryRead(out var result)) 233 | { 234 | return null; 235 | } 236 | 237 | var buffer = result.Buffer; 238 | 239 | try 240 | { 241 | if (!isHandshake) 242 | { 243 | if (_protocol.TryParseMessage(ref buffer, _invocationBinder, out var message)) 244 | { 245 | return message; 246 | } 247 | } 248 | else 249 | { 250 | // read first message out of the incoming data 251 | if (HandshakeProtocol.TryParseResponseMessage(ref buffer, out var responseMessage)) 252 | { 253 | return responseMessage; 254 | } 255 | } 256 | } 257 | finally 258 | { 259 | Connection.Application.Input.AdvanceTo(buffer.Start); 260 | } 261 | 262 | return null; 263 | } 264 | 265 | public void Dispose() 266 | { 267 | _cts.Cancel(); 268 | 269 | Connection.Application.Output.Complete(); 270 | } 271 | 272 | private static string GetInvocationId() 273 | { 274 | return Guid.NewGuid().ToString("N"); 275 | } 276 | 277 | public void OnHeartbeat(Action action, object state) 278 | { 279 | lock (_heartbeatLock) 280 | { 281 | if (_heartbeatHandlers == null) 282 | { 283 | _heartbeatHandlers = new List<(Action handler, object state)>(); 284 | } 285 | _heartbeatHandlers.Add((action, state)); 286 | } 287 | } 288 | 289 | public void TickHeartbeat() 290 | { 291 | lock (_heartbeatLock) 292 | { 293 | if (_heartbeatHandlers == null) 294 | { 295 | return; 296 | } 297 | 298 | foreach (var (handler, state) in _heartbeatHandlers) 299 | { 300 | handler(state); 301 | } 302 | } 303 | } 304 | 305 | private class DefaultInvocationBinder : IInvocationBinder 306 | { 307 | public IReadOnlyList GetParameterTypes(string methodName) 308 | { 309 | // TODO: Possibly support actual client methods 310 | return new[] { typeof(object) }; 311 | } 312 | 313 | public Type GetReturnType(string invocationId) 314 | { 315 | return typeof(object); 316 | } 317 | 318 | public Type GetStreamItemType(string streamId) 319 | { 320 | throw new NotImplementedException(); 321 | } 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/AspNetCore.SignalR.Orleans/AspNetCore.SignalR.Orleans.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | latest 6 | https://github.com/slango0513/AspNetCore.SignalR.Orleans#license 7 | https://github.com/slango0513/AspNetCore.SignalR.Orleans 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/AspNetCore.SignalR.Orleans/HubProxy`T.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.SignalR; 2 | 3 | namespace Orleans.Messaging.SignalR 4 | { 5 | public class HubProxy : HubProxy, IHubProxy where THub : Hub 6 | { 7 | public HubProxy(IClusterClient clusterClient) 8 | : base(clusterClient, clusterClient.GetStreamProvider(SignalRConstants.STREAM_PROVIDER), typeof(THub).GUID) 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/AspNetCore.SignalR.Orleans/IHubProxy`T.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.SignalR; 2 | 3 | namespace Orleans.Messaging.SignalR 4 | { 5 | public interface IHubProxy : IHubProxy where THub : Hub 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/AspNetCore.SignalR.Orleans/Internal/OrleansLog.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | 4 | namespace AspNetCore.SignalR.Orleans.Internal 5 | { 6 | internal static class OrleansLog 7 | { 8 | private static readonly Action _connectingToEndpoints = 9 | LoggerMessage.Define(LogLevel.Information, new EventId(1, "ConnectingToEndpoints"), "Connecting to Orleans endpoints: {Endpoints}. Using Server Name: {ServerName}"); 10 | 11 | private static readonly Action _connected = 12 | LoggerMessage.Define(LogLevel.Information, new EventId(2, "Connected"), "Connected to Orleans."); 13 | 14 | private static readonly Action _subscribing = 15 | LoggerMessage.Define(LogLevel.Trace, new EventId(3, "Subscribing"), "Subscribing to stream: {Stream}."); 16 | 17 | private static readonly Action _receivedFromStream = 18 | LoggerMessage.Define(LogLevel.Trace, new EventId(4, "ReceivedFromStream"), "Received message from Orleans stream {Stream}."); 19 | 20 | private static readonly Action _publishToStream = 21 | LoggerMessage.Define(LogLevel.Trace, new EventId(5, "PublishToStream"), "Publishing message to Orleans stream {Stream}."); 22 | 23 | private static readonly Action _unsubscribe = 24 | LoggerMessage.Define(LogLevel.Trace, new EventId(6, "Unsubscribe"), "Unsubscribing from stream: {Stream}."); 25 | 26 | private static readonly Action _notConnected = 27 | LoggerMessage.Define(LogLevel.Error, new EventId(7, "Connected"), "Not connected to Orleans."); 28 | 29 | private static readonly Action _connectionRestored = 30 | LoggerMessage.Define(LogLevel.Information, new EventId(8, "ConnectionRestored"), "Connection to Orleans restored."); 31 | 32 | private static readonly Action _connectionFailed = 33 | LoggerMessage.Define(LogLevel.Error, new EventId(9, "ConnectionFailed"), "Connection to Orleans failed."); 34 | 35 | private static readonly Action _failedWritingMessage = 36 | LoggerMessage.Define(LogLevel.Warning, new EventId(10, "FailedWritingMessage"), "Failed writing message."); 37 | 38 | private static readonly Action _internalMessageFailed = 39 | LoggerMessage.Define(LogLevel.Warning, new EventId(11, "InternalMessageFailed"), "Error processing message for internal server message."); 40 | 41 | 42 | public static void Connected(ILogger logger) 43 | { 44 | _connected(logger, null); 45 | } 46 | 47 | public static void Subscribing(ILogger logger, Guid streamId) 48 | { 49 | _subscribing(logger, streamId, null); 50 | } 51 | 52 | public static void ReceivedFromStream(ILogger logger, Guid streamId) 53 | { 54 | _receivedFromStream(logger, streamId, null); 55 | } 56 | 57 | public static void PublishToStream(ILogger logger, Guid streamId) 58 | { 59 | _publishToStream(logger, streamId, null); 60 | } 61 | 62 | public static void Unsubscribe(ILogger logger, Guid streamId) 63 | { 64 | _unsubscribe(logger, streamId, null); 65 | } 66 | 67 | public static void NotConnected(ILogger logger) 68 | { 69 | _notConnected(logger, null); 70 | } 71 | 72 | public static void ConnectionRestored(ILogger logger) 73 | { 74 | _connectionRestored(logger, null); 75 | } 76 | 77 | public static void ConnectionFailed(ILogger logger, Exception exception) 78 | { 79 | _connectionFailed(logger, exception); 80 | } 81 | 82 | public static void FailedWritingMessage(ILogger logger, Exception exception) 83 | { 84 | _failedWritingMessage(logger, exception); 85 | } 86 | 87 | public static void InternalMessageFailed(ILogger logger, Exception exception) 88 | { 89 | _internalMessageFailed(logger, exception); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/AspNetCore.SignalR.Orleans/OrleansDependencyInjectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using AspNetCore.SignalR.Orleans; 2 | using Microsoft.AspNetCore.SignalR; 3 | using Microsoft.Extensions.Options; 4 | using Orleans; 5 | using Orleans.Messaging.SignalR; 6 | using System; 7 | 8 | namespace Microsoft.Extensions.DependencyInjection 9 | { 10 | public static partial class OrleansDependencyInjectionExtensions 11 | { 12 | /// 13 | /// Adds scale-out to a , using a shared Orleans cluster. 14 | /// 15 | /// The . 16 | /// The . 17 | /// The same instance of the for chaining. 18 | public static ISignalRServerBuilder AddOrleans(this ISignalRServerBuilder signalrBuilder, Action> configure) where THub : Hub 19 | { 20 | signalrBuilder.Services.Configure(configure); 21 | signalrBuilder.Services.AddSingleton(typeof(HubLifetimeManager), typeof(OrleansHubLifetimeManager)); 22 | signalrBuilder.Services.AddSingleton(typeof(IHubProxy), provider => new HubProxy(provider.GetRequiredService>>().Value.ClusterClient)); 23 | return signalrBuilder; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/AspNetCore.SignalR.Orleans/OrleansHubLifetimeManager.cs: -------------------------------------------------------------------------------- 1 | using AspNetCore.SignalR.Orleans.Internal; 2 | using Microsoft.AspNetCore.SignalR; 3 | using Microsoft.AspNetCore.SignalR.Protocol; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Options; 6 | using Orleans; 7 | using Orleans.Messaging.SignalR; 8 | using Orleans.Messaging.SignalR.Internal; 9 | using Orleans.Streams; 10 | using System; 11 | using System.Collections.Concurrent; 12 | using System.Collections.Generic; 13 | using System.Linq; 14 | using System.Threading; 15 | using System.Threading.Tasks; 16 | 17 | namespace AspNetCore.SignalR.Orleans 18 | { 19 | public class OrleansHubLifetimeManager : HubLifetimeManager, IDisposable where THub : Hub 20 | { 21 | private readonly Guid _id = Guid.NewGuid(); 22 | private readonly Guid _hubTypeId = typeof(THub).GUID; 23 | 24 | private readonly IClusterClient _clusterClient; 25 | private readonly ILogger _logger; 26 | 27 | public OrleansHubLifetimeManager(IOptions> options, IHubProxy hubProxy, ILogger> logger) 28 | { 29 | _clusterClient = options.Value.ClusterClient; 30 | HubProxy = hubProxy; 31 | _logger = logger; 32 | } 33 | 34 | public IHubProxy HubProxy { get; } 35 | 36 | private bool _initialized; 37 | private IAsyncStream _clientMessageStream; 38 | private StreamSubscriptionHandle _clientMessageHandle; 39 | private StreamSubscriptionHandle _allMessageHandle; 40 | private ConcurrentDictionary connectionsById = new ConcurrentDictionary(); 41 | 42 | private async Task InitializeAsync() 43 | { 44 | await EnsureOrleansClusterConnection(); 45 | 46 | OrleansLog.Subscribing(_logger, _id); 47 | 48 | try 49 | { 50 | _clientMessageStream = _clusterClient.GetStreamProvider(SignalRConstants.STREAM_PROVIDER).GetStream(_id, InternalSignalRConstants.SEND_CLIENT_MESSAGE_STREAM_NAMESPACE); 51 | _clientMessageHandle = await _clientMessageStream.SubscribeAsync(async (message, token) => await OnReceivedAsync(message)); 52 | _allMessageHandle = await HubProxy.AllMessageStream.SubscribeAsync(async (message, token) => await OnReceivedAsync(message)); 53 | } 54 | catch (Exception e) 55 | { 56 | OrleansLog.InternalMessageFailed(_logger, e); 57 | } 58 | } 59 | 60 | private int _retries = 0; 61 | 62 | private async Task EnsureOrleansClusterConnection() 63 | { 64 | if (_clusterClient == null) 65 | { 66 | throw new NullReferenceException(nameof(_clusterClient)); 67 | } 68 | 69 | if (_clusterClient.IsInitialized) 70 | { 71 | OrleansLog.Connected(_logger); 72 | return; 73 | } 74 | 75 | OrleansLog.NotConnected(_logger); 76 | await _clusterClient.Connect(async ex => 77 | { 78 | if (_retries >= 5) 79 | { 80 | throw ex; 81 | } 82 | 83 | OrleansLog.ConnectionFailed(_logger, ex); 84 | await Task.Delay(2000); 85 | _retries++; 86 | return true; 87 | }); 88 | OrleansLog.ConnectionRestored(_logger); 89 | } 90 | 91 | private async Task OnReceivedAsync(SendClientInvocationMessage message) 92 | { 93 | OrleansLog.ReceivedFromStream(_logger, _id); 94 | if (connectionsById.TryGetValue(message.ConnectionId, out var connection)) 95 | { 96 | var invocationMessage = new InvocationMessage(message.MethodName, message.Args); 97 | 98 | try 99 | { 100 | await connection.WriteAsync(invocationMessage).AsTask(); 101 | } 102 | catch (Exception e) 103 | { 104 | OrleansLog.FailedWritingMessage(_logger, e); 105 | } 106 | } 107 | } 108 | 109 | private async Task OnReceivedAsync(SendAllInvocationMessage message) 110 | { 111 | OrleansLog.ReceivedFromStream(_logger, _id); 112 | var invocationMessage = new InvocationMessage(message.MethodName, message.Args); 113 | 114 | var tasks = connectionsById 115 | .Where(pair => !pair.Value.ConnectionAborted.IsCancellationRequested && !message.ExcludedConnectionIds.Contains(pair.Key)) 116 | .Select(pair => pair.Value.WriteAsync(invocationMessage).AsTask()); 117 | 118 | try 119 | { 120 | await Task.WhenAll(tasks); 121 | } 122 | catch (Exception e) 123 | { 124 | OrleansLog.FailedWritingMessage(_logger, e); 125 | } 126 | } 127 | 128 | public override async Task OnConnectedAsync(HubConnectionContext connection) 129 | { 130 | if (!_initialized) 131 | { 132 | _initialized = true; 133 | await InitializeAsync(); 134 | } 135 | 136 | var connectionId = connection.ConnectionId; 137 | await HubProxy.OnConnectedAsync(_id, connectionId, connection.UserIdentifier); 138 | connectionsById.TryAdd(connectionId, connection); 139 | } 140 | 141 | public override async Task OnDisconnectedAsync(HubConnectionContext connection) 142 | { 143 | var connectionId = connection.ConnectionId; 144 | await HubProxy.OnDisconnectedAsync(connectionId, connection.UserIdentifier); 145 | connectionsById.TryRemove(connectionId, out _); 146 | } 147 | 148 | public override Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) 149 | { 150 | return HubProxy.AddToGroupAsync(connectionId, groupName); 151 | } 152 | 153 | public override Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) 154 | { 155 | return HubProxy.RemoveFromGroupAsync(connectionId, groupName); 156 | } 157 | 158 | public override Task SendAllAsync(string methodName, object[] args, CancellationToken cancellationToken = default) 159 | { 160 | return HubProxy.SendAllAsync(methodName, args); 161 | } 162 | 163 | public override Task SendAllExceptAsync(string methodName, object[] args, IReadOnlyList excludedConnectionIds, CancellationToken cancellationToken = default) 164 | { 165 | return HubProxy.SendAllExceptAsync(methodName, args, excludedConnectionIds); 166 | } 167 | 168 | public override Task SendConnectionAsync(string connectionId, string methodName, object[] args, CancellationToken cancellationToken = default) 169 | { 170 | return HubProxy.SendClientAsync(connectionId, methodName, args); 171 | } 172 | 173 | public override Task SendConnectionsAsync(IReadOnlyList connectionIds, string methodName, object[] args, CancellationToken cancellationToken = default) 174 | { 175 | return HubProxy.SendClientsAsync(connectionIds, methodName, args); 176 | } 177 | 178 | public override Task SendGroupAsync(string groupName, string methodName, object[] args, CancellationToken cancellationToken = default) 179 | { 180 | return HubProxy.SendGroupAsync(groupName, methodName, args); 181 | } 182 | 183 | public override Task SendGroupExceptAsync(string groupName, string methodName, object[] args, IReadOnlyList excludedConnectionIds, CancellationToken cancellationToken = default) 184 | { 185 | return HubProxy.SendGroupExceptAsync(groupName, methodName, args, excludedConnectionIds); 186 | } 187 | 188 | public override Task SendGroupsAsync(IReadOnlyList groupNames, string methodName, object[] args, CancellationToken cancellationToken = default) 189 | { 190 | return HubProxy.SendGroupsAsync(groupNames, methodName, args); 191 | } 192 | 193 | public override Task SendUserAsync(string userId, string methodName, object[] args, CancellationToken cancellationToken = default) 194 | { 195 | return HubProxy.SendUserAsync(userId, methodName, args); 196 | } 197 | 198 | public override Task SendUsersAsync(IReadOnlyList userIds, string methodName, object[] args, CancellationToken cancellationToken = default) 199 | { 200 | return HubProxy.SendUsersAsync(userIds, methodName, args); 201 | } 202 | 203 | public void Dispose() 204 | { 205 | if (!_initialized) 206 | { 207 | return; 208 | } 209 | 210 | OrleansLog.Unsubscribe(_logger, _id); 211 | 212 | var tasks = connectionsById.Keys.Select(id => _clusterClient.GetStreamProvider(SignalRConstants.STREAM_PROVIDER) 213 | .GetStream(InternalSignalRConstants.DISCONNECTION_STREAM_ID, id) 214 | .OnNextAsync(EventArgs.Empty)); 215 | Task.WaitAll(tasks.ToArray()); 216 | 217 | Task.WaitAll(_allMessageHandle.UnsubscribeAsync()); 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/AspNetCore.SignalR.Orleans/OrleansOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.SignalR; 2 | using Orleans; 3 | 4 | namespace AspNetCore.SignalR.Orleans 5 | { 6 | /// 7 | /// Options used to configure . 8 | /// 9 | public class OrleansOptions where THub : Hub 10 | { 11 | public IClusterClient ClusterClient { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/AspNetCore.SignalR.Orleans/SignalRClientBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Orleans.Configuration; 2 | using Orleans.Hosting; 3 | using Orleans.Messaging.SignalR; 4 | using Orleans.Messaging.SignalR.Internal; 5 | 6 | namespace Orleans 7 | { 8 | public static partial class SignalRClientBuilderExtensions 9 | { 10 | public static IClientBuilder UseSignalR(this IClientBuilder builder, 11 | bool fireAndForgetDelivery = SimpleMessageStreamProviderOptions.DEFAULT_VALUE_FIRE_AND_FORGET_DELIVERY) 12 | { 13 | builder.AddSimpleMessageStreamProvider(SignalRConstants.STREAM_PROVIDER, options => 14 | { 15 | options.FireAndForgetDelivery = fireAndForgetDelivery; 16 | }) 17 | .ConfigureApplicationParts(manager => 18 | { 19 | manager.AddApplicationPart(typeof(IClientGrain).Assembly).WithReferences(); 20 | }); 21 | 22 | return builder; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR.Common/HubInvocationMessage.cs: -------------------------------------------------------------------------------- 1 | namespace Orleans.Messaging.SignalR 2 | { 3 | public abstract class HubInvocationMessage 4 | { 5 | public HubInvocationMessage(string methodName, object[] args) 6 | { 7 | MethodName = methodName; 8 | Args = args; 9 | } 10 | 11 | public string MethodName { get; } 12 | 13 | public object[] Args { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR.Common/HubProxy.cs: -------------------------------------------------------------------------------- 1 | using Orleans.Messaging.SignalR.Internal; 2 | using Orleans.Streams; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace Orleans.Messaging.SignalR 9 | { 10 | public class HubProxy : IHubProxy 11 | { 12 | private readonly IGrainFactory _grainFactory; 13 | private readonly IStreamProvider _streamProvider; 14 | private readonly Guid _hubTypeId; 15 | 16 | public HubProxy(IGrainFactory grainFactory, IStreamProvider streamProvider, Guid hubTypeId) 17 | { 18 | _grainFactory = grainFactory ?? throw new ArgumentNullException(nameof(grainFactory)); 19 | _streamProvider = streamProvider ?? throw new ArgumentNullException(nameof(streamProvider)); 20 | _hubTypeId = hubTypeId; 21 | 22 | AllMessageStream = _streamProvider.GetStream(_hubTypeId, InternalSignalRConstants.SEND_All_MESSAGE_STREAM_NAMESPACE); 23 | } 24 | 25 | public IAsyncStream AllMessageStream { get; } 26 | 27 | #region HubLifetimeManager Support 28 | public async Task OnConnectedAsync(Guid managerId, string connectionId, string userId = default) 29 | { 30 | if (connectionId == null) 31 | { 32 | throw new ArgumentNullException(nameof(connectionId)); 33 | } 34 | 35 | if (userId != null) 36 | { 37 | await _grainFactory.GetUserGrain(userId, _hubTypeId).AddToClientsAsync(connectionId); 38 | } 39 | await _grainFactory.GetClientGrain(connectionId, _hubTypeId).OnConnectedAsync(managerId); 40 | } 41 | 42 | public async Task OnDisconnectedAsync(string connectionId, string userId = default) 43 | { 44 | if (connectionId == null) 45 | { 46 | throw new ArgumentNullException(nameof(connectionId)); 47 | } 48 | 49 | if (userId != null) 50 | { 51 | await _grainFactory.GetUserGrain(userId, _hubTypeId).RemoveFromClientsAsync(connectionId); 52 | } 53 | await _grainFactory.GetClientGrain(connectionId, _hubTypeId).OnDisconnectedAsync(); 54 | } 55 | #endregion 56 | 57 | #region GroupManager Support 58 | public Task AddToGroupAsync(string connectionId, string groupName) 59 | { 60 | if (connectionId == null) 61 | { 62 | throw new ArgumentNullException(nameof(connectionId)); 63 | } 64 | if (groupName == null) 65 | { 66 | throw new ArgumentNullException(nameof(groupName)); 67 | } 68 | 69 | return _grainFactory.GetGroupGrain(groupName, _hubTypeId).AddToClientsAsync(connectionId); 70 | } 71 | 72 | public Task RemoveFromGroupAsync(string connectionId, string groupName) 73 | { 74 | if (connectionId == null) 75 | { 76 | throw new ArgumentNullException(nameof(connectionId)); 77 | } 78 | if (groupName == null) 79 | { 80 | throw new ArgumentNullException(nameof(groupName)); 81 | } 82 | 83 | return _grainFactory.GetGroupGrain(groupName, _hubTypeId).RemoveFromClientsAsync(connectionId); 84 | } 85 | #endregion 86 | 87 | #region HubClients Support 88 | public Task SendAllAsync(string method, object[] args) 89 | { 90 | return SendAllExceptAsync(method, args, Enumerable.Empty().ToList()); 91 | } 92 | 93 | public Task SendAllExceptAsync(string method, object[] args, IReadOnlyList excludedConnectionIds) 94 | { 95 | if (excludedConnectionIds == null) 96 | { 97 | throw new ArgumentNullException(nameof(excludedConnectionIds)); 98 | } 99 | 100 | return AllMessageStream.OnNextAsync(new SendAllInvocationMessage(method, args, excludedConnectionIds)); 101 | } 102 | 103 | public Task SendClientAsync(string connectionId, string method, object[] args) 104 | { 105 | if (connectionId == null) 106 | { 107 | throw new ArgumentNullException(nameof(connectionId)); 108 | } 109 | 110 | return _grainFactory.GetClientGrain(connectionId, _hubTypeId).SendConnectionAsync(method, args); 111 | } 112 | 113 | public Task SendClientsAsync(IReadOnlyList connectionIds, string method, object[] args) 114 | { 115 | if (connectionIds == null) 116 | { 117 | throw new ArgumentNullException(nameof(connectionIds)); 118 | } 119 | 120 | return SendClientsExceptAsync(connectionIds, method, args, Enumerable.Empty().ToList()); 121 | } 122 | 123 | public Task SendClientsExceptAsync(IReadOnlyList connectionIds, string method, object[] args, IReadOnlyList excludedConnectionIds) 124 | { 125 | if (connectionIds == null) 126 | { 127 | throw new ArgumentNullException(nameof(connectionIds)); 128 | } 129 | if (excludedConnectionIds == null) 130 | { 131 | throw new ArgumentNullException(nameof(excludedConnectionIds)); 132 | } 133 | var tasks = connectionIds.Where(id => !excludedConnectionIds.Contains(id)).Select(id => _grainFactory.GetClientGrain(id, _hubTypeId).SendConnectionAsync(method, args)); 134 | return Task.WhenAll(tasks); 135 | } 136 | 137 | public Task SendGroupAsync(string groupName, string method, object[] args) 138 | { 139 | if (groupName == null) 140 | { 141 | throw new ArgumentNullException(nameof(groupName)); 142 | } 143 | 144 | return _grainFactory.GetGroupGrain(groupName, _hubTypeId).SendConnectionsAsync(method, args); 145 | } 146 | 147 | public Task SendGroupExceptAsync(string groupName, string method, object[] args, IReadOnlyList excludedConnectionIds) 148 | { 149 | if (groupName == null) 150 | { 151 | throw new ArgumentNullException(nameof(groupName)); 152 | } 153 | if (excludedConnectionIds == null) 154 | { 155 | throw new ArgumentNullException(nameof(excludedConnectionIds)); 156 | } 157 | 158 | return _grainFactory.GetGroupGrain(groupName, _hubTypeId).SendConnectionsExceptAsync(method, args, excludedConnectionIds); 159 | } 160 | 161 | public Task SendGroupsAsync(IReadOnlyList groupNames, string method, object[] args) 162 | { 163 | if (groupNames == null) 164 | { 165 | throw new ArgumentNullException(nameof(groupNames)); 166 | } 167 | 168 | var tasks = groupNames.Select(name => _grainFactory.GetGroupGrain(name, _hubTypeId).SendConnectionsAsync(method, args)); 169 | return Task.WhenAll(tasks); 170 | } 171 | 172 | public Task SendUserAsync(string userId, string method, object[] args) 173 | { 174 | if (userId == null) 175 | { 176 | throw new ArgumentNullException(nameof(userId)); 177 | } 178 | 179 | return _grainFactory.GetUserGrain(userId, _hubTypeId).SendConnectionsAsync(method, args); 180 | } 181 | 182 | public Task SendUsersAsync(IReadOnlyList userIds, string method, object[] args) 183 | { 184 | if (userIds == null) 185 | { 186 | throw new ArgumentNullException(nameof(userIds)); 187 | } 188 | 189 | var tasks = userIds.Select(id => _grainFactory.GetUserGrain(id, _hubTypeId).SendConnectionsAsync(method, args)); 190 | return Task.WhenAll(tasks); 191 | } 192 | 193 | public Task SendUsersExceptAsync(IReadOnlyList userIds, string method, object[] args, IReadOnlyList excludedConnectionIds) 194 | { 195 | if (userIds == null) 196 | { 197 | throw new ArgumentNullException(nameof(userIds)); 198 | } 199 | if (excludedConnectionIds == null) 200 | { 201 | throw new ArgumentNullException(nameof(excludedConnectionIds)); 202 | } 203 | 204 | var tasks = userIds.Select(id => _grainFactory.GetUserGrain(id, _hubTypeId).SendConnectionsExceptAsync(method, args, excludedConnectionIds)); 205 | return Task.WhenAll(tasks); 206 | } 207 | #endregion 208 | 209 | #region Deactive Support 210 | public Task DeactiveClientAsync(string connectionId) 211 | { 212 | if (connectionId == null) 213 | { 214 | throw new ArgumentNullException(nameof(connectionId)); 215 | } 216 | 217 | return _grainFactory.GetClientGrain(connectionId, _hubTypeId).DeactivateAsync(); 218 | } 219 | 220 | public Task DeactiveGroupAsync(string groupName) 221 | { 222 | if (groupName == null) 223 | { 224 | throw new ArgumentNullException(nameof(groupName)); 225 | } 226 | 227 | return _grainFactory.GetGroupGrain(groupName, _hubTypeId).DeactivateAsync(); 228 | } 229 | 230 | public Task DeactiveUserAsync(string userId) 231 | { 232 | if (userId == null) 233 | { 234 | throw new ArgumentNullException(nameof(userId)); 235 | } 236 | 237 | return _grainFactory.GetUserGrain(userId, _hubTypeId).DeactivateAsync(); 238 | } 239 | #endregion 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR.Common/HubProxyGrainFactoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using Orleans.Messaging.SignalR; 2 | using Orleans.Streams; 3 | using System; 4 | 5 | namespace Orleans 6 | { 7 | public static partial class HubProxyGrainFactoryExtensions 8 | { 9 | public static IHubProxy GetHubProxy(this IGrainFactory grainFactory, IStreamProvider streamProvider, Guid hubTypeId) 10 | { 11 | return new HubProxy(grainFactory, streamProvider, hubTypeId); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR.Common/IHubProxy.cs: -------------------------------------------------------------------------------- 1 | using Orleans.Streams; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace Orleans.Messaging.SignalR 7 | { 8 | public interface IHubProxy 9 | { 10 | IAsyncStream AllMessageStream { get; } 11 | 12 | Task OnConnectedAsync(Guid managerId, string connectionId, string userId = default); 13 | 14 | Task OnDisconnectedAsync(string connectionId, string userId = default); 15 | 16 | Task AddToGroupAsync(string connectionId, string groupName); 17 | 18 | Task RemoveFromGroupAsync(string connectionId, string groupName); 19 | 20 | Task SendAllAsync(string method, object[] args); 21 | 22 | Task SendAllExceptAsync(string method, object[] args, IReadOnlyList excludedConnectionIds); 23 | 24 | Task SendClientAsync(string connectionId, string method, object[] args); 25 | 26 | Task SendClientsAsync(IReadOnlyList connectionIds, string method, object[] args); 27 | 28 | Task SendClientsExceptAsync(IReadOnlyList connectionIds, string method, object[] args, IReadOnlyList excludedConnectionIds); 29 | 30 | Task SendGroupAsync(string groupName, string method, object[] args); 31 | 32 | Task SendGroupExceptAsync(string groupName, string method, object[] args, IReadOnlyList excludedConnectionIds); 33 | 34 | Task SendGroupsAsync(IReadOnlyList groupNames, string method, object[] args); 35 | 36 | Task SendUserAsync(string userId, string method, object[] args); 37 | 38 | Task SendUsersAsync(IReadOnlyList userIds, string method, object[] args); 39 | 40 | Task SendUsersExceptAsync(IReadOnlyList userIds, string method, object[] args, IReadOnlyList excludedConnectionIds); 41 | 42 | Task DeactiveClientAsync(string connectionId); 43 | 44 | Task DeactiveGroupAsync(string groupName); 45 | 46 | Task DeactiveUserAsync(string userId); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR.Common/Internal/GrainInterfaces/IClientGrain.cs: -------------------------------------------------------------------------------- 1 | using Orleans.Concurrency; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace Orleans.Messaging.SignalR.Internal 6 | { 7 | public interface IClientGrain : IGrainWithHubTypedStringKey 8 | { 9 | Task OnConnectedAsync(Guid managerId); 10 | 11 | Task OnDisconnectedAsync(); 12 | 13 | [AlwaysInterleave] 14 | Task SendConnectionAsync(string methodName, object[] args); 15 | 16 | Task DeactivateAsync(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR.Common/Internal/GrainInterfaces/IClientsGrain.cs: -------------------------------------------------------------------------------- 1 | using Orleans.Concurrency; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Orleans.Messaging.SignalR.Internal 6 | { 7 | public interface IClientsGrain : IGrainWithHubTypedStringKey 8 | { 9 | Task AddToClientsAsync(string connectionId); 10 | 11 | Task RemoveFromClientsAsync(string connectionId); 12 | 13 | [AlwaysInterleave] 14 | Task SendConnectionsAsync(string methodName, object[] args); 15 | 16 | [AlwaysInterleave] 17 | Task SendConnectionsExceptAsync(string methodName, object[] args, IReadOnlyList excludedConnectionIds); 18 | 19 | Task DeactivateAsync(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR.Common/Internal/GrainInterfaces/IGrainWithHubTypedStringKey.cs: -------------------------------------------------------------------------------- 1 | namespace Orleans.Messaging.SignalR.Internal 2 | { 3 | public interface IGrainWithHubTypedStringKey : IGrainWithStringKey 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR.Common/Internal/GrainInterfaces/IGroupGrain.cs: -------------------------------------------------------------------------------- 1 | namespace Orleans.Messaging.SignalR.Internal 2 | { 3 | public interface IGroupGrain : IClientsGrain 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR.Common/Internal/GrainInterfaces/IUserGrain.cs: -------------------------------------------------------------------------------- 1 | namespace Orleans.Messaging.SignalR.Internal 2 | { 3 | public interface IUserGrain : IClientsGrain 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR.Common/Internal/HubTypedKeyUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Orleans.Messaging.SignalR.Internal 4 | { 5 | public class HubTypedKeyUtils 6 | { 7 | public const char SEPARATOR = ':'; 8 | 9 | public static string ToHubTypedKeyString(string id, Guid hubTypeId) 10 | { 11 | if (id == null) 12 | { 13 | throw new ArgumentNullException(nameof(id)); 14 | } 15 | 16 | return $"{id}{SEPARATOR}{hubTypeId}"; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR.Common/Internal/InternalGrainFactoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using Orleans.Messaging.SignalR.Internal; 2 | using System; 3 | 4 | namespace Orleans 5 | { 6 | internal static partial class InternalGrainFactoryExtensions 7 | { 8 | public static IClientGrain GetClientGrain(this IGrainFactory grainFactory, string connectionId, Guid hubTypeId) 9 | { 10 | return grainFactory.GetGrain(HubTypedKeyUtils.ToHubTypedKeyString(connectionId, hubTypeId)); 11 | } 12 | 13 | public static IGroupGrain GetGroupGrain(this IGrainFactory grainFactory, string groupName, Guid hubTypeId) 14 | { 15 | return grainFactory.GetGrain(HubTypedKeyUtils.ToHubTypedKeyString(groupName, hubTypeId)); 16 | } 17 | 18 | public static IUserGrain GetUserGrain(this IGrainFactory grainFactory, string userId, Guid hubTypeId) 19 | { 20 | return grainFactory.GetGrain(HubTypedKeyUtils.ToHubTypedKeyString(userId, hubTypeId)); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR.Common/Internal/InternalSignalRConstants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Orleans.Messaging.SignalR.Internal 4 | { 5 | public static class InternalSignalRConstants 6 | { 7 | public const string PREFIX = "ORLEANS_MESSAGING_SIGNALR_"; 8 | 9 | public const string STORAGE_PROVIDER = PREFIX + "STORAGE_PROVIDER"; 10 | 11 | public const string SEND_CLIENT_MESSAGE_STREAM_NAMESPACE = PREFIX + "SEND_CLIENT_MESSAGE_STREAM"; 12 | 13 | public const string SEND_All_MESSAGE_STREAM_NAMESPACE = PREFIX + "SEND_ALL_MESSAGE_STREAM"; 14 | 15 | public static readonly Guid DISCONNECTION_STREAM_ID = Guid.Parse("888AAEAD-7A35-4017-8302-B7C520C3D107"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR.Common/Orleans.Messaging.SignalR.Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | latest 6 | https://github.com/slango0513/AspNetCore.SignalR.Orleans#license 7 | https://github.com/slango0513/AspNetCore.SignalR.Orleans 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR.Common/SendAllInvocationMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Orleans.Messaging.SignalR 4 | { 5 | public class SendAllInvocationMessage : HubInvocationMessage 6 | { 7 | public SendAllInvocationMessage(string methodName, object[] args, IReadOnlyList excludedConnectionIds) 8 | : base(methodName, args) 9 | { 10 | ExcludedConnectionIds = excludedConnectionIds; 11 | } 12 | 13 | public IReadOnlyList ExcludedConnectionIds { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR.Common/SendClientInvocationMessage.cs: -------------------------------------------------------------------------------- 1 | namespace Orleans.Messaging.SignalR.Internal 2 | { 3 | public class SendClientInvocationMessage : HubInvocationMessage 4 | { 5 | public SendClientInvocationMessage(string methodName, object[] args, string connectionId) 6 | : base(methodName, args) 7 | { 8 | ConnectionId = connectionId; 9 | } 10 | 11 | public string ConnectionId { get; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR.Common/SignalRConstants.cs: -------------------------------------------------------------------------------- 1 | using Orleans.Messaging.SignalR.Internal; 2 | 3 | namespace Orleans.Messaging.SignalR 4 | { 5 | public class SignalRConstants 6 | { 7 | public const string STREAM_PROVIDER = InternalSignalRConstants.PREFIX + "STREAM_PROVIDER"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR/GrainWithHubTypedKeyExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Orleans.Messaging.SignalR.Internal 4 | { 5 | internal static partial class GrainWithHubTypedKeyExtensions 6 | { 7 | public static Guid GetHubTypeId(this IGrainWithHubTypedStringKey grain) 8 | { 9 | return Guid.Parse(grain.GetPrimaryKeyString().Split(HubTypedKeyUtils.SEPARATOR)[1]); 10 | } 11 | 12 | public static string GetId(this IGrainWithHubTypedStringKey grain) 13 | { 14 | return grain.GetPrimaryKeyString().Split(HubTypedKeyUtils.SEPARATOR)[0]; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR/Grains/ClientGrain.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Orleans.Providers; 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | namespace Orleans.Messaging.SignalR.Internal 7 | { 8 | internal class ClientGrainState 9 | { 10 | public bool Connected { get; set; } 11 | 12 | public Guid HubLifetimeManagerId { get; set; } 13 | } 14 | 15 | [StorageProvider(ProviderName = InternalSignalRConstants.STORAGE_PROVIDER)] 16 | internal class ClientGrain : Grain, IClientGrain 17 | { 18 | private readonly ILogger _logger; 19 | 20 | public ClientGrain(ILogger logger) 21 | { 22 | _logger = logger; 23 | } 24 | 25 | public async Task OnConnectedAsync(Guid hubLifetimeManagerId) 26 | { 27 | State.HubLifetimeManagerId = hubLifetimeManagerId; 28 | 29 | State.Connected = true; 30 | await WriteStateAsync(); 31 | } 32 | 33 | public async Task OnDisconnectedAsync() 34 | { 35 | var connectionId = this.GetId(); 36 | 37 | await GetStreamProvider(SignalRConstants.STREAM_PROVIDER) 38 | .GetStream(InternalSignalRConstants.DISCONNECTION_STREAM_ID, connectionId) 39 | .OnNextAsync(EventArgs.Empty); 40 | 41 | State.Connected = false; 42 | await WriteStateAsync(); 43 | 44 | DeactivateOnIdle(); 45 | } 46 | 47 | public Task SendConnectionAsync(string methodName, object[] args) 48 | { 49 | var connectionId = this.GetId(); 50 | return GetStreamProvider(SignalRConstants.STREAM_PROVIDER) 51 | .GetStream(State.HubLifetimeManagerId, InternalSignalRConstants.SEND_CLIENT_MESSAGE_STREAM_NAMESPACE) 52 | .OnNextAsync(new SendClientInvocationMessage(methodName, args, connectionId)); 53 | } 54 | 55 | public Task DeactivateAsync() 56 | { 57 | DeactivateOnIdle(); 58 | return Task.CompletedTask; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR/Grains/ClientsGrain.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Orleans.Streams; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace Orleans.Messaging.SignalR.Internal 9 | { 10 | internal abstract class ClientsGrainState 11 | { 12 | public Dictionary> HandlesByConnectionId { get; set; } 13 | = new Dictionary>(); 14 | } 15 | 16 | internal abstract class ClientsGrain : Grain, IClientsGrain 17 | where TGrainState : ClientsGrainState, new() 18 | { 19 | protected readonly ILogger _logger; 20 | 21 | public ClientsGrain(ILogger logger) : base() 22 | { 23 | _logger = logger; 24 | } 25 | 26 | public override async Task OnActivateAsync() 27 | { 28 | var tasks = State.HandlesByConnectionId.Keys.Select(id => ResumeRemoveHandleAsync(id)); 29 | await Task.WhenAll(tasks); 30 | 31 | async Task ResumeRemoveHandleAsync(string connectionId) 32 | { 33 | var handles = await GetStreamProvider(SignalRConstants.STREAM_PROVIDER) 34 | .GetStream(InternalSignalRConstants.DISCONNECTION_STREAM_ID, connectionId) 35 | .GetAllSubscriptionHandles(); 36 | var _tasks = handles.Select(handle => handle.ResumeAsync(async (_, token) => await RemoveFromClientsAsync(connectionId))); 37 | await Task.WhenAll(_tasks); 38 | } 39 | } 40 | 41 | public virtual async Task AddToClientsAsync(string connectionId) 42 | { 43 | if (!State.HandlesByConnectionId.ContainsKey(connectionId)) 44 | { 45 | var handle = await GetStreamProvider(SignalRConstants.STREAM_PROVIDER) 46 | .GetStream(InternalSignalRConstants.DISCONNECTION_STREAM_ID, connectionId) 47 | .SubscribeAsync(async (_, token) => await RemoveFromClientsAsync(connectionId)); 48 | State.HandlesByConnectionId.Add(connectionId, handle); 49 | await WriteStateAsync(); 50 | } 51 | } 52 | 53 | public virtual async Task RemoveFromClientsAsync(string connectionId) 54 | { 55 | if (State.HandlesByConnectionId.ContainsKey(connectionId)) 56 | { 57 | var handle = State.HandlesByConnectionId[connectionId]; 58 | await handle.UnsubscribeAsync(); 59 | State.HandlesByConnectionId.Remove(connectionId); 60 | await WriteStateAsync(); 61 | } 62 | 63 | if (State.HandlesByConnectionId.Count == 0) 64 | { 65 | DeactivateOnIdle(); 66 | } 67 | } 68 | 69 | public virtual Task SendConnectionsAsync(string methodName, object[] args) 70 | { 71 | var hubTypeId = this.GetHubTypeId(); 72 | var hubProxy = GrainFactory.GetHubProxy(GetStreamProvider(SignalRConstants.STREAM_PROVIDER), hubTypeId); 73 | return hubProxy.SendClientsAsync(State.HandlesByConnectionId.Keys.ToList(), methodName, args); 74 | } 75 | 76 | public virtual Task SendConnectionsExceptAsync(string methodName, object[] args, IReadOnlyList excludedConnectionIds) 77 | { 78 | var hubTypeId = this.GetHubTypeId(); 79 | var hubProxy = GrainFactory.GetHubProxy(GetStreamProvider(SignalRConstants.STREAM_PROVIDER), hubTypeId); 80 | return hubProxy.SendClientsExceptAsync(State.HandlesByConnectionId.Keys.ToList(), methodName, args, excludedConnectionIds); 81 | } 82 | 83 | public virtual Task DeactivateAsync() 84 | { 85 | DeactivateOnIdle(); 86 | return Task.CompletedTask; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR/Grains/GroupGrain.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Orleans.Providers; 3 | 4 | namespace Orleans.Messaging.SignalR.Internal 5 | { 6 | internal class GroupGrainState : ClientsGrainState 7 | { 8 | } 9 | 10 | [StorageProvider(ProviderName = InternalSignalRConstants.STORAGE_PROVIDER)] 11 | internal class GroupGrain : ClientsGrain, IGroupGrain 12 | { 13 | public GroupGrain(ILogger logger) : base(logger) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR/Grains/UserGrain.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Orleans.Providers; 3 | 4 | namespace Orleans.Messaging.SignalR.Internal 5 | { 6 | internal class UserGrainState : ClientsGrainState 7 | { 8 | } 9 | 10 | [StorageProvider(ProviderName = InternalSignalRConstants.STORAGE_PROVIDER)] 11 | internal class UserGrain : ClientsGrain, IUserGrain 12 | { 13 | public UserGrain(ILogger logger) : base(logger) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR/Orleans.Messaging.SignalR.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | latest 6 | https://github.com/slango0513/AspNetCore.SignalR.Orleans#license 7 | https://github.com/slango0513/AspNetCore.SignalR.Orleans 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Orleans.Messaging.SignalR/SignalRSiloHostBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Orleans.Configuration; 2 | using Orleans.Messaging.SignalR; 3 | using Orleans.Messaging.SignalR.Internal; 4 | 5 | 6 | namespace Orleans.Hosting 7 | { 8 | public static partial class SignalRSiloHostBuilderExtensions 9 | { 10 | public static ISiloHostBuilder UseSignalR(this ISiloHostBuilder builder, 11 | bool fireAndForgetDelivery = SimpleMessageStreamProviderOptions.DEFAULT_VALUE_FIRE_AND_FORGET_DELIVERY) 12 | { 13 | builder.AddSimpleMessageStreamProvider(SignalRConstants.STREAM_PROVIDER, options => 14 | { 15 | options.FireAndForgetDelivery = fireAndForgetDelivery; 16 | }) 17 | .ConfigureApplicationParts(manager => 18 | { 19 | manager.AddApplicationPart(typeof(ClientGrain).Assembly).WithReferences(); 20 | }) 21 | .AddMemoryGrainStorage("PubSubStore") 22 | .AddMemoryGrainStorage(InternalSignalRConstants.STORAGE_PROVIDER); 23 | 24 | return builder; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/AspNetCore.SignalR.Orleans.Tests/AspNetCore.SignalR.Orleans.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | 6 | false 7 | 8 | latest 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/AspNetCore.SignalR.Orleans.Tests/BuilderConfigurators/TestClientBuilderConfigurator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Orleans; 3 | using Orleans.Hosting; 4 | using Orleans.TestingHost; 5 | 6 | namespace AspNetCore.SignalR.Orleans.Tests 7 | { 8 | public class TestClientBuilderConfigurator : IClientBuilderConfigurator 9 | { 10 | public void Configure(IConfiguration configuration, IClientBuilder clientBuilder) 11 | { 12 | clientBuilder.UseSignalR(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/AspNetCore.SignalR.Orleans.Tests/BuilderConfigurators/TestSiloBuilderConfigurator.cs: -------------------------------------------------------------------------------- 1 | using Orleans.Hosting; 2 | using Orleans.TestingHost; 3 | 4 | namespace AspNetCore.SignalR.Orleans.Tests 5 | { 6 | public class TestSiloBuilderConfigurator : ISiloBuilderConfigurator 7 | { 8 | public void Configure(ISiloHostBuilder hostBuilder) 9 | { 10 | hostBuilder.UseSignalR(); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/AspNetCore.SignalR.Orleans.Tests/OrleansHubLifetimeManagerTests.Base.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.SignalR.Protocol; 5 | using Microsoft.AspNetCore.SignalR.Tests; 6 | using Orleans.Messaging.SignalR; 7 | using System.Threading.Tasks; 8 | using Xunit; 9 | 10 | namespace AspNetCore.SignalR.Orleans.Tests 11 | { 12 | public partial class OrleansHubLifetimeManagerTests 13 | { 14 | [Fact] 15 | public async Task SendAllAsyncWritesToAllConnectionsOutput() 16 | { 17 | using (var manager = CreateNewHubLifetimeManager()) 18 | using (var client1 = new TestClient()) 19 | using (var client2 = new TestClient()) 20 | { 21 | var connection1 = HubConnectionContextUtils.Create(client1.Connection); 22 | var connection2 = HubConnectionContextUtils.Create(client2.Connection); 23 | 24 | await manager.OnConnectedAsync(connection1).OrTimeout(); 25 | await manager.OnConnectedAsync(connection2).OrTimeout(); 26 | 27 | await manager.SendAllAsync("Hello", new object[] { "World" }).OrTimeout(); 28 | 29 | var message = Assert.IsType(client1.TryRead()); 30 | Assert.Equal("Hello", message.Target); 31 | Assert.Single(message.Arguments); 32 | Assert.Equal("World", (string)message.Arguments[0]); 33 | 34 | await AssertMessageAsync(client2); 35 | } 36 | } 37 | 38 | [Fact] 39 | public async Task SendAllAsyncDoesNotWriteToDisconnectedConnectionsOutput() 40 | { 41 | using (var manager = CreateNewHubLifetimeManager()) 42 | using (var client1 = new TestClient()) 43 | using (var client2 = new TestClient()) 44 | { 45 | var connection1 = HubConnectionContextUtils.Create(client1.Connection); 46 | var connection2 = HubConnectionContextUtils.Create(client2.Connection); 47 | 48 | await manager.OnConnectedAsync(connection1).OrTimeout(); 49 | await manager.OnConnectedAsync(connection2).OrTimeout(); 50 | 51 | await manager.OnDisconnectedAsync(connection2).OrTimeout(); 52 | 53 | await manager.SendAllAsync("Hello", new object[] { "World" }).OrTimeout(); 54 | 55 | await AssertMessageAsync(client1); 56 | Assert.Null(client2.TryRead()); 57 | } 58 | } 59 | 60 | [Theory] 61 | [InlineData(true)] 62 | [InlineData(false)] 63 | public async Task SendGroupAsyncWritesToAllConnectionsInGroupOutput(bool fromManager) 64 | { 65 | var hubProxy = new HubProxy(_fixture.TestCluster.Client); 66 | using (var manager = CreateNewHubLifetimeManager()) 67 | using (var client1 = new TestClient()) 68 | using (var client2 = new TestClient()) 69 | { 70 | var connection1 = HubConnectionContextUtils.Create(client1.Connection); 71 | var connection2 = HubConnectionContextUtils.Create(client2.Connection); 72 | 73 | await manager.OnConnectedAsync(connection1).OrTimeout(); 74 | await manager.OnConnectedAsync(connection2).OrTimeout(); 75 | 76 | if (fromManager) 77 | { 78 | await manager.AddToGroupAsync(connection1.ConnectionId, "group").OrTimeout(); 79 | 80 | await manager.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(); 81 | } 82 | else 83 | { 84 | await hubProxy.AddToGroupAsync(connection1.ConnectionId, "group").OrTimeout(); 85 | 86 | await hubProxy.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(); 87 | } 88 | 89 | await AssertMessageAsync(client1); 90 | Assert.Null(client2.TryRead()); 91 | } 92 | } 93 | 94 | [Theory] 95 | [InlineData(true)] 96 | [InlineData(false)] 97 | public async Task SendGroupExceptAsyncDoesNotWriteToExcludedConnections(bool fromManager) 98 | { 99 | var hubProxy = new HubProxy(_fixture.TestCluster.Client); 100 | using (var manager = CreateNewHubLifetimeManager()) 101 | using (var client1 = new TestClient()) 102 | using (var client2 = new TestClient()) 103 | { 104 | var connection1 = HubConnectionContextUtils.Create(client1.Connection); 105 | var connection2 = HubConnectionContextUtils.Create(client2.Connection); 106 | 107 | await manager.OnConnectedAsync(connection1).OrTimeout(); 108 | await manager.OnConnectedAsync(connection2).OrTimeout(); 109 | 110 | if (fromManager) 111 | { 112 | await manager.AddToGroupAsync(connection1.ConnectionId, "group").OrTimeout(); 113 | await manager.AddToGroupAsync(connection2.ConnectionId, "group").OrTimeout(); 114 | 115 | await manager.SendGroupExceptAsync("group", "Hello", new object[] { "World" }, new[] { connection2.ConnectionId }).OrTimeout(); 116 | } 117 | else 118 | { 119 | await hubProxy.AddToGroupAsync(connection1.ConnectionId, "group").OrTimeout(); 120 | await hubProxy.AddToGroupAsync(connection2.ConnectionId, "group").OrTimeout(); 121 | 122 | await hubProxy.SendGroupExceptAsync("group", "Hello", new object[] { "World" }, new[] { connection2.ConnectionId }).OrTimeout(); 123 | } 124 | 125 | await AssertMessageAsync(client1); 126 | Assert.Null(client2.TryRead()); 127 | } 128 | } 129 | 130 | // ADDED: SendGroupsAsync 131 | [Theory] 132 | [InlineData(true)] 133 | [InlineData(false)] 134 | public async Task SendGroupAsyncWritesToAllConnectionsInGroupsOutput(bool fromManager) 135 | { 136 | var hubProxy = new HubProxy(_fixture.TestCluster.Client); 137 | using (var manager = CreateNewHubLifetimeManager()) 138 | using (var client1 = new TestClient()) 139 | using (var client2 = new TestClient()) 140 | { 141 | var connection1 = HubConnectionContextUtils.Create(client1.Connection); 142 | var connection2 = HubConnectionContextUtils.Create(client2.Connection); 143 | 144 | await manager.OnConnectedAsync(connection1).OrTimeout(); 145 | await manager.OnConnectedAsync(connection2).OrTimeout(); 146 | 147 | if (fromManager) 148 | { 149 | await manager.AddToGroupAsync(connection1.ConnectionId, "group").OrTimeout(); 150 | await manager.AddToGroupAsync(connection2.ConnectionId, "group2").OrTimeout(); 151 | 152 | await manager.SendGroupsAsync(new string[] { "group", "group2" }, "Hello", new object[] { "World" }).OrTimeout(); 153 | } 154 | else 155 | { 156 | await hubProxy.AddToGroupAsync(connection1.ConnectionId, "group").OrTimeout(); 157 | await hubProxy.AddToGroupAsync(connection2.ConnectionId, "group2").OrTimeout(); 158 | 159 | await hubProxy.SendGroupsAsync(new string[] { "group", "group2" }, "Hello", new object[] { "World" }).OrTimeout(); 160 | } 161 | 162 | await AssertMessageAsync(client1); 163 | await AssertMessageAsync(client2); 164 | } 165 | } 166 | 167 | [Theory] 168 | [InlineData(true)] 169 | [InlineData(false)] 170 | public async Task SendConnectionAsyncWritesToConnectionOutput(bool fromManager) 171 | { 172 | var hubProxy = new HubProxy(_fixture.TestCluster.Client); 173 | using (var manager = CreateNewHubLifetimeManager()) 174 | using (var client = new TestClient()) 175 | { 176 | var connection = HubConnectionContextUtils.Create(client.Connection); 177 | 178 | await manager.OnConnectedAsync(connection).OrTimeout(); 179 | 180 | if (fromManager) 181 | { 182 | await manager.SendConnectionAsync(connection.ConnectionId, "Hello", new object[] { "World" }).OrTimeout(); 183 | } 184 | else 185 | { 186 | await hubProxy.SendClientAsync(connection.ConnectionId, "Hello", new object[] { "World" }).OrTimeout(); 187 | } 188 | 189 | await AssertMessageAsync(client); 190 | } 191 | } 192 | 193 | // ADDED: SendConnectionsAsync 194 | [Theory] 195 | [InlineData(true)] 196 | [InlineData(false)] 197 | public async Task SendConnectionAsyncWritesToConnectionsOutput(bool fromManager) 198 | { 199 | var hubProxy = new HubProxy(_fixture.TestCluster.Client); 200 | using (var manager = CreateNewHubLifetimeManager()) 201 | using (var client1 = new TestClient()) 202 | using (var client2 = new TestClient()) 203 | { 204 | var connection1 = HubConnectionContextUtils.Create(client1.Connection); 205 | var connection2 = HubConnectionContextUtils.Create(client2.Connection); 206 | 207 | await manager.OnConnectedAsync(connection1).OrTimeout(); 208 | await manager.OnConnectedAsync(connection2).OrTimeout(); 209 | 210 | if (fromManager) 211 | { 212 | await manager.SendConnectionsAsync(new string[] { connection1.ConnectionId, connection2.ConnectionId }, "Hello", new object[] { "World" }).OrTimeout(); 213 | } 214 | else 215 | { 216 | await hubProxy.SendClientsAsync(new string[] { connection1.ConnectionId, connection2.ConnectionId }, "Hello", new object[] { "World" }).OrTimeout(); 217 | } 218 | 219 | await AssertMessageAsync(client1); 220 | await AssertMessageAsync(client2); 221 | } 222 | } 223 | 224 | [Theory] 225 | [InlineData(true)] 226 | [InlineData(false)] 227 | public async Task DisconnectConnectionRemovesConnectionFromGroup(bool fromManager) 228 | { 229 | var hubProxy = new HubProxy(_fixture.TestCluster.Client); 230 | using (var manager = CreateNewHubLifetimeManager()) 231 | using (var client = new TestClient()) 232 | { 233 | var connection = HubConnectionContextUtils.Create(client.Connection); 234 | 235 | await manager.OnConnectedAsync(connection).OrTimeout(); 236 | 237 | if (fromManager) 238 | { 239 | await manager.AddToGroupAsync(connection.ConnectionId, "group").OrTimeout(); 240 | } 241 | else 242 | { 243 | await hubProxy.AddToGroupAsync(connection.ConnectionId, "group").OrTimeout(); 244 | } 245 | 246 | await manager.OnDisconnectedAsync(connection).OrTimeout(); 247 | 248 | if (fromManager) 249 | { 250 | await manager.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(); 251 | } 252 | else 253 | { 254 | await hubProxy.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(); 255 | } 256 | 257 | Assert.Null(client.TryRead()); 258 | 259 | // ADDED: GetConnectionIdsAsync 260 | //var result = await hubProxy.GetGroupNamesAsync("group").OrTimeout(); 261 | //Assert.Equal(0, result.Count); 262 | } 263 | } 264 | 265 | [Theory] 266 | [InlineData(true)] 267 | [InlineData(false)] 268 | public async Task RemoveGroupFromLocalConnectionNotInGroupDoesNothing(bool fromManager) 269 | { 270 | var hubProxy = new HubProxy(_fixture.TestCluster.Client); 271 | using (var manager = CreateNewHubLifetimeManager()) 272 | using (var client = new TestClient()) 273 | { 274 | var connection = HubConnectionContextUtils.Create(client.Connection); 275 | 276 | await manager.OnConnectedAsync(connection).OrTimeout(); 277 | 278 | if (fromManager) 279 | { 280 | await manager.RemoveFromGroupAsync(connection.ConnectionId, "group").OrTimeout(); 281 | } 282 | else 283 | { 284 | await hubProxy.RemoveFromGroupAsync(connection.ConnectionId, "group").OrTimeout(); 285 | } 286 | } 287 | } 288 | 289 | [Theory] 290 | [InlineData(true)] 291 | [InlineData(false)] 292 | public async Task AddGroupAsyncForLocalConnectionAlreadyInGroupDoesNothing(bool fromManager) 293 | { 294 | var hubProxy = new HubProxy(_fixture.TestCluster.Client); 295 | using (var manager = CreateNewHubLifetimeManager()) 296 | using (var client = new TestClient()) 297 | { 298 | var connection = HubConnectionContextUtils.Create(client.Connection); 299 | 300 | await manager.OnConnectedAsync(connection).OrTimeout(); 301 | 302 | await manager.AddToGroupAsync(connection.ConnectionId, "group").OrTimeout(); 303 | await manager.AddToGroupAsync(connection.ConnectionId, "group").OrTimeout(); 304 | 305 | if (fromManager) 306 | { 307 | await manager.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(); 308 | } 309 | else 310 | { 311 | await hubProxy.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(); 312 | } 313 | 314 | await AssertMessageAsync(client); 315 | Assert.Null(client.TryRead()); 316 | 317 | // ADDED: GetConnectionIdsAsync 318 | //var result = await hubProxy.GetConnectionIdsFromGroupAsync("group").OrTimeout(); 319 | //Assert.Equal(1, result.Count); 320 | } 321 | } 322 | 323 | [Theory] 324 | [InlineData(true)] 325 | [InlineData(false)] 326 | public async Task WritingToGroupWithOneConnectionFailingSecondConnectionStillReceivesMessage(bool fromManager) 327 | { 328 | var hubProxy = new HubProxy(_fixture.TestCluster.Client); 329 | using (var manager = CreateNewHubLifetimeManager()) 330 | using (var client1 = new TestClient()) 331 | using (var client2 = new TestClient()) 332 | { 333 | // Force an exception when writing to connection 334 | var connectionMock = HubConnectionContextUtils.CreateMock(client1.Connection); 335 | 336 | var connection1 = connectionMock; 337 | var connection2 = HubConnectionContextUtils.Create(client2.Connection); 338 | 339 | await manager.OnConnectedAsync(connection1).OrTimeout(); 340 | await manager.AddToGroupAsync(connection1.ConnectionId, "group"); 341 | await manager.OnConnectedAsync(connection2).OrTimeout(); 342 | await manager.AddToGroupAsync(connection2.ConnectionId, "group"); 343 | 344 | if (fromManager) 345 | { 346 | await manager.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(); 347 | } 348 | else 349 | { 350 | await hubProxy.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(); 351 | } 352 | // connection1 will throw when receiving a group message, we are making sure other connections 353 | // are not affected by another connection throwing 354 | await AssertMessageAsync(client2); 355 | 356 | // Repeat to check that group can still be sent to 357 | await manager.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(); 358 | await AssertMessageAsync(client2); 359 | } 360 | } 361 | 362 | [Theory] 363 | [InlineData(true)] 364 | [InlineData(false)] 365 | public async Task InvokeUserSendsToAllConnectionsForUser(bool fromManager) 366 | { 367 | var hubProxy = new HubProxy(_fixture.TestCluster.Client); 368 | using (var manager = CreateNewHubLifetimeManager()) 369 | using (var client1 = new TestClient()) 370 | using (var client2 = new TestClient()) 371 | using (var client3 = new TestClient()) 372 | { 373 | var connection1 = HubConnectionContextUtils.Create(client1.Connection, userIdentifier: "userA"); 374 | var connection2 = HubConnectionContextUtils.Create(client2.Connection, userIdentifier: "userA"); 375 | var connection3 = HubConnectionContextUtils.Create(client3.Connection, userIdentifier: "userB"); 376 | 377 | await manager.OnConnectedAsync(connection1).OrTimeout(); 378 | await manager.OnConnectedAsync(connection2).OrTimeout(); 379 | await manager.OnConnectedAsync(connection3).OrTimeout(); 380 | 381 | if (fromManager) 382 | { 383 | await manager.SendUserAsync("userA", "Hello", new object[] { "World" }).OrTimeout(); 384 | } 385 | else 386 | { 387 | await hubProxy.SendUserAsync("userA", "Hello", new object[] { "World" }).OrTimeout(); 388 | } 389 | await AssertMessageAsync(client1); 390 | await AssertMessageAsync(client2); 391 | } 392 | } 393 | 394 | [Theory] 395 | [InlineData(true)] 396 | [InlineData(false)] 397 | public async Task StillSubscribedToUserAfterOneOfMultipleConnectionsAssociatedWithUserDisconnects(bool fromManager) 398 | { 399 | var hubProxy = new HubProxy(_fixture.TestCluster.Client); 400 | using (var manager = CreateNewHubLifetimeManager()) 401 | using (var client1 = new TestClient()) 402 | using (var client2 = new TestClient()) 403 | using (var client3 = new TestClient()) 404 | { 405 | var connection1 = HubConnectionContextUtils.Create(client1.Connection, userIdentifier: "userA"); 406 | var connection2 = HubConnectionContextUtils.Create(client2.Connection, userIdentifier: "userA"); 407 | var connection3 = HubConnectionContextUtils.Create(client3.Connection, userIdentifier: "userB"); 408 | 409 | await manager.OnConnectedAsync(connection1).OrTimeout(); 410 | await manager.OnConnectedAsync(connection2).OrTimeout(); 411 | await manager.OnConnectedAsync(connection3).OrTimeout(); 412 | 413 | if (fromManager) 414 | { 415 | await manager.SendUserAsync("userA", "Hello", new object[] { "World" }).OrTimeout(); 416 | } 417 | else 418 | { 419 | await hubProxy.SendUserAsync("userA", "Hello", new object[] { "World" }).OrTimeout(); 420 | } 421 | 422 | await AssertMessageAsync(client1); 423 | await AssertMessageAsync(client2); 424 | 425 | // Disconnect one connection for the user 426 | await manager.OnDisconnectedAsync(connection1).OrTimeout(); 427 | 428 | _output.WriteLine("Sending."); 429 | if (fromManager) 430 | { 431 | await manager.SendUserAsync("userA", "Hello", new object[] { "World" }).OrTimeout(); 432 | } 433 | else 434 | { 435 | await hubProxy.SendUserAsync("userA", "Hello", new object[] { "World" }).OrTimeout(); 436 | } 437 | _output.WriteLine("Sent."); 438 | await AssertMessageAsync(client2); 439 | } 440 | } 441 | 442 | // ADDED 443 | [Theory] 444 | [InlineData(true)] 445 | [InlineData(false)] 446 | public async Task StillSubscribedToGroupAfterOneOfMultipleConnectionsAssociatedWithGroupDisconnects(bool fromManager) 447 | { 448 | var hubProxy = new HubProxy(_fixture.TestCluster.Client); 449 | using (var manager = CreateNewHubLifetimeManager()) 450 | using (var client1 = new TestClient()) 451 | using (var client2 = new TestClient()) 452 | { 453 | var connection1 = HubConnectionContextUtils.Create(client1.Connection); 454 | var connection2 = HubConnectionContextUtils.Create(client2.Connection); 455 | 456 | await manager.OnConnectedAsync(connection1).OrTimeout(); 457 | await manager.OnConnectedAsync(connection2).OrTimeout(); 458 | 459 | if (fromManager) 460 | { 461 | await manager.AddToGroupAsync(connection2.ConnectionId, "group"); 462 | await manager.AddToGroupAsync(connection1.ConnectionId, "group"); 463 | } 464 | else 465 | { 466 | await hubProxy.AddToGroupAsync(connection1.ConnectionId, "group"); 467 | await hubProxy.AddToGroupAsync(connection2.ConnectionId, "group"); 468 | } 469 | 470 | // Disconnect one connection for the group 471 | await manager.OnDisconnectedAsync(connection1).OrTimeout(); 472 | if (fromManager) 473 | { 474 | await manager.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(); 475 | } 476 | else 477 | { 478 | await hubProxy.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(); 479 | } 480 | await AssertMessageAsync(client2); 481 | } 482 | } 483 | } 484 | } 485 | -------------------------------------------------------------------------------- /test/AspNetCore.SignalR.Orleans.Tests/OrleansHubLifetimeManagerTests.Scaleout.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.SignalR.Protocol; 5 | using Microsoft.AspNetCore.SignalR.Tests; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace AspNetCore.SignalR.Orleans.Tests 10 | { 11 | public partial class OrleansHubLifetimeManagerTests 12 | { 13 | private async Task AssertMessageAsync(TestClient client) 14 | { 15 | var message = Assert.IsType(await client.ReadAsync().OrTimeout()); 16 | Assert.Equal("Hello", message.Target); 17 | Assert.Single(message.Arguments); 18 | Assert.Equal("World", (string)message.Arguments[0]); 19 | } 20 | 21 | [Fact] 22 | public async Task InvokeAllAsyncWithMultipleServersWritesToAllConnectionsOutput() 23 | { 24 | using (var manager1 = CreateNewHubLifetimeManager()) 25 | using (var manager2 = CreateNewHubLifetimeManager()) 26 | using (var client1 = new TestClient()) 27 | using (var client2 = new TestClient()) 28 | { 29 | var connection1 = HubConnectionContextUtils.Create(client1.Connection); 30 | var connection2 = HubConnectionContextUtils.Create(client2.Connection); 31 | 32 | await manager1.OnConnectedAsync(connection1).OrTimeout(); 33 | await manager2.OnConnectedAsync(connection2).OrTimeout(); 34 | 35 | await manager1.SendAllAsync("Hello", new object[] { "World" }).OrTimeout(); 36 | 37 | await AssertMessageAsync(client1); 38 | await AssertMessageAsync(client2); 39 | } 40 | } 41 | 42 | // ADDED: ForSpecificHub 43 | [Fact] 44 | public async Task InvokeAllAsyncWithMultipleServersWritesToAllConnectionsForSpecificHubOutput() 45 | { 46 | using (var manager1 = CreateNewHubLifetimeManager()) 47 | using (var manager2 = CreateNewHubLifetimeManager()) 48 | using (var manager3 = CreateNewHubLifetimeManager()) 49 | using (var client1 = new TestClient()) 50 | using (var client2 = new TestClient()) 51 | using (var client3 = new TestClient()) 52 | { 53 | var connection1 = HubConnectionContextUtils.Create(client1.Connection); 54 | var connection2 = HubConnectionContextUtils.Create(client2.Connection); 55 | var connection3 = HubConnectionContextUtils.Create(client3.Connection); 56 | 57 | await manager1.OnConnectedAsync(connection1).OrTimeout(); 58 | await manager2.OnConnectedAsync(connection2).OrTimeout(); 59 | await manager3.OnConnectedAsync(connection3).OrTimeout(); 60 | 61 | await manager1.SendAllAsync("Hello", new object[] { "World" }).OrTimeout(); 62 | 63 | await AssertMessageAsync(client1); 64 | await AssertMessageAsync(client2); 65 | Assert.Null(client3.TryRead()); 66 | } 67 | } 68 | 69 | [Fact] 70 | public async Task InvokeAllAsyncWithMultipleServersDoesNotWriteToDisconnectedConnectionsOutput() 71 | { 72 | using (var manager1 = CreateNewHubLifetimeManager()) 73 | using (var manager2 = CreateNewHubLifetimeManager()) 74 | using (var client1 = new TestClient()) 75 | using (var client2 = new TestClient()) 76 | { 77 | var connection1 = HubConnectionContextUtils.Create(client1.Connection); 78 | var connection2 = HubConnectionContextUtils.Create(client2.Connection); 79 | 80 | await manager1.OnConnectedAsync(connection1).OrTimeout(); 81 | await manager2.OnConnectedAsync(connection2).OrTimeout(); 82 | 83 | await manager2.OnDisconnectedAsync(connection2).OrTimeout(); 84 | 85 | await manager2.SendAllAsync("Hello", new object[] { "World" }).OrTimeout(); 86 | 87 | await AssertMessageAsync(client1); 88 | Assert.Null(client2.TryRead()); 89 | } 90 | } 91 | 92 | [Fact] 93 | public async Task InvokeConnectionAsyncOnServerWithoutConnectionWritesOutputToConnection() 94 | { 95 | using (var manager1 = CreateNewHubLifetimeManager()) 96 | using (var manager2 = CreateNewHubLifetimeManager()) 97 | using (var client = new TestClient()) 98 | { 99 | var connection = HubConnectionContextUtils.Create(client.Connection); 100 | 101 | await manager1.OnConnectedAsync(connection).OrTimeout(); 102 | 103 | await manager2.SendConnectionAsync(connection.ConnectionId, "Hello", new object[] { "World" }).OrTimeout(); 104 | 105 | await AssertMessageAsync(client); 106 | } 107 | } 108 | 109 | [Fact] 110 | public async Task InvokeGroupAsyncOnServerWithoutConnectionWritesOutputToGroupConnection() 111 | { 112 | using (var manager1 = CreateNewHubLifetimeManager()) 113 | using (var manager2 = CreateNewHubLifetimeManager()) 114 | using (var client = new TestClient()) 115 | { 116 | var connection = HubConnectionContextUtils.Create(client.Connection); 117 | 118 | await manager1.OnConnectedAsync(connection).OrTimeout(); 119 | 120 | await manager1.AddToGroupAsync(connection.ConnectionId, "group").OrTimeout(); 121 | 122 | await manager2.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(); 123 | 124 | await AssertMessageAsync(client); 125 | } 126 | } 127 | 128 | [Fact] 129 | public async Task RemoveGroupFromConnectionOnDifferentServerNotInGroupDoesNothing() 130 | { 131 | using (var manager1 = CreateNewHubLifetimeManager()) 132 | using (var manager2 = CreateNewHubLifetimeManager()) 133 | using (var client = new TestClient()) 134 | { 135 | var connection = HubConnectionContextUtils.Create(client.Connection); 136 | 137 | await manager1.OnConnectedAsync(connection).OrTimeout(); 138 | 139 | await manager2.RemoveFromGroupAsync(connection.ConnectionId, "group").OrTimeout(); 140 | } 141 | } 142 | 143 | [Fact] 144 | public async Task AddGroupAsyncForConnectionOnDifferentServerWorks() 145 | { 146 | using (var manager1 = CreateNewHubLifetimeManager()) 147 | using (var manager2 = CreateNewHubLifetimeManager()) 148 | using (var client = new TestClient()) 149 | { 150 | var connection = HubConnectionContextUtils.Create(client.Connection); 151 | 152 | await manager1.OnConnectedAsync(connection).OrTimeout(); 153 | 154 | await manager2.AddToGroupAsync(connection.ConnectionId, "group").OrTimeout(); 155 | 156 | await manager2.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(); 157 | 158 | await AssertMessageAsync(client); 159 | } 160 | } 161 | 162 | [Fact] 163 | public async Task AddGroupAsyncForConnectionOnDifferentServerAlreadyInGroupDoesNothing() 164 | { 165 | using (var manager1 = CreateNewHubLifetimeManager()) 166 | using (var manager2 = CreateNewHubLifetimeManager()) 167 | using (var client = new TestClient()) 168 | { 169 | var connection = HubConnectionContextUtils.Create(client.Connection); 170 | 171 | await manager1.OnConnectedAsync(connection).OrTimeout(); 172 | 173 | await manager1.AddToGroupAsync(connection.ConnectionId, "group").OrTimeout(); 174 | await manager2.AddToGroupAsync(connection.ConnectionId, "group").OrTimeout(); 175 | 176 | await manager2.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(); 177 | 178 | await AssertMessageAsync(client); 179 | Assert.Null(client.TryRead()); 180 | } 181 | } 182 | 183 | [Fact] 184 | public async Task RemoveGroupAsyncForConnectionOnDifferentServerWorks() 185 | { 186 | using (var manager1 = CreateNewHubLifetimeManager()) 187 | using (var manager2 = CreateNewHubLifetimeManager()) 188 | using (var client = new TestClient()) 189 | { 190 | var connection = HubConnectionContextUtils.Create(client.Connection); 191 | 192 | await manager1.OnConnectedAsync(connection).OrTimeout(); 193 | 194 | await manager1.AddToGroupAsync(connection.ConnectionId, "group").OrTimeout(); 195 | 196 | await manager2.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(); 197 | 198 | await AssertMessageAsync(client); 199 | 200 | await manager2.RemoveFromGroupAsync(connection.ConnectionId, "group").OrTimeout(); 201 | 202 | await manager2.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(); 203 | 204 | Assert.Null(client.TryRead()); 205 | } 206 | } 207 | 208 | [Fact] 209 | public async Task InvokeConnectionAsyncForLocalConnectionDoesNotPublishToBackplane() 210 | { 211 | using (var manager1 = CreateNewHubLifetimeManager()) 212 | using (var manager2 = CreateNewHubLifetimeManager()) 213 | using (var client = new TestClient()) 214 | { 215 | var connection = HubConnectionContextUtils.Create(client.Connection); 216 | 217 | // Add connection to both "servers" to see if connection receives message twice 218 | await manager1.OnConnectedAsync(connection).OrTimeout(); 219 | await manager2.OnConnectedAsync(connection).OrTimeout(); 220 | 221 | await manager1.SendConnectionAsync(connection.ConnectionId, "Hello", new object[] { "World" }).OrTimeout(); 222 | 223 | await AssertMessageAsync(client); 224 | Assert.Null(client.TryRead()); 225 | } 226 | } 227 | 228 | [Fact] 229 | public async Task WritingToRemoteConnectionThatFailsDoesNotThrow() 230 | { 231 | using (var manager1 = CreateNewHubLifetimeManager()) 232 | using (var manager2 = CreateNewHubLifetimeManager()) 233 | using (var client = new TestClient()) 234 | { 235 | // Force an exception when writing to connection 236 | var connectionMock = HubConnectionContextUtils.CreateMock(client.Connection); 237 | 238 | await manager2.OnConnectedAsync(connectionMock).OrTimeout(); 239 | 240 | // This doesn't throw because there is no connection.ConnectionId on this server so it has to publish to the backplane. 241 | // And once that happens there is no way to know if the invocation was successful or not. 242 | await manager1.SendConnectionAsync(connectionMock.ConnectionId, "Hello", new object[] { "World" }).OrTimeout(); 243 | } 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /test/AspNetCore.SignalR.Orleans.Tests/OrleansHubLifetimeManagerTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.SignalR; 2 | using Microsoft.Extensions.Logging.Abstractions; 3 | using Microsoft.Extensions.Options; 4 | using Orleans.Messaging.SignalR; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | namespace AspNetCore.SignalR.Orleans.Tests 9 | { 10 | public partial class OrleansHubLifetimeManagerTests : IClassFixture 11 | { 12 | private readonly TestClusterFixture _fixture; 13 | private readonly ITestOutputHelper _output; 14 | 15 | public OrleansHubLifetimeManagerTests(TestClusterFixture fixture, ITestOutputHelper output) 16 | { 17 | _fixture = fixture; 18 | _output = output; 19 | } 20 | 21 | public OrleansHubLifetimeManager CreateNewHubLifetimeManager() where THub : Hub 22 | { 23 | var options = new OrleansOptions { ClusterClient = _fixture.TestCluster.Client }; 24 | 25 | return new OrleansHubLifetimeManager(Options.Create(options), 26 | new HubProxy(options.ClusterClient), 27 | NullLogger>.Instance); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/AspNetCore.SignalR.Orleans.Tests/TestClusterFixture.cs: -------------------------------------------------------------------------------- 1 | using Orleans.TestingHost; 2 | using System; 3 | 4 | namespace AspNetCore.SignalR.Orleans.Tests 5 | { 6 | public class TestClusterFixture : IDisposable 7 | { 8 | public TestCluster TestCluster { get; } 9 | 10 | public TestClusterFixture() 11 | { 12 | var builder = new TestClusterBuilder(2); 13 | builder.Options.ServiceId = Guid.NewGuid().ToString(); 14 | builder.AddSiloBuilderConfigurator(); 15 | builder.AddClientBuilderConfigurator(); 16 | var testCluster = builder.Build(); 17 | 18 | testCluster.Deploy(); 19 | 20 | TestCluster = testCluster; 21 | } 22 | 23 | public void Dispose() 24 | { 25 | TestCluster.Client.Close().Wait(); 26 | TestCluster.StopAllSilos(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/AspNetCore.SignalR.Orleans.Tests/TestHubs.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.SignalR; 2 | 3 | namespace AspNetCore.SignalR.Orleans.Tests 4 | { 5 | public class MyHub : Hub 6 | { 7 | } 8 | 9 | public class AnotherHub : Hub 10 | { 11 | } 12 | } 13 | --------------------------------------------------------------------------------