├── .gitattributes ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.txt ├── NuGet.config ├── README.md ├── SignalR-SqlServer.sln ├── appveyor.yml ├── build.cmd ├── build.ps1 ├── build.sh ├── global.json ├── src └── Microsoft.AspNetCore.SignalR.SqlServer │ ├── AssemblyExtensions.cs │ ├── DbDataReaderExtensions.cs │ ├── DbOperation.cs │ ├── DbParameterExtensions.cs │ ├── DbProviderFactoryAdapter.cs │ ├── DbProviderFactoryExtensions.cs │ ├── IDataRecordExtensions.cs │ ├── IDbBehavior.cs │ ├── IDbCommandExtensions.cs │ ├── IDbProviderFactory.cs │ ├── Microsoft.AspNetCore.SignalR.SqlServer.xproj │ ├── NotNullAttribute.cs │ ├── ObservableDbOperation.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── SqlInstaller.cs │ ├── SqlMessageBus.cs │ ├── SqlMessageBusException.cs │ ├── SqlPayload.cs │ ├── SqlReceiver.cs │ ├── SqlScaleoutOptions.cs │ ├── SqlSender.cs │ ├── SqlServerSignalRServicesBuilderExtensions.cs │ ├── SqlStream.cs │ ├── install.sql │ ├── project.json │ └── send.sql ├── test └── Microsoft.AspNetCore.SignalR.SqlServer.Tests │ ├── Microsoft.AspNetCore.SignalR.SqlServer.Tests.xproj │ ├── ObservableSqlOperationFacts.cs │ ├── SqlScaleoutOptionsFacts.cs │ └── project.json └── tools └── Key.snk /.gitattributes: -------------------------------------------------------------------------------- 1 | *.doc diff=astextplain 2 | *.DOC diff=astextplain 3 | *.docx diff=astextplain 4 | *.DOCX diff=astextplain 5 | *.dot diff=astextplain 6 | *.DOT diff=astextplain 7 | *.pdf diff=astextplain 8 | *.PDF diff=astextplain 9 | *.rtf diff=astextplain 10 | *.RTF diff=astextplain 11 | 12 | *.jpg binary 13 | *.png binary 14 | *.gif binary 15 | 16 | *.cs text=auto diff=csharp 17 | *.vb text=auto 18 | *.resx text=auto 19 | *.c text=auto 20 | *.cpp text=auto 21 | *.cxx text=auto 22 | *.h text=auto 23 | *.hxx text=auto 24 | *.py text=auto 25 | *.rb text=auto 26 | *.java text=auto 27 | *.html text=auto 28 | *.htm text=auto 29 | *.css text=auto 30 | *.scss text=auto 31 | *.sass text=auto 32 | *.less text=auto 33 | *.js text=auto 34 | *.lisp text=auto 35 | *.clj text=auto 36 | *.sql text=auto 37 | *.php text=auto 38 | *.lua text=auto 39 | *.m text=auto 40 | *.asm text=auto 41 | *.erl text=auto 42 | *.fs text=auto 43 | *.fsx text=auto 44 | *.hs text=auto 45 | 46 | *.csproj text=auto 47 | *.vbproj text=auto 48 | *.fsproj text=auto 49 | *.dbproj text=auto 50 | *.sln text=auto eol=crlf 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Oo]bj/ 2 | [Bb]in/ 3 | TestResults/ 4 | .nuget/ 5 | _ReSharper.*/ 6 | packages/ 7 | artifacts/ 8 | PublishProfiles/ 9 | *.user 10 | *.suo 11 | *.cache 12 | *.docstates 13 | _ReSharper.* 14 | nuget.exe 15 | *net45.csproj 16 | *net451.csproj 17 | *k10.csproj 18 | *.psess 19 | *.vsp 20 | *.pidb 21 | *.userprefs 22 | *DS_Store 23 | *.ncrunchsolution 24 | *.*sdf 25 | *.ipch 26 | *.sln.ide 27 | project.lock.json 28 | runtimes/ 29 | .build/ 30 | .testPublish/ 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | sudo: required 3 | dist: trusty 4 | addons: 5 | apt: 6 | packages: 7 | - gettext 8 | - libcurl4-openssl-dev 9 | - libicu-dev 10 | - libssl-dev 11 | - libunwind8 12 | - zlib1g 13 | env: 14 | global: 15 | - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 16 | - DOTNET_CLI_TELEMETRY_OPTOUT: 1 17 | mono: 18 | - 4.0.5 19 | os: 20 | - linux 21 | - osx 22 | osx_image: xcode7.1 23 | branches: 24 | only: 25 | - master 26 | - release 27 | - dev 28 | - /^(.*\/)?ci-.*$/ 29 | before_install: 30 | - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/; fi 31 | script: 32 | - ./build.sh --quiet verify 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ====== 3 | 4 | Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/dev/CONTRIBUTING.md) in the Home repo. 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) .NET Foundation. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | these files except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ASP.NET SignalR SQL Server Scaleout 2 | ======== 3 | 4 | ## This repository is obsolete and no longer used or maintained. 5 | 6 | All SignalR work for ASP.NET Core is located at https://github.com/aspnet/SignalR 7 | 8 | As a result, we're not accepting anymore changes to this project. Please file any new issues on https://github.com/aspnet/SignalR. 9 | -------------------------------------------------------------------------------- /SignalR-SqlServer.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.22506.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{34D5CA7F-AA6E-45AF-87B8-13DC970D68C9}" 7 | EndProject 8 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.SignalR.SqlServer", "src\Microsoft.AspNetCore.SignalR.SqlServer\Microsoft.AspNetCore.SignalR.SqlServer.xproj", "{EFCF27EC-CB9B-4F3A-91BE-154B8AB5B5E0}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{6AA3BF40-BE9D-4212-9826-138CE61FF42F}" 11 | EndProject 12 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.SignalR.SqlServer.Tests", "test\Microsoft.AspNetCore.SignalR.SqlServer.Tests\Microsoft.AspNetCore.SignalR.SqlServer.Tests.xproj", "{0A4487F1-9374-4E7B-957F-99647319C540}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{37B07241-CC90-45A4-BB9D-BBBFA1A9E3FE}" 15 | ProjectSection(SolutionItems) = preProject 16 | global.json = global.json 17 | EndProjectSection 18 | ProjectSection(FolderGlobals) = preProject 19 | global_1json__JSONSchema = http://json.schemastore.org/global 20 | EndProjectSection 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Debug|Mixed Platforms = Debug|Mixed Platforms 26 | Debug|x86 = Debug|x86 27 | Release|Any CPU = Release|Any CPU 28 | Release|Mixed Platforms = Release|Mixed Platforms 29 | Release|x86 = Release|x86 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {EFCF27EC-CB9B-4F3A-91BE-154B8AB5B5E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {EFCF27EC-CB9B-4F3A-91BE-154B8AB5B5E0}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {EFCF27EC-CB9B-4F3A-91BE-154B8AB5B5E0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 35 | {EFCF27EC-CB9B-4F3A-91BE-154B8AB5B5E0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 36 | {EFCF27EC-CB9B-4F3A-91BE-154B8AB5B5E0}.Debug|x86.ActiveCfg = Debug|Any CPU 37 | {EFCF27EC-CB9B-4F3A-91BE-154B8AB5B5E0}.Debug|x86.Build.0 = Debug|Any CPU 38 | {EFCF27EC-CB9B-4F3A-91BE-154B8AB5B5E0}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {EFCF27EC-CB9B-4F3A-91BE-154B8AB5B5E0}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {EFCF27EC-CB9B-4F3A-91BE-154B8AB5B5E0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 41 | {EFCF27EC-CB9B-4F3A-91BE-154B8AB5B5E0}.Release|Mixed Platforms.Build.0 = Release|Any CPU 42 | {EFCF27EC-CB9B-4F3A-91BE-154B8AB5B5E0}.Release|x86.ActiveCfg = Release|Any CPU 43 | {EFCF27EC-CB9B-4F3A-91BE-154B8AB5B5E0}.Release|x86.Build.0 = Release|Any CPU 44 | {0A4487F1-9374-4E7B-957F-99647319C540}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {0A4487F1-9374-4E7B-957F-99647319C540}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {0A4487F1-9374-4E7B-957F-99647319C540}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 47 | {0A4487F1-9374-4E7B-957F-99647319C540}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 48 | {0A4487F1-9374-4E7B-957F-99647319C540}.Debug|x86.ActiveCfg = Debug|Any CPU 49 | {0A4487F1-9374-4E7B-957F-99647319C540}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {0A4487F1-9374-4E7B-957F-99647319C540}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {0A4487F1-9374-4E7B-957F-99647319C540}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 52 | {0A4487F1-9374-4E7B-957F-99647319C540}.Release|Mixed Platforms.Build.0 = Release|Any CPU 53 | {0A4487F1-9374-4E7B-957F-99647319C540}.Release|x86.ActiveCfg = Release|Any CPU 54 | EndGlobalSection 55 | GlobalSection(SolutionProperties) = preSolution 56 | HideSolutionNode = FALSE 57 | EndGlobalSection 58 | GlobalSection(NestedProjects) = preSolution 59 | {EFCF27EC-CB9B-4F3A-91BE-154B8AB5B5E0} = {34D5CA7F-AA6E-45AF-87B8-13DC970D68C9} 60 | {0A4487F1-9374-4E7B-957F-99647319C540} = {6AA3BF40-BE9D-4212-9826-138CE61FF42F} 61 | EndGlobalSection 62 | EndGlobal 63 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf true 3 | branches: 4 | only: 5 | - master 6 | - release 7 | - dev 8 | - /^(.*\/)?ci-.*$/ 9 | build_script: 10 | - build.cmd --quiet verify 11 | clone_depth: 1 12 | test: off 13 | deploy: off -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0build.ps1' %*; exit $LASTEXITCODE" -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | function DownloadWithRetry([string] $url, [string] $downloadLocation, [int] $retries) 4 | { 5 | while($true) 6 | { 7 | try 8 | { 9 | Invoke-WebRequest $url -OutFile $downloadLocation 10 | break 11 | } 12 | catch 13 | { 14 | $exceptionMessage = $_.Exception.Message 15 | Write-Host "Failed to download '$url': $exceptionMessage" 16 | if ($retries -gt 0) { 17 | $retries-- 18 | Write-Host "Waiting 10 seconds before retrying. Retries left: $retries" 19 | Start-Sleep -Seconds 10 20 | 21 | } 22 | else 23 | { 24 | $exception = $_.Exception 25 | throw $exception 26 | } 27 | } 28 | } 29 | } 30 | 31 | cd $PSScriptRoot 32 | 33 | $repoFolder = $PSScriptRoot 34 | $env:REPO_FOLDER = $repoFolder 35 | 36 | $koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" 37 | if ($env:KOREBUILD_ZIP) 38 | { 39 | $koreBuildZip=$env:KOREBUILD_ZIP 40 | } 41 | 42 | $buildFolder = ".build" 43 | $buildFile="$buildFolder\KoreBuild.ps1" 44 | 45 | if (!(Test-Path $buildFolder)) { 46 | Write-Host "Downloading KoreBuild from $koreBuildZip" 47 | 48 | $tempFolder=$env:TEMP + "\KoreBuild-" + [guid]::NewGuid() 49 | New-Item -Path "$tempFolder" -Type directory | Out-Null 50 | 51 | $localZipFile="$tempFolder\korebuild.zip" 52 | 53 | DownloadWithRetry -url $koreBuildZip -downloadLocation $localZipFile -retries 6 54 | 55 | Add-Type -AssemblyName System.IO.Compression.FileSystem 56 | [System.IO.Compression.ZipFile]::ExtractToDirectory($localZipFile, $tempFolder) 57 | 58 | New-Item -Path "$buildFolder" -Type directory | Out-Null 59 | copy-item "$tempFolder\**\build\*" $buildFolder -Recurse 60 | 61 | # Cleanup 62 | if (Test-Path $tempFolder) { 63 | Remove-Item -Recurse -Force $tempFolder 64 | } 65 | } 66 | 67 | &"$buildFile" $args -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 3 | cd $repoFolder 4 | 5 | koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" 6 | if [ ! -z $KOREBUILD_ZIP ]; then 7 | koreBuildZip=$KOREBUILD_ZIP 8 | fi 9 | 10 | buildFolder=".build" 11 | buildFile="$buildFolder/KoreBuild.sh" 12 | 13 | if test ! -d $buildFolder; then 14 | echo "Downloading KoreBuild from $koreBuildZip" 15 | 16 | tempFolder="/tmp/KoreBuild-$(uuidgen)" 17 | mkdir $tempFolder 18 | 19 | localZipFile="$tempFolder/korebuild.zip" 20 | 21 | retries=6 22 | until (wget -O $localZipFile $koreBuildZip 2>/dev/null || curl -o $localZipFile --location $koreBuildZip 2>/dev/null) 23 | do 24 | echo "Failed to download '$koreBuildZip'" 25 | if [ "$retries" -le 0 ]; then 26 | exit 1 27 | fi 28 | retries=$((retries - 1)) 29 | echo "Waiting 10 seconds before retrying. Retries left: $retries" 30 | sleep 10s 31 | done 32 | 33 | unzip -q -d $tempFolder $localZipFile 34 | 35 | mkdir $buildFolder 36 | cp -r $tempFolder/**/build/** $buildFolder 37 | 38 | chmod +x $buildFile 39 | 40 | # Cleanup 41 | if test ! -d $tempFolder; then 42 | rm -rf $tempFolder 43 | fi 44 | fi 45 | 46 | $buildFile -r $repoFolder "$@" -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": ["src", "test"] 3 | } 4 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/AssemblyExtensions.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.IO; 5 | using System.Reflection; 6 | 7 | namespace Microsoft.AspNetCore.SignalR 8 | { 9 | internal static class AssemblyExtensions 10 | { 11 | /// 12 | /// Loads an embedded string resource from the assembly. 13 | /// 14 | /// The assembly containing the embedded resource. 15 | /// The resource name. 16 | /// The embedded resource string. 17 | public static string StringResource(this Assembly assembly, string name) 18 | { 19 | string resource; 20 | using (var resourceStream = assembly.GetManifestResourceStream(name)) 21 | { 22 | var reader = new StreamReader(resourceStream); 23 | resource = reader.ReadToEnd(); 24 | } 25 | return resource; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/DbDataReaderExtensions.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.Data.Common; 6 | using System.Data.SqlClient; 7 | using JetBrains.Annotations; 8 | 9 | namespace Microsoft.AspNetCore.SignalR.SqlServer 10 | { 11 | internal static class DbDataReaderExtensions 12 | { 13 | public static byte[] GetBinary([NotNull]this DbDataReader reader, int ordinalIndex) 14 | { 15 | var sqlReader = reader as SqlDataReader; 16 | if (sqlReader == null) 17 | { 18 | throw new NotSupportedException(); 19 | } 20 | 21 | return sqlReader.GetSqlBinary(ordinalIndex).Value; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/DbOperation.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.Data; 7 | using System.Data.Common; 8 | using System.Data.SqlClient; 9 | using System.Diagnostics.CodeAnalysis; 10 | using System.Globalization; 11 | using System.Linq; 12 | using System.Threading.Tasks; 13 | using Microsoft.Extensions.Logging; 14 | 15 | namespace Microsoft.AspNetCore.SignalR.SqlServer 16 | { 17 | internal class DbOperation 18 | { 19 | private List _parameters = new List(); 20 | private readonly IDbProviderFactory _dbProviderFactory; 21 | 22 | public DbOperation(string connectionString, string commandText, ILogger logger) 23 | : this(connectionString, commandText, logger, SqlClientFactory.Instance.AsIDbProviderFactory()) 24 | { 25 | 26 | } 27 | 28 | public DbOperation(string connectionString, string commandText, ILogger logger, IDbProviderFactory dbProviderFactory) 29 | { 30 | ConnectionString = connectionString; 31 | CommandText = commandText; 32 | Logger = logger; 33 | _dbProviderFactory = dbProviderFactory; 34 | } 35 | 36 | public DbOperation(string connectionString, string commandText, ILogger logger, params DbParameter[] parameters) 37 | : this(connectionString, commandText, logger) 38 | { 39 | if (parameters != null) 40 | { 41 | _parameters.AddRange(parameters); 42 | } 43 | } 44 | 45 | public string LoggerPrefix { get; set; } 46 | 47 | public IList Parameters 48 | { 49 | get { return _parameters; } 50 | } 51 | 52 | protected ILogger Logger { get; private set; } 53 | 54 | protected string ConnectionString { get; private set; } 55 | 56 | protected string CommandText { get; private set; } 57 | 58 | public virtual object ExecuteScalar() 59 | { 60 | return Execute(cmd => cmd.ExecuteScalar()); 61 | } 62 | 63 | public virtual int ExecuteNonQuery() 64 | { 65 | return Execute(cmd => cmd.ExecuteNonQuery()); 66 | } 67 | 68 | public virtual Task ExecuteNonQueryAsync() 69 | { 70 | return Execute(cmd => cmd.ExecuteNonQueryAsync()); 71 | } 72 | 73 | #if NET451 74 | public virtual int ExecuteReader(Action processRecord) 75 | #else 76 | public virtual int ExecuteReader(Action processRecord) 77 | #endif 78 | { 79 | return ExecuteReader(processRecord, null); 80 | } 81 | 82 | #if NET451 83 | protected virtual int ExecuteReader(Action processRecord, Action commandAction) 84 | #else 85 | protected virtual int ExecuteReader(Action processRecord, Action commandAction) 86 | #endif 87 | { 88 | return Execute(cmd => 89 | { 90 | if (commandAction != null) 91 | { 92 | commandAction(cmd); 93 | } 94 | 95 | var reader = cmd.ExecuteReader(); 96 | var count = 0; 97 | 98 | while (reader.Read()) 99 | { 100 | count++; 101 | processRecord(reader, this); 102 | } 103 | 104 | return count; 105 | }); 106 | } 107 | 108 | #if NET451 109 | protected virtual IDbCommand CreateCommand(IDbConnection connection) 110 | #else 111 | protected virtual DbCommand CreateCommand(DbConnection connection) 112 | #endif 113 | { 114 | var command = connection.CreateCommand(); 115 | command.CommandText = CommandText; 116 | 117 | if (Parameters != null && Parameters.Count > 0) 118 | { 119 | for (var i = 0; i < Parameters.Count; i++) 120 | { 121 | command.Parameters.Add(Parameters[i].Clone(_dbProviderFactory)); 122 | } 123 | } 124 | 125 | return command; 126 | } 127 | 128 | #if NET451 129 | private T Execute(Func commandFunc) 130 | #else 131 | private T Execute(Func commandFunc) 132 | #endif 133 | { 134 | using (var connection = _dbProviderFactory.CreateConnection()) 135 | { 136 | connection.ConnectionString = ConnectionString; 137 | var command = CreateCommand(connection); 138 | connection.Open(); 139 | LoggerCommand(command); 140 | return commandFunc(command); 141 | } 142 | } 143 | 144 | #if NET451 145 | private void LoggerCommand(IDbCommand command) 146 | #else 147 | private void LoggerCommand(DbCommand command) 148 | #endif 149 | { 150 | if (Logger.IsEnabled(LogLevel.Debug)) 151 | { 152 | Logger.LogDebug(String.Format("Created DbCommand: CommandType={0}, CommandText={1}, Parameters={2}", command.CommandType, command.CommandText, 153 | command.Parameters.Cast() 154 | .Aggregate(string.Empty, (msg, p) => string.Format(CultureInfo.InvariantCulture, "{0} [Name={1}, Value={2}]", msg, p.ParameterName, p.Value))) 155 | ); 156 | } 157 | } 158 | 159 | #if NET451 160 | private async Task Execute(Func> commandFunc) 161 | #else 162 | private async Task Execute(Func> commandFunc) 163 | #endif 164 | { 165 | using (var connection = _dbProviderFactory.CreateConnection()) 166 | { 167 | connection.ConnectionString = ConnectionString; 168 | var command = CreateCommand(connection); 169 | 170 | connection.Open(); 171 | 172 | try 173 | { 174 | return await commandFunc(command); 175 | } 176 | catch (Exception ex) 177 | { 178 | Logger.LogWarning(0, ex, "Exception thrown by Task"); 179 | throw; 180 | } 181 | } 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/DbParameterExtensions.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.Data.Common; 5 | 6 | namespace Microsoft.AspNetCore.SignalR.SqlServer 7 | { 8 | internal static class DbParameterExtensions 9 | { 10 | public static DbParameter Clone(this DbParameter sourceParameter, IDbProviderFactory dbProviderFactory) 11 | { 12 | var newParameter = dbProviderFactory.CreateParameter(); 13 | 14 | newParameter.ParameterName = sourceParameter.ParameterName; 15 | newParameter.DbType = sourceParameter.DbType; 16 | newParameter.Value = sourceParameter.Value; 17 | newParameter.Direction = sourceParameter.Direction; 18 | 19 | return newParameter; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/DbProviderFactoryAdapter.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.Data; 5 | using System.Data.Common; 6 | 7 | namespace Microsoft.AspNetCore.SignalR.SqlServer 8 | { 9 | internal class DbProviderFactoryAdapter : IDbProviderFactory 10 | { 11 | private readonly DbProviderFactory _dbProviderFactory; 12 | 13 | public DbProviderFactoryAdapter(DbProviderFactory dbProviderFactory) 14 | { 15 | _dbProviderFactory = dbProviderFactory; 16 | } 17 | 18 | #if NET451 19 | public IDbConnection CreateConnection() 20 | #else 21 | public DbConnection CreateConnection() 22 | #endif 23 | { 24 | return _dbProviderFactory.CreateConnection(); 25 | } 26 | 27 | public DbParameter CreateParameter() 28 | { 29 | return _dbProviderFactory.CreateParameter(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/DbProviderFactoryExtensions.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.Data.Common; 5 | 6 | namespace Microsoft.AspNetCore.SignalR.SqlServer 7 | { 8 | internal static class DbProviderFactoryExtensions 9 | { 10 | public static IDbProviderFactory AsIDbProviderFactory(this DbProviderFactory dbProviderFactory) 11 | { 12 | return new DbProviderFactoryAdapter(dbProviderFactory); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/IDataRecordExtensions.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 | #if NET451 5 | using System; 6 | using System.Data; 7 | using System.Data.SqlClient; 8 | 9 | namespace Microsoft.AspNetCore.SignalR.SqlServer 10 | { 11 | internal static class IDataRecordExtensions 12 | { 13 | public static byte[] GetBinary(this IDataRecord reader, int ordinalIndex) 14 | { 15 | var sqlReader = reader as SqlDataReader; 16 | if (sqlReader == null) 17 | { 18 | throw new NotSupportedException(); 19 | } 20 | 21 | return sqlReader.GetSqlBinary(ordinalIndex).Value; 22 | } 23 | } 24 | } 25 | #endif 26 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/IDbBehavior.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.Data; 7 | using System.Data.Common; 8 | using System.Data.SqlClient; 9 | 10 | namespace Microsoft.AspNetCore.SignalR.SqlServer 11 | { 12 | public interface IDbBehavior 13 | { 14 | bool StartSqlDependencyListener(); 15 | IList> UpdateLoopRetryDelays { get; } 16 | 17 | #if NET451 18 | void AddSqlDependency(IDbCommand command, Action callback); 19 | #endif 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/IDbCommandExtensions.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.Data; 6 | using System.Data.Common; 7 | using System.Data.SqlClient; 8 | using System.Threading.Tasks; 9 | using JetBrains.Annotations; 10 | 11 | 12 | namespace Microsoft.AspNetCore.SignalR.SqlServer 13 | { 14 | internal static class IDbCommandExtensions 15 | { 16 | private readonly static TimeSpan _dependencyTimeout = TimeSpan.FromSeconds(60); 17 | 18 | #if NET451 19 | public static void AddSqlDependency([NotNull]this IDbCommand command, Action callback) 20 | { 21 | var sqlCommand = command as SqlCommand; 22 | if (sqlCommand == null) 23 | { 24 | throw new NotSupportedException(); 25 | } 26 | 27 | var dependency = new SqlDependency(sqlCommand, null, (int)_dependencyTimeout.TotalSeconds); 28 | dependency.OnChange += (o, e) => callback(e); 29 | } 30 | #endif 31 | 32 | #if NET451 33 | public static Task ExecuteNonQueryAsync(this IDbCommand command) 34 | #else 35 | public static Task ExecuteNonQueryAsync(this DbCommand command) 36 | #endif 37 | { 38 | var sqlCommand = command as SqlCommand; 39 | 40 | if (sqlCommand != null) 41 | { 42 | #if NET451 43 | return Task.Factory.FromAsync( 44 | (cb, state) => sqlCommand.BeginExecuteNonQuery(cb, state), 45 | iar => sqlCommand.EndExecuteNonQuery(iar), 46 | null); 47 | #else 48 | return sqlCommand.ExecuteNonQueryAsync(); 49 | #endif 50 | } 51 | else 52 | { 53 | return Task.FromResult(command.ExecuteNonQuery()); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/IDbProviderFactory.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.Data; 5 | using System.Data.Common; 6 | 7 | namespace Microsoft.AspNetCore.SignalR.SqlServer 8 | { 9 | public interface IDbProviderFactory 10 | { 11 | #if NET451 12 | IDbConnection CreateConnection(); 13 | #else 14 | DbConnection CreateConnection(); 15 | #endif 16 | DbParameter CreateParameter(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/Microsoft.AspNetCore.SignalR.SqlServer.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | EFCF27EC-CB9B-4F3A-91BE-154B8AB5B5E0 10 | .\obj 11 | .\bin\ 12 | 13 | 14 | 2.0 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/NotNullAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace JetBrains.Annotations 4 | { 5 | [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] 6 | internal sealed class NotNullAttribute : Attribute 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/ObservableDbOperation.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.Data; 7 | using System.Data.Common; 8 | using System.Data.SqlClient; 9 | using System.Diagnostics; 10 | using System.Diagnostics.CodeAnalysis; 11 | using System.Globalization; 12 | using System.Threading; 13 | using Microsoft.Extensions.Logging; 14 | 15 | namespace Microsoft.AspNetCore.SignalR.SqlServer 16 | { 17 | /// 18 | /// A DbOperation that continues to execute over and over as new results arrive. 19 | /// Will attempt to use SQL Query Notifications, otherwise falls back to a polling receive loop. 20 | /// 21 | internal class ObservableDbOperation : DbOperation, IDisposable, IDbBehavior 22 | { 23 | private readonly Tuple[] _updateLoopRetryDelays = new[] { 24 | Tuple.Create(0, 3), // 0ms x 3 25 | Tuple.Create(10, 3), // 10ms x 3 26 | Tuple.Create(50, 2), // 50ms x 2 27 | Tuple.Create(100, 2), // 100ms x 2 28 | Tuple.Create(200, 2), // 200ms x 2 29 | Tuple.Create(1000, 2), // 1000ms x 2 30 | Tuple.Create(1500, 2), // 1500ms x 2 31 | Tuple.Create(3000, 1) // 3000ms x 1 32 | }; 33 | private readonly object _stopLocker = new object(); 34 | private readonly ManualResetEventSlim _stopHandle = new ManualResetEventSlim(true); 35 | private readonly IDbBehavior _dbBehavior; 36 | 37 | private volatile bool _disposing; 38 | private long _notificationState; 39 | private readonly ILogger _logger; 40 | 41 | public ObservableDbOperation(string connectionString, string commandText, ILogger logger, IDbProviderFactory dbProviderFactory, IDbBehavior dbBehavior) 42 | : base(connectionString, commandText, logger, dbProviderFactory) 43 | { 44 | _dbBehavior = dbBehavior ?? this; 45 | _logger = logger; 46 | 47 | InitEvents(); 48 | } 49 | 50 | public ObservableDbOperation(string connectionString, string commandText, ILogger logger, params DbParameter[] parameters) 51 | : base(connectionString, commandText, logger, parameters) 52 | { 53 | _dbBehavior = this; 54 | _logger = logger; 55 | 56 | InitEvents(); 57 | } 58 | 59 | /// 60 | /// For use from tests only. 61 | /// 62 | internal long CurrentNotificationState 63 | { 64 | get { return _notificationState; } 65 | set { _notificationState = value; } 66 | } 67 | 68 | private void InitEvents() 69 | { 70 | Faulted += _ => { }; 71 | Queried += () => { }; 72 | #if NET451 73 | Changed += () => { }; 74 | #endif 75 | } 76 | 77 | public event Action Queried; 78 | #if NET451 79 | public event Action Changed; 80 | #endif 81 | public event Action Faulted; 82 | 83 | /// 84 | /// Note this blocks the calling thread until a SQL Query Notification can be set up 85 | /// 86 | [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Needs refactoring"), 87 | SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Errors are reported via the callback")] 88 | #if NET451 89 | public void ExecuteReaderWithUpdates(Action processRecord) 90 | #else 91 | public void ExecuteReaderWithUpdates(Action processRecord) 92 | #endif 93 | { 94 | lock (_stopLocker) 95 | { 96 | if (_disposing) 97 | { 98 | return; 99 | } 100 | _stopHandle.Reset(); 101 | } 102 | 103 | var useNotifications = _dbBehavior.StartSqlDependencyListener(); 104 | 105 | var delays = _dbBehavior.UpdateLoopRetryDelays; 106 | 107 | for (var i = 0; i < delays.Count; i++) 108 | { 109 | if (i == 0 && useNotifications) 110 | { 111 | // Reset the state to ProcessingUpdates if this is the start of the loop. 112 | // This should be safe to do here without Interlocked because the state is protected 113 | // in the other two cases using Interlocked, i.e. there should only be one instance of 114 | // this running at any point in time. 115 | _notificationState = NotificationState.ProcessingUpdates; 116 | } 117 | 118 | Tuple retry = delays[i]; 119 | var retryDelay = retry.Item1; 120 | var retryCount = retry.Item2; 121 | 122 | for (var j = 0; j < retryCount; j++) 123 | { 124 | if (_disposing) 125 | { 126 | Stop(null); 127 | return; 128 | } 129 | 130 | if (retryDelay > 0) 131 | { 132 | Logger.LogDebug(String.Format("{0}Waiting {1}ms before checking for messages again", LoggerPrefix, retryDelay)); 133 | 134 | Thread.Sleep(retryDelay); 135 | } 136 | 137 | var recordCount = 0; 138 | try 139 | { 140 | recordCount = ExecuteReader(processRecord); 141 | 142 | Queried(); 143 | } 144 | catch (Exception ex) 145 | { 146 | Logger.LogError(String.Format("{0}Error in SQL receive loop: {1}", LoggerPrefix, ex)); 147 | 148 | Faulted(ex); 149 | } 150 | 151 | if (recordCount > 0) 152 | { 153 | Logger.LogDebug(String.Format("{0}{1} records received", LoggerPrefix, recordCount)); 154 | 155 | // We got records so start the retry loop again 156 | i = -1; 157 | break; 158 | } 159 | 160 | Logger.LogDebug("{0}No records received", LoggerPrefix); 161 | 162 | var isLastRetry = i == delays.Count - 1 && j == retryCount - 1; 163 | 164 | if (isLastRetry) 165 | { 166 | // Last retry loop iteration 167 | if (!useNotifications) 168 | { 169 | // Last retry loop and we're not using notifications so just stay looping on the last retry delay 170 | j = j - 1; 171 | } 172 | else 173 | { 174 | #if NET451 175 | // No records after all retries, set up a SQL notification 176 | try 177 | { 178 | Logger.LogDebug("{0}Setting up SQL notification", LoggerPrefix); 179 | 180 | recordCount = ExecuteReader(processRecord, command => 181 | { 182 | _dbBehavior.AddSqlDependency(command, e => SqlDependency_OnChange(e, processRecord)); 183 | }); 184 | 185 | Queried(); 186 | 187 | if (recordCount > 0) 188 | { 189 | Logger.LogDebug("{0}Records were returned by the command that sets up the SQL notification, restarting the receive loop", LoggerPrefix); 190 | 191 | i = -1; 192 | break; // break the inner for loop 193 | } 194 | else 195 | { 196 | var previousState = Interlocked.CompareExchange(ref _notificationState, NotificationState.AwaitingNotification, 197 | NotificationState.ProcessingUpdates); 198 | 199 | if (previousState == NotificationState.AwaitingNotification) 200 | { 201 | Logger.LogError("{0}A SQL notification was already running. Overlapping receive loops detected, this should never happen. BUG!", LoggerPrefix); 202 | 203 | return; 204 | } 205 | 206 | if (previousState == NotificationState.NotificationReceived) 207 | { 208 | // Failed to change _notificationState from ProcessingUpdates to AwaitingNotification, it was already NotificationReceived 209 | 210 | Logger.LogDebug("{0}The SQL notification fired before the receive loop returned, restarting the receive loop", LoggerPrefix); 211 | 212 | i = -1; 213 | break; // break the inner for loop 214 | } 215 | 216 | } 217 | 218 | Logger.LogDebug("{0}No records received while setting up SQL notification", LoggerPrefix); 219 | 220 | // We're in a wait state for a notification now so check if we're disposing 221 | lock (_stopLocker) 222 | { 223 | if (_disposing) 224 | { 225 | _stopHandle.Set(); 226 | } 227 | } 228 | } 229 | catch (Exception ex) 230 | { 231 | Logger.LogError(String.Format("{0}Error in SQL receive loop: {1}", LoggerPrefix, ex)); 232 | Faulted(ex); 233 | 234 | // Re-enter the loop on the last retry delay 235 | j = j - 1; 236 | 237 | if (retryDelay > 0) 238 | { 239 | Logger.LogDebug(String.Format("{0}Waiting {1}ms before checking for messages again", LoggerPrefix, retryDelay)); 240 | 241 | Thread.Sleep(retryDelay); 242 | } 243 | } 244 | #endif 245 | } 246 | } 247 | } 248 | } 249 | 250 | Logger.LogDebug("{0}Receive loop exiting", LoggerPrefix); 251 | } 252 | 253 | [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Disposing")] 254 | public void Dispose() 255 | { 256 | lock (_stopLocker) 257 | { 258 | _disposing = true; 259 | } 260 | 261 | #if NET451 262 | if (_notificationState != NotificationState.Disabled) 263 | { 264 | try 265 | { 266 | SqlDependency.Stop(ConnectionString); 267 | } 268 | catch (Exception) { } 269 | } 270 | #endif 271 | if (Interlocked.Read(ref _notificationState) == NotificationState.ProcessingUpdates) 272 | { 273 | _stopHandle.Wait(); 274 | } 275 | _stopHandle.Dispose(); 276 | } 277 | 278 | #if NET451 279 | protected virtual void AddSqlDependency(IDbCommand command, Action callback) 280 | { 281 | command.AddSqlDependency(e => callback(e)); 282 | } 283 | 284 | [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "On a background thread and we report exceptions asynchronously"), 285 | SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "sender", Justification = "Event handler")] 286 | protected virtual void SqlDependency_OnChange(SqlNotificationEventArgs e, Action processRecord) 287 | { 288 | Logger.LogInformation("{0}SQL notification change fired", LoggerPrefix); 289 | 290 | lock (_stopLocker) 291 | { 292 | if (_disposing) 293 | { 294 | return; 295 | } 296 | } 297 | 298 | var previousState = Interlocked.CompareExchange(ref _notificationState, 299 | NotificationState.NotificationReceived, NotificationState.ProcessingUpdates); 300 | 301 | if (previousState == NotificationState.NotificationReceived) 302 | { 303 | Logger.LogError("{0}Overlapping SQL change notifications received, this should never happen, BUG!", LoggerPrefix); 304 | 305 | return; 306 | } 307 | if (previousState == NotificationState.ProcessingUpdates) 308 | { 309 | // We're still in the original receive loop 310 | 311 | // New updates will be retreived by the original reader thread 312 | Logger.LogDebug("{0}Original reader processing is still in progress and will pick up the changes", LoggerPrefix); 313 | 314 | return; 315 | } 316 | 317 | // _notificationState wasn't ProcessingUpdates (likely AwaitingNotification) 318 | 319 | // Check notification args for issues 320 | if (e.Type == SqlNotificationType.Change) 321 | { 322 | if (e.Info == SqlNotificationInfo.Update) 323 | { 324 | Logger.LogDebug(string.Format("{0}SQL notification details: Type={1}, Source={2}, Info={3}", LoggerPrefix, e.Type, e.Source, e.Info)); 325 | } 326 | else if (e.Source == SqlNotificationSource.Timeout) 327 | { 328 | Logger.LogDebug("{0}SQL notification timed out", LoggerPrefix); 329 | } 330 | else 331 | { 332 | Logger.LogError(string.Format("{0}Unexpected SQL notification details: Type={1}, Source={2}, Info={3}", LoggerPrefix, e.Type, e.Source, e.Info)); 333 | 334 | Faulted(new SqlMessageBusException(String.Format(CultureInfo.InvariantCulture, Resources.Error_UnexpectedSqlNotificationType, e.Type, e.Source, e.Info))); 335 | } 336 | } 337 | else if (e.Type == SqlNotificationType.Subscribe) 338 | { 339 | Debug.Assert(e.Info != SqlNotificationInfo.Invalid, "Ensure the SQL query meets the requirements for query notifications at http://msdn.microsoft.com/en-US/library/ms181122.aspx"); 340 | 341 | Logger.LogError(string.Format("{0}SQL notification subscription error: Type={1}, Source={2}, Info={3}", LoggerPrefix, e.Type, e.Source, e.Info)); 342 | 343 | if (e.Info == SqlNotificationInfo.TemplateLimit) 344 | { 345 | // We've hit a subscription limit, pause for a bit then start again 346 | Thread.Sleep(2000); 347 | } 348 | else 349 | { 350 | // Unknown subscription error, let's stop using query notifications 351 | _notificationState = NotificationState.Disabled; 352 | try 353 | { 354 | SqlDependency.Stop(ConnectionString); 355 | } 356 | catch (Exception) { } 357 | } 358 | } 359 | 360 | Changed(); 361 | } 362 | #endif 363 | 364 | 365 | [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "I need to")] 366 | protected virtual bool StartSqlDependencyListener() 367 | { 368 | #if NETSTANDARD1_6 369 | return false; 370 | #else 371 | lock (_stopLocker) 372 | { 373 | if (_disposing) 374 | { 375 | return false; 376 | } 377 | } 378 | 379 | if (_notificationState == NotificationState.Disabled) 380 | { 381 | return false; 382 | } 383 | 384 | Logger.LogDebug("{0}Starting SQL notification listener", LoggerPrefix); 385 | try 386 | { 387 | if (SqlDependency.Start(ConnectionString)) 388 | { 389 | Logger.LogDebug("{0}SQL notification listener started", LoggerPrefix); 390 | } 391 | else 392 | { 393 | Logger.LogDebug("{0}SQL notification listener was already running", LoggerPrefix); 394 | } 395 | return true; 396 | } 397 | catch (InvalidOperationException) 398 | { 399 | Logger.LogInformation("{0}SQL Service Broker is disabled, disabling query notifications", LoggerPrefix); 400 | 401 | _notificationState = NotificationState.Disabled; 402 | return false; 403 | } 404 | catch (Exception ex) 405 | { 406 | Logger.LogError(String.Format("{0}Error starting SQL notification listener: {1}", LoggerPrefix, ex)); 407 | 408 | return false; 409 | } 410 | #endif 411 | } 412 | 413 | [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Stopping is a terminal state on a bg thread")] 414 | protected virtual void Stop(Exception ex) 415 | { 416 | if (ex != null) 417 | { 418 | Faulted(ex); 419 | } 420 | 421 | if (_notificationState != NotificationState.Disabled) 422 | { 423 | #if NET451 424 | try 425 | { 426 | Logger.LogDebug("{0}Stopping SQL notification listener", LoggerPrefix); 427 | SqlDependency.Stop(ConnectionString); 428 | Logger.LogDebug("{0}SQL notification listener stopped", LoggerPrefix); 429 | } 430 | catch (Exception stopEx) 431 | { 432 | Logger.LogError(String.Format("{0}Error occured while stopping SQL notification listener: {1}", LoggerPrefix, stopEx)); 433 | } 434 | #endif 435 | } 436 | 437 | lock (_stopLocker) 438 | { 439 | if (_disposing) 440 | { 441 | _stopHandle.Set(); 442 | } 443 | } 444 | } 445 | 446 | internal static class NotificationState 447 | { 448 | public const long Enabled = 0; 449 | public const long ProcessingUpdates = 1; 450 | public const long AwaitingNotification = 2; 451 | public const long NotificationReceived = 3; 452 | public const long Disabled = 4; 453 | } 454 | 455 | bool IDbBehavior.StartSqlDependencyListener() 456 | { 457 | return StartSqlDependencyListener(); 458 | } 459 | 460 | IList> IDbBehavior.UpdateLoopRetryDelays 461 | { 462 | get { return _updateLoopRetryDelays; } 463 | } 464 | 465 | #if NET451 466 | void IDbBehavior.AddSqlDependency(IDbCommand command, Action callback) 467 | { 468 | AddSqlDependency(command, callback); 469 | } 470 | #endif 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/Properties/AssemblyInfo.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.Reflection; 5 | using System.Runtime.CompilerServices; 6 | 7 | [assembly: InternalsVisibleTo("Microsoft.AspNetCore.SignalR.SqlServer.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] 8 | [assembly: AssemblyMetadata("Serviceable", "True")] 9 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.34003 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Microsoft.AspNetCore.SignalR.SqlServer { 12 | using System; 13 | using System.Reflection; 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNetCore.SignalR.SqlServer.Resources", typeof(Resources).GetTypeInfo().Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to An unexpected SqlNotificationType was received. Details: Type={0}, Source={1}, Info={2}. 65 | /// 66 | internal static string Error_UnexpectedSqlNotificationType { 67 | get { 68 | return ResourceManager.GetString("Error_UnexpectedSqlNotificationType", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to The SQL Server edition of the target server is unsupported, e.g. SQL Azure.. 74 | /// 75 | internal static string Error_UnsupportedSqlEdition { 76 | get { 77 | return ResourceManager.GetString("Error_UnsupportedSqlEdition", resourceCulture); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | An unexpected SqlNotificationType was received. Details: Type={0}, Source={1}, Info={2} 122 | 123 | 124 | The SQL Server edition of the target server is unsupported, e.g. SQL Azure. 125 | 126 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/SqlInstaller.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.Reflection; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Microsoft.AspNetCore.SignalR.SqlServer 9 | { 10 | internal class SqlInstaller 11 | { 12 | private const int SchemaVersion = 1; 13 | private const string SchemaTableName = "Schema"; 14 | 15 | private readonly string _connectionString; 16 | private readonly string _messagesTableNamePrefix; 17 | private readonly int _tableCount; 18 | private readonly ILogger _logger; 19 | 20 | public SqlInstaller(string connectionString, string tableNamePrefix, int tableCount, ILogger logger) 21 | { 22 | _connectionString = connectionString; 23 | _messagesTableNamePrefix = tableNamePrefix; 24 | _tableCount = tableCount; 25 | _logger = logger; 26 | } 27 | 28 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities", Justification = "Query doesn't come from user code")] 29 | public void Install() 30 | { 31 | _logger.LogInformation("Start installing SignalR SQL objects"); 32 | 33 | if (!IsSqlEditionSupported(_connectionString)) 34 | { 35 | throw new PlatformNotSupportedException(Resources.Error_UnsupportedSqlEdition); 36 | } 37 | 38 | var script = GetType().GetTypeInfo().Assembly.StringResource("install.sql"); 39 | 40 | script = script.Replace("SET @SCHEMA_NAME = 'SignalR';", "SET @SCHEMA_NAME = '" + SqlMessageBus.SchemaName + "';"); 41 | script = script.Replace("SET @SCHEMA_TABLE_NAME = 'Schema';", "SET @SCHEMA_TABLE_NAME = '" + SchemaTableName + "';"); 42 | script = script.Replace("SET @TARGET_SCHEMA_VERSION = 1;", "SET @TARGET_SCHEMA_VERSION = " + SchemaVersion + ";"); 43 | script = script.Replace("SET @MESSAGE_TABLE_COUNT = 1;", "SET @MESSAGE_TABLE_COUNT = " + _tableCount + ";"); 44 | script = script.Replace("SET @MESSAGE_TABLE_NAME = 'Messages';", "SET @MESSAGE_TABLE_NAME = '" + _messagesTableNamePrefix + "';"); 45 | 46 | var operation = new DbOperation(_connectionString, script, _logger); 47 | operation.ExecuteNonQuery(); 48 | 49 | _logger.LogInformation("SignalR SQL objects installed"); 50 | } 51 | 52 | private bool IsSqlEditionSupported(string connectionString) 53 | { 54 | var operation = new DbOperation(connectionString, "SELECT SERVERPROPERTY ( 'EngineEdition' )", _logger); 55 | var edition = (int)operation.ExecuteScalar(); 56 | 57 | return edition >= SqlEngineEdition.Standard && edition <= SqlEngineEdition.Express; 58 | } 59 | 60 | private static class SqlEngineEdition 61 | { 62 | // See article http://technet.microsoft.com/en-us/library/ms174396.aspx for details on EngineEdition 63 | public const int Personal = 1; 64 | public const int Standard = 2; 65 | public const int Enterprise = 3; 66 | public const int Express = 4; 67 | public const int SqlAzure = 5; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/SqlMessageBus.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.Data.SqlClient; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Globalization; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Microsoft.AspNetCore.SignalR.Messaging; 12 | using Microsoft.AspNetCore.SignalR.Infrastructure; 13 | using Microsoft.Extensions.Logging; 14 | using Microsoft.Extensions.Options; 15 | 16 | namespace Microsoft.AspNetCore.SignalR.SqlServer 17 | { 18 | /// 19 | /// Uses SQL Server tables to scale-out SignalR applications in web farms. 20 | /// 21 | public class SqlMessageBus : ScaleoutMessageBus 22 | { 23 | internal const string SchemaName = "SignalR"; 24 | 25 | private const string _tableNamePrefix = "Messages"; 26 | 27 | private readonly string _connectionString; 28 | private readonly SqlScaleoutOptions _configuration; 29 | 30 | private readonly ILogger _logger; 31 | private readonly IDbProviderFactory _dbProviderFactory; 32 | private readonly List _streams = new List(); 33 | 34 | /// 35 | /// Creates a new instance of the SqlMessageBus class. 36 | /// 37 | /// The resolver to use. 38 | /// The SQL scale-out configuration options. 39 | public SqlMessageBus(IStringMinifier stringMinifier, 40 | ILoggerFactory loggerFactory, 41 | IPerformanceCounterManager performanceCounterManager, 42 | IOptions optionsAccessor, 43 | IOptions scaleoutOptionsAccessor) 44 | : this(stringMinifier, loggerFactory, performanceCounterManager, optionsAccessor, scaleoutOptionsAccessor, SqlClientFactory.Instance.AsIDbProviderFactory()) 45 | { 46 | 47 | } 48 | 49 | internal SqlMessageBus(IStringMinifier stringMinifier, 50 | ILoggerFactory loggerFactory, 51 | IPerformanceCounterManager performanceCounterManager, 52 | IOptions optionsAccessor, 53 | IOptions scaleoutOptionsAccessor, 54 | IDbProviderFactory dbProviderFactory) 55 | : base(stringMinifier, loggerFactory, performanceCounterManager, optionsAccessor, scaleoutOptionsAccessor) 56 | { 57 | var configuration = scaleoutOptionsAccessor.Value; 58 | _connectionString = configuration.ConnectionString; 59 | _configuration = configuration; 60 | _dbProviderFactory = dbProviderFactory; 61 | 62 | _logger = loggerFactory.CreateLogger(); 63 | ThreadPool.QueueUserWorkItem(Initialize); 64 | } 65 | 66 | protected override int StreamCount 67 | { 68 | get 69 | { 70 | return _configuration.TableCount; 71 | } 72 | } 73 | 74 | protected override Task Send(int streamIndex, IList messages) 75 | { 76 | return _streams[streamIndex].Send(messages); 77 | } 78 | 79 | protected override void Dispose(bool disposing) 80 | { 81 | _logger.LogInformation("SQL message bus disposing, disposing streams"); 82 | 83 | for (var i = 0; i < _streams.Count; i++) 84 | { 85 | _streams[i].Dispose(); 86 | } 87 | 88 | base.Dispose(disposing); 89 | } 90 | 91 | [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "They're stored in a List and disposed in the Dispose method"), 92 | SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "On a background thread and we report exceptions asynchronously")] 93 | private void Initialize(object state) 94 | { 95 | // NOTE: Called from a ThreadPool thread 96 | _logger.LogInformation(String.Format("SQL message bus initializing, TableCount={0}", _configuration.TableCount)); 97 | 98 | while (true) 99 | { 100 | try 101 | { 102 | var installer = new SqlInstaller(_connectionString, _tableNamePrefix, _configuration.TableCount, _logger); 103 | installer.Install(); 104 | break; 105 | } 106 | catch (Exception ex) 107 | { 108 | // Exception while installing 109 | for (var i = 0; i < _configuration.TableCount; i++) 110 | { 111 | OnError(i, ex); 112 | } 113 | 114 | _logger.LogError("Error trying to install SQL server objects, trying again in 2 seconds: {0}", ex); 115 | 116 | // Try again in a little bit 117 | Thread.Sleep(2000); 118 | } 119 | } 120 | 121 | for (var i = 0; i < _configuration.TableCount; i++) 122 | { 123 | var streamIndex = i; 124 | var tableName = String.Format(CultureInfo.InvariantCulture, "{0}_{1}", _tableNamePrefix, streamIndex); 125 | 126 | var stream = new SqlStream(streamIndex, _connectionString, tableName, _logger, _dbProviderFactory); 127 | stream.Queried += () => Open(streamIndex); 128 | stream.Faulted += (ex) => OnError(streamIndex, ex); 129 | stream.Received += (id, messages) => OnReceived(streamIndex, id, messages); 130 | 131 | _streams.Add(stream); 132 | 133 | StartReceiving(streamIndex); 134 | } 135 | } 136 | 137 | private void StartReceiving(int streamIndex) 138 | { 139 | var stream = _streams[streamIndex]; 140 | 141 | stream.StartReceiving().ContinueWith(async task => 142 | { 143 | try 144 | { 145 | await task; 146 | // Open the stream once receiving has started 147 | Open(streamIndex); 148 | } 149 | catch (Exception ex) 150 | { 151 | OnError(streamIndex, ex); 152 | 153 | _logger.LogWarning(0, ex, "Exception thrown by Task"); 154 | Thread.Sleep(2000); 155 | StartReceiving(streamIndex); 156 | } 157 | }); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/SqlMessageBusException.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 | 6 | namespace Microsoft.AspNetCore.SignalR.SqlServer 7 | { 8 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors", Justification="Should never have inner exceptions")] 9 | #if NET451 10 | [Serializable] 11 | #endif 12 | public class SqlMessageBusException : Exception 13 | { 14 | public SqlMessageBusException(string message) 15 | : base(message) 16 | { 17 | 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/SqlPayload.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.Data; 7 | using System.Data.Common; 8 | using JetBrains.Annotations; 9 | using Microsoft.AspNetCore.SignalR.Messaging; 10 | 11 | namespace Microsoft.AspNetCore.SignalR.SqlServer 12 | { 13 | public static class SqlPayload 14 | { 15 | public static byte[] ToBytes([NotNull] IList messages) 16 | { 17 | var message = new ScaleoutMessage(messages); 18 | return message.ToBytes(); 19 | } 20 | 21 | #if NET451 22 | public static ScaleoutMessage FromBytes(IDataRecord record) 23 | #else 24 | public static ScaleoutMessage FromBytes(DbDataReader record) 25 | #endif 26 | { 27 | var message = ScaleoutMessage.FromBytes(record.GetBinary(1)); 28 | 29 | return message; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/SqlReceiver.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.Data; 6 | using System.Data.Common; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Globalization; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Microsoft.AspNetCore.SignalR.Messaging; 12 | using Microsoft.Extensions.Logging; 13 | 14 | namespace Microsoft.AspNetCore.SignalR.SqlServer 15 | { 16 | internal class SqlReceiver : IDisposable 17 | { 18 | private readonly string _connectionString; 19 | private readonly string _tableName; 20 | private readonly ILogger _logger; 21 | private readonly string _loggerPrefix; 22 | private readonly IDbProviderFactory _dbProviderFactory; 23 | 24 | private long? _lastPayloadId = null; 25 | private string _maxIdSql = "SELECT [PayloadId] FROM [{0}].[{1}_Id]"; 26 | private string _selectSql = "SELECT [PayloadId], [Payload], [InsertedOn] FROM [{0}].[{1}] WHERE [PayloadId] > @PayloadId"; 27 | private ObservableDbOperation _dbOperation; 28 | private volatile bool _disposed; 29 | 30 | public SqlReceiver(string connectionString, string tableName, ILogger logger, string loggerPrefix, IDbProviderFactory dbProviderFactory) 31 | { 32 | _connectionString = connectionString; 33 | _tableName = tableName; 34 | _loggerPrefix = loggerPrefix; 35 | _logger = logger; 36 | _dbProviderFactory = dbProviderFactory; 37 | 38 | Queried += () => { }; 39 | Received += (_, __) => { }; 40 | Faulted += _ => { }; 41 | 42 | _maxIdSql = String.Format(CultureInfo.InvariantCulture, _maxIdSql, SqlMessageBus.SchemaName, _tableName); 43 | _selectSql = String.Format(CultureInfo.InvariantCulture, _selectSql, SqlMessageBus.SchemaName, _tableName); 44 | } 45 | 46 | public event Action Queried; 47 | 48 | public event Action Received; 49 | 50 | public event Action Faulted; 51 | 52 | public Task StartReceiving() 53 | { 54 | var tcs = new TaskCompletionSource(); 55 | 56 | ThreadPool.QueueUserWorkItem(Receive, tcs); 57 | 58 | return tcs.Task; 59 | } 60 | 61 | public void Dispose() 62 | { 63 | lock (this) 64 | { 65 | if (_dbOperation != null) 66 | { 67 | _dbOperation.Dispose(); 68 | } 69 | _disposed = true; 70 | _logger.LogInformation("{0}SqlReceiver disposed", _loggerPrefix); 71 | } 72 | } 73 | 74 | [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Class level variable"), 75 | SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "On a background thread with explicit error processing")] 76 | private void Receive(object state) 77 | { 78 | var tcs = (TaskCompletionSource)state; 79 | 80 | if (!_lastPayloadId.HasValue) 81 | { 82 | var lastPayloadIdOperation = new DbOperation(_connectionString, _maxIdSql, _logger) 83 | { 84 | LoggerPrefix = _loggerPrefix 85 | }; 86 | 87 | try 88 | { 89 | _lastPayloadId = (long?)lastPayloadIdOperation.ExecuteScalar(); 90 | Queried(); 91 | 92 | _logger.LogDebug(String.Format("{0}SqlReceiver started, initial payload id={1}", _loggerPrefix, _lastPayloadId)); 93 | 94 | // Complete the StartReceiving task as we've successfully initialized the payload ID 95 | tcs.TrySetResult(null); 96 | } 97 | catch (Exception ex) 98 | { 99 | _logger.LogError(String.Format("{0}SqlReceiver error starting: {1}", _loggerPrefix, ex)); 100 | 101 | tcs.TrySetException(ex); 102 | return; 103 | } 104 | } 105 | 106 | // NOTE: This is called from a BG thread so any uncaught exceptions will crash the process 107 | lock (this) 108 | { 109 | if (_disposed) 110 | { 111 | return; 112 | } 113 | 114 | var parameter = _dbProviderFactory.CreateParameter(); 115 | parameter.ParameterName = "PayloadId"; 116 | parameter.Value = _lastPayloadId.Value; 117 | 118 | _dbOperation = new ObservableDbOperation(_connectionString, _selectSql, _logger, parameter) 119 | { 120 | LoggerPrefix = _loggerPrefix 121 | }; 122 | } 123 | 124 | _dbOperation.Queried += () => Queried(); 125 | _dbOperation.Faulted += ex => Faulted(ex); 126 | #if NET451 127 | _dbOperation.Changed += () => 128 | { 129 | _logger.LogInformation("{0}Starting receive loop again to process updates", _loggerPrefix); 130 | 131 | _dbOperation.ExecuteReaderWithUpdates(ProcessRecord); 132 | }; 133 | #endif 134 | _logger.LogDebug(String.Format("{0}Executing receive reader, initial payload ID parameter={1}", _loggerPrefix, _dbOperation.Parameters[0].Value)); 135 | 136 | _dbOperation.ExecuteReaderWithUpdates(ProcessRecord); 137 | 138 | _logger.LogInformation("{0}SqlReceiver.Receive returned", _loggerPrefix); 139 | } 140 | 141 | #if NET451 142 | private void ProcessRecord(IDataRecord record, DbOperation dbOperation) 143 | #else 144 | private void ProcessRecord(DbDataReader record, DbOperation dbOperation) 145 | #endif 146 | { 147 | var id = record.GetInt64(0); 148 | ScaleoutMessage message = SqlPayload.FromBytes(record); 149 | 150 | _logger.LogDebug(String.Format("{0}SqlReceiver last payload ID={1}, new payload ID={2}", _loggerPrefix, _lastPayloadId, id)); 151 | 152 | if (id > _lastPayloadId + 1) 153 | { 154 | _logger.LogError(String.Format("{0}Missed message(s) from SQL Server. Expected payload ID {1} but got {2}.", _loggerPrefix, _lastPayloadId + 1, id)); 155 | } 156 | else if (id <= _lastPayloadId) 157 | { 158 | _logger.LogInformation(String.Format("{0}Duplicate message(s) or payload ID reset from SQL Server. Last payload ID {1}, this payload ID {2}", _loggerPrefix, _lastPayloadId, id)); 159 | } 160 | 161 | _lastPayloadId = id; 162 | 163 | // Update the Parameter with the new payload ID 164 | dbOperation.Parameters[0].Value = _lastPayloadId; 165 | 166 | _logger.LogDebug(String.Format("{0}Updated receive reader initial payload ID parameter={1}", _loggerPrefix, _dbOperation.Parameters[0].Value)); 167 | 168 | _logger.LogDebug(String.Format("{0}Payload {1} containing {2} message(s) received", _loggerPrefix, id, message.Messages.Count)); 169 | 170 | Received((ulong)id, message); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/SqlScaleoutOptions.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 Microsoft.AspNetCore.SignalR.Messaging; 6 | 7 | namespace Microsoft.AspNetCore.SignalR 8 | { 9 | /// 10 | /// Settings for the SQL Server scale-out message bus implementation. 11 | /// 12 | public class SqlScaleoutOptions : ScaleoutOptions 13 | { 14 | private string _connectionString; 15 | private int _tableCount; 16 | 17 | public SqlScaleoutOptions() 18 | { 19 | _tableCount = 1; 20 | } 21 | /// 22 | /// The SQL Server connection string to use. 23 | /// 24 | public string ConnectionString 25 | { 26 | get 27 | { 28 | return _connectionString; 29 | } 30 | set 31 | { 32 | if (String.IsNullOrEmpty(value)) 33 | { 34 | throw new ArgumentNullException("connectionString"); 35 | } 36 | 37 | _connectionString = value; 38 | } 39 | } 40 | 41 | /// 42 | /// The number of tables to store messages in. Using more tables reduces lock contention and may increase throughput. 43 | /// This must be consistent between all nodes in the web farm. 44 | /// Defaults to 1. 45 | /// 46 | public int TableCount 47 | { 48 | get 49 | { 50 | return _tableCount; 51 | } 52 | set 53 | { 54 | if (value < 1) 55 | { 56 | throw new ArgumentOutOfRangeException("value"); 57 | } 58 | _tableCount = value; 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/SqlSender.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.Data; 7 | using System.Globalization; 8 | using System.Reflection; 9 | using System.Threading.Tasks; 10 | using Microsoft.AspNetCore.SignalR.Messaging; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace Microsoft.AspNetCore.SignalR.SqlServer 14 | { 15 | internal class SqlSender 16 | { 17 | private readonly string _connectionString; 18 | private readonly string _insertDml; 19 | private readonly ILogger _logger; 20 | private readonly IDbProviderFactory _dbProviderFactory; 21 | 22 | public SqlSender(string connectionString, string tableName, ILogger logger, IDbProviderFactory dbProviderFactory) 23 | { 24 | _connectionString = connectionString; 25 | _insertDml = BuildInsertString(tableName); 26 | _logger = logger; 27 | _dbProviderFactory = dbProviderFactory; 28 | } 29 | 30 | private string BuildInsertString(string tableName) 31 | { 32 | var insertDml = GetType().GetTypeInfo().Assembly.StringResource("send.sql"); 33 | 34 | return insertDml.Replace("[SignalR]", String.Format(CultureInfo.InvariantCulture, "[{0}]", SqlMessageBus.SchemaName)) 35 | .Replace("[Messages_0", String.Format(CultureInfo.InvariantCulture, "[{0}", tableName)); 36 | } 37 | 38 | public Task Send(IList messages) 39 | { 40 | if (messages == null || messages.Count == 0) 41 | { 42 | return Task.FromResult(null); 43 | } 44 | 45 | var parameter = _dbProviderFactory.CreateParameter(); 46 | parameter.ParameterName = "Payload"; 47 | parameter.DbType = DbType.Binary; 48 | parameter.Value = SqlPayload.ToBytes(messages); 49 | 50 | var operation = new DbOperation(_connectionString, _insertDml, _logger, parameter); 51 | 52 | return operation.ExecuteNonQueryAsync(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/SqlServerSignalRServicesBuilderExtensions.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 Microsoft.AspNetCore.SignalR; 6 | using Microsoft.AspNetCore.SignalR.Messaging; 7 | using Microsoft.AspNetCore.SignalR.SqlServer; 8 | using Microsoft.Extensions.Configuration; 9 | 10 | namespace Microsoft.Extensions.DependencyInjection 11 | { 12 | public static class SqlServerSignalRServicesBuilderExtensions 13 | { 14 | public static SignalRServicesBuilder AddSqlServer(this SignalRServicesBuilder builder, Action configureOptions = null) 15 | { 16 | return builder.AddSqlServer(configuration: null, configureOptions: configureOptions); 17 | } 18 | public static SignalRServicesBuilder AddSqlServer(this SignalRServicesBuilder builder, IConfiguration configuration, Action configureOptions = null) 19 | { 20 | builder.ServiceCollection.Add(ServiceDescriptor.Singleton()); 21 | 22 | if (configuration != null) 23 | { 24 | builder.ServiceCollection.Configure(configuration); 25 | } 26 | 27 | if (configureOptions != null) 28 | { 29 | builder.ServiceCollection.Configure(configureOptions); 30 | } 31 | 32 | return builder; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/SqlStream.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.Globalization; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.SignalR.Messaging; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Microsoft.AspNetCore.SignalR.SqlServer 12 | { 13 | internal class SqlStream : IDisposable 14 | { 15 | private readonly int _streamIndex; 16 | private readonly ILogger _logger; 17 | private readonly SqlSender _sender; 18 | private readonly SqlReceiver _receiver; 19 | private readonly string _loggerPrefix; 20 | 21 | public SqlStream(int streamIndex, string connectionString, string tableName, ILogger logger, IDbProviderFactory dbProviderFactory) 22 | { 23 | _streamIndex = streamIndex; 24 | _logger = logger; 25 | _loggerPrefix = String.Format(CultureInfo.InvariantCulture, "Stream {0} : ", _streamIndex); 26 | 27 | Queried += () => { }; 28 | Received += (_, __) => { }; 29 | Faulted += _ => { }; 30 | 31 | _sender = new SqlSender(connectionString, tableName, _logger, dbProviderFactory); 32 | _receiver = new SqlReceiver(connectionString, tableName, _logger, _loggerPrefix, dbProviderFactory); 33 | _receiver.Queried += () => Queried(); 34 | _receiver.Faulted += (ex) => Faulted(ex); 35 | _receiver.Received += (id, messages) => Received(id, messages); 36 | } 37 | 38 | public event Action Queried; 39 | 40 | public event Action Received; 41 | 42 | public event Action Faulted; 43 | 44 | public Task StartReceiving() 45 | { 46 | return _receiver.StartReceiving(); 47 | } 48 | 49 | public Task Send(IList messages) 50 | { 51 | _logger.LogDebug(String.Format("{0}Saving payload with {1} messages(s) to SQL server", _loggerPrefix, messages.Count, _streamIndex)); 52 | 53 | return _sender.Send(messages); 54 | } 55 | 56 | public void Dispose() 57 | { 58 | _logger.LogInformation(String.Format("{0}Disposing stream {1}", _loggerPrefix, _streamIndex)); 59 | 60 | _receiver.Dispose(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/install.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 2 | -- Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | DECLARE @SCHEMA_NAME nvarchar(32), 5 | @SCHEMA_TABLE_NAME nvarchar(100), 6 | @TARGET_SCHEMA_VERSION int, 7 | @MESSAGE_TABLE_NAME nvarchar(100), 8 | @MESSAGE_TABLE_COUNT int, 9 | @CREATE_MESSAGE_TABLE_DDL nvarchar(1000), 10 | @CREATE_MESSAGE_ID_TABLE_DDL nvarchar(1000); 11 | 12 | SET @SCHEMA_NAME = 'SignalR'; 13 | SET @SCHEMA_TABLE_NAME = 'Schema'; 14 | SET @TARGET_SCHEMA_VERSION = 1; 15 | SET @MESSAGE_TABLE_COUNT = 1; 16 | SET @MESSAGE_TABLE_NAME = 'Messages'; 17 | SET @CREATE_MESSAGE_TABLE_DDL = 18 | N'CREATE TABLE [' + @SCHEMA_NAME + N'].[@TableName]( 19 | [PayloadId] [bigint] NOT NULL, 20 | [Payload] [varbinary](max) NOT NULL, 21 | [InsertedOn] [datetime] NOT NULL, 22 | PRIMARY KEY CLUSTERED ([PayloadId] ASC) 23 | );' 24 | SET @CREATE_MESSAGE_ID_TABLE_DDL = 25 | N'CREATE TABLE [' + @SCHEMA_NAME + N'].[@TableName] ( 26 | [PayloadId] [bigint] NOT NULL, 27 | PRIMARY KEY CLUSTERED ([PayloadId] ASC) 28 | ); 29 | -- Initialize PayloadId row with value 0 30 | INSERT INTO [' + @SCHEMA_NAME + N'].[@TableName] (PayloadId) VALUES (0);'; 31 | 32 | PRINT 'Installing SignalR SQL objects'; 33 | 34 | SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; 35 | BEGIN TRANSACTION; 36 | 37 | -- Create the DB schema if it doesn't exist 38 | IF NOT EXISTS(SELECT [schema_id] FROM [sys].[schemas] WHERE [name] = @SCHEMA_NAME) 39 | BEGIN 40 | BEGIN TRY 41 | EXEC(N'CREATE SCHEMA [' + @SCHEMA_NAME + '];'); 42 | PRINT 'Created database schema [' + @SCHEMA_NAME + ']'; 43 | END TRY 44 | BEGIN CATCH 45 | DECLARE @ErrorNumber int, 46 | @ErrorSeverity int, 47 | @ErrorState int; 48 | 49 | SELECT @ErrorNumber = ERROR_NUMBER(), 50 | @ErrorSeverity = ERROR_SEVERITY(), 51 | @ErrorState = ERROR_STATE(); 52 | 53 | IF @ErrorNumber = 2759 54 | -- If it's an object already exists error then ignore 55 | PRINT 'Database schema [' + @SCHEMA_NAME + '] already exists' 56 | ELSE 57 | RAISERROR (@ErrorNumber, @ErrorSeverity, @ErrorState); 58 | END CATCH; 59 | END 60 | ELSE 61 | PRINT 'Database schema [' + @SCHEMA_NAME + '] already exists'; 62 | 63 | DECLARE @SCHEMA_ID int; 64 | SELECT @SCHEMA_ID = [schema_id] FROM [sys].[schemas] WHERE [name] = @SCHEMA_NAME; 65 | 66 | -- Create the SignalR_Schema table if it doesn't exist 67 | IF NOT EXISTS(SELECT [object_id] FROM [sys].[tables] WHERE [name] = @SCHEMA_TABLE_NAME AND [schema_id] = @SCHEMA_ID) 68 | BEGIN 69 | -- Create the empty SignalR schema table 70 | EXEC(N'CREATE TABLE [' + @SCHEMA_NAME + '].[' + @SCHEMA_TABLE_NAME + ']('+ 71 | N'[SchemaVersion] [int] NOT NULL,'+ 72 | N'PRIMARY KEY CLUSTERED ([SchemaVersion] ASC)'+ 73 | N')'); 74 | PRINT 'Created table [' + @SCHEMA_NAME + '].[' + @SCHEMA_TABLE_NAME + ']'; 75 | END 76 | ELSE 77 | PRINT 'Table [' + @SCHEMA_NAME + '].[' + @SCHEMA_TABLE_NAME + '] already exists'; 78 | 79 | DECLARE @GET_SCHEMA_VERSION_SQL nvarchar(1000); 80 | SET @GET_SCHEMA_VERSION_SQL = N'SELECT @schemaVersion = [SchemaVersion] FROM [' + @SCHEMA_NAME + N'].[' + @SCHEMA_TABLE_NAME + N']'; 81 | 82 | DECLARE @CURRENT_SCHEMA_VERSION int; 83 | EXEC sp_executesql @GET_SCHEMA_VERSION_SQL, N'@schemaVersion int output', 84 | @schemaVersion = @CURRENT_SCHEMA_VERSION output; 85 | 86 | PRINT 'Current SignalR schema version: ' + CASE @CURRENT_SCHEMA_VERSION WHEN NULL THEN 'none' ELSE CONVERT(nvarchar, @CURRENT_SCHEMA_VERSION) END; 87 | 88 | -- Install tables, etc. 89 | IF @CURRENT_SCHEMA_VERSION IS NULL OR @CURRENT_SCHEMA_VERSION <= @TARGET_SCHEMA_VERSION 90 | BEGIN 91 | IF @CURRENT_SCHEMA_VERSION IS NULL OR @CURRENT_SCHEMA_VERSION = @TARGET_SCHEMA_VERSION 92 | BEGIN 93 | -- Install version 1 94 | PRINT 'Installing schema version 1'; 95 | 96 | DECLARE @counter int; 97 | SET @counter = 0; 98 | WHILE @counter < @MESSAGE_TABLE_COUNT 99 | BEGIN 100 | DECLARE @table_name nvarchar(100); 101 | DECLARE @ddl nvarchar(max); 102 | 103 | -- Create the message table 104 | SET @table_name = @MESSAGE_TABLE_NAME + '_' + CONVERT(nvarchar, @counter); 105 | SET @ddl = REPLACE(@CREATE_MESSAGE_TABLE_DDL, '@TableName', @table_name); 106 | 107 | IF NOT EXISTS(SELECT [object_id] 108 | FROM [sys].[tables] 109 | WHERE [name] = @table_name 110 | AND [schema_id] = @SCHEMA_ID) 111 | BEGIN 112 | EXEC(@ddl); 113 | PRINT 'Created message table [' + @SCHEMA_NAME + '].[' + @table_name + ']'; 114 | END 115 | ELSE 116 | PRINT 'Mesage table [' + @SCHEMA_NAME + '].[' + @table_name + '] already exists'; 117 | 118 | -- Create the id table 119 | SET @table_name = @table_name + '_Id'; 120 | SET @ddl = REPLACE(@CREATE_MESSAGE_ID_TABLE_DDL, '@TableName', @table_name); 121 | 122 | IF NOT EXISTS(SELECT [object_id] 123 | FROM [sys].[tables] 124 | WHERE [name] = @table_name 125 | AND [schema_id] = @SCHEMA_ID) 126 | BEGIN 127 | EXEC(@ddl); 128 | PRINT 'Created message ID table [' + @SCHEMA_NAME + '].[PayloadId]'; 129 | END 130 | ELSE 131 | PRINT 'Message ID table [' + @SCHEMA_NAME + '].[' + @table_name + '] alread exists'; 132 | 133 | SET @counter = @counter + 1; 134 | END 135 | 136 | IF @CURRENT_SCHEMA_VERSION IS NULL 137 | BEGIN 138 | DECLARE @insert_dml nvarchar(1000); 139 | SET @CURRENT_SCHEMA_VERSION = 1; 140 | SET @insert_dml = 'INSERT INTO [' + @SCHEMA_NAME + '].[' + @SCHEMA_TABLE_NAME + '] ([SchemaVersion]) VALUES(' + CONVERT(nvarchar, @CURRENT_SCHEMA_VERSION) + ')'; 141 | EXEC(@insert_dml); 142 | END 143 | 144 | PRINT 'Schema version 1 installed'; 145 | END 146 | /*IF @CURRENT_SCHEMA_VERSION = 1 147 | BEGIN 148 | -- Update to version 2 149 | -- TODO: Add schema updates here when we go to v2 150 | SELECT GETDATE() -- dummy 151 | END*/ 152 | 153 | COMMIT TRANSACTION; 154 | 155 | PRINT 'SignalR SQL objects installed'; 156 | END 157 | 158 | ELSE -- @CURRENT_SCHEMA_VERSION > @TARGET_SCHEMA_VERSION 159 | BEGIN 160 | -- Configured SqlMessageBus is lower version than current DB schema, just bail out 161 | ROLLBACK TRANSACTION; 162 | RAISERROR(N'SignalR database current schema version %d is newer than the configured SqlMessageBus schema version %d. Please update to the latest Microsoft.AspNetCore.SignalR.SqlServer NuGet package.', 11, 1, 163 | @CURRENT_SCHEMA_VERSION, @TARGET_SCHEMA_VERSION); 164 | END -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0-*", 3 | "description": "Core server components for ASP.NET SignalR.", 4 | "dependencies": { 5 | "Microsoft.AspNetCore.SignalR.Server": "0.2.0-*", 6 | "Microsoft.Extensions.Options.ConfigurationExtensions": "1.2.0-*", 7 | "NETStandard.Library": "1.6.1-*" 8 | }, 9 | "buildOptions": { 10 | "warningsAsErrors": true, 11 | "keyFile": "../../tools/Key.snk" 12 | }, 13 | "resources": "install.sql;send.sql", 14 | "frameworks": { 15 | "net451": { 16 | "frameworkAssemblies": { 17 | "System.Data": "" 18 | } 19 | }, 20 | "netstandard1.6": { 21 | "dependencies": { 22 | "System.Data.SqlClient": "4.3.0-*" 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.SignalR.SqlServer/send.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 2 | -- Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | -- Params: @Payload varbinary(max) 5 | -- Replace: [SignalR] => [schema_name], [Messages_0 => [table_prefix_index 6 | 7 | -- We need to ensure that the payload id increment and payload insert are atomic. 8 | -- Hence, we explicitly need to ensure that the order of operations is correct 9 | -- such that an exclusive lock is taken on the ID table to effectively serialize 10 | -- the insert of new messages . It is critical that once a message with PayloadID = N 11 | -- has been committed into the message table that a message with PayloadID < N can 12 | -- *never* be committed. 13 | 14 | SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; 15 | 16 | -- START: TEST DATA -- 17 | --DECLARE @Payload varbinary(max); 18 | --SET @Payload = 0x2605260626402642; 19 | -- END: TEST DATA -- 20 | 21 | DECLARE @NewPayloadId bigint; 22 | 23 | -- Update last payload id, find new PayloadId and insert new message at once. 24 | -- Now we don't need explicit transaction here. 25 | UPDATE TOP(1) [SignalR].[Messages_0_Id] SET @NewPayloadId = [PayloadId] = [PayloadId] + 1 26 | OUTPUT INSERTED.[PayloadId], @Payload, GETDATE() 27 | -- Insert payload 28 | INTO [SignalR].[Messages_0] ([PayloadId], [Payload], [InsertedOn]); 29 | 30 | -- Garbage collection 31 | SET TRANSACTION ISOLATION LEVEL READ COMMITTED; 32 | DECLARE @MaxTableSize int, 33 | @BlockSize int; 34 | 35 | SET @MaxTableSize = 10000; 36 | SET @BlockSize = 2500; 37 | 38 | -- Check the table size on every Nth insert where N is @BlockSize 39 | IF @NewPayloadId % @BlockSize = 0 40 | BEGIN 41 | -- SET NOCOUNT ON added to prevent extra result sets from 42 | -- interfering with SELECT statements 43 | SET NOCOUNT ON; 44 | 45 | DECLARE @RowCount int, 46 | @StartPayloadId bigint, 47 | @EndPayloadId bigint; 48 | 49 | BEGIN TRANSACTION; 50 | 51 | SELECT @RowCount = COUNT([PayloadId]), @StartPayloadId = MIN([PayloadId]) 52 | FROM [SignalR].[Messages_0]; 53 | 54 | -- Check if we're over the max table size 55 | IF @RowCount >= @MaxTableSize 56 | BEGIN 57 | DECLARE @OverMaxBy int; 58 | 59 | -- We want to delete enough rows to bring the table back to max size - block size 60 | SET @OverMaxBy = @RowCount - @MaxTableSize; 61 | SET @EndPayloadId = @StartPayloadId + @BlockSize + @OverMaxBy; 62 | 63 | -- Delete oldest block of messages 64 | DELETE FROM [SignalR].[Messages_0] 65 | WHERE [PayloadId] BETWEEN @StartPayloadId AND @EndPayloadId; 66 | END 67 | 68 | COMMIT TRANSACTION; 69 | END -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.SignalR.SqlServer.Tests/Microsoft.AspNetCore.SignalR.SqlServer.Tests.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 0A4487F1-9374-4E7B-957F-99647319C540 10 | .\obj 11 | .\bin\ 12 | 13 | 14 | 2.0 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.SignalR.SqlServer.Tests/ObservableSqlOperationFacts.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.Data; 7 | using System.Data.Common; 8 | using System.Data.SqlClient; 9 | using System.Linq; 10 | using System.Threading; 11 | using Microsoft.AspNetCore.SignalR.SqlServer; 12 | using Microsoft.Extensions.Logging; 13 | using Moq; 14 | using Xunit; 15 | 16 | namespace Microsoft.AspNetCore.SignalR.Tests.SqlServer 17 | { 18 | public class ObservableSqlOperationFacts 19 | { 20 | private static readonly List> _defaultRetryDelays = new List> { new Tuple(0, 1) }; 21 | 22 | [Theory] 23 | [InlineData(true)] 24 | [InlineData(false)] 25 | public void UseSqlNotificationsIfAvailable(bool supportSqlNotifications) 26 | { 27 | // Arrange 28 | var sqlDependencyAdded = false; 29 | var retryLoopCount = 0; 30 | var mre = new ManualResetEventSlim(); 31 | var dbProviderFactory = new MockDbProviderFactory(); 32 | var dbBehavior = new Mock(); 33 | var logger = new Mock(); 34 | dbBehavior.Setup(db => db.UpdateLoopRetryDelays).Returns(_defaultRetryDelays); 35 | dbBehavior.Setup(db => db.StartSqlDependencyListener()).Returns(supportSqlNotifications); 36 | dbBehavior.Setup(db => db.AddSqlDependency(It.IsAny(), It.IsAny>())) 37 | .Callback(() => 38 | { 39 | sqlDependencyAdded = true; 40 | mre.Set(); 41 | }); 42 | var operation = new ObservableDbOperation("test", "test", logger.Object, dbProviderFactory, dbBehavior.Object); 43 | operation.Faulted += _ => mre.Set(); 44 | operation.Queried += () => 45 | { 46 | retryLoopCount++; 47 | if (retryLoopCount > 1) 48 | { 49 | mre.Set(); 50 | } 51 | }; 52 | 53 | // Act 54 | ThreadPool.QueueUserWorkItem(_ => operation.ExecuteReaderWithUpdates((record, o) => { })); 55 | mre.Wait(); 56 | operation.Dispose(); 57 | 58 | // Assert 59 | Assert.Equal(supportSqlNotifications, sqlDependencyAdded); 60 | } 61 | 62 | [Theory] 63 | [InlineData(1, null, null)] 64 | [InlineData(5, null, null)] 65 | [InlineData(10, null, null)] 66 | [InlineData(1, 5, 10)] 67 | public void DoesRetryLoopConfiguredNumberOfTimes(int? length1, int? length2, int? length3) 68 | { 69 | // Arrange 70 | var retryLoopCount = 0; 71 | var mre = new ManualResetEventSlim(); 72 | var retryLoopArgs = new List(new[] { length1, length2, length3 }).Where(l => l.HasValue); 73 | var retryLoopTotal = retryLoopArgs.Sum().Value; 74 | var retryLoopDelays = new List>(retryLoopArgs.Select(l => new Tuple(0, l.Value))); 75 | var sqlDependencyCreated = false; 76 | var dbProviderFactory = new MockDbProviderFactory(); 77 | var dbBehavior = new Mock(); 78 | var logger = new Mock(); 79 | dbBehavior.Setup(db => db.UpdateLoopRetryDelays).Returns(retryLoopDelays); 80 | dbBehavior.Setup(db => db.StartSqlDependencyListener()).Returns(true); 81 | dbBehavior.Setup(db => db.AddSqlDependency(It.IsAny(), It.IsAny>())) 82 | .Callback(() => 83 | { 84 | sqlDependencyCreated = true; 85 | mre.Set(); 86 | }); 87 | var operation = new ObservableDbOperation("test", "test", logger.Object, dbProviderFactory, dbBehavior.Object); 88 | operation.Faulted += _ => mre.Set(); 89 | operation.Queried += () => 90 | { 91 | if (!sqlDependencyCreated) 92 | { 93 | // Only update the loop count if the SQL dependency hasn't been created yet (we're still in the loop) 94 | retryLoopCount++; 95 | } 96 | if (retryLoopCount == retryLoopTotal) 97 | { 98 | mre.Set(); 99 | } 100 | }; 101 | 102 | // Act 103 | ThreadPool.QueueUserWorkItem(_ => operation.ExecuteReaderWithUpdates((record, o) => { })); 104 | mre.Wait(); 105 | operation.Dispose(); 106 | 107 | // Assert 108 | Assert.Equal(retryLoopTotal, retryLoopCount); 109 | } 110 | 111 | [Fact] 112 | public void CallsOnErrorOnException() 113 | { 114 | // Arrange 115 | var mre = new ManualResetEventSlim(false); 116 | var onErrorCalled = false; 117 | var dbProviderFactory = new MockDbProviderFactory(); 118 | var dbBehavior = new Mock(); 119 | var logger = new Mock(); 120 | dbBehavior.Setup(db => db.UpdateLoopRetryDelays).Returns(_defaultRetryDelays); 121 | dbBehavior.Setup(db => db.StartSqlDependencyListener()).Returns(false); 122 | dbProviderFactory.MockDataReader.Setup(r => r.Read()).Throws(new ApplicationException("test")); 123 | var operation = new ObservableDbOperation("test", "test", logger.Object, dbProviderFactory, dbBehavior.Object); 124 | operation.Faulted += _ => 125 | { 126 | onErrorCalled = true; 127 | mre.Set(); 128 | }; 129 | 130 | // Act 131 | ThreadPool.QueueUserWorkItem(_ => operation.ExecuteReaderWithUpdates((record, o) => { })); 132 | mre.Wait(); 133 | operation.Dispose(); 134 | 135 | // Assert 136 | Assert.True(onErrorCalled); 137 | } 138 | 139 | [Fact] 140 | public void ExecuteReaderSetsNotificationStateCorrectlyUpToAwaitingNotification() 141 | { 142 | // Arrange 143 | var retryLoopDelays = new[] { Tuple.Create(0, 1) }; 144 | var dbProviderFactory = new MockDbProviderFactory(); 145 | var dbBehavior = new Mock(); 146 | var logger = new Mock(); 147 | dbBehavior.Setup(db => db.UpdateLoopRetryDelays).Returns(retryLoopDelays); 148 | dbBehavior.Setup(db => db.StartSqlDependencyListener()).Returns(true); 149 | dbBehavior.Setup(db => db.AddSqlDependency(It.IsAny(), It.IsAny>())); 150 | var operation = new ObservableDbOperation("test", "test", logger.Object, dbProviderFactory, dbBehavior.Object); 151 | operation.Queried += () => 152 | { 153 | // Currently in the query loop 154 | Assert.Equal(ObservableDbOperation.NotificationState.ProcessingUpdates, operation.CurrentNotificationState); 155 | }; 156 | 157 | // Act 158 | operation.ExecuteReaderWithUpdates((_, __) => { }); 159 | 160 | // Assert 161 | Assert.Equal(ObservableDbOperation.NotificationState.AwaitingNotification, operation.CurrentNotificationState); 162 | 163 | operation.Dispose(); 164 | } 165 | 166 | [Fact] 167 | public void ExecuteReaderSetsNotificationStateCorrectlyWhenRecordsReceivedWhileSettingUpSqlDependency() 168 | { 169 | // Arrange 170 | var mre = new ManualResetEventSlim(false); 171 | var retryLoopDelays = new[] { Tuple.Create(0, 1) }; 172 | var dbProviderFactory = new MockDbProviderFactory(); 173 | var readCount = 0; 174 | var sqlDependencyAddedCount = 0; 175 | dbProviderFactory.MockDataReader.Setup(r => r.Read()).Returns(() => ++readCount == 2 && sqlDependencyAddedCount == 1); 176 | var dbBehavior = new Mock(); 177 | var logger = new Mock(); 178 | dbBehavior.Setup(db => db.UpdateLoopRetryDelays).Returns(retryLoopDelays); 179 | dbBehavior.Setup(db => db.StartSqlDependencyListener()).Returns(true); 180 | dbBehavior.Setup(db => db.AddSqlDependency(It.IsAny(), It.IsAny>())).Callback(() => sqlDependencyAddedCount++); 181 | var operation = new ObservableDbOperation("test", "test", logger.Object, dbProviderFactory, dbBehavior.Object); 182 | long? stateOnLoopRestart = null; 183 | var queriedCount = 0; 184 | operation.Queried += () => 185 | { 186 | queriedCount++; 187 | 188 | if (queriedCount == 3) 189 | { 190 | // First query after the loop starts again, check the state is reset 191 | stateOnLoopRestart = operation.CurrentNotificationState; 192 | mre.Set(); 193 | } 194 | }; 195 | 196 | // Act 197 | ThreadPool.QueueUserWorkItem(_ => operation.ExecuteReaderWithUpdates((__, ___) => { })); 198 | 199 | mre.Wait(); 200 | 201 | Assert.True(stateOnLoopRestart.HasValue); 202 | Assert.Equal(ObservableDbOperation.NotificationState.ProcessingUpdates, stateOnLoopRestart.Value); 203 | 204 | operation.Dispose(); 205 | } 206 | 207 | [Fact] 208 | public void ExecuteReaderSetsNotificationStateCorrectlyWhenNotificationReceivedBeforeChangingStateToAwaitingNotification() 209 | { 210 | // Arrange 211 | var mre = new ManualResetEventSlim(false); 212 | var retryLoopDelays = new[] { Tuple.Create(0, 1) }; 213 | var dbProviderFactory = new MockDbProviderFactory(); 214 | var sqlDependencyAdded = false; 215 | var dbBehavior = new Mock(); 216 | var logger = new Mock(); 217 | dbBehavior.Setup(db => db.UpdateLoopRetryDelays).Returns(retryLoopDelays); 218 | dbBehavior.Setup(db => db.StartSqlDependencyListener()).Returns(true); 219 | dbBehavior.Setup(db => db.AddSqlDependency(It.IsAny(), It.IsAny>())) 220 | .Callback(() => sqlDependencyAdded = true); 221 | var operation = new ObservableDbOperation("test", "test", logger.Object, dbProviderFactory, dbBehavior.Object); 222 | dbProviderFactory.MockDataReader.Setup(r => r.Read()).Returns(() => 223 | { 224 | if (sqlDependencyAdded) 225 | { 226 | // Fake the SQL dependency firing while we're setting it up 227 | operation.CurrentNotificationState = ObservableDbOperation.NotificationState.NotificationReceived; 228 | sqlDependencyAdded = false; 229 | } 230 | return false; 231 | }); 232 | long? stateOnLoopRestart = null; 233 | var queriedCount = 0; 234 | operation.Queried += () => 235 | { 236 | queriedCount++; 237 | 238 | if (queriedCount == 3) 239 | { 240 | // First query after the loop starts again, capture the state 241 | stateOnLoopRestart = operation.CurrentNotificationState; 242 | mre.Set(); 243 | } 244 | }; 245 | 246 | // Act 247 | ThreadPool.QueueUserWorkItem(_ => operation.ExecuteReaderWithUpdates((__, ___) => { })); 248 | 249 | mre.Wait(); 250 | 251 | Assert.True(stateOnLoopRestart.HasValue); 252 | Assert.Equal(ObservableDbOperation.NotificationState.ProcessingUpdates, stateOnLoopRestart.Value); 253 | 254 | operation.Dispose(); 255 | } 256 | 257 | private class MockDbProviderFactory : IDbProviderFactory 258 | { 259 | public MockDbProviderFactory() 260 | { 261 | MockDbConnection = new Mock(); 262 | MockDbCommand = new Mock(); 263 | MockDataReader = new Mock(); 264 | 265 | MockDbConnection.Setup(c => c.CreateCommand()).Returns(MockDbCommand.Object); 266 | MockDbCommand.SetupAllProperties(); 267 | MockDbCommand.Setup(cmd => cmd.ExecuteReader()).Returns(MockDataReader.Object); 268 | } 269 | 270 | public Mock MockDbConnection { get; private set; } 271 | public Mock MockDbCommand { get; private set; } 272 | public Mock MockDataReader { get; private set; } 273 | 274 | public IDbConnection CreateConnection() 275 | { 276 | return MockDbConnection.Object; 277 | } 278 | 279 | public virtual DbParameter CreateParameter() 280 | { 281 | return new Mock().SetupAllProperties().Object; 282 | } 283 | } 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.SignalR.SqlServer.Tests/SqlScaleoutOptionsFacts.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 Xunit; 6 | 7 | namespace Microsoft.AspNetCore.SignalR.SqlServer.Tests 8 | { 9 | public class SqlScaleoutConfigurationFacts 10 | { 11 | [Theory] 12 | [InlineData(null, false)] 13 | [InlineData("", false)] 14 | [InlineData("dummy", true)] 15 | public void ConnectionStringValidated(string connectionString, bool isValid) 16 | { 17 | var config = new SqlScaleoutOptions(); 18 | if (isValid) 19 | { 20 | config.ConnectionString = connectionString; 21 | } 22 | else 23 | { 24 | Assert.Throws(typeof(ArgumentNullException), () => config.ConnectionString = connectionString); 25 | } 26 | } 27 | 28 | [Theory] 29 | [InlineData(-1, false)] 30 | [InlineData(0, false)] 31 | [InlineData(1, true)] 32 | [InlineData(10, true)] 33 | public void TableCountValidated(int tableCount, bool isValid) 34 | { 35 | var config = new SqlScaleoutOptions(); 36 | 37 | if (isValid) 38 | { 39 | config.ConnectionString = "dummy"; 40 | config.TableCount = tableCount; 41 | } 42 | else 43 | { 44 | Assert.Throws(typeof(ArgumentOutOfRangeException), () => config.TableCount = tableCount); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.SignalR.SqlServer.Tests/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "Microsoft.AspNetCore.SignalR.SqlServer": "0.2.0-*", 4 | "Moq": "4.6.36-*", 5 | "NETStandard.Library": "1.6.1-*", 6 | "xunit": "2.2.0-*" 7 | }, 8 | "buildOptions": { 9 | "allowUnsafe": true, 10 | "keyFile": "../../tools/Key.snk", 11 | "warningsAsErrors": true 12 | }, 13 | "frameworks": { 14 | "net451": { 15 | "frameworkAssemblies": { 16 | "System.Threading.Tasks": "" 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /tools/Key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspnet/SignalR-SqlServer/d40384710ef24de455c54eb94d5c8c145170bb72/tools/Key.snk --------------------------------------------------------------------------------