├── .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