diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d79ffba337..8030c82207 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,8 +14,8 @@ High quality descriptions will lead to a smoother review experience. ## Issues -Link to any relevant issues, bugs, or discussions (e.g., "Closes \#123", "Fixes -issue \#456"). +Link to any relevant issues, bugs, or discussions (e.g., `Closes #123`, `Fixes +issue #456`). ## Testing diff --git a/BUILDGUIDE.md b/BUILDGUIDE.md index aa334124b3..efeb747cad 100644 --- a/BUILDGUIDE.md +++ b/BUILDGUIDE.md @@ -22,12 +22,12 @@ Once the environment is setup properly, execute the desired set of commands belo |`BuildNetCore`|Builds the .NET driver for all target frameworks.| |`BuildNetCoreAllOS`|Builds the .NET driver for all target frameworks and operating systems.| |`BuildNetFx`|Builds the .NET Framework driver for all target frameworks.| -|`BuildTests`|Builds tests for the .NET and .NET Framework drivers.| |`BuildTestsNetCore`|Builds tests for the .NET driver.| |`BuildTestsNetFx`|Builds tests for the .NET Framework driver.| |`Clean`|Cleans generated files.| |`Restore`|Restores Nuget packages.| -|`RunTests`|Runs the functional and manual tests for the .NET Framework and .NET drivers| +|`RunTests`|Runs the unit, functional, and manual tests for the .NET Framework and .NET drivers| +|`RunUnitTests`|Runs just the unit tests for the .NET Framework and .NET drivers| |`RunFunctionalTests`|Runs just the functional tests for the .NET Framework and .NET drivers| |`RunManualTests`|Runs just the manual tests for the .NET Framework and .NET drivers| |`BuildAkv`|Builds the Azure Key Vault Provider package for all supported platforms.| @@ -51,39 +51,35 @@ Using the default configuration and running all tests: ```bash msbuild -msbuild -t:BuildTests +msbuild -t:BuildTestsNetFx -p:TF=net462 +msbuild -t:BuildTestsNetCore msbuild -t:RunTests ``` -Targeting .NET Framework (or any specific supported version): - -```bash -msbuild -p:TF=net462 -msbuild -t:BuildTests -p:TF=net462 -msbuild -t:RunTests -p:TF=net462 -``` - Using the Release configuration: ```bash msbuild -p:configuration=Release -msbuild -t:BuildTests -p:configuration=Release +msbuild -t:BuildTestsNetFx -p:TF=net462 -p:configuration=Release +msbuild -t:BuildTestsNetCore -p:configuration=Release msbuild -t:RunTests -p:configuration=Release ``` -Running only the functional tests: +Running only the unit tests: ```bash msbuild -msbuild -t:BuildTests -msbuild -t:RunFunctionalTests +msbuild -t:BuildTestsNetFx -p:TF=net462 +msbuild -t:BuildTestsNetCore +msbuild -t:RunUnitTests ``` Using a specific dotnet version/architecture: ```bash msbuild -p:configuration=Release -msbuild -t:BuildTests -p:configuration=Release +msbuild -t:BuildTestsNetFx -p:TF=net462 -p:configuration=Release +msbuild -t:BuildTestsNetCore -p:configuration=Release msbuild -t:RunTests -p:configuration=Release -p:DotnetPath=C:\net8-win-x86\ ``` @@ -224,10 +220,10 @@ msbuild -t:BuildTestsNetCore -p:ReferenceType=Package For .NET Framework, below reference types are supported: ```bash -msbuild -t:BuildTestsNetFx -p:ReferenceType=Project +msbuild -t:BuildTestsNetFx -p:TF=net462 -p:ReferenceType=Project # Default setting uses Project Reference. -msbuild -t:BuildTestsNetFx -p:ReferenceType=Package +msbuild -t:BuildTestsNetFx -p:TF=net462 -p:ReferenceType=Package ``` ### Running Tests with Reference Type @@ -245,12 +241,12 @@ Tests can be built and run with custom Target Frameworks. See the below examples ### Building Tests with custom target framework ```bash -msbuild -t:BuildTestsNetFx -p:TargetNetFxVersion=net462 +msbuild -t:BuildTestsNetFx -p:TF=net462 # Build the tests for custom .NET Framework target ``` ```bash -msbuild -t:BuildTestsNetCore -p:TargetNetCoreVersion=net8.0 +msbuild -t:BuildTestsNetCore -p:TF=net8.0 # Build the tests for custom .NET target ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index e9e3474a18..ddcb3e38bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,89 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) -# Release Notes +## [Stable Release 6.1.1] - 2025-08-14 + +This update includes the following changes since the [6.1.0](6.1.0.md) release: + +### Fixed + +- Reverted changes related to improving partial packet detection, fixup, and replay functionality. This revert addresses regressions introduced in 6.1.0. ([#3556](https://github.com/dotnet/SqlClient/pull/3556)) +- Applied reference assembly corrections supporting vector, fixed JSON tests, and ensured related tests are enabled. [#3562](https://github.com/dotnet/SqlClient/pull/3562) +- Fixed `SqlVector.Null` API signature in Reference assembly. [#3521](https://github.com/dotnet/SqlClient/pull/3521) + +### Changed + +- Upgraded `Azure.Identity` and other dependencies to newer versions. ([#3538](https://github.com/dotnet/SqlClient/pull/3538)) ([#3552](https://github.com/dotnet/SqlClient/pull/3552)) + +## [Stable Release 6.1.0] - 2025-07-25 + +This update brings the following changes since the +[6.1.0-preview2](release-notes/6.1/6.1.0-preview2.md) release: + +### Added + +No new features were added. + +### Fixed + +- Fixed missing socket error codes on non-Windows platforms. + ([#3475](https://github.com/dotnet/SqlClient/pull/3475)) +- Fixed primary/secondary server SPN handling during SSPI negotiation. + ([#3478](https://github.com/dotnet/SqlClient/pull/3478)) +- Fixed AzureKeyVaultProvider package key caching to serialize Azure key fetch + operations. + ([#3477](https://github.com/dotnet/SqlClient/pull/3477)) +- Fixed a rare error related to multi-packet async text reads. + ([#3474](https://github.com/dotnet/SqlClient/pull/3474)) +- Fixed some spelling errors in the API docs. + ([#3500](https://github.com/dotnet/SqlClient/pull/3500)) +- Fixed a rare multi-packet string corruption bug. + ([#3513](https://github.com/dotnet/SqlClient/pull/3513)) + +### Changed + +#### SqlDecimal type workarounds conversions + +*What Changed:* + +- Changed how SqlDecimal type workarounds perform conversions to meet + compliance policies. + ([#3467](https://github.com/dotnet/SqlClient/pull/3467)) + +*Who Benefits:* + +- Microsoft products must not use undocumented APIs on other Microsoft products. + This change removes calls to undocumented APIs and replaces them with + compliant API use. + +*Impact:* + +- These changes impose an observed 5% decrease in performance on .NET Framework. + +#### SqlVector API improvements + +*What Changed:* + +- Several changes were made to the SqlVector API published in the + [6.1.0-preview2](release-notes/6.1/6.1.0-preview2.md) release + ([#3472](https://github.com/dotnet/SqlClient/pull/3472)): + - The SqlVector class was changed to a readonly struct. + - The null value constructor was changed to a static `CreateNull()` method. + - The `Size` property was removed. + +*Who Benefits:* + +- SqlVector instances gain the efficiencies of struct handling. + +*Impact:* + +- Early-adopter applications may require updates if they rely on the old APIs + and any class-specific behaviour. ## [Preview Release 6.1.0-preview2.25178.5] - 2025-06-27 -This update brings the following changes since the [6.1.0-preview1](release-notes/6.1/6.1.0-preview1.md) release: +This update brings the following changes since the +[6.1.0-preview1](release-notes/6.1/6.1.0-preview1.md) release: ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf3bbd952f..7761fe2f99 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ General contribution guidance is included in this document. Additional guidance is defined in the documents linked below. -- [Copyright](copyright.md) describes the licensing practices for the project. +- [Copyright](COPYRIGHT.md) describes the licensing practices for the project. - [Contribution Workflow](contributing-workflow.md) describes the workflow that the team uses for considering and accepting changes. ## Up for Grabs diff --git a/RunPackageReferenceTests.cmd b/RunPackageReferenceTests.cmd deleted file mode 100644 index 4813c182ce..0000000000 --- a/RunPackageReferenceTests.cmd +++ /dev/null @@ -1,65 +0,0 @@ -@echo off - -:: .NET CORE TEST CASES -echo Building .NET Core Tests -call :pauseOnError msbuild -t:Clean - -:: ************** IMPORTANT NOTE BEFORE PROCEEDING WITH "PACKAGE" REFERENCE TYPE *************** -:: THESE ARE NOT STAND ALONE TEST COMMANDS AND NEED A DEVELOPER'S SPECIAL ATTENTION TO WORK. ATTEMPTING TO RUN THE ENTIRE SET OF COMMANDS AS-IS IS LIKELY TO FAIL! - -:: CREATE A NUGET PACKAGE WITH BELOW COMMAND AND ADD TO LOCAL FOLDER + UPDATE NUGET CONFIG FILE TO READ FROM THAT LOCATION -:: > MSBuild -p:Configuration=Release - -:: Based on `dotnet test` documentation, the `Platform` property has no effect on choosing the underlying architecture for the test execution environment. -:: You need to install and run the `dotnet` command for a specific architecture (x64, x86, Arm64). - -:: REFERENCE TYPE "PACKAGE" -call :pauseOnError msbuild -p:Configuration="Release" -t:BuildAKVNetFx -p:ReferenceType=Package -call :pauseOnError msbuild -p:Configuration="Release" -t:BuildAKVNetCore -p:ReferenceType=Package -call :pauseOnError msbuild -p:Configuration="Release" -t:BuildAKVNetSt -p:ReferenceType=Package - -:: .NET - REFERENCE TYPE "PACKAGE" -call :pauseOnError msbuild -p:Configuration="Release" -t:BuildTestsNetCore -p:ReferenceType=Package -p:TargetNetCoreVersion=net8.0 -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.Tests.csproj" -p:Configuration="Release" -p:TestTargetOS="Windowsnetcoreapp" --no-build -v n --filter "category!=nonnetcoreapptests&category!=failing&category!=nonwindowstests" -p:Platform="AnyCPU" -p:TargetNetCoreVersion=net8.0 -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net8.0-functional-anycpu.xml -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj" -p:Configuration="Release" -p:TestTargetOS="Windowsnetcoreapp" --no-build -v n --filter "category!=nonnetcoreapptests&category!=failing&category!=nonwindowstests" -p:Platform="AnyCPU" -p:TargetNetCoreVersion=net8.0 -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net8.0-manual-anycpu.xml - -call :pauseOnError msbuild -p:Configuration="Release" -t:BuildTestsNetCore -p:ReferenceType=Package -p:Platform=x64 -p:TargetNetCoreVersion=net8.0 -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.Tests.csproj" -p:Configuration="Release" -p:TestTargetOS="Windowsnetcoreapp" --no-build -v n --filter "category!=nonnetcoreapptests&category!=failing&category!=nonwindowstests" -p:Platform="x64" -p:TargetNetCoreVersion=net8.0 -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net8.0-functional-x64.xml -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj" -p:Configuration="Release" -p:TestTargetOS="Windowsnetcoreapp" --no-build -v n --filter "category!=nonnetcoreapptests&category!=failing&category!=nonwindowstests" -p:Platform="x64" -p:TargetNetCoreVersion=net8.0 -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net8.0-manual-x64.xml - -call :pauseOnError msbuild -p:Configuration="Release" -t:BuildTestsNetCore -p:ReferenceType=Package -p:Platform=Win32 -p:TargetNetCoreVersion=net8.0 -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.Tests.csproj" -p:Configuration="Release" -p:TestTargetOS="Windowsnetcoreapp" --no-build -v n --filter "category!=nonnetcoreapptests&category!=failing&category!=nonwindowstests" -p:Platform="Win32" -p:TargetNetCoreVersion=net8.0 -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net8.0-functional-win32.xml -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj" -p:Configuration="Release" -p:TestTargetOS="Windowsnetcoreapp" --no-build -v n --filter "category!=nonnetcoreapptests&category!=failing&category!=nonwindowstests" -p:Platform="Win32" -p:TargetNetCoreVersion=net8.0 -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net8.0-manual-win32.xml - -:: .NET Framework - REFERENCE TYPE "PACKAGE" - -call :pauseOnError msbuild -p:Configuration="Release" -t:BuildTestsNetFx -p:ReferenceType=Package -p:Platform=AnyCPU -p:TargetNetFxVersion=net462 -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.Tests.csproj" -p:Platform="AnyCPU" -p:Configuration="Release" -p:TestTargetOS="Windowsnetfx" --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net462-functional-anycpu.xml -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj" -p:Platform="AnyCPU" -p:Configuration="Release" -p:TestTargetOS="Windowsnetfx" --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net462-manual-anycpu.xml - -call :pauseOnError msbuild -p:Configuration="Release" -t:BuildTestsNetFx -p:ReferenceType=Package -p:Platform=AnyCPU -p:TargetNetFxVersion=net48 -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.Tests.csproj" -p:Platform="AnyCPU" -p:Configuration="Release" -p:TestTargetOS="Windowsnetfx" --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net48-functional-anycpu.xml -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj" -p:Platform="AnyCPU" -p:Configuration="Release" -p:TestTargetOS="Windowsnetfx" --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net48-manual-anycpu.xml - -call :pauseOnError msbuild -p:Configuration="Release" -t:BuildTestsNetFx -p:ReferenceType=Package -p:Platform=x64 -p:TargetNetFxVersion=net462 -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.Tests.csproj" -p:Platform="x64" -p:Configuration="Release" -p:TestTargetOS="Windowsnetfx" --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net462-functional-x64.xml -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj" -p:Platform="x64" -p:Configuration="Release" -p:TestTargetOS="Windowsnetfx" --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net462-manual-x64.xml - -call :pauseOnError msbuild -p:Configuration="Release" -t:BuildTestsNetFx -p:TargetNetFxVersion=net48 -p:Platform=x64 -p:ReferenceType=Package -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.Tests.csproj" -p:Platform="x64" -p:Configuration="Release" -p:TestTargetOS="Windowsnetfx" -p:TargetNetFxVersion=net48 --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net48-functional-x64.xml -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj" -p:Platform="x64" -p:Configuration="Release" -p:TestTargetOS="Windowsnetfx" -p:TargetNetFxVersion=net48 --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net48-manual-x64.xml - -call :pauseOnError msbuild -p:Configuration="Release" -t:BuildTestsNetFx -p:ReferenceType=Package -p:Platform=Win32 -p:TargetNetFxVersion=net462 -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.Tests.csproj" -p:Platform="Win32" -p:Configuration="Release" -p:TestTargetOS="Windowsnetfx" --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net462-functional-win32.xml -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj" -p:Platform="Win32" -p:Configuration="Release" -p:TestTargetOS="Windowsnetfx" --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net462-manual-win32.xml - -call :pauseOnError msbuild -p:Configuration="Release" -t:BuildTestsNetFx -p:TargetNetFxVersion=net48 -p:Platform=Win32 -p:ReferenceType=Package -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.Tests.csproj" -p:Platform="Win32" -p:Configuration="Release" -p:TestTargetOS="Windowsnetfx" -p:TargetNetFxVersion=net48 --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net48-functional-Win32.xml -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj" -p:Platform="Win32" -p:Configuration="Release" -p:TestTargetOS="Windowsnetfx" -p:TargetNetFxVersion=net48 --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" -p:ReferenceType=Package -l:trx;LogFileName=..\..\..\..\..\artifacts\Results\package-net48-manual-Win32.xml - -goto :eof - -:pauseOnError -%* -if ERRORLEVEL 1 pause -goto :eof diff --git a/RunProjectReferenceTests.cmd b/RunProjectReferenceTests.cmd deleted file mode 100644 index 2e37d0b712..0000000000 --- a/RunProjectReferenceTests.cmd +++ /dev/null @@ -1,54 +0,0 @@ -@echo off - -:: Default target frameworks -set netfxVersion=net462 -set netcoreVersion=net8.0 - -:: Accept two parameters for .NET Framework and .NET versions. -:: Examples: -:: - uses net48 and net8.0: -:: > RunProjectReferenceTests.cmd net48 net8.0 -:: - uses default target frameworks: -:: > RunProjectReferenceTests.cmd -:: - uses net48 and default target frameworks for netcore: -:: > RunProjectReferenceTests.cmd net48 - -if not "%~1" == "" set netfxVersion=%1 -if not "%~2" == "" set netcoreVersion=%2 - -echo .NET Framework = %netfxVersion% -echo .NET Core = %netcoreVersion% - -call :pauseOnError msbuild -t:Clean -:: .NET FRAMEWORK - REFERENCE TYPE "PROJECT" -:: Only Builds AnyCPU for project reference! - -:: Based on `dotnet test` documentation, the `Platform` property has no effect on choosing the underlying architecture for the test execution environment. -:: You need to install and run the `dotnet` command for a specific architecture (x64, x86, Arm64). - -echo Building .NET Framework %netfxVersion% Tests ... -call :pauseOnError msbuild -p:Configuration="Release" -call :pauseOnError msbuild -p:Configuration="Release" -t:BuildAKVNetFx -call :pauseOnError msbuild -p:Configuration="Release" -t:BuildTestsNetFx -p:TargetNetFxVersion=%netfxVersion% - -echo Running .NET Framework %netfxVersion% Tests ... -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.Tests.csproj" -p:Configuration="Release" -p:TestTargetOS="Windowsnetfx" -p:TargetNetFxVersion=%netfxVersion% --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" -l:"trx;LogFileName=..\..\..\..\..\artifacts\Results\project-%netfxVersion%-functional-anycpu.xml" -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj" -p:Configuration="Release" -p:TestTargetOS="Windowsnetfx" -p:TargetNetFxVersion=%netfxVersion% --no-build -v n --filter "category!=nonnetfxtests&category!=failing&category!=nonwindowstests" -l:"trx;LogFileName=..\..\..\..\..\artifacts\Results\project-%netfxVersion%-manual-anycpu.xml" - -echo Building .NET %netcoreVersion% Tests ... -call pause -call :pauseOnError msbuild -t:Clean -call :pauseOnError msbuild -p:Configuration="Release" -call :pauseOnError msbuild -p:Configuration="Release" -t:BuildAKVNetCoreAllOS -call :pauseOnError msbuild -p:Configuration="Release" -t:BuildTestsNetCore -p:TargetNetCoreVersion=%netcoreVersion% - -echo Running .NET %netcoreVersion% Tests ... -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\FunctionalTests\Microsoft.Data.SqlClient.Tests.csproj" -p:Configuration="Release" -p:TestTargetOS="Windowsnetcoreapp" -p:TargetNetCoreVersion=%netcoreVersion% --no-build -v n --filter "category!=nonnetcoreapptests&category!=failing&category!=nonwindowstests" -l:"trx;LogFileName=..\..\..\..\..\artifacts\Results\project-%netcoreVersion%-functional-anycpu.xml" -call :pauseOnError dotnet test "src\Microsoft.Data.SqlClient\tests\ManualTests\Microsoft.Data.SqlClient.ManualTesting.Tests.csproj" -p:Configuration="Release" -p:TestTargetOS="Windowsnetcoreapp" -p:TargetNetCoreVersion=%netcoreVersion% --no-build -v n --filter "category!=nonnetcoreapptests&category!=failing&category!=nonwindowstests" -l:"trx;LogFileName=..\..\..\..\..\artifacts\Results\project-%netcoreVersion%-manual-anycpu.xml" - -goto :eof - -:pauseOnError -%* -if ERRORLEVEL 1 pause -goto :eof diff --git a/RunTests.sh b/RunTests.sh deleted file mode 100644 index ed3f22a9a1..0000000000 --- a/RunTests.sh +++ /dev/null @@ -1,8 +0,0 @@ -dotnet msbuild -p:Configuration="Release" -t:Clean,BuildAll -p:GenerateDocumentationFile=false -echo Building Add-Ons -dotnet msbuild -p:Configuration="Release" -t:BuildAKVNetCore -p:OSGroup=Unix -p:Platform=AnyCPU -echo Building tests -dotnet msbuild -p:Configuration="Release" -t:BuildTestsNetCore -p:OSGroup=Unix -p:Platform=AnyCPU -echo Running SqlClient test suite -dotnet test "src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj" -p:Platform="AnyCPU" -p:Configuration="Release" -p:TestTargetOS="Unixnetcoreapp" --no-build -v n --filter "category!=nonnetcoreapptests&category!=failing&category!=nonlinuxtests&category!=nonuaptests" -dotnet test "src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj" -p:Platform="AnyCPU" -p:Configuration="Release" -p:TestTargetOS="Unixnetcoreapp" --no-build -v n --filter "category!=nonnetcoreapptests&category!=failing&category!=nonlinuxtests&category!=nonuaptests" diff --git a/build.proj b/build.proj index 90f5f0fc37..0443bbcf4e 100644 --- a/build.proj +++ b/build.proj @@ -26,7 +26,7 @@ netcoreapp $(TF) $(TF) - true + true Configuration=$(Configuration);AssemblyVersion=$(SqlServerAssemblyVersion);AssemblyFileVersion=$(SqlServerAssemblyFileVersion);Version=$(SqlServerPackageVersion); Configuration=$(Configuration);AssemblyFileVersion=$(AssemblyFileVersion);TargetsWindows=$(TargetsWindows);TargetsUnix=$(TargetsUnix); BuildProjectReferences=false;$(ProjectProperties);BuildForRelease=false;TargetNetCoreVersion=$(TargetNetCoreVersion);TargetNetFxVersion=$(TargetNetFxVersion) @@ -90,7 +90,6 @@ - @@ -118,7 +117,7 @@ - $(DotNetCmd) dotnet build -c Release -p:ReferenceType=$(ReferenceType) + dotnet build -c Release -p:ReferenceType=$(ReferenceType) diff --git a/buildAddons.cmd b/buildAddons.cmd deleted file mode 100644 index 70d1797ddf..0000000000 --- a/buildAddons.cmd +++ /dev/null @@ -1,12 +0,0 @@ -call :pauseOnError msbuild -p:configuration=Release -t:clean -call :pauseOnError msbuild -p:configuration=Release -t:BuildAllConfigurations -call :pauseOnError msbuild -p:configuration=Release -t:BuildAKVNetFx -call :pauseOnError msbuild -p:configuration=Release -t:BuildAKVNetCoreAllOS -call :pauseOnError msbuild -p:configuration=Release -t:GenerateAKVProviderNugetPackage - -goto :eof - -:pauseOnError -%* -if ERRORLEVEL 1 pause -goto :eof diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlCommand.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlCommand.xml index 78b6fefe5e..a3d87c843d 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlCommand.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlCommand.xml @@ -150,7 +150,7 @@ Next, compile and execute the following: - The following example creates a , passing in the connection string and command text. + The following example creates a , passing in the command text. diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml index 96f72293df..a68c0a323b 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionStringBuilder.xml @@ -361,7 +361,7 @@ The following example supplies a simple SQL Server connection string in the connection string. > [!NOTE] -> Since version 5.x the default value for none Azure endpoints is 1 and for Azure SQL and Azure Synapse has increased to 2 and 5 to imporve the recovery against on high demand Azure endpoints. It should be detected first, and Synapse could be detected as a regular Azure SQL DB endpoint. +> Since version 5.x the default value for none Azure endpoints is 1 and for Azure SQL and Azure Synapse has increased to 2 and 5 to improve the recovery against on high demand Azure endpoints. It should be detected first, and Synapse could be detected as a regular Azure SQL DB endpoint. ]]> @@ -539,7 +539,7 @@ This property corresponds to the "Connect Retry Count" key within the - This property corresponds to the "Data Source", "server", "address", "addr", and "network address" keys within the connection string. Regardless of which of these values has been supplied within the supplied connection string, the connection string created by the SqlConnectionStringBuilder will use the well-known "Data Source" key. The port number can be specified after the server name: server=tcp:servername,portnumber . + This property corresponds to the "Data Source", "server", "address", "addr", and "network address" keys within the connection string. Regardless of which of these values has been supplied within the supplied connection string, the connection string created by the SqlConnectionStringBuilder will use the well-known "Data Source" key. The port number can be specified after the server name: server=tcp:servername,port . When specifying a local instance, always use (local). To force a protocol, add one of the following prefixes: np:(local), tcp:(local), lpc:(local) . @@ -664,7 +664,7 @@ When `TrustServerCertificate` is false and `Encrypt` is - Gets or sets a Boolean value that indicates whether the SQL Server connection pooler automatically enlists the connection in the creation thread's current transaction context. + Gets or sets a Boolean value that indicates whether the SQL Server connection pool automatically enlists the connection in the creation thread's current transaction context. The value of the property, or if none has been supplied. @@ -727,7 +727,7 @@ This property corresponds to the "FailoverPartnerSPN" and "Failover Partner SPN" [!NOTE] > This property only applies when using `Encrypt` in or mode, otherwise it is ignored. diff --git a/eng/pipelines/variables/akv-official-variables.yml b/eng/pipelines/variables/akv-official-variables.yml index 4fa517341d..aaf8de7c5e 100644 --- a/eng/pipelines/variables/akv-official-variables.yml +++ b/eng/pipelines/variables/akv-official-variables.yml @@ -22,15 +22,15 @@ variables: # Base Variables ------------------------------------------------------- - name: mdsPackageVersion - value: '6.0.1' + value: '6.1.1' # @TODO: Version should ideally be pulled from one location (versions.props?) - name: versionMajor value: '6' - name: versionMinor - value: '0' + value: '1' - name: versionPatch - value: '0' + value: '1' - name: versionPreview value: '-preview1' diff --git a/override.ps1 b/override.ps1 deleted file mode 100644 index 4c1a37983f..0000000000 --- a/override.ps1 +++ /dev/null @@ -1,43 +0,0 @@ -#Purpose: Updates SNI Version -Write-Host "SNI Version to test = 123" - -##Get the shared SNI Version from the downloaded artifact -#$SharedSNIVersion = Get-Content -path "$(Pipeline.Workspace)/SharedSNIVersion.txt" - -#Get the SNI Version to test from the user entered version. -$SharedSNIVersion = "123" - -# define file to update -$PropsPath = 'C:\Users\mdaigle\SqlClient\tools\props\Versions.props' -type $PropsPath - -# new version number to update to -##Write-Host "SNI Version to test = $(SNIValidationVersion)" -Write-Host "SNI Version to test = $SharedSNIVersion" - - -# define an xml object -$xml = New-Object XML - -# load content of xml from file defined above -$xml.Load($PropsPath) - -# define namespace used to read a node -$nsm = New-Object Xml.XmlNamespaceManager($xml.NameTable) -$nsm.AddNamespace('ns', $xml.DocumentElement.NamespaceURI) -$netFxSniVersion = $xml.SelectSingleNode('//ns:MicrosoftDataSqlClientSniVersion', $nsm) - -Write-Host "Node NetFx SNI Version = $($netFxSniVersion.InnerText)" - -# update the node inner text -$netFxSniVersion.InnerText = "$SharedSNIVersion" - -$netCoreSniVersion = $xml.SelectSingleNode('//ns:MicrosoftDataSqlClientSNIRuntimeVersion', $nsm) - -# update the node inner text -$netCoreSniVersion.InnerText = "$SharedSNIVersion" - -# save the xml file -$xml.Save($PropsPath) - -type $PropsPath diff --git a/release-notes/6.1/6.1.0.md b/release-notes/6.1/6.1.0.md new file mode 100644 index 0000000000..2d8d20364b --- /dev/null +++ b/release-notes/6.1/6.1.0.md @@ -0,0 +1,300 @@ +# Release Notes + +## Stable Release 6.1.0 - 2025-07-25 + +This update brings the following changes since the +[6.0.2](../6.0/6.0.2.md) stable release: + +### Added + +#### Added dedicated SQL Server vector datatype support + +*What Changed:* + +- Optimized vector communications between MDS and SQL Server 2025, employing a + custom binary format over the TDS protocol. + ([#3433](https://github.com/dotnet/SqlClient/pull/3433), + [#3443](https://github.com/dotnet/SqlClient/pull/3443)) +- Reduced processing load compared to existing JSON-based vector support. +- Initial support for 32-bit single-precision floating point vectors. + +*Who Benefits:* + +- Applications moving large vector data sets will see beneficial improvements + to processing times and memory requirements. +- Vector-specific APIs are ready to support future numeric representations with + a consistent look-and-feel. + +*Impact:* + +- Reduced transmission and processing times for vector operations versus JSON + using SQL Server 2025 preview: + - Reads: 50x improvement + - Writes: 3.3x improvement + - Bulk Copy: 19x improvement + - (Observed with vector column of max 1998 size, and 10,000 records for each + operation.) +- Improved memory footprint due to the elimination of JSON + serialization/deserialization and string representation bloat. +- For backwards compatibility with earlier SQL Server Vector implementations, + applications may continue to use JSON strings to send/receive vector data, + although they will not see any of the performance improvements noted above. + +#### Revived .NET Standard 2.0 target support + +*What Changed:* + +- Support for targeting .NET Standard 2.0 has returned. + ([#3381](https://github.com/dotnet/SqlClient/pull/3381)) +- Support had previously been removed in the 6.0 release, with the + [community voicing concerns](https://github.com/dotnet/SqlClient/discussions/3115). + +*Who Benefits:* + +- Libraries that depend on MDS may seamlessly target any of the following + frameworks: + - .NET Standard 2.0 + - .NET Framework 4.6.2 and above + - .NET 8.0 + - .NET 9.0 +- Applications should continue to target runtimes. + - The MDS .NET Standard 2.0 target framework support does not include an + actual implementation, and cannot be used with a runtime. + - An application's build/publish process should always pick the appropriate + MDS .NET/.NET Framework runtime implementation. + - Custom build/publish actions that incorrectly try to deploy the MDS .NET + Standard 2.0 reference DLL at runtime are not supported. + +*Impact:* + +- Libraries targeting .NET Standard 2.0 will no longer receive warnings like + this: + - `warning NU1701: Package 'Microsoft.Data.SqlClient 6.0.2' was restored using '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8, .NETFramework,Version=v4.8.1' instead of the project target framework '.NETStandard,Version=v2.0'. This package may not be fully compatible with your project.` + +#### Other Additions + +- Added packet multiplexing support to improve large data read performance. + ([#2714](https://github.com/dotnet/SqlClient/pull/2714), + [#3161](https://github.com/dotnet/SqlClient/pull/3161), + [#3202](https://github.com/dotnet/SqlClient/pull/3202)) +- Added support for special casing with Fabric endpoints. + ([#3084](https://github.com/dotnet/SqlClient/pull/3084)) + +### Fixed + +- Fixed distributed transactions to be preserved during pooled connection resets. + ([#3019](https://github.com/dotnet/SqlClient/pull/3019)) +- Fixed application crash when the `Data Source` parameter begins with a comma. + ([#3250](https://github.com/dotnet/SqlClient/pull/3250)) +- Resolved synonym count discrepancies in debug mode. + ([#3098](https://github.com/dotnet/SqlClient/pull/3098)) +- Addressed warnings for down-level SSL/TLS versions. + ([#3126](https://github.com/dotnet/SqlClient/pull/3126)) +- Fixed missing <NeutralLanguage> property. + ([#3325](https://github.com/dotnet/SqlClient/pull/3325)) +- Fixed injection of UTF-8 BOM during bulk copy. + ([#3399](https://github.com/dotnet/SqlClient/pull/3399)) +- Fixed `SqlCachedBuffer` async read edge case. + ([#3329](https://github.com/dotnet/SqlClient/pull/3329)) +- Fixed `SqlSequentialTextReader` edge case with single-byte reads. + ([#3383](https://github.com/dotnet/SqlClient/pull/3383)) +- Fixed an incorrect error message when parsing connection string `PoolBlockingPeriod`. + ([#3411](https://github.com/dotnet/SqlClient/pull/3411)) +- Added missing `ToString()` override to `SqlJson`. + ([#3427](https://github.com/dotnet/SqlClient/pull/3427)) +- Fixed missing socket error codes on non-Windows platforms. + ([#3475](https://github.com/dotnet/SqlClient/pull/3475)) +- Fixed primary/secondary server SPN handling during SSPI negotiation. + ([#3478](https://github.com/dotnet/SqlClient/pull/3478)) +- Fixed AzureKeyVaultProvider package key caching to serialize Azure key fetch + operations. + ([#3477](https://github.com/dotnet/SqlClient/pull/3477)) +- Fixed a rare error related to multi-packet async text reads. + ([#3474](https://github.com/dotnet/SqlClient/pull/3474)) +- Fixed some spelling errors in the API docs. + ([#3500](https://github.com/dotnet/SqlClient/pull/3500)) +- Fixed a rare multi-packet string corruption bug. + ([#3513](https://github.com/dotnet/SqlClient/pull/3513)) + +### Changed + +#### SqlDecimal type workarounds conversions + +*What Changed:* + +- Changed how SqlDecimal type workarounds perform conversions to meet + compliance policies. + ([#3467](https://github.com/dotnet/SqlClient/pull/3467)) + +*Who Benefits:* + +- Microsoft products must not use undocumented APIs on other Microsoft products. + This change removes calls to undocumented APIs and replaces them with + compliant API use. + +*Impact:* + +- These changes impose an observed 5% decrease in performance on .NET Framework. + +#### SqlVector API improvements (since [6.1.0-preview2](6.1.0-preview2.md)) + +*What Changed:* + +- Several changes were made to the SqlVector API published in the + [6.1.0-preview2](6.1.0-preview2.md) release + ([#3472](https://github.com/dotnet/SqlClient/pull/3472)): + - The SqlVector class was changed to a readonly struct. + - The null value constructor was changed to a static `CreateNull()` method. + - The `Size` property was removed. + +*Who Benefits:* + +- SqlVector instances gain the efficiencies of struct handling. + +*Impact:* + +- Early-adopter applications may require updates if they rely on the old APIs + and any class-specific behaviour. + +#### Other Changes + +- Reduced allocations when opening a connection. + ([#3364](https://github.com/dotnet/SqlClient/pull/3364)) +- Various performance improvements related to TDS parsing. + ([#3337](https://github.com/dotnet/SqlClient/pull/3337), + [#3377](https://github.com/dotnet/SqlClient/pull/3377), + [#3422](https://github.com/dotnet/SqlClient/pull/3422)) +- Improved native AOT support. + ([#3364](https://github.com/dotnet/SqlClient/pull/3364), + [#3369](https://github.com/dotnet/SqlClient/pull/3369), + [#3401](https://github.com/dotnet/SqlClient/pull/3401)) +- Progress towards [SSPI extensibility](https://github.com/dotnet/SqlClient/issues/2253). + ([#2454](https://github.com/dotnet/SqlClient/pull/2454)) +- Progress towards [connection pooling improvements](https://github.com/dotnet/SqlClient/issues/3356). + ([#3352](https://github.com/dotnet/SqlClient/pull/3352), + [#3396](https://github.com/dotnet/SqlClient/pull/3396)) +- Expanded/clarified SqlConnection's + [AccessToken](https://learn.microsoft.com/en-us/dotnet/api/microsoft.data.sqlclient.sqlconnection.accesstoken) and + [AccessTokenCallback](https://learn.microsoft.com/en-us/dotnet/api/microsoft.data.sqlclient.sqlconnection.accesstokencallback) + documentation. + ([#3339](https://github.com/dotnet/SqlClient/pull/3339)) +- Fixed some poorly formatted tables in the API docs. + ([#3391](https://github.com/dotnet/SqlClient/pull/3391)) +- Optimized binary size for AOT. + ([#3091](https://github.com/dotnet/SqlClient/pull/3091)) +- Refined bulk copy operations to handle unmatched column names more effectively. + ([#3205](https://github.com/dotnet/SqlClient/pull/3205)) +- Enhanced `SqlBulkCopy` to explicitly identify mismatched column names. + ([#3183](https://github.com/dotnet/SqlClient/pull/3183)) +- Optimized outgoing SSPI blob handling using `IBufferWriter`. + ([#2452](https://github.com/dotnet/SqlClient/pull/2452)) +- Replaced `byte[]` with `string` for SNI to improve efficiency. + ([#2790](https://github.com/dotnet/SqlClient/pull/2790)) +- Code cleanup to remove SQL 2000 support. + ([#2839](https://github.com/dotnet/SqlClient/pull/2839), + [#3206](https://github.com/dotnet/SqlClient/pull/3206), + [#3217](https://github.com/dotnet/SqlClient/pull/3217)) +- Connection pool design refactor for a modular connection pool design. + ([#3199](https://github.com/dotnet/SqlClient/pull/3199)) +- Added dependency on `System.Text.Json` + [8.0.5](https://www.nuget.org/packages/System.Text.Json/8.0.5) (.NET 8.0) and + [9.0.5](https://www.nuget.org/packages/System.Text.Json/9.0.5) (.NET Standard 2.0, .NET 9.0) + to avoid transitive vulnerabilities ([CVE-2024-43485](https://github.com/advisories/GHSA-8g4q-xg66-9fp4)). + ([#3403](https://github.com/dotnet/SqlClient/pull/3403)) +- Updated various dependencies [#3229](https://github.com/dotnet/SqlClient/pull/3229), primarily: + - System.Text.Encodings.Web to v8.0.0 + - System.Text.Json to v8.0.5 + - Azure.Identity to v1.13.2 + - Microsoft.Identity.Model.Json.Web.Tokens to v7.7.1 + - Microsoft.Identity.Model.Protocols.OpenIdConnect to v7.7.1 +- Code merge towards a unified SqlClient project, aligning .NET Framework and .NET Core implementations. + (Many PRs - see [6.1.0-preview1](6.1.0-preview1.md) and [6.1.0-preview2](6.1.0-preview2.md) for details.) +- Test improvements include a new unit test project, updates to test + dependencies, removal of hardcoded credentials, and improved robustness. + (Many PRs - see [6.1.0-preview1](6.1.0-preview1.md) and [6.1.0-preview2](6.1.0-preview2.md) for details.) + +## Contributors + +We thank the following public contributors. Their efforts toward this project +are very much appreciated. + +- [edwardneal](https://github.com/edwardneal) +- [ErikEJ](https://github.com/ErikEJ) +- [MichelZ](https://github.com/MichelZ) +- [twsouthwick](https://github.com/twsouthwick) +- [Wraith2](https://github.com/Wraith2) + +### New Contributors + +- [BradBarnich](https://github.com/BradBarnich) made their first contribution in + [#3325](https://github.com/dotnet/SqlClient/pull/3325) +- [deusanyjunior](https://github.com/deusanyjunior) made their first contribution in + [#3310](https://github.com/dotnet/SqlClient/pull/3310) +- [emmanuel-ferdman](https://github.com/emmanuel-ferdman) made their first contribution in + [#3041](https://github.com/dotnet/SqlClient/pull/3041) +- [JNjenga](https://github.com/JNjenga) made their first contribution in + [#3183](https://github.com/dotnet/SqlClient/pull/3183) +- [MaceWindu](https://github.com/MaceWindu) made their first contribution in + [#3134](https://github.com/dotnet/SqlClient/pull/3134) +- [Midhunnnk](https://github.com/Midhunnnk) made their first contribution in + [#3250](https://github.com/dotnet/SqlClient/pull/3250) + +## Target Platform Support + +- .NET Framework 4.6.2+ (Windows ARM64, Windows x86, Windows x64) +- .NET 8.0+ (Windows x86, Windows x64, Windows ARM64, Windows ARM, Linux, macOS) + +### Dependencies + +#### .NET Standard 2.0 + +- Azure.Identity 1.13.2 +- Microsoft.Bcl.Cryptography 9.0.4 +- Microsoft.Data.SqlClient.SNI.runtime 6.0.2 +- Microsoft.Extensions.Caching.Memory 9.0.4 +- Microsoft.IdentityModel.JsonWebTokens 7.7.1 +- Microsoft.IdentityModel.Protocols.OpenIdConnect 7.7.1 +- Microsoft.SqlServer.Server 1.0.0 +- System.Configuration.ConfigurationManager 9.0.4 +- System.Security.Cryptography.Pkcs 9.0.4 +- System.Text.Json 9.0.5 + +#### .NET Framework 4.6.2+ + +- Azure.Identity 1.13.2 +- Microsoft.Bcl.Cryptography 8.0.0 +- Microsoft.Data.SqlClient.SNI 6.0.2 +- Microsoft.Extensions.Caching.Memory 8.0.1 +- Microsoft.IdentityModel.JsonWebTokens 7.7.1 +- Microsoft.IdentityModel.Protocols.OpenIdConnect 7.7.1 +- System.Buffers 4.5.1 +- System.Data.Common 4.3.0 +- System.Security.Cryptography.Pkcs 8.0.1 +- System.Text.Encodings.Web 8.0.0 +- System.Text.Json 8.0.5 + +#### .NET 8.0 + +- Azure.Identity 1.13.2 +- Microsoft.Bcl.Cryptography 8.0.0 +- Microsoft.Data.SqlClient.SNI.runtime 6.0.2 +- Microsoft.Extensions.Caching.Memory 8.0.1 +- Microsoft.IdentityModel.JsonWebTokens 7.7.1 +- Microsoft.IdentityModel.Protocols.OpenIdConnect 7.7.1 +- Microsoft.SqlServer.Server 1.0.0 +- System.Configuration.ConfigurationManager 8.0.1 +- System.Security.Cryptography.Pkcs 8.0.1 +- System.Text.Json 8.0.5 + +#### .NET 9.0 + +- Azure.Identity 1.13.2 +- Microsoft.Bcl.Cryptography 9.0.4 +- Microsoft.Data.SqlClient.SNI.runtime 6.0.2 +- Microsoft.Extensions.Caching.Memory 9.0.4 +- Microsoft.IdentityModel.JsonWebTokens 7.7.1 +- Microsoft.IdentityModel.Protocols.OpenIdConnect 7.7.1 +- Microsoft.SqlServer.Server 1.0.0 +- System.Configuration.ConfigurationManager 9.0.4 +- System.Security.Cryptography.Pkcs 9.0.4 +- System.Text.Json 9.0.5 diff --git a/release-notes/6.1/6.1.1.md b/release-notes/6.1/6.1.1.md new file mode 100644 index 0000000000..c9a6bfac90 --- /dev/null +++ b/release-notes/6.1/6.1.1.md @@ -0,0 +1,79 @@ +# Release Notes + +## Stable Release 6.1.1 - 2025-08-14 + +This update includes the following changes since the [6.1.0](6.1.0.md) release: + +### Fixed + +- Reverted changes related to improving partial packet detection, fixup, and replay functionality. This revert addresses regressions introduced in 6.1.0. ([#3556](https://github.com/dotnet/SqlClient/pull/3556)) +- Applied reference assembly corrections supporting vector, fixed JSON tests, and ensured related tests are enabled. [#3562](https://github.com/dotnet/SqlClient/pull/3562) +- Fixed `SqlVector.Null` API signature in Reference assembly. [#3521](https://github.com/dotnet/SqlClient/pull/3521) + +### Changed + +- Upgraded `Azure.Identity` and other dependencies to newer versions. ([#3538](https://github.com/dotnet/SqlClient/pull/3538)) ([#3552](https://github.com/dotnet/SqlClient/pull/3552)) + +## Target Platform Support + +- .NET Framework 4.6.2+ (Windows ARM64, Windows x86, Windows x64) +- .NET 8.0+ (Windows x86, Windows x64, Windows ARM64, Windows ARM, Linux, macOS) + +### Dependencies + +#### .NET Framework 4.6.2+ + +- Azure.Core 1.47.1 +- Azure.Identity 1.14.2 +- Microsoft.Bcl.Cryptography 8.0.0 +- Microsoft.Data.SqlClient.SNI 6.0.2 +- Microsoft.Extensions.Caching.Memory 8.0.1 +- Microsoft.IdentityModel.JsonWebTokens 7.7.1 +- Microsoft.IdentityModel.Protocols.OpenIdConnect 7.7.1 +- System.Buffers 4.5.1 +- System.Data.Common 4.3.0 +- System.Security.Cryptography.Pkcs 8.0.1 +- System.Text.Encodings.Web 8.0.0 +- System.Text.Json 8.0.5 + +#### .NET 8.0 + +- Azure.Core 1.47.1 +- Azure.Identity 1.14.2 +- Microsoft.Bcl.Cryptography 8.0.0 +- Microsoft.Data.SqlClient.SNI.runtime 6.0.2 +- Microsoft.Extensions.Caching.Memory 8.0.1 +- Microsoft.IdentityModel.JsonWebTokens 7.7.1 +- Microsoft.IdentityModel.Protocols.OpenIdConnect 7.7.1 +- Microsoft.SqlServer.Server 1.0.0 +- System.Configuration.ConfigurationManager 8.0.1 +- System.Security.Cryptography.Pkcs 8.0.1 +- System.Text.Json 8.0.5 + +#### .NET 9.0 + +- Azure.Core 1.47.1 +- Azure.Identity 1.14.2 +- Microsoft.Bcl.Cryptography 9.0.4 +- Microsoft.Data.SqlClient.SNI.runtime 6.0.2 +- Microsoft.Extensions.Caching.Memory 9.0.4 +- Microsoft.IdentityModel.JsonWebTokens 7.7.1 +- Microsoft.IdentityModel.Protocols.OpenIdConnect 7.7.1 +- Microsoft.SqlServer.Server 1.0.0 +- System.Configuration.ConfigurationManager 9.0.4 +- System.Security.Cryptography.Pkcs 9.0.4 +- System.Text.Json 9.0.5 + +#### .NET Standard 2.0 + +- Azure.Core 1.47.1 +- Azure.Identity 1.14.2 +- Microsoft.Bcl.Cryptography 9.0.4 +- Microsoft.Data.SqlClient.SNI.runtime 6.0.2 +- Microsoft.Extensions.Caching.Memory 9.0.4 +- Microsoft.IdentityModel.JsonWebTokens 7.7.1 +- Microsoft.IdentityModel.Protocols.OpenIdConnect 7.7.1 +- Microsoft.SqlServer.Server 1.0.0 +- System.Configuration.ConfigurationManager 9.0.4 +- System.Security.Cryptography.Pkcs 9.0.4 +- System.Text.Json 9.0.5 diff --git a/release-notes/6.1/README.md b/release-notes/6.1/README.md index a6e90b1e25..83a106b744 100644 --- a/release-notes/6.1/README.md +++ b/release-notes/6.1/README.md @@ -4,5 +4,7 @@ The following Microsoft.Data.SqlClient 6.1 preview releases have been shipped: | Release Date | Version | Notes | | :-- | :-- | :--: | +| 2025-08-14 | 6.1.1 | [Release Notes](6.1.1.md) | +| 2025-07-25 | 6.1.0 | [Release Notes](6.1.0.md) | | 2025-06-27 | 6.1.0-preview2.25178.5 | [Release Notes](6.1.0-preview2.md) | | 2025-04-30 | 6.1.0-preview1.25120.4 | [Release Notes](6.1.0-preview1.md) | diff --git a/release-notes/README.md b/release-notes/README.md index a9e62ae167..612316204e 100644 --- a/release-notes/README.md +++ b/release-notes/README.md @@ -20,10 +20,13 @@ The latest stable release is [Microsoft.Data.SqlClient 6.0](6.0). # Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider Release Notes -The latest stable release is [Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 5.1](add-ons/AzureKeyVaultProvider/5.1). +The latest stable release is +[Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 6.1](add-ons/AzureKeyVaultProvider/6.1). ## Release Information +- [Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 6.1](add-ons/AzureKeyVaultProvider/6.1) +- [Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 6.0](add-ons/AzureKeyVaultProvider/6.0) - [Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 5.1](add-ons/AzureKeyVaultProvider/5.1) - [Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 3.0](add-ons/AzureKeyVaultProvider/3.0) - [Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 2.0](add-ons/AzureKeyVaultProvider/2.0) diff --git a/release-notes/add-ons/AzureKeyVaultProvider/6.0/6.0.0.md b/release-notes/add-ons/AzureKeyVaultProvider/6.0/6.0.0.md new file mode 100644 index 0000000000..31b4e85bf7 --- /dev/null +++ b/release-notes/add-ons/AzureKeyVaultProvider/6.0/6.0.0.md @@ -0,0 +1,40 @@ +# Release Notes + +## 6.0.0 - 30 January 2025 + +### Changed + +- Removed use of `System.Linq`. + ([#1949](https://github.com/dotnet/SqlClient/pull/1949)) +- Publish symbols packages with source link. + ([#2137](https://github.com/dotnet/SqlClient/pull/2137)) +- Signing package with strong name. + ([Commit `dbe09f1`](https://github.com/dotnet/SqlClient/commit/dbe09f1152f448ec7b6037579d9bcbf98b016fce)) + +## Target Platform Support + +- .NET Framework 4.6.2+ (Windows ARM64, Windows x86, Windows x64) +- .NET 8.0+ (Windows x86, Windows x64, Windows ARM64, Windows ARM, Linux, macOS) + +## Dependencies + +### .NET Framework + +- Azure.Core 1.38.0 +- Azure.Security.KeyVault.Keys 4.5.0 +- Microsoft.Data.SqlClient 6.0.0 +- Microsoft.Extensions.Caching.Memory 6.0.1 + +### .NET 8.0 + +- Azure.Core 1.38.0 +- Azure.Security.KeyVault.Keys 4.5.0 +- Microsoft.Data.SqlClient 6.0.0 +- Microsoft.Extensions.Caching.Memory 8.0.1 + +### .NET 9.0 + +- Azure.Core 1.38.0 +- Azure.Security.KeyVault.Keys 4.5.0 +- Microsoft.Data.SqlClient 6.0.0 +- Microsoft.Extensions.Caching.Memory 9.0.0 diff --git a/release-notes/add-ons/AzureKeyVaultProvider/6.0/README.md b/release-notes/add-ons/AzureKeyVaultProvider/6.0/README.md new file mode 100644 index 0000000000..dde236e287 --- /dev/null +++ b/release-notes/add-ons/AzureKeyVaultProvider/6.0/README.md @@ -0,0 +1,8 @@ +# Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 6.0 Releases + +The following `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` +6.0 stable releases have been shipped: + +| Release Date | Description | Notes | +| :-- | :-- | :--: | +| 2025/01/30 | 6.0.0 | [Release Notes](6.0.0.md) | diff --git a/release-notes/add-ons/AzureKeyVaultProvider/6.1/6.1.0.md b/release-notes/add-ons/AzureKeyVaultProvider/6.1/6.1.0.md new file mode 100644 index 0000000000..dbd458047a --- /dev/null +++ b/release-notes/add-ons/AzureKeyVaultProvider/6.1/6.1.0.md @@ -0,0 +1,39 @@ +# Release Notes + +## 6.1.0 - 31 July 2025 + +### Changed + +- Fixed AzureKeyVaultProvider package key caching to serialize Azure key fetch + operations. + ([#3477](https://github.com/dotnet/SqlClient/pull/3477)) +- Marked AKV provider as trimmable and enabled AOT support. + ([#3401](https://github.com/dotnet/SqlClient/pull/3401)) + +## Target Platform Support + +- .NET Framework 4.6.2+ (Windows ARM64, Windows x86, Windows x64) +- .NET 8.0+ (Windows x86, Windows x64, Windows ARM64, Windows ARM, Linux, macOS) + +## Dependencies + +### .NET Framework + +- Azure.Core 1.47.1 +- Azure.Security.KeyVault.Keys 4.7.0 +- Microsoft.Data.SqlClient 6.1.0 +- Microsoft.Extensions.Caching.Memory 8.0.1 + +### .NET 8.0 + +- Azure.Core 1.47.1 +- Azure.Security.KeyVault.Keys 4.7.0 +- Microsoft.Data.SqlClient 6.1.0 +- Microsoft.Extensions.Caching.Memory 8.0.1 + +### .NET 9.0 + +- Azure.Core 1.47.1 +- Azure.Security.KeyVault.Keys 4.7.0 +- Microsoft.Data.SqlClient 6.1.0 +- Microsoft.Extensions.Caching.Memory 9.0.4 diff --git a/release-notes/add-ons/AzureKeyVaultProvider/6.1/6.1.1.md b/release-notes/add-ons/AzureKeyVaultProvider/6.1/6.1.1.md new file mode 100644 index 0000000000..cc6768493f --- /dev/null +++ b/release-notes/add-ons/AzureKeyVaultProvider/6.1/6.1.1.md @@ -0,0 +1,35 @@ +# Release Notes + +## 6.1.1 - 15 August 2025 + +### Changed + +- Changed Microsoft.Data.SqlClient version range to `[6.1.1,7.0.0)`. ([#3569](https://github.com/dotnet/SqlClient/pull/3569)) + +## Target Platform Support + +- .NET Framework 4.6.2+ (Windows ARM64, Windows x86, Windows x64) +- .NET 8.0+ (Windows x86, Windows x64, Windows ARM64, Windows ARM, Linux, macOS) + +## Dependencies + +### .NET Framework + +- Azure.Core 1.47.1 +- Azure.Security.KeyVault.Keys 4.7.0 +- Microsoft.Data.SqlClient 6.1.1 +- Microsoft.Extensions.Caching.Memory 8.0.1 + +### .NET 8.0 + +- Azure.Core 1.47.1 +- Azure.Security.KeyVault.Keys 4.7.0 +- Microsoft.Data.SqlClient 6.1.1 +- Microsoft.Extensions.Caching.Memory 8.0.1 + +### .NET 9.0 + +- Azure.Core 1.47.1 +- Azure.Security.KeyVault.Keys 4.7.0 +- Microsoft.Data.SqlClient 6.1.1 +- Microsoft.Extensions.Caching.Memory 9.0.4 diff --git a/release-notes/add-ons/AzureKeyVaultProvider/6.1/README.md b/release-notes/add-ons/AzureKeyVaultProvider/6.1/README.md new file mode 100644 index 0000000000..5009ea2e67 --- /dev/null +++ b/release-notes/add-ons/AzureKeyVaultProvider/6.1/README.md @@ -0,0 +1,9 @@ +# Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 6.1 Releases + +The following `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` +6.1 stable releases have been shipped: + +| Release Date | Description | Notes | +| :-- | :-- | :--: | +| 2025/08/15 | 6.1.1 | [Release Notes](6.1.1.md) | +| 2025/07/31 | 6.1.0 | [Release Notes](6.1.0.md) | diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index f897e55f77..4e4c30ae4a 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -14,7 +14,7 @@ - + @@ -26,7 +26,7 @@ - + diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs index 3e26aa251a..a07133eb07 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs @@ -131,7 +131,7 @@ public SqlVector(System.ReadOnlyMemory memory) { } /// public bool IsNull => throw null; /// - public static SqlVector Null => throw null; + public static SqlVector? Null => throw null; /// public int Length { get { throw null; } } /// diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj index 60eff24c1c..0944a4aea0 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj @@ -32,6 +32,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 01f4de3352..c3b06897c1 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -99,9 +99,6 @@ Microsoft\Data\ProviderBase\DbConnectionClosed.cs - - Microsoft\Data\ProviderBase\DbConnectionFactory.cs - Microsoft\Data\SqlClient\ConnectionPool\ChannelDbConnectionPool.cs @@ -792,6 +789,12 @@ Microsoft\Data\SqlTypes\SqlVector.cs + + Microsoft\Data\SqlClient\UserAgent\UserAgentInfo.cs + + + Microsoft\Data\SqlClient\UserAgent\UserAgentInfoDto.cs + Resources\ResCategoryAttribute.cs @@ -961,8 +964,12 @@ Microsoft\Data\SqlTypes\SqlFileStream.Windows.cs - + + + ILLink.Substitutions.xml + Resources\ILLink.Substitutions.Windows.xml + @@ -1004,7 +1011,10 @@ Microsoft\Data\SqlTypes\SqlFileStream.netcore.Unix.cs - + + ILLink.Substitutions.xml + Resources\ILLink.Substitutions.Unix.xml + @@ -1037,6 +1047,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs index 9ae11c13f9..f4f38dbe73 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -168,7 +168,7 @@ protected override void AfterCleared(SqlCommand owner) // Prepare // Against 7.0 Serve a prepare/unprepare requires an extra roundtrip to the server. // - // From 8.0 and above the preparation can be done as part of the command execution. + // From 8.0 and above, the preparation can be done as part of the command execution. private enum EXECTYPE { @@ -204,6 +204,7 @@ private enum EXECTYPE // cached metadata private _SqlMetaDataSet _cachedMetaData; + // @TODO: Make properties internal ConcurrentDictionary keysToBeSentToEnclave; internal bool requiresEnclaveComputations = false; @@ -253,7 +254,7 @@ internal bool IsColumnEncryptionEnabled internal bool ShouldUseEnclaveBasedWorkflow => (!string.IsNullOrWhiteSpace(_activeConnection.EnclaveAttestationUrl) || Connection.AttestationProtocol == SqlConnectionAttestationProtocol.None) && - IsColumnEncryptionEnabled; + IsColumnEncryptionEnabled; /// /// Per-command custom providers. It can be provided by the user and can be set more than once. @@ -264,8 +265,9 @@ internal bool IsColumnEncryptionEnabled _customColumnEncryptionKeyStoreProviders is not null && _customColumnEncryptionKeyStoreProviders.Count > 0; // Cached info for async executions - private sealed class CachedAsyncState + private sealed class AsyncState { + // @TODO: Autoproperties private int _cachedAsyncCloseCount = -1; // value of the connection's CloseCount property when the asyncResult was set; tracks when connections are closed after an async operation private TaskCompletionSource _cachedAsyncResult = null; private SqlConnection _cachedAsyncConnection = null; // Used to validate that the connection hasn't changed when end the connection; @@ -274,7 +276,7 @@ private sealed class CachedAsyncState private string _cachedSetOptions = null; private string _cachedEndMethod = null; - internal CachedAsyncState() + internal AsyncState() { } @@ -353,16 +355,14 @@ internal void SetAsyncReaderState(SqlDataReader ds, RunBehavior runBehavior, str } } - private CachedAsyncState _cachedAsyncState = null; + private AsyncState _cachedAsyncState = null; - private CachedAsyncState cachedAsyncState + // @TODO: This is never null, so we can remove the null checks from usages of it. + private AsyncState CachedAsyncState { get { - if (_cachedAsyncState == null) - { - _cachedAsyncState = new CachedAsyncState(); - } + _cachedAsyncState ??= new AsyncState(); return _cachedAsyncState; } } @@ -474,9 +474,8 @@ private SqlCommand(SqlCommand from) : this() } /// - /// [ [DefaultValue(null)] - [ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Data)] + [ResCategory(StringsHelper.ResourceNames.DataCategory_Data)] [ResDescription(StringsHelper.ResourceNames.DbCommand_Connection)] new public SqlConnection Connection { @@ -488,9 +487,11 @@ private SqlCommand(SqlCommand from) : this() { // Don't allow the connection to be changed while in an async operation. if (_activeConnection != value && _activeConnection != null) - { // If new value... - if (_cachedAsyncState != null && _cachedAsyncState.PendingAsyncOperation) - { // If in pending async state, throw. + { + // If new value... + if (CachedAsyncState != null && CachedAsyncState.PendingAsyncOperation) + { + // If in pending async state, throw. throw SQL.CannotModifyPropertyAsyncOperationInProgress(); } } @@ -502,7 +503,6 @@ private SqlCommand(SqlCommand from) : this() _transaction = null; } - // Command is no longer prepared on new connection, cleanup prepare status if (IsPrepared) { @@ -626,9 +626,11 @@ internal SqlStatistics Statistics { // Don't allow the transaction to be changed while in an async operation. if (_transaction != value && _activeConnection != null) - { // If new value... - if (cachedAsyncState.PendingAsyncOperation) - { // If in pending async state, throw + { + // If new value... + if (CachedAsyncState.PendingAsyncOperation) + { + // If in pending async state, throw throw SQL.CannotModifyPropertyAsyncOperationInProgress(); } } @@ -654,7 +656,7 @@ protected override DbTransaction DbTransaction /// [DefaultValue("")] [RefreshProperties(RefreshProperties.All)] // MDAC 67707 - [ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Data)] + [ResCategory(StringsHelper.ResourceNames.DataCategory_Data)] [ResDescription(StringsHelper.ResourceNames.DbCommand_CommandText)] public override string CommandText { @@ -673,12 +675,12 @@ public override string CommandText /// [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - [ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Data)] + [ResCategory(StringsHelper.ResourceNames.DataCategory_Data)] [ResDescription(StringsHelper.ResourceNames.TCE_SqlCommand_ColumnEncryptionSetting)] public SqlCommandColumnEncryptionSetting ColumnEncryptionSetting => _columnEncryptionSetting; /// - [ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Data)] + [ResCategory(StringsHelper.ResourceNames.DataCategory_Data)] [ResDescription(StringsHelper.ResourceNames.DbCommand_CommandTimeout)] public override int CommandTimeout { @@ -692,11 +694,13 @@ public override int CommandTimeout { throw ADP.InvalidCommandTimeout(value); } + if (value != _commandTimeout) { PropertyChanging(); _commandTimeout = value; } + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.Set_CommandTimeout | API | ObjectId {0}, Command Timeout value {1}, Client Connection Id {2}", ObjectID, value, Connection?.ClientConnectionId); } } @@ -720,9 +724,9 @@ private int DefaultCommandTimeout } /// - [DefaultValue(System.Data.CommandType.Text)] + [DefaultValue(CommandType.Text)] [RefreshProperties(RefreshProperties.All)] - [ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Data)] + [ResCategory(StringsHelper.ResourceNames.DataCategory_Data)] [ResDescription(StringsHelper.ResourceNames.DbCommand_CommandType)] public override CommandType CommandType { @@ -747,12 +751,13 @@ public override CommandType CommandType default: throw ADP.InvalidCommandType(value); } + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.Set_CommandType | API | ObjectId {0}, Command type value {1}, Client Connection Id {2}", ObjectID, (int)value, Connection?.ClientConnectionId); } } } - // @devnote: By default, the cmd object is visible on the design surface (i.e. VS7 Server Tray) + // By default, the cmd object is visible on the design surface (i.e. VS7 Server Tray) // to limit the number of components that clutter the design surface, // when the DataAdapter design wizard generates the insert/update/delete commands it will // set the DesignTimeVisible property to false so that cmds won't appear as individual objects @@ -760,7 +765,7 @@ public override CommandType CommandType [DefaultValue(true)] [DesignOnly(true)] [Browsable(false)] - [EditorBrowsableAttribute(EditorBrowsableState.Never)] + [EditorBrowsable(EditorBrowsableState.Never)] public override bool DesignTimeVisible { get @@ -778,7 +783,7 @@ public override bool DesignTimeVisible /// [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] - [ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Data)] + [ResCategory(StringsHelper.ResourceNames.DataCategory_Data)] [ResDescription(StringsHelper.ResourceNames.DbCommand_Parameters)] new public SqlParameterCollection Parameters { @@ -804,8 +809,8 @@ protected override DbParameterCollection DbParameterCollection } /// - [DefaultValue(System.Data.UpdateRowSource.Both)] - [ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Update)] + [DefaultValue(UpdateRowSource.Both)] + [ResCategory(StringsHelper.ResourceNames.DataCategory_Update)] [ResDescription(StringsHelper.ResourceNames.DbCommand_UpdatedRowSource)] public override UpdateRowSource UpdatedRowSource { @@ -826,12 +831,13 @@ public override UpdateRowSource UpdatedRowSource default: throw ADP.InvalidUpdateRowSource(value); } + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.UpdatedRowSource | API | ObjectId {0}, Updated row source value {1}, Client Connection Id {2}", ObjectID, (int)value, Connection?.ClientConnectionId); } } /// - [ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_StatementCompleted)] + [ResCategory(StringsHelper.ResourceNames.DataCategory_StatementCompleted)] [ResDescription(StringsHelper.ResourceNames.DbCommand_StatementCompleted)] public event StatementCompletedEventHandler StatementCompleted { @@ -869,7 +875,8 @@ internal void OnStatementCompleted(int recordCount) } private void PropertyChanging() - { // also called from SqlParameterCollection + { + // also called from SqlParameterCollection this.IsDirty = true; } @@ -1046,7 +1053,8 @@ public override void Cancel() lock (connection) { if (connection != (_activeConnection.InnerConnection as SqlInternalConnectionTds)) - { // make sure the connection held on the active connection is what we have stored in our temp connection variable, if not between getting "connection" and taking the lock, the connection has been closed + { + // make sure the connection held on the active connection is what we have stored in our temp connection variable, if not between getting "connection" and taking the lock, the connection has been closed return; } @@ -1057,7 +1065,8 @@ public override void Cancel() } if (!_pendingCancel) - { // Do nothing if already pending. + { + // Do nothing if already pending. // Before attempting actual cancel, set the _pendingCancel flag to false. // This denotes to other thread before obtaining stateObject from the // session pool that there is another thread wishing to cancel. @@ -1109,14 +1118,22 @@ protected override void Dispose(bool disposing) _cachedMetaData = null; // reset async cache information to allow a second async execute - _cachedAsyncState?.ResetAsyncState(); + CachedAsyncState?.ResetAsyncState(); } // release unmanaged objects base.Dispose(disposing); } - private SqlDataReader RunExecuteReaderWithRetry(CommandBehavior cmdBehavior, RunBehavior runBehavior, bool returnStream, [CallerMemberName] string method = "") - => RetryLogicProvider.Execute(this, () => RunExecuteReader(cmdBehavior, runBehavior, returnStream, method)); + private SqlDataReader RunExecuteReaderWithRetry( + CommandBehavior cmdBehavior, + RunBehavior runBehavior, + bool returnStream, + [CallerMemberName] string method = "") + { + return RetryLogicProvider.Execute( + this, + () => RunExecuteReader(cmdBehavior, runBehavior, returnStream, method)); + } /// public override object ExecuteScalar() @@ -1137,9 +1154,9 @@ public override object ExecuteScalar() { statistics = SqlStatistics.StartTimer(Statistics); WriteBeginExecuteEvent(); - SqlDataReader ds = IsProviderRetriable ? - RunExecuteReaderWithRetry(0, RunBehavior.ReturnImmediately, returnStream: true) : - RunExecuteReader(0, RunBehavior.ReturnImmediately, returnStream: true, method: nameof(ExecuteScalar)); + SqlDataReader ds = IsProviderRetriable + ? RunExecuteReaderWithRetry(0, RunBehavior.ReturnImmediately, returnStream: true) + : RunExecuteReader(0, RunBehavior.ReturnImmediately, returnStream: true); success = true; return CompleteExecuteScalar(ds, _batchRPCMode); } @@ -1186,10 +1203,25 @@ private object CompleteExecuteScalar(SqlDataReader ds, bool returnLastResult) return retResult; } - private Task InternalExecuteNonQueryWithRetry(bool sendToPipe, int timeout, out bool usedCache, bool asyncWrite, bool inRetry, [CallerMemberName] string methodName = "") + private Task InternalExecuteNonQueryWithRetry( + bool sendToPipe, + int timeout, + out bool usedCache, + bool asyncWrite, + bool isRetry, + [CallerMemberName] string methodName = "") { bool innerUsedCache = false; - Task result = RetryLogicProvider.Execute(this, () => InternalExecuteNonQuery(completion: null, sendToPipe, timeout, out innerUsedCache, asyncWrite, inRetry, methodName)); + Task result = RetryLogicProvider.Execute( + this, + () => InternalExecuteNonQuery( + completion: null, + sendToPipe, + timeout, + out innerUsedCache, + asyncWrite, + isRetry, + methodName)); usedCache = innerUsedCache; return result; } @@ -1215,11 +1247,16 @@ public override int ExecuteNonQuery() WriteBeginExecuteEvent(); if (IsProviderRetriable) { - InternalExecuteNonQueryWithRetry(sendToPipe: false, timeout: CommandTimeout, out _, asyncWrite: false, inRetry: false); + InternalExecuteNonQueryWithRetry( + sendToPipe: false, + timeout: CommandTimeout, + out _, + asyncWrite: false, + isRetry: false); } else { - InternalExecuteNonQuery(completion: null, sendToPipe: false, timeout: CommandTimeout, out _); + InternalExecuteNonQuery(completion: null, sendToPipe: false, CommandTimeout, out _); } success = true; return _rowsAffected; @@ -1242,27 +1279,28 @@ public override int ExecuteNonQuery() } /// - public IAsyncResult BeginExecuteNonQuery() => BeginExecuteNonQuery(null, null); // BeginExecuteNonQuery will track ExecutionTime for us + public IAsyncResult BeginExecuteNonQuery() => + BeginExecuteNonQuery(null, null); /// public IAsyncResult BeginExecuteNonQuery(AsyncCallback callback, object stateObject) { SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.BeginExecuteNonQuery | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - return BeginExecuteNonQueryInternal(0, callback, stateObject, 0, inRetry: false); + return BeginExecuteNonQueryInternal(0, callback, stateObject, 0, isRetry: false); } private IAsyncResult BeginExecuteNonQueryAsync(AsyncCallback callback, object stateObject) { SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.BeginExecuteNonQueryAsync | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - return BeginExecuteNonQueryInternal(0, callback, stateObject, CommandTimeout, inRetry: false, asyncWrite: true); + return BeginExecuteNonQueryInternal(0, callback, stateObject, CommandTimeout, isRetry: false, asyncWrite: true); } - private IAsyncResult BeginExecuteNonQueryInternal(CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool inRetry, bool asyncWrite = false) + private IAsyncResult BeginExecuteNonQueryInternal(CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool isRetry, bool asyncWrite = false) { TaskCompletionSource globalCompletion = new TaskCompletionSource(stateObject); TaskCompletionSource localCompletion = new TaskCompletionSource(stateObject); - if (!inRetry) + if (!isRetry) { // Reset _pendingCancel upon entry into any Execute - used to synchronize state // between entry into Execute* API and the thread obtaining the stateObject. @@ -1275,7 +1313,7 @@ private IAsyncResult BeginExecuteNonQueryInternal(CommandBehavior behavior, Asyn SqlStatistics statistics = null; try { - if (!inRetry) + if (!isRetry) { statistics = SqlStatistics.StartTimer(Statistics); } @@ -1284,7 +1322,14 @@ private IAsyncResult BeginExecuteNonQueryInternal(CommandBehavior behavior, Asyn try { // InternalExecuteNonQuery already has reliability block, but if failure will not put stateObj back into pool. - Task execNQ = InternalExecuteNonQuery(localCompletion, false, timeout, out usedCache, asyncWrite, inRetry: inRetry, methodName: nameof(BeginExecuteNonQuery)); + Task execNQ = InternalExecuteNonQuery( + localCompletion, + sendToPipe: false, + timeout, + out usedCache, + asyncWrite, + isRetry, + methodName: nameof(BeginExecuteNonQuery)); if (execNQ != null) { @@ -1323,7 +1368,7 @@ private IAsyncResult BeginExecuteNonQueryInternal(CommandBehavior behavior, Asyn stateObject, timeout, usedCache, - inRetry, + isRetry, asyncWrite, globalCompletion, localCompletion, @@ -1331,13 +1376,11 @@ private IAsyncResult BeginExecuteNonQueryInternal(CommandBehavior behavior, Asyn { return command.InternalEndExecuteNonQuery(asyncResult, isInternal, endMethod); }, - retryFunc: static (SqlCommand command, CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool inRetry, bool asyncWrite) => + retryFunc: static (SqlCommand command, CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool isRetry, bool asyncWrite) => { - return command.BeginExecuteNonQueryInternal(behavior, callback, stateObject, timeout, inRetry, asyncWrite); + return command.BeginExecuteNonQueryInternal(behavior, callback, stateObject, timeout, isRetry, asyncWrite); }, - nameof(EndExecuteNonQuery) - ) - ) + nameof(EndExecuteNonQuery))) { globalCompletion = localCompletion; } @@ -1365,7 +1408,7 @@ private void BeginExecuteNonQueryInternalReadStage(TaskCompletionSource try { // must finish caching information before ReadSni which can activate the callback before returning - cachedAsyncState.SetActiveConnectionAndResult(completion, nameof(EndExecuteNonQuery), _activeConnection); + CachedAsyncState.SetActiveConnectionAndResult(completion, nameof(EndExecuteNonQuery), _activeConnection); _stateObj.ReadSni(completion); } // Cause of a possible unstable runtime situation on facing with `OutOfMemoryException` and `StackOverflowException` exceptions, @@ -1384,7 +1427,7 @@ private void BeginExecuteNonQueryInternalReadStage(TaskCompletionSource { // Similarly, if an exception occurs put the stateObj back into the pool. // and reset async cache information to allow a second async execute - _cachedAsyncState?.ResetAsyncState(); + CachedAsyncState?.ResetAsyncState(); ReliablePutStateObject(); throw; } @@ -1396,6 +1439,7 @@ private void VerifyEndExecuteState(Task completionTask, string endMethod, bool f SqlClientEventSource.Log.TryTraceEvent("SqlCommand.VerifyEndExecuteState | API | ObjectId {0}, Client Connection Id {1}, MARS={2}, AsyncCommandInProgress={3}", _activeConnection?.ObjectID, _activeConnection?.ClientConnectionId, _activeConnection?.Parser?.MARSOn, _activeConnection?.AsyncCommandInProgress); + if (completionTask.IsCanceled) { if (_stateObj != null) @@ -1428,15 +1472,15 @@ private void VerifyEndExecuteState(Task completionTask, string endMethod, bool f return; } - if (cachedAsyncState.EndMethodName == null) + if (CachedAsyncState.EndMethodName == null) { throw ADP.MethodCalledTwice(endMethod); } - if (endMethod != cachedAsyncState.EndMethodName) + if (endMethod != CachedAsyncState.EndMethodName) { - throw ADP.MismatchedAsyncResult(cachedAsyncState.EndMethodName, endMethod); + throw ADP.MismatchedAsyncResult(CachedAsyncState.EndMethodName, endMethod); } - if ((_activeConnection.State != ConnectionState.Open) || (!cachedAsyncState.IsActiveConnectionValid(_activeConnection))) + if ((_activeConnection.State != ConnectionState.Open) || (!CachedAsyncState.IsActiveConnectionValid(_activeConnection))) { // If the connection is not 'valid' then it was closed while we were executing throw ADP.ClosedConnectionError(); @@ -1501,7 +1545,7 @@ private int EndExecuteNonQueryAsync(IAsyncResult asyncResult) if (asyncException != null) { // Leftover exception from the Begin...InternalReadStage - cachedAsyncState?.ResetAsyncState(); + CachedAsyncState?.ResetAsyncState(); ReliablePutStateObject(); throw asyncException.InnerException; } @@ -1540,7 +1584,7 @@ private int EndExecuteNonQueryInternal(IAsyncResult asyncResult) catch (SqlException e) { sqlExceptionNumber = e.Number; - _cachedAsyncState?.ResetAsyncState(); + CachedAsyncState?.ResetAsyncState(); // SqlException is always catchable ReliablePutStateObject(); @@ -1548,7 +1592,7 @@ private int EndExecuteNonQueryInternal(IAsyncResult asyncResult) } catch (Exception e) { - _cachedAsyncState?.ResetAsyncState(); + CachedAsyncState?.ResetAsyncState(); if (ADP.IsCatchableExceptionType(e)) { ReliablePutStateObject(); @@ -1562,7 +1606,10 @@ private int EndExecuteNonQueryInternal(IAsyncResult asyncResult) } } - private object InternalEndExecuteNonQuery(IAsyncResult asyncResult, bool isInternal, [CallerMemberName] string endMethod = "") + private object InternalEndExecuteNonQuery( + IAsyncResult asyncResult, + bool isInternal, + [CallerMemberName] string endMethod = "") { SqlClientEventSource.Log.TryTraceEvent("SqlCommand.InternalEndExecuteNonQuery | INFO | ObjectId {0}, Client Connection Id {1}, MARS={2}, AsyncCommandInProgress={3}", _activeConnection?.ObjectID, _activeConnection?.ClientConnectionId, @@ -1591,7 +1638,7 @@ private object InternalEndExecuteNonQuery(IAsyncResult asyncResult, bool isInter Debug.Assert(_stateObj == null); // Reset the state since we exit early. - cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); return _rowsAffected; } @@ -1616,7 +1663,7 @@ private object InternalEndExecuteNonQuery(IAsyncResult asyncResult, bool isInter // Don't reset the state for internal End. The user End will do that eventually. if (!isInternal) { - cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); } } } @@ -1647,10 +1694,17 @@ private object InternalEndExecuteNonQuery(IAsyncResult asyncResult, bool isInter return _rowsAffected; } - private Task InternalExecuteNonQuery(TaskCompletionSource completion, bool sendToPipe, int timeout, out bool usedCache, bool asyncWrite = false, bool inRetry = false, [CallerMemberName] string methodName = "") + private Task InternalExecuteNonQuery( + TaskCompletionSource completion, + bool sendToPipe, + int timeout, + out bool usedCache, + bool asyncWrite = false, + bool isRetry = false, + [CallerMemberName] string methodName = "") { SqlClientEventSource.Log.TryTraceEvent("SqlCommand.InternalExecuteNonQuery | INFO | ObjectId {0}, Client Connection Id {1}, AsyncCommandInProgress={2}", - _activeConnection?.ObjectID, _activeConnection?.ClientConnectionId, _activeConnection?.AsyncCommandInProgress); + _activeConnection?.ObjectID, _activeConnection?.ClientConnectionId, _activeConnection?.AsyncCommandInProgress); bool isAsync = completion != null; usedCache = false; @@ -1659,7 +1713,7 @@ private Task InternalExecuteNonQuery(TaskCompletionSource completion, bo // this function may throw for an invalid connection // returns false for empty command text - if (!inRetry) + if (!isRetry) { ValidateCommand(isAsync, methodName); } @@ -1687,8 +1741,8 @@ private Task InternalExecuteNonQuery(TaskCompletionSource completion, bo } // We should never get here for a retry since we only have retries for parameters. - Debug.Assert(!inRetry); - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.InternalExecuteNonQuery | INFO | Object Id {0}, RPC execute method name {1}, isAsync {2}, inRetry {3}", ObjectID, methodName, isAsync, inRetry); + Debug.Assert(!isRetry); + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.InternalExecuteNonQuery | INFO | Object Id {0}, RPC execute method name {1}, isAsync {2}, isRetry {3}", ObjectID, methodName, isAsync, isRetry); task = RunExecuteNonQueryTds(methodName, isAsync, timeout, asyncWrite); } @@ -1696,9 +1750,20 @@ private Task InternalExecuteNonQuery(TaskCompletionSource completion, bo { // otherwise, use a full-fledged execute that can handle params and stored procs Debug.Assert(!sendToPipe, "Trying to send non-context command to pipe"); - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.InternalExecuteNonQuery | INFO | Object Id {0}, RPC execute method name {1}, isAsync {2}, inRetry {3}", ObjectID, methodName, isAsync, inRetry); - - SqlDataReader reader = RunExecuteReader(0, RunBehavior.UntilDone, false, completion, timeout, out task, out usedCache, asyncWrite, inRetry, methodName); + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.InternalExecuteNonQuery | INFO | Object Id {0}, RPC execute method name {1}, isAsync {2}, isRetry {3}", ObjectID, methodName, isAsync, isRetry); + + SqlDataReader reader = RunExecuteReader( + CommandBehavior.Default, + RunBehavior.UntilDone, + returnStream: false, + completion, + timeout, + out task, + out usedCache, + asyncWrite, + isRetry, + methodName); + if (reader != null) { if (task != null) @@ -1738,6 +1803,7 @@ public XmlReader ExecuteXmlReader() { statistics = SqlStatistics.StartTimer(Statistics); WriteBeginExecuteEvent(); + // use the reader to consume metadata SqlDataReader ds = IsProviderRetriable ? RunExecuteReaderWithRetry(CommandBehavior.SequentialAccess, RunBehavior.ReturnImmediately, returnStream: true) : @@ -1773,21 +1839,21 @@ public IAsyncResult BeginExecuteXmlReader() public IAsyncResult BeginExecuteXmlReader(AsyncCallback callback, object stateObject) { SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.BeginExecuteXmlReader | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - return BeginExecuteXmlReaderInternal(CommandBehavior.SequentialAccess, callback, stateObject, 0, inRetry: false); + return BeginExecuteXmlReaderInternal(CommandBehavior.SequentialAccess, callback, stateObject, 0, isRetry: false); } private IAsyncResult BeginExecuteXmlReaderAsync(AsyncCallback callback, object stateObject) { SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.BeginExecuteXmlReaderAsync | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - return BeginExecuteXmlReaderInternal(CommandBehavior.SequentialAccess, callback, stateObject, CommandTimeout, inRetry: false, asyncWrite: true); + return BeginExecuteXmlReaderInternal(CommandBehavior.SequentialAccess, callback, stateObject, CommandTimeout, isRetry: false, asyncWrite: true); } - private IAsyncResult BeginExecuteXmlReaderInternal(CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool inRetry, bool asyncWrite = false) + private IAsyncResult BeginExecuteXmlReaderInternal(CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool isRetry, bool asyncWrite = false) { TaskCompletionSource globalCompletion = new TaskCompletionSource(stateObject); TaskCompletionSource localCompletion = new TaskCompletionSource(stateObject); - if (!inRetry) + if (!isRetry) { // Reset _pendingCancel upon entry into any Execute - used to synchronize state // between entry into Execute* API and the thread obtaining the stateObject. @@ -1800,7 +1866,7 @@ private IAsyncResult BeginExecuteXmlReaderInternal(CommandBehavior behavior, Asy SqlStatistics statistics = null; try { - if (!inRetry) + if (!isRetry) { statistics = SqlStatistics.StartTimer(Statistics); WriteBeginExecuteEvent(); @@ -1811,7 +1877,16 @@ private IAsyncResult BeginExecuteXmlReaderInternal(CommandBehavior behavior, Asy try { // InternalExecuteNonQuery already has reliability block, but if failure will not put stateObj back into pool. - RunExecuteReader(behavior, RunBehavior.ReturnImmediately, true, localCompletion, timeout, out writeTask, out usedCache, asyncWrite, inRetry); + RunExecuteReader( + behavior, + RunBehavior.ReturnImmediately, + returnStream: true, + localCompletion, + timeout, + out writeTask, + out usedCache, + asyncWrite, + isRetry); } catch (Exception e) { @@ -1850,7 +1925,7 @@ private IAsyncResult BeginExecuteXmlReaderInternal(CommandBehavior behavior, Asy stateObject, timeout, usedCache, - inRetry, + isRetry, asyncWrite, globalCompletion, localCompletion, @@ -1858,13 +1933,11 @@ private IAsyncResult BeginExecuteXmlReaderInternal(CommandBehavior behavior, Asy { return command.InternalEndExecuteReader(asyncResult, isInternal, endMethod); }, - retryFunc: static (SqlCommand command, CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool inRetry, bool asyncWrite) => + retryFunc: static (SqlCommand command, CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool isRetry, bool asyncWrite) => { - return command.BeginExecuteXmlReaderInternal(behavior, callback, stateObject, timeout, inRetry, asyncWrite); + return command.BeginExecuteXmlReaderInternal(behavior, callback, stateObject, timeout, isRetry, asyncWrite); }, - endMethod: nameof(EndExecuteXmlReader) - ) - ) + endMethod: nameof(EndExecuteXmlReader))) { globalCompletion = localCompletion; } @@ -1892,7 +1965,7 @@ private void BeginExecuteXmlReaderInternalReadStage(TaskCompletionSource try { // must finish caching information before ReadSni which can activate the callback before returning - _cachedAsyncState.SetActiveConnectionAndResult(completion, nameof(EndExecuteXmlReader), _activeConnection); + CachedAsyncState.SetActiveConnectionAndResult(completion, nameof(EndExecuteXmlReader), _activeConnection); _stateObj.ReadSni(completion); } // Cause of a possible unstable runtime situation on facing with `OutOfMemoryException` and `StackOverflowException` exceptions, @@ -1913,7 +1986,7 @@ private void BeginExecuteXmlReaderInternalReadStage(TaskCompletionSource { // Similarly, if an exception occurs put the stateObj back into the pool. // and reset async cache information to allow a second async execute - _cachedAsyncState?.ResetAsyncState(); + CachedAsyncState?.ResetAsyncState(); ReliablePutStateObject(); completion.TrySetException(e); } @@ -1940,7 +2013,7 @@ private XmlReader EndExecuteXmlReaderAsync(IAsyncResult asyncResult) Exception asyncException = ((Task)asyncResult).Exception; if (asyncException != null) { - cachedAsyncState?.ResetAsyncState(); + CachedAsyncState?.ResetAsyncState(); ReliablePutStateObject(); throw asyncException.InnerException; } @@ -1979,9 +2052,9 @@ private XmlReader EndExecuteXmlReaderInternal(IAsyncResult asyncResult) SqlException ex = (SqlException)e; sqlExceptionNumber = ex.Number; } - if (cachedAsyncState != null) + if (CachedAsyncState != null) { - cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); }; if (ADP.IsCatchableExceptionType(e)) { @@ -2029,19 +2102,22 @@ private XmlReader CompleteXmlReader(SqlDataReader ds, bool isAsync = false) } /// - public IAsyncResult BeginExecuteReader() => BeginExecuteReader(null, null, CommandBehavior.Default); + public IAsyncResult BeginExecuteReader() => + BeginExecuteReader(callback: null, stateObject: null, CommandBehavior.Default); /// - public IAsyncResult BeginExecuteReader(AsyncCallback callback, object stateObject) => BeginExecuteReader(callback, stateObject, CommandBehavior.Default); + public IAsyncResult BeginExecuteReader(AsyncCallback callback, object stateObject) => + BeginExecuteReader(callback, stateObject, CommandBehavior.Default); /// - public IAsyncResult BeginExecuteReader(CommandBehavior behavior) => BeginExecuteReader(null, null, behavior); + public IAsyncResult BeginExecuteReader(CommandBehavior behavior) => + BeginExecuteReader(callback: null, stateObject: null, behavior); /// public IAsyncResult BeginExecuteReader(AsyncCallback callback, object stateObject, CommandBehavior behavior) { SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.BeginExecuteReader | API | Correlation | Object Id {0}, Behavior {1}, Activity Id {2}, Client Connection Id {3}, Command Text '{4}'", ObjectID, (int)behavior, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); - return BeginExecuteReaderInternal(behavior, callback, stateObject, 0, inRetry: false); + return BeginExecuteReaderInternal(behavior, callback, stateObject, 0, isRetry: false); } /// @@ -2129,7 +2205,7 @@ public SqlDataReader EndExecuteReader(IAsyncResult asyncResult) } } - internal SqlDataReader EndExecuteReaderAsync(IAsyncResult asyncResult) + private SqlDataReader EndExecuteReaderAsync(IAsyncResult asyncResult) { SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.EndExecuteReaderAsync | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); Debug.Assert(!_internalEndExecuteInitiated || _stateObj == null); @@ -2137,7 +2213,7 @@ internal SqlDataReader EndExecuteReaderAsync(IAsyncResult asyncResult) Exception asyncException = ((Task)asyncResult).Exception; if (asyncException != null) { - cachedAsyncState?.ResetAsyncState(); + CachedAsyncState?.ResetAsyncState(); ReliablePutStateObject(); throw asyncException.InnerException; } @@ -2181,9 +2257,9 @@ private SqlDataReader EndExecuteReaderInternal(IAsyncResult asyncResult) sqlExceptionNumber = exception.Number; } - if (cachedAsyncState != null) + if (CachedAsyncState != null) { - cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); }; if (ADP.IsCatchableExceptionType(e)) { @@ -2226,12 +2302,12 @@ private void CleanupExecuteReaderAsync(Task task, TaskCompletionS } } - private IAsyncResult BeginExecuteReaderInternal(CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool inRetry, bool asyncWrite = false) + private IAsyncResult BeginExecuteReaderInternal(CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool isRetry, bool asyncWrite = false) { TaskCompletionSource globalCompletion = new TaskCompletionSource(stateObject); TaskCompletionSource localCompletion = new TaskCompletionSource(stateObject); - if (!inRetry) + if (!isRetry) { // Reset _pendingCancel upon entry into any Execute - used to synchronize state // between entry into Execute* API and the thread obtaining the stateObject. @@ -2241,10 +2317,11 @@ private IAsyncResult BeginExecuteReaderInternal(CommandBehavior behavior, AsyncC SqlStatistics statistics = null; try { - if (!inRetry) + if (!isRetry) { statistics = SqlStatistics.StartTimer(Statistics); WriteBeginExecuteEvent(); + ValidateAsyncCommand(); // Special case - done outside of try/catches to prevent putting a stateObj // back into pool when we should not. } @@ -2254,7 +2331,17 @@ private IAsyncResult BeginExecuteReaderInternal(CommandBehavior behavior, AsyncC try { // InternalExecuteNonQuery already has reliability block, but if failure will not put stateObj back into pool. - RunExecuteReader(behavior, RunBehavior.ReturnImmediately, true, localCompletion, timeout, out writeTask, out usedCache, asyncWrite, inRetry, nameof(BeginExecuteReader)); + RunExecuteReader( + behavior, + RunBehavior.ReturnImmediately, + returnStream: true, + localCompletion, + timeout, + out writeTask, + out usedCache, + asyncWrite, + isRetry, + nameof(BeginExecuteReader)); } catch (Exception e) { @@ -2266,7 +2353,7 @@ private IAsyncResult BeginExecuteReaderInternal(CommandBehavior behavior, AsyncC // For async, RunExecuteReader will never put the stateObj back into the pool, so do so now. ReliablePutStateObject(); - if (inRetry || e is not EnclaveDelegate.RetryableEnclaveQueryExecutionException) + if (isRetry || e is not EnclaveDelegate.RetryableEnclaveQueryExecutionException) { throw; } @@ -2296,7 +2383,7 @@ private IAsyncResult BeginExecuteReaderInternal(CommandBehavior behavior, AsyncC stateObject, timeout, usedCache, - inRetry, + isRetry, asyncWrite, globalCompletion, localCompletion, @@ -2304,13 +2391,11 @@ private IAsyncResult BeginExecuteReaderInternal(CommandBehavior behavior, AsyncC { return command.InternalEndExecuteReader(asyncResult, isInternal, endMethod); }, - retryFunc: static (SqlCommand command, CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool inRetry, bool asyncWrite) => + retryFunc: static (SqlCommand command, CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool isRetry, bool asyncWrite) => { - return command.BeginExecuteReaderInternal(behavior, callback, stateObject, timeout, inRetry, asyncWrite); + return command.BeginExecuteReaderInternal(behavior, callback, stateObject, timeout, isRetry, asyncWrite); }, - nameof(EndExecuteReader) - ) - ) + nameof(EndExecuteReader))) { globalCompletion = localCompletion; } @@ -2337,21 +2422,20 @@ private bool TriggerInternalEndAndRetryIfNecessary( object stateObject, int timeout, bool usedCache, - bool inRetry, + bool isRetry, bool asyncWrite, TaskCompletionSource globalCompletion, TaskCompletionSource localCompletion, Func endFunc, Func retryFunc, - string endMethod - ) + string endMethod) { // We shouldn't be using the cache if we are in retry. - Debug.Assert(!usedCache || !inRetry); + Debug.Assert(!usedCache || !isRetry); // If column encryption is enabled and we used the cache, we want to catch any potential exceptions that were caused by the query cache and retry if the error indicates that we should. // So, try to read the result of the query before completing the overall task and trigger a retry if appropriate. - if ((IsColumnEncryptionEnabled && !inRetry && (usedCache || ShouldUseEnclaveBasedWorkflow)) + if ((IsColumnEncryptionEnabled && !isRetry && (usedCache || ShouldUseEnclaveBasedWorkflow)) #if DEBUG || _forceInternalEndQuery #endif @@ -2359,7 +2443,18 @@ string endMethod { long firstAttemptStart = ADP.TimerCurrent(); - CreateLocalCompletionTask(behavior, stateObject, timeout, usedCache, asyncWrite, globalCompletion, localCompletion, endFunc, retryFunc, endMethod, firstAttemptStart); + CreateLocalCompletionTask( + behavior, + stateObject, + timeout, + usedCache, + asyncWrite, + globalCompletion, + localCompletion, + endFunc, + retryFunc, + endMethod, + firstAttemptStart); return true; } @@ -2404,7 +2499,7 @@ long firstAttemptStart // lock on _stateObj prevents races with close/cancel. lock (_stateObj) { - endFunc(this, tsk, true, endMethod /*inInternal*/); + endFunc(this, tsk, /*isInternal:*/ true, endMethod); } globalCompletion.TrySetResult(tsk.Result); @@ -2440,9 +2535,9 @@ long firstAttemptStart if (!shouldRetry) { // If we cannot retry, Reset the async state to make sure we leave a clean state. - if (_cachedAsyncState != null) + if (CachedAsyncState != null) { - _cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); } try @@ -2467,8 +2562,13 @@ long firstAttemptStart { // Kick off the retry. _internalEndExecuteInitiated = false; - Task retryTask = (Task)retryFunc(this, behavior, null, stateObject, - TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), true /*inRetry*/, + Task retryTask = (Task)retryFunc( + this, + behavior, + null, + stateObject, + TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), + /*isRetry:*/ true, asyncWrite); retryTask.ContinueWith( @@ -2530,7 +2630,7 @@ private void BeginExecuteReaderInternalReadStage(TaskCompletionSource co try { // must finish caching information before ReadSni which can activate the callback before returning - cachedAsyncState.SetActiveConnectionAndResult(completion, nameof(EndExecuteReader), _activeConnection); + CachedAsyncState.SetActiveConnectionAndResult(completion, nameof(EndExecuteReader), _activeConnection); _stateObj.ReadSni(completion); } // Cause of a possible unstable runtime situation on facing with `OutOfMemoryException` and `StackOverflowException` exceptions, @@ -2551,7 +2651,7 @@ private void BeginExecuteReaderInternalReadStage(TaskCompletionSource co { // Similarly, if an exception occurs put the stateObj back into the pool. // and reset async cache information to allow a second async execute - _cachedAsyncState?.ResetAsyncState(); + CachedAsyncState?.ResetAsyncState(); ReliablePutStateObject(); completion.TrySetException(e); } @@ -2580,13 +2680,16 @@ private SqlDataReader InternalEndExecuteReader(IAsyncResult asyncResult, bool is } /// - public override Task ExecuteNonQueryAsync(CancellationToken cancellationToken) - => IsProviderRetriable ? - InternalExecuteNonQueryWithRetryAsync(cancellationToken) : - InternalExecuteNonQueryAsync(cancellationToken); + public override Task ExecuteNonQueryAsync(CancellationToken cancellationToken) => + IsProviderRetriable + ? InternalExecuteNonQueryWithRetryAsync(cancellationToken) + : InternalExecuteNonQueryAsync(cancellationToken); - private Task InternalExecuteNonQueryWithRetryAsync(CancellationToken cancellationToken) - => RetryLogicProvider.ExecuteAsync(this, () => InternalExecuteNonQueryAsync(cancellationToken), cancellationToken); + private Task InternalExecuteNonQueryWithRetryAsync(CancellationToken cancellationToken) => + RetryLogicProvider.ExecuteAsync( + sender: this, + () => InternalExecuteNonQueryAsync(cancellationToken), + cancellationToken); private Task InternalExecuteNonQueryAsync(CancellationToken cancellationToken) { @@ -2692,31 +2795,28 @@ protected override Task ExecuteDbDataReaderAsync(CommandBehavior b } /// - new public Task ExecuteReaderAsync() - { - return ExecuteReaderAsync(CommandBehavior.Default, CancellationToken.None); - } + public new Task ExecuteReaderAsync() => + ExecuteReaderAsync(CommandBehavior.Default, CancellationToken.None); /// - new public Task ExecuteReaderAsync(CommandBehavior behavior) - { - return ExecuteReaderAsync(behavior, CancellationToken.None); - } + public new Task ExecuteReaderAsync(CommandBehavior behavior) => + ExecuteReaderAsync(behavior, CancellationToken.None); /// - new public Task ExecuteReaderAsync(CancellationToken cancellationToken) - { - return ExecuteReaderAsync(CommandBehavior.Default, cancellationToken); - } + public new Task ExecuteReaderAsync(CancellationToken cancellationToken) => + ExecuteReaderAsync(CommandBehavior.Default, cancellationToken); /// - new public Task ExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) - => IsProviderRetriable ? - InternalExecuteReaderWithRetryAsync(behavior, cancellationToken) : - InternalExecuteReaderAsync(behavior, cancellationToken); + public new Task ExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) => + IsProviderRetriable + ? InternalExecuteReaderWithRetryAsync(behavior, cancellationToken) + : InternalExecuteReaderAsync(behavior, cancellationToken); - private Task InternalExecuteReaderWithRetryAsync(CommandBehavior behavior, CancellationToken cancellationToken) - => RetryLogicProvider.ExecuteAsync(this, () => InternalExecuteReaderAsync(behavior, cancellationToken), cancellationToken); + private Task InternalExecuteReaderWithRetryAsync(CommandBehavior behavior, CancellationToken cancellationToken) => + RetryLogicProvider.ExecuteAsync( + sender: this, + () => InternalExecuteReaderAsync(behavior, cancellationToken), + cancellationToken); private Task InternalExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) { @@ -2764,7 +2864,7 @@ private Task InternalExecuteReaderAsync(CommandBehavior behavior, beginMethod: static (AsyncCallback callback, object stateObject) => { ExecuteReaderAsyncCallContext args = (ExecuteReaderAsyncCallContext)stateObject; - return args.Command.BeginExecuteReaderInternal(args.CommandBehavior, callback, stateObject, args.Command.CommandTimeout, inRetry: false, asyncWrite: true); + return args.Command.BeginExecuteReaderInternal(args.CommandBehavior, callback, stateObject, args.Command.CommandTimeout, isRetry: false, asyncWrite: true); }, endMethod: static (IAsyncResult asyncResult) => { @@ -3005,19 +3105,20 @@ private async Task ExecuteScalarUntilEndAsync(SqlDataReader reader, Canc } /// - public Task ExecuteXmlReaderAsync() - { - return ExecuteXmlReaderAsync(CancellationToken.None); - } + public Task ExecuteXmlReaderAsync() => + ExecuteXmlReaderAsync(CancellationToken.None); /// - public Task ExecuteXmlReaderAsync(CancellationToken cancellationToken) - => IsProviderRetriable ? - InternalExecuteXmlReaderWithRetryAsync(cancellationToken) : - InternalExecuteXmlReaderAsync(cancellationToken); + public Task ExecuteXmlReaderAsync(CancellationToken cancellationToken) => + IsProviderRetriable + ? InternalExecuteXmlReaderWithRetryAsync(cancellationToken) + : InternalExecuteXmlReaderAsync(cancellationToken); - private Task InternalExecuteXmlReaderWithRetryAsync(CancellationToken cancellationToken) - => RetryLogicProvider.ExecuteAsync(this, () => InternalExecuteXmlReaderAsync(cancellationToken), cancellationToken); + private Task InternalExecuteXmlReaderWithRetryAsync(CancellationToken cancellationToken) => + RetryLogicProvider.ExecuteAsync( + sender: this, + () => InternalExecuteXmlReaderAsync(cancellationToken), + cancellationToken); private Task InternalExecuteXmlReaderAsync(CancellationToken cancellationToken) { @@ -3211,14 +3312,17 @@ private static string UnquoteProcedureName(string name, out object groupNumber) if (sproc != null) { if (char.IsDigit(sproc[sproc.Length - 1])) - { // If last char is a digit, parse. + { + // If last char is a digit, parse. int semicolon = sproc.LastIndexOf(';'); if (semicolon != -1) - { // If we found a semicolon, obtain the integer. + { + // If we found a semicolon, obtain the integer. string part = sproc.Substring(semicolon + 1); int number = 0; if (int.TryParse(part, out number)) - { // No checking, just fail if this doesn't work. + { + // No checking, just fail if this doesn't work. groupNumber = number; sproc = sproc.Substring(0, semicolon); } @@ -3303,7 +3407,7 @@ internal void DeriveParameters() } // validate that we have a valid connection - ValidateCommand(false /*not async*/, nameof(DeriveParameters)); + ValidateCommand(isAsync: false); // Use common parser for SqlClient and OleDb - parse into 4 parts - Server, Catalog, Schema, ProcedureName string[] parsedSProc = MultipartIdentifier.ParseMultipartIdentifier(CommandText, "[\"", "]\"", Strings.SQL_SqlCommandCommandText, false); @@ -3379,7 +3483,8 @@ internal void DeriveParameters() } if (!string.IsNullOrEmpty(parsedSProc[2])) - { // SchemaName is 3rd element in parsed array + { + // SchemaName is 3rd element in parsed array SqlParameter param = paramsCmd.Parameters.Add(new SqlParameter("@procedure_schema", SqlDbType.NVarChar, 255)); param.Value = UnquoteProcedurePart(parsedSProc[2]); } @@ -3821,17 +3926,22 @@ private void PrepareTransparentEncryptionFinallyBlock(bool closeDataReader, /// Executes the reader after checking to see if we need to encrypt input parameters and then encrypting it if required. /// TryFetchInputParameterEncryptionInfo() -> ReadDescribeEncryptionParameterResults()-> EncryptInputParameters() ->RunExecuteReaderTds() /// - /// - /// /// /// /// /// /// /// - /// + /// /// - private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool returnStream, bool isAsync, int timeout, TaskCompletionSource completion, out Task returnTask, bool asyncWrite, out bool usedCache, bool inRetry) + private void PrepareForTransparentEncryption( + bool isAsync, + int timeout, + TaskCompletionSource completion, + out Task returnTask, + bool asyncWrite, + out bool usedCache, + bool isRetry) { // Fetch reader with input params Task fetchInputParameterEncryptionInfoTask = null; @@ -3851,7 +3961,7 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r // If we are not in Batch RPC and not already retrying, attempt to fetch the cipher MD for each parameter from the cache. // If this succeeds then return immediately, otherwise just fall back to the full crypto MD discovery. - if (!_batchRPCMode && !inRetry && (this._parameters != null && this._parameters.Count > 0) && SqlQueryMetadataCache.GetInstance().GetQueryMetadataIfExists(this)) + if (!_batchRPCMode && !isRetry && (this._parameters != null && this._parameters.Count > 0) && SqlQueryMetadataCache.GetInstance().GetQueryMetadataIfExists(this)) { usedCache = true; return; @@ -3881,7 +3991,7 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r out describeParameterEncryptionNeeded, out fetchInputParameterEncryptionInfoTask, out describeParameterEncryptionRpcOriginalRpcMap, - inRetry); + isRetry); Debug.Assert(describeParameterEncryptionNeeded || describeParameterEncryptionDataReader == null, "describeParameterEncryptionDataReader should be null if we don't need to request describe parameter encryption request."); @@ -3922,7 +4032,7 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r describeParameterEncryptionDataReader, describeParameterEncryptionRpcOriginalRpcMap, describeParameterEncryptionNeeded, - inRetry); + isRetry); decrementAsyncCountInFinallyBlock = false; } @@ -3939,7 +4049,7 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r describeParameterEncryptionDataReader, describeParameterEncryptionRpcOriginalRpcMap, describeParameterEncryptionNeeded, - inRetry); + isRetry); decrementAsyncCountInFinallyBlock = false; } @@ -3949,7 +4059,7 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r ReadDescribeEncryptionParameterResults( describeParameterEncryptionDataReader, describeParameterEncryptionRpcOriginalRpcMap, - inRetry); + isRetry); } #if DEBUG @@ -3980,9 +4090,9 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r } catch (Exception e) { - if (cachedAsyncState != null) + if (CachedAsyncState != null) { - cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); } if (ADP.IsCatchableExceptionType(e)) @@ -3996,7 +4106,7 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r private SqlDataReader GetParameterEncryptionDataReader(out Task returnTask, Task fetchInputParameterEncryptionInfoTask, SqlDataReader describeParameterEncryptionDataReader, - ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, bool describeParameterEncryptionNeeded, bool inRetry) + ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, bool describeParameterEncryptionNeeded, bool isRetry) { returnTask = AsyncHelper.CreateContinuationTaskWithState(fetchInputParameterEncryptionInfoTask, this, (object state) => @@ -4026,7 +4136,7 @@ private SqlDataReader GetParameterEncryptionDataReader(out Task returnTask, Task Debug.Assert(command._stateObj == null, "non-null state object in PrepareForTransparentEncryption."); // Read the results of describe parameter encryption. - command.ReadDescribeEncryptionParameterResults(describeParameterEncryptionDataReader, describeParameterEncryptionRpcOriginalRpcMap, inRetry); + command.ReadDescribeEncryptionParameterResults(describeParameterEncryptionDataReader, describeParameterEncryptionRpcOriginalRpcMap, isRetry); #if DEBUG // Failpoint to force the thread to halt to simulate cancellation of SqlCommand. @@ -4054,9 +4164,9 @@ private SqlDataReader GetParameterEncryptionDataReader(out Task returnTask, Task onFailure: static (Exception exception, object state) => { SqlCommand command = (SqlCommand)state; - if (command._cachedAsyncState != null) + if (command.CachedAsyncState != null) { - command._cachedAsyncState.ResetAsyncState(); + command.CachedAsyncState.ResetAsyncState(); } if (exception != null) @@ -4071,7 +4181,7 @@ private SqlDataReader GetParameterEncryptionDataReader(out Task returnTask, Task private SqlDataReader GetParameterEncryptionDataReaderAsync(out Task returnTask, SqlDataReader describeParameterEncryptionDataReader, - ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, bool describeParameterEncryptionNeeded, bool inRetry) + ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, bool describeParameterEncryptionNeeded, bool isRetry) { returnTask = Task.Run(() => { @@ -4101,7 +4211,7 @@ private SqlDataReader GetParameterEncryptionDataReaderAsync(out Task returnTask, // Read the results of describe parameter encryption. ReadDescribeEncryptionParameterResults(describeParameterEncryptionDataReader, - describeParameterEncryptionRpcOriginalRpcMap, inRetry); + describeParameterEncryptionRpcOriginalRpcMap, isRetry); #if DEBUG // Failpoint to force the thread to halt to simulate cancellation of SqlCommand. if (_sleepAfterReadDescribeEncryptionParameterResults) @@ -4140,13 +4250,14 @@ private SqlDataReader GetParameterEncryptionDataReaderAsync(out Task returnTask, /// /// Indicates if this is a retry from a failed call. /// - private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, - bool isAsync, - bool asyncWrite, - out bool inputParameterEncryptionNeeded, - out Task task, - out ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, - bool isRetry) + private SqlDataReader TryFetchInputParameterEncryptionInfo( + int timeout, + bool isAsync, + bool asyncWrite, + out bool inputParameterEncryptionNeeded, + out Task task, + out ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, + bool isRetry) { inputParameterEncryptionNeeded = false; task = null; @@ -4250,7 +4361,17 @@ private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, #endif // Execute the RPC. - return RunExecuteReaderTds(CommandBehavior.Default, runBehavior: RunBehavior.ReturnImmediately, returnStream: true, isAsync: isAsync, timeout: timeout, task: out task, asyncWrite: asyncWrite, inRetry: false, ds: null, describeParameterEncryptionRequest: true); + return RunExecuteReaderTds( + CommandBehavior.Default, + runBehavior: RunBehavior.ReturnImmediately, + returnStream: true, + isAsync: isAsync, + timeout: timeout, + task: out task, + asyncWrite: asyncWrite, + isRetry: false, + ds: null, + describeParameterEncryptionRequest: true); } else { @@ -4271,7 +4392,14 @@ private SqlParameter GetSqlParameterWithQueryText(string queryText) return sqlParam; } - + /// + /// Constructs the sp_describe_parameter_encryption request with the values from the original RPC call. + /// Prototype for <sp_describe_parameter_encryption> is + /// exec sp_describe_parameter_encryption @tsql=N'[SQL Statement]', @params=N'@p1 varbinary(256)' + /// + /// + /// + /// private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcRequest, ref _SqlRPC describeParameterEncryptionRequest, byte[] attestationParameters = null) { Debug.Assert(originalRpcRequest != null); @@ -4289,7 +4417,7 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques if (_batchRPCMode) { Debug.Assert(originalRpcRequest.systemParamCount > 0, - "originalRpcRequest didn't have at-least 1 parameter in _batchRPCMode, in PrepareDescribeParameterEncryptionRequest."); + "originalRpcRequest didn't have at-least 1 parameter in BatchRPCMode, in PrepareDescribeParameterEncryptionRequest."); text = (string)originalRpcRequest.systemParams[0].Value; //@tsql SqlParameter tsqlParam = describeParameterEncryptionRequest.systemParams[0]; @@ -4385,8 +4513,6 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques parameterList = BuildParamList(tdsParser, tempCollection, includeReturnValue: true); } - //@parameters - SqlParameter paramsParam = describeParameterEncryptionRequest.systemParams[1]; paramsParam.SqlDbType = ((parameterList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; paramsParam.Size = parameterList.Length; @@ -4409,7 +4535,10 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques /// Resultset from calling to sp_describe_parameter_encryption /// Readonly dictionary with the map of parameter encryption rpc requests with the corresponding original rpc requests. /// Indicates if this is a retry from a failed call. - private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, bool isRetry) + private void ReadDescribeEncryptionParameterResults( + SqlDataReader ds, + ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, + bool isRetry) { _SqlRPC rpc = null; int currentOrdinal = -1; @@ -4552,8 +4681,6 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi throw SQL.UnexpectedDescribeParamFormatParameterMetadata(); } - - // Find the RPC command that generated this tce request if (_batchRPCMode) { @@ -4580,7 +4707,6 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi int receivedMetadataCount = 0; if (!enclaveMetadataExists || ds.NextResult()) { - // Iterate over the parameter names to read the encryption type info while (ds.Read()) { @@ -4592,7 +4718,6 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi // When the RPC object gets reused, the parameter array has more parameters that the valid params for the command. // Null is used to indicate the end of the valid part of the array. Refer to GetRPCObject(). - for (int index = 0; index < userParamCount; index++) { SqlParameter sqlParameter = rpc.userParams[index]; @@ -4628,8 +4753,8 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi Debug.Assert(_activeConnection != null, @"_activeConnection should not be null"); SqlSecurityUtility.DecryptSymmetricKey(sqlParameter.CipherMetadata, _activeConnection, this); - // This is effective only for _batchRPCMode even though we set it for non-_batchRPCMode also, - // since for non-_batchRPCMode mode, paramoptions gets thrown away and reconstructed in BuildExecuteSql. + // This is effective only for BatchRPCMode even though we set it for non-BatchRPCMode also, + // since for non-BatchRPCMode mode, paramoptions gets thrown away and reconstructed in BuildExecuteSql. int options = (int)(rpc.userParamMap[index] >> 32); options |= TdsEnums.RPC_PARAM_ENCRYPTED; rpc.userParamMap[index] = ((((long)options) << 32) | (long)index); @@ -4643,7 +4768,6 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi // When the RPC object gets reused, the parameter array has more parameters that the valid params for the command. // Null is used to indicate the end of the valid part of the array. Refer to GetRPCObject(). - if (receivedMetadataCount != userParamCount) { for (int index = 0; index < userParamCount; index++) @@ -4731,16 +4855,39 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi } } - internal SqlDataReader RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, bool returnStream, [CallerMemberName] string method = "") + internal SqlDataReader RunExecuteReader( + CommandBehavior cmdBehavior, + RunBehavior runBehavior, + bool returnStream, + [CallerMemberName] string method = "") { Task unused; // sync execution - SqlDataReader reader = RunExecuteReader(cmdBehavior, runBehavior, returnStream, completion: null, timeout: CommandTimeout, task: out unused, usedCache: out bool usedCache, method: method); + SqlDataReader reader = RunExecuteReader( + cmdBehavior, + runBehavior, + returnStream, + completion: null, + timeout: CommandTimeout, + task: out unused, + usedCache: out _, + method: method); + Debug.Assert(unused == null, "returned task during synchronous execution"); return reader; } // task is created in case of pending asynchronous write, returned SqlDataReader should not be utilized until that task is complete - internal SqlDataReader RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, bool returnStream, TaskCompletionSource completion, int timeout, out Task task, out bool usedCache, bool asyncWrite = false, bool inRetry = false, [CallerMemberName] string method = "") + internal SqlDataReader RunExecuteReader( + CommandBehavior cmdBehavior, + RunBehavior runBehavior, + bool returnStream, + TaskCompletionSource completion, + int timeout, + out Task task, + out bool usedCache, + bool asyncWrite = false, + bool isRetry = false, + [CallerMemberName] string method = "") { bool isAsync = completion != null; usedCache = false; @@ -4758,7 +4905,7 @@ internal SqlDataReader RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior // this function may throw for an invalid connection // returns false for empty command text - if (!inRetry) + if (!isRetry) { ValidateCommand(isAsync, method); } @@ -4785,20 +4932,29 @@ internal SqlDataReader RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior if (IsColumnEncryptionEnabled) { Task returnTask = null; - PrepareForTransparentEncryption(cmdBehavior, returnStream, isAsync, timeout, completion, out returnTask, asyncWrite && isAsync, out usedCache, inRetry); + PrepareForTransparentEncryption(isAsync, timeout, completion, out returnTask, asyncWrite && isAsync, out usedCache, isRetry); Debug.Assert(usedCache || (isAsync == (returnTask != null)), @"if we didn't use the cache, returnTask should be null if and only if async is false."); long firstAttemptStart = ADP.TimerCurrent(); try { - return RunExecuteReaderTdsWithTransparentParameterEncryption(cmdBehavior, runBehavior, returnStream, isAsync, timeout, out task, asyncWrite && isAsync, inRetry: inRetry, ds: null, - describeParameterEncryptionRequest: false, describeParameterEncryptionTask: returnTask); + return RunExecuteReaderTdsWithTransparentParameterEncryption( + cmdBehavior, + runBehavior, + returnStream, + isAsync, + timeout, + out task, + asyncWrite && isAsync, + isRetry: isRetry, + ds: null, + describeParameterEncryptionTask: returnTask); } catch (EnclaveDelegate.RetryableEnclaveQueryExecutionException) { - if (inRetry) + if (isRetry) { throw; } @@ -4809,14 +4965,24 @@ internal SqlDataReader RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior InvalidateEnclaveSession(); - return RunExecuteReader(cmdBehavior, runBehavior, returnStream, completion, TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), out task, out usedCache, isAsync, inRetry: true, method: method); + return RunExecuteReader( + cmdBehavior, + runBehavior, + returnStream, + completion, + TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), + out task, + out usedCache, + isAsync, + isRetry: true, + method: method); } catch (SqlException ex) { // We only want to retry once, so don't retry if we are already in retry. // If we didn't use the cache, we don't want to retry. - if (inRetry || (!usedCache && !ShouldUseEnclaveBasedWorkflow)) + if (isRetry || (!usedCache && !ShouldUseEnclaveBasedWorkflow)) { throw; } @@ -4847,13 +5013,23 @@ internal SqlDataReader RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior InvalidateEnclaveSession(); - return RunExecuteReader(cmdBehavior, runBehavior, returnStream, completion, TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), out task, out usedCache, isAsync, inRetry: true, method: method); + return RunExecuteReader( + cmdBehavior, + runBehavior, + returnStream, + completion, + TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), + out task, + out usedCache, + isAsync, + isRetry: true, + method: method); } } } else { - return RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, timeout, out task, asyncWrite && isAsync, inRetry: inRetry); + return RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, timeout, out task, asyncWrite && isAsync, isRetry: isRetry); } } @@ -4866,9 +5042,8 @@ private SqlDataReader RunExecuteReaderTdsWithTransparentParameterEncryption( int timeout, out Task task, bool asyncWrite, - bool inRetry, + bool isRetry, SqlDataReader ds = null, - bool describeParameterEncryptionRequest = false, Task describeParameterEncryptionTask = null) { Debug.Assert(!asyncWrite || isAsync, "AsyncWrite should be always accompanied by Async"); @@ -4888,7 +5063,7 @@ private SqlDataReader RunExecuteReaderTdsWithTransparentParameterEncryption( SqlCommand command = (SqlCommand)state; Task subTask = null; command.GenerateEnclavePackage(); - command.RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, TdsParserStaticMethods.GetRemainingTimeout(timeout, parameterEncryptionStart), out subTask, asyncWrite, inRetry, ds); + command.RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, TdsParserStaticMethods.GetRemainingTimeout(timeout, parameterEncryptionStart), out subTask, asyncWrite, isRetry, ds); if (subTask == null) { completion.SetResult(null); @@ -4900,7 +5075,7 @@ private SqlDataReader RunExecuteReaderTdsWithTransparentParameterEncryption( }, onFailure: static (Exception exception, object state) => { - ((SqlCommand)state)._cachedAsyncState?.ResetAsyncState(); + ((SqlCommand)state).CachedAsyncState?.ResetAsyncState(); if (exception != null) { throw exception; @@ -4908,7 +5083,7 @@ private SqlDataReader RunExecuteReaderTdsWithTransparentParameterEncryption( }, onCancellation: static (object state) => { - ((SqlCommand)state)._cachedAsyncState?.ResetAsyncState(); + ((SqlCommand)state).CachedAsyncState?.ResetAsyncState(); } ); task = completion.Task; @@ -4918,7 +5093,7 @@ private SqlDataReader RunExecuteReaderTdsWithTransparentParameterEncryption( { // Synchronous execution. GenerateEnclavePackage(); - return RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, timeout, out task, asyncWrite, inRetry, ds); + return RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, timeout, out task, asyncWrite, isRetry, ds); } } @@ -4967,7 +5142,17 @@ private void GenerateEnclavePackage() } } - private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, bool returnStream, bool isAsync, int timeout, out Task task, bool asyncWrite, bool inRetry, SqlDataReader ds = null, bool describeParameterEncryptionRequest = false) + private SqlDataReader RunExecuteReaderTds( + CommandBehavior cmdBehavior, + RunBehavior runBehavior, + bool returnStream, + bool isAsync, + int timeout, + out Task task, + bool asyncWrite, + bool isRetry, + SqlDataReader ds = null, + bool describeParameterEncryptionRequest = false) { Debug.Assert(!asyncWrite || isAsync, "AsyncWrite should be always accompanied by Async"); @@ -4986,7 +5171,7 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi TaskCompletionSource completion = new TaskCompletionSource(); _activeConnection.RegisterWaitingForReconnect(completion.Task); _reconnectionCompletionSource = completion; - RunExecuteReaderTdsSetupReconnectContinuation(cmdBehavior, runBehavior, returnStream, isAsync, timeout, asyncWrite, inRetry, ds, reconnectTask, reconnectionStart, completion); + RunExecuteReaderTdsSetupReconnectContinuation(cmdBehavior, runBehavior, returnStream, isAsync, timeout, asyncWrite, isRetry, ds, reconnectTask, reconnectionStart, completion); task = completion.Task; return ds; } @@ -5096,7 +5281,7 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi if (_execType == EXECTYPE.PREPARED) { - Debug.Assert(this.IsPrepared && ((int)_prepareHandle != -1), "invalid attempt to call sp_execute without a handle!"); + Debug.Assert(IsPrepared && _prepareHandle != s_cachedInvalidPrepareHandle, "invalid attempt to call sp_execute without a handle!"); rpc = BuildExecute(inSchema); } else if (_execType == EXECTYPE.PREPAREPENDING) @@ -5170,7 +5355,7 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi } else { - cachedAsyncState.SetAsyncReaderState(ds, runBehavior, optionSettings); + CachedAsyncState.SetAsyncReaderState(ds, runBehavior, optionSettings); } } else @@ -5186,7 +5371,8 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi { SqlInternalConnectionTds innerConnectionTds = (_activeConnection.InnerConnection as SqlInternalConnectionTds); if (innerConnectionTds != null) - { // it may be closed + { + // it may be closed innerConnectionTds.DecrementAsyncCount(); } } @@ -5214,7 +5400,7 @@ private Task RunExecuteReaderTdsSetupContinuation(RunBehavior runBehavior, SqlDa { SqlConnection sqlConnection = (SqlConnection)state; sqlConnection.GetOpenTdsConnection(); // it will throw if connection is closed - cachedAsyncState.SetAsyncReaderState(ds, runBehavior, optionSettings); + CachedAsyncState.SetAsyncReaderState(ds, runBehavior, optionSettings); }, onFailure: static (Exception exc, object state) => { @@ -5225,7 +5411,7 @@ private Task RunExecuteReaderTdsSetupContinuation(RunBehavior runBehavior, SqlDa } // This is in its own method to avoid always allocating the lambda in RunExecuteReaderTds - private void RunExecuteReaderTdsSetupReconnectContinuation(CommandBehavior cmdBehavior, RunBehavior runBehavior, bool returnStream, bool isAsync, int timeout, bool asyncWrite, bool inRetry, SqlDataReader ds, Task reconnectTask, long reconnectionStart, TaskCompletionSource completion) + private void RunExecuteReaderTdsSetupReconnectContinuation(CommandBehavior cmdBehavior, RunBehavior runBehavior, bool returnStream, bool isAsync, int timeout, bool asyncWrite, bool isRetry, SqlDataReader ds, Task reconnectTask, long reconnectionStart, TaskCompletionSource completion) { CancellationTokenSource timeoutCTS = new CancellationTokenSource(); AsyncHelper.SetTimeoutException(completion, timeout, static () => SQL.CR_ReconnectTimeout(), timeoutCTS.Token); @@ -5239,7 +5425,7 @@ private void RunExecuteReaderTdsSetupReconnectContinuation(CommandBehavior cmdBe Interlocked.CompareExchange(ref _reconnectionCompletionSource, null, completion); timeoutCTS.Cancel(); Task subTask; - RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, TdsParserStaticMethods.GetRemainingTimeout(timeout, reconnectionStart), out subTask, asyncWrite, inRetry, ds); + RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, TdsParserStaticMethods.GetRemainingTimeout(timeout, reconnectionStart), out subTask, asyncWrite, isRetry, ds); if (subTask == null) { completion.SetResult(null); @@ -5258,11 +5444,11 @@ private void RunExecuteReaderTdsSetupReconnectContinuation(CommandBehavior cmdBe private SqlDataReader CompleteAsyncExecuteReader(bool isInternal = false, bool forDescribeParameterEncryption = false) { - SqlDataReader ds = cachedAsyncState.CachedAsyncReader; // should not be null + SqlDataReader ds = CachedAsyncState.CachedAsyncReader; // should not be null bool processFinallyBlock = true; try { - FinishExecuteReader(ds, cachedAsyncState.CachedRunBehavior, cachedAsyncState.CachedSetOptions, isInternal, forDescribeParameterEncryption, shouldCacheForAlwaysEncrypted: !forDescribeParameterEncryption); + FinishExecuteReader(ds, CachedAsyncState.CachedRunBehavior, CachedAsyncState.CachedSetOptions, isInternal, forDescribeParameterEncryption, shouldCacheForAlwaysEncrypted: !forDescribeParameterEncryption); } catch (Exception e) { @@ -5276,7 +5462,7 @@ private SqlDataReader CompleteAsyncExecuteReader(bool isInternal = false, bool f // Don't reset the state for internal End. The user End will do that eventually. if (!isInternal) { - cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); } PutStateObject(); } @@ -5348,6 +5534,7 @@ private void FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, stri ds.Bind(_stateObj); _stateObj = null; // the reader now owns this... ds.ResetOptionsString = resetOptionsString; + // bind this reader to this connection now _activeConnection.AddWeakReference(ds, SqlReferenceCollection.DataReaderTag); @@ -5402,6 +5589,16 @@ private void FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, stri } } + /// + public SqlCommand Clone() + { + SqlCommand clone = new SqlCommand(this); + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.Clone | API | Object Id {0}, Clone Object Id {1}, Client Connection Id {2}", ObjectID, clone.ObjectID, Connection?.ClientConnectionId); + return clone; + } + + object ICloneable.Clone() => + Clone(); private Task RegisterForConnectionCloseNotification(Task outerTask) { @@ -5493,16 +5690,17 @@ private void ValidateCommand(bool isAsync, [CallerMemberName] string method = "" private void ValidateAsyncCommand() { - if (_cachedAsyncState != null && _cachedAsyncState.PendingAsyncOperation) - { // Enforce only one pending async execute at a time. - if (cachedAsyncState.IsActiveConnectionValid(_activeConnection)) + if (CachedAsyncState != null && CachedAsyncState.PendingAsyncOperation) + { + // Enforce only one pending async execute at a time. + if (CachedAsyncState.IsActiveConnectionValid(_activeConnection)) { throw SQL.PendingBeginXXXExists(); } else { _stateObj = null; // Session was re-claimed by session pool upon connection close. - _cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); } } } @@ -5930,8 +6128,8 @@ private void GetRPCObject(int systemParamCount, int userParamCount, ref _SqlRPC rpc.needsFetchParameterEncryptionMetadata = false; int currentCount = rpc.systemParams?.Length ?? 0; - // Make sure there is enough space in the parameters and paramoptions arrays + // Make sure there is enough space in the parameters and paramoptions arrays if (currentCount < systemParamCount) { Array.Resize(ref rpc.systemParams, systemParamCount); @@ -6019,6 +6217,7 @@ private void SetUpRPCParameters(_SqlRPC rpc, bool inSchema, SqlParameterCollecti rpc.userParamMap[userParamCount] = ((((long)options) << 32) | (long)index); userParamCount += 1; + // Must set parameter option bit for LOB_COOKIE if unfilled LazyMat blob } } @@ -6130,7 +6329,6 @@ private void BuildRPC(bool inSchema, SqlParameterCollection parameters, ref _Sql // 4-part name 1 + 128 + 1 + 1 + 1 + 128 + 1 + 1 + 1 + 128 + 1 + 1 + 1 + 128 + 1 = 523 // each char takes 2 bytes. 523 * 2 = 1046 int commandTextLength = ADP.CharSize * CommandText.Length; - if (commandTextLength <= MaxRPCNameLength) { rpc.rpcName = CommandText; // just get the raw command text @@ -6151,9 +6349,9 @@ private void BuildRPC(bool inSchema, SqlParameterCollection parameters, ref _Sql // private _SqlRPC BuildExecute(bool inSchema) { - Debug.Assert((int)_prepareHandle != -1, "Invalid call to sp_execute without a valid handle!"); - const int systemParameterCount = 1; + Debug.Assert(_prepareHandle != s_cachedInvalidPrepareHandle, "Invalid call to sp_execute without a valid handle!"); + const int systemParameterCount = 1; int userParameterCount = CountSendableParameters(_parameters); _SqlRPC rpc = null; @@ -6181,7 +6379,7 @@ private _SqlRPC BuildExecute(bool inSchema) private void BuildExecuteSql(CommandBehavior behavior, string commandText, SqlParameterCollection parameters, ref _SqlRPC rpc) { - Debug.Assert((int)_prepareHandle == -1, "This command has an existing handle, use sp_execute!"); + Debug.Assert(_prepareHandle == s_cachedInvalidPrepareHandle, "This command has an existing handle, use sp_execute!"); Debug.Assert(CommandType.Text == this.CommandType, "invalid use of sp_executesql for stored proc invocation!"); int systemParamCount; SqlParameter sqlParam; @@ -6348,9 +6546,7 @@ internal string BuildParamList(TdsParser parser, SqlParameterCollection paramete StringBuilder paramList = new StringBuilder(); bool fAddSeparator = false; - int count = 0; - - count = parameters.Count; + int count = parameters.Count; for (int i = 0; i < count; i++) { SqlParameter sqlParam = parameters[i]; @@ -6834,63 +7030,19 @@ internal SqlException GetErrors(int commandIndex) return result; } - -#if DEBUG - internal void CompletePendingReadWithSuccess(bool resetForcePendingReadsToWait) - { - var stateObj = _stateObj; - if (stateObj != null) - { - stateObj.CompletePendingReadWithSuccess(resetForcePendingReadsToWait); - } - else - { - var tempCachedAsyncState = cachedAsyncState; - if (tempCachedAsyncState != null) - { - var reader = tempCachedAsyncState.CachedAsyncReader; - if (reader != null) - { - reader.CompletePendingReadWithSuccess(resetForcePendingReadsToWait); - } - } - } - } - - internal void CompletePendingReadWithFailure(int errorCode, bool resetForcePendingReadsToWait) - { - var stateObj = _stateObj; - if (stateObj != null) - { - stateObj.CompletePendingReadWithFailure(errorCode, resetForcePendingReadsToWait); - } - else - { - var tempCachedAsyncState = _cachedAsyncState; - if (tempCachedAsyncState != null) - { - var reader = tempCachedAsyncState.CachedAsyncReader; - if (reader != null) - { - reader.CompletePendingReadWithFailure(errorCode, resetForcePendingReadsToWait); - } - } - } - } -#endif - internal static void CancelIgnoreFailureCallback(object state) + private static void CancelIgnoreFailureCallback(object state) { SqlCommand command = (SqlCommand)state; command.CancelIgnoreFailure(); } - internal void CancelIgnoreFailure() + private void CancelIgnoreFailure() { // This method is used to route CancellationTokens to the Cancel method. // Cancellation is a suggestion, and exceptions should be ignored // rather than allowed to be unhandled, as there is no way to route // them to the caller. It would be expected that the error will be - // observed anyway from the regular method. An example is canceling + // observed anyway from the regular method. An example is cancelling // an operation on a closed connection. try { @@ -6909,22 +7061,17 @@ private void NotifyDependency() } } - object ICloneable.Clone() => Clone(); - - - /// - public SqlCommand Clone() - { - SqlCommand clone = new SqlCommand(this); - SqlClientEventSource.Log.TryTraceEvent("SqlCommand.Clone | API | Object Id {0}, Clone Object Id {1}, Client Connection Id {2}", ObjectID, clone.ObjectID, Connection?.ClientConnectionId); - return clone; - } - private void WriteBeginExecuteEvent() { SqlClientEventSource.Log.TryBeginExecuteEvent(ObjectID, Connection?.DataSource, Connection?.Database, CommandText, Connection?.ClientConnectionId); } + /// + /// Writes and end execute event in Event Source. + /// + /// True if SQL command finished successfully, otherwise false. + /// Gets a number that identifies the type of error. + /// True if SQL command was executed synchronously, otherwise false. private void WriteEndExecuteEvent(bool success, int? sqlExceptionNumber, bool synchronous) { if (SqlClientEventSource.Log.IsExecutionTraceEnabled()) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs index ce5e960f8d..d4f13efd80 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -31,13 +31,6 @@ namespace Microsoft.Data.SqlClient [DesignerCategory("")] public sealed partial class SqlConnection : DbConnection, ICloneable { - private enum CultureCheckState : uint - { - Unknown = 0, - Standard = 1, - Invariant = 2 - } - private bool _AsyncCommandInProgress; // SQLStatistics support @@ -75,9 +68,6 @@ private enum CultureCheckState : uint // using SqlConnection.Open() method. internal bool _applyTransientFaultHandling = false; - // status of invariant culture environment check - private static CultureCheckState _cultureCheckState; - // System column encryption key store providers are added by default private static readonly Dictionary s_systemColumnEncryptionKeyStoreProviders = new(capacity: 3, comparer: StringComparer.OrdinalIgnoreCase) @@ -91,6 +81,7 @@ private static readonly Dictionary private IReadOnlyDictionary _customColumnEncryptionKeyStoreProviders; private Func> _accessTokenCallback; + private SspiContextProvider _sspiContextProvider; internal bool HasColumnEncryptionKeyStoreProvidersRegistered => _customColumnEncryptionKeyStoreProviders is not null && _customColumnEncryptionKeyStoreProviders.Count > 0; @@ -650,7 +641,7 @@ public override string ConnectionString CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessTokenCallback(connectionOptions); } } - ConnectionString_Set(new SqlConnectionPoolKey(value, _credential, _accessToken, _accessTokenCallback)); + ConnectionString_Set(new SqlConnectionPoolKey(value, _credential, _accessToken, _accessTokenCallback, _sspiContextProvider)); _connectionString = value; // Change _connectionString value only after value is validated CacheConnectionStringProperties(); } @@ -710,7 +701,7 @@ public string AccessToken } // Need to call ConnectionString_Set to do proper pool group check - ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, credential: _credential, accessToken: value, accessTokenCallback: null)); + ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, credential: _credential, accessToken: value, accessTokenCallback: null, sspiContextProvider: null)); _accessToken = value; } } @@ -733,11 +724,21 @@ public Func [ResDescription(StringsHelper.ResourceNames.SqlConnection_Database)] [ResCategory(StringsHelper.ResourceNames.SqlConnection_DataSource)] @@ -1035,7 +1036,7 @@ public SqlCredential Credential _credential = value; // Need to call ConnectionString_Set to do proper pool group check - ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, _credential, accessToken: _accessToken, accessTokenCallback: _accessTokenCallback)); + ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, _credential, accessToken: _accessToken, accessTokenCallback: _accessTokenCallback, sspiContextProvider: null)); } } @@ -1274,7 +1275,7 @@ public override void ChangeDatabase(string database) /// public static void ClearAllPools() { - SqlConnectionFactory.SingletonInstance.ClearAllPools(); + SqlConnectionFactory.Instance.ClearAllPools(); } /// @@ -1285,7 +1286,7 @@ public static void ClearPool(SqlConnection connection) DbConnectionOptions connectionOptions = connection.UserConnectionOptions; if (connectionOptions != null) { - SqlConnectionFactory.SingletonInstance.ClearPool(connection); + SqlConnectionFactory.Instance.ClearPool(connection); } } @@ -1956,48 +1957,9 @@ private bool TryOpen(TaskCompletionSource retry, SqlConnec { SqlConnectionString connectionOptions = (SqlConnectionString)ConnectionOptions; - if (_cultureCheckState != CultureCheckState.Standard) + if (LocalAppContextSwitches.GlobalizationInvariantMode) { - // .NET Core 2.0 and up supports a Globalization Invariant Mode to reduce the size of - // required libraries for applications which don't need globalization support. SqlClient - // requires those libraries for core functionality and will throw exceptions later if they - // are not present. Throwing on open with a meaningful message helps identify the issue. - if (_cultureCheckState == CultureCheckState.Unknown) - { - // check if invariant state has been set by appcontext switch directly - if (AppContext.TryGetSwitch("System.Globalization.Invariant", out bool isEnabled) && isEnabled) - { - _cultureCheckState = CultureCheckState.Invariant; - } - else - { - // check if invariant state has been set through environment variables - string envValue = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"); - if (string.Equals(envValue, bool.TrueString, StringComparison.OrdinalIgnoreCase) || string.Equals(envValue, "1", StringComparison.OrdinalIgnoreCase)) - { - _cultureCheckState = CultureCheckState.Invariant; - } - else - { - // if it hasn't been manually set it could still apply if the os doesn't have - // icu libs installed or is a native binary with icu support trimmed away - // netcore 3.1 to net5 do not throw in attempting to create en-us in inariant mode - // net6 and greater will throw so catch and infer invariant mode from the exception - try - { - _cultureCheckState = CultureInfo.GetCultureInfo("en-US").EnglishName.Contains("Invariant") ? CultureCheckState.Invariant : CultureCheckState.Standard; - } - catch (CultureNotFoundException) - { - _cultureCheckState = CultureCheckState.Invariant; - } - } - } - } - if (_cultureCheckState == CultureCheckState.Invariant) - { - throw SQL.GlobalizationInvariantModeNotSupported(); - } + throw SQL.GlobalizationInvariantModeNotSupported(); } _applyTransientFaultHandling = (!overrides.HasFlag(SqlConnectionOverrides.OpenWithoutRetry) && connectionOptions != null && connectionOptions.ConnectRetryCount > 0); @@ -2265,9 +2227,9 @@ public static void ChangePassword(string connectionString, string newPassword) throw ADP.InvalidArgumentLength(nameof(newPassword), TdsEnums.MAXLEN_NEWPASSWORD); } - SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential: null, accessToken: null, accessTokenCallback: null); + SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential: null, accessToken: null, accessTokenCallback: null, sspiContextProvider: null); - SqlConnectionString connectionOptions = SqlConnectionFactory.FindSqlConnectionOptions(key); + SqlConnectionString connectionOptions = SqlConnectionFactory.Instance.FindSqlConnectionOptions(key); if (connectionOptions.IntegratedSecurity) { throw SQL.ChangePasswordConflictsWithSSPI(); @@ -2314,9 +2276,9 @@ public static void ChangePassword(string connectionString, SqlCredential credent throw ADP.InvalidArgumentLength(nameof(newSecurePassword), TdsEnums.MAXLEN_NEWPASSWORD); } - SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential, accessToken: null, accessTokenCallback: null); + SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential, accessToken: null, accessTokenCallback: null, sspiContextProvider: null); - SqlConnectionString connectionOptions = SqlConnectionFactory.FindSqlConnectionOptions(key); + SqlConnectionString connectionOptions = SqlConnectionFactory.Instance.FindSqlConnectionOptions(key); // Check for connection string values incompatible with SqlCredential if (!string.IsNullOrEmpty(connectionOptions.UserID) || !string.IsNullOrEmpty(connectionOptions.Password)) @@ -2352,9 +2314,9 @@ private static void ChangePassword(string connectionString, SqlConnectionString { con?.Dispose(); } - SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential, accessToken: null, accessTokenCallback: null); + SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential, accessToken: null, accessTokenCallback: null, sspiContextProvider: null); - SqlConnectionFactory.SingletonInstance.ClearPool(key); + SqlConnectionFactory.Instance.ClearPool(key); } internal Task RegisterForConnectionCloseNotification(Task outerTask, object value, int tag) @@ -2547,11 +2509,10 @@ internal object GetUdtValue(object value, SqlMetaDataPriv metaData, bool returnD } } - internal byte[] GetBytes(object o, out Format format, out int maxSize) + internal byte[] GetBytes(object o, out int maxSize) { SqlUdtInfo attr = GetInfoFromType(o.GetType()); maxSize = attr.MaxByteSize; - format = attr.SerializationFormat; if (maxSize < -1 || maxSize >= ushort.MaxValue) { diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionHelper.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionHelper.cs index 133854c1f0..9095c9b454 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionHelper.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionHelper.cs @@ -17,7 +17,7 @@ namespace Microsoft.Data.SqlClient { public sealed partial class SqlConnection : DbConnection { - private static readonly DbConnectionFactory s_connectionFactory = SqlConnectionFactory.SingletonInstance; + private static readonly SqlConnectionFactory s_connectionFactory = SqlConnectionFactory.Instance; private DbConnectionOptions _userConnectionOptions; private DbConnectionPoolGroup _poolGroup; @@ -42,7 +42,7 @@ internal int CloseCount } } - internal DbConnectionFactory ConnectionFactory + internal SqlConnectionFactory ConnectionFactory { get { @@ -156,9 +156,7 @@ override protected DbCommand CreateDbCommand() { using (TryEventScope.Create(" {0}", ObjectID)) { - DbCommand command = null; - DbProviderFactory providerFactory = ConnectionFactory.ProviderFactory; - command = providerFactory.CreateCommand(); + DbCommand command = SqlClientFactory.Instance.CreateCommand(); command.Connection = this; return command; } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index c75eb35a0e..24082bf046 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -135,6 +135,7 @@ internal sealed class SqlInternalConnectionTds : SqlInternalConnection, IDisposa SqlFedAuthToken _fedAuthToken = null; internal byte[] _accessTokenInBytes; internal readonly Func> _accessTokenCallback; + internal readonly SspiContextProvider _sspiContextProvider; private readonly ActiveDirectoryAuthenticationTimeoutRetryHelper _activeDirectoryAuthTimeoutRetryHelper; @@ -460,8 +461,8 @@ internal SqlInternalConnectionTds( bool applyTransientFaultHandling = false, string accessToken = null, IDbConnectionPool pool = null, - Func> accessTokenCallback = null) : base(connectionOptions) + Func> accessTokenCallback = null, + SspiContextProvider sspiContextProvider = null) : base(connectionOptions) { #if DEBUG if (reconnectSessionData != null) @@ -514,6 +515,7 @@ internal SqlInternalConnectionTds( } _accessTokenCallback = accessTokenCallback; + _sspiContextProvider = sspiContextProvider; _activeDirectoryAuthTimeoutRetryHelper = new ActiveDirectoryAuthenticationTimeoutRetryHelper(); @@ -3078,9 +3080,13 @@ internal bool ThreadHasParserLockForClose } } - internal override bool TryReplaceConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) + internal override bool TryReplaceConnection( + DbConnection outerConnection, + SqlConnectionFactory connectionFactory, + TaskCompletionSource retry, + DbConnectionOptions userOptions) { - return base.TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions); + return TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions); } } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Unix.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Unix.cs deleted file mode 100644 index c993df824c..0000000000 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Unix.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Microsoft.Data.SqlClient.ManagedSni; - -namespace Microsoft.Data.SqlClient -{ - sealed internal partial class TdsParser - { - internal void PostReadAsyncForMars() - { - // No-Op - } - - private void LoadSSPILibrary() - { - // No - Op - } - - private void WaitForSSLHandShakeToComplete(ref uint error, ref int protocolVersion) - { - // No - Op - } - } -} diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs deleted file mode 100644 index 2c924b3b79..0000000000 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Diagnostics; - -namespace Microsoft.Data.SqlClient -{ - internal sealed partial class TdsParser - { - internal void PostReadAsyncForMars() - { - if (TdsParserStateObjectFactory.UseManagedSNI) - return; - - // HACK HACK HACK - for Async only - // Have to post read to initialize MARS - will get callback on this when connection goes - // down or is closed. - - PacketHandle temp = default; - uint error = TdsEnums.SNI_SUCCESS; - - _pMarsPhysicalConObj.IncrementPendingCallbacks(); - SessionHandle handle = _pMarsPhysicalConObj.SessionHandle; - // we do not need to consider partial packets when making this read because we - // expect this read to pend. a partial packet should not exist at setup of the - // parser - Debug.Assert(_physicalStateObj.PartialPacket==null); - temp = _pMarsPhysicalConObj.ReadAsync(handle, out error); - - Debug.Assert(temp.Type == PacketHandle.NativePointerType, "unexpected packet type when requiring NativePointer"); - - if (temp.NativePointer != IntPtr.Zero) - { - // Be sure to release packet, otherwise it will be leaked by native. - _pMarsPhysicalConObj.ReleasePacket(temp); - } - - Debug.Assert(IntPtr.Zero == temp.NativePointer, "unexpected syncReadPacket without corresponding SNIPacketRelease"); - if (TdsEnums.SNI_SUCCESS_IO_PENDING != error) - { - Debug.Assert(TdsEnums.SNI_SUCCESS != error, "Unexpected successful read async on physical connection before enabling MARS!"); - _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); - ThrowExceptionAndWarning(_physicalStateObj); - } - } - - private void WaitForSSLHandShakeToComplete(ref uint error, ref int protocolVersion) - { - // in the case where an async connection is made, encryption is used and Windows Authentication is used, - // wait for SSL handshake to complete, so that the SSL context is fully negotiated before we try to use its - // Channel Bindings as part of the Windows Authentication context build (SSL handshake must complete - // before calling SNISecGenClientContext). - error = _physicalStateObj.WaitForSSLHandShakeToComplete(out protocolVersion); - if (error != TdsEnums.SNI_SUCCESS) - { - _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); - ThrowExceptionAndWarning(_physicalStateObj); - } - } - } // tdsparser -}//namespace diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index 4b6a82b7ba..8bb547497a 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -11,9 +11,8 @@ using System.Diagnostics; using System.Globalization; using System.IO; -#if NET using System.Security.Authentication; -#else +#if NETFRAMEWORK using System.Runtime.CompilerServices; #endif using System.Text; @@ -69,7 +68,6 @@ internal sealed partial class TdsParser // Constants private const int constBinBufferSize = 4096; // Size of the buffer used to read input parameter of type Stream private const int constTextBufferSize = 4096; // Size of the buffer (in chars) user to read input parameter of type TextReader - private const string enableTruncateSwitch = "Switch.Microsoft.Data.SqlClient.TruncateScaledDecimal"; // for applications that need to maintain backwards compatibility with the previous behavior // State variables internal TdsParserState _state = TdsParserState.Closed; // status flag for connection @@ -204,16 +202,6 @@ internal SqlInternalConnectionTds Connection } } - private static bool EnableTruncateSwitch - { - get - { - bool value; - value = AppContext.TryGetSwitch(enableTruncateSwitch, out value) ? value : false; - return value; - } - } - internal SqlInternalTransaction CurrentTransaction { get @@ -409,7 +397,7 @@ internal void Connect(ServerInfo serverInfo, // AD Integrated behaves like Windows integrated when connecting to a non-fedAuth server if (integratedSecurity || authType == SqlAuthenticationMethod.ActiveDirectoryIntegrated) { - _authenticationProvider = _physicalStateObj.CreateSspiContextProvider(); + _authenticationProvider = Connection._sspiContextProvider ?? _physicalStateObj.CreateSspiContextProvider(); SqlClientEventSource.Log.TryTraceEvent("TdsParser.Connect | SEC | SSPI or Active Directory Authentication Library loaded for SQL Server based integrated authentication"); } @@ -631,7 +619,7 @@ internal void EnableMars() // Cache physical stateObj and connection. _pMarsPhysicalConObj = _physicalStateObj; - if (TdsParserStateObjectFactory.UseManagedSNI) + if (LocalAppContextSwitches.UseManagedNetworking) _pMarsPhysicalConObj.IncrementPendingCallbacks(); uint info = 0; @@ -643,7 +631,13 @@ internal void EnableMars() ThrowExceptionAndWarning(_physicalStateObj); } - PostReadAsyncForMars(); + error = _pMarsPhysicalConObj.PostReadAsyncForMars(_physicalStateObj); + if (error != TdsEnums.SNI_SUCCESS_IO_PENDING) + { + Debug.Assert(error != TdsEnums.SNI_SUCCESS, "Unexpected successful read async on physical connection before enabling MARS!"); + _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); + ThrowExceptionAndWarning(_physicalStateObj); + } _physicalStateObj = CreateSession(); // Create and open default MARS stateObj and connection. } @@ -899,10 +893,24 @@ private void EnableSsl(uint info, SqlConnectionEncryptOption encrypt, bool integ ThrowExceptionAndWarning(_physicalStateObj); } - int protocolVersion = 0; - WaitForSSLHandShakeToComplete(ref error, ref protocolVersion); + SslProtocols protocol = 0; + + // in the case where an async connection is made, encryption is used and Windows Authentication is used, + // wait for SSL handshake to complete, so that the SSL context is fully negotiated before we try to use its + // Channel Bindings as part of the Windows Authentication context build (SSL handshake must complete + // before calling SNISecGenClientContext). +#if NET + if (OperatingSystem.IsWindows()) +#endif + { + error = _physicalStateObj.WaitForSSLHandShakeToComplete(out protocol); + if (error != TdsEnums.SNI_SUCCESS) + { + _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); + ThrowExceptionAndWarning(_physicalStateObj); + } + } - SslProtocols protocol = (SslProtocols)protocolVersion; string warningMessage = protocol.GetProtocolWarning(); if (!string.IsNullOrEmpty(warningMessage)) { @@ -1489,7 +1497,7 @@ internal SqlError ProcessSNIError(TdsParserStateObject stateObj) * !=null | == 0 | replace text left of errorMessage */ - if (TdsParserStateObjectFactory.UseManagedSNI) + if (LocalAppContextSwitches.UseManagedNetworking) { Debug.Assert(!string.IsNullOrEmpty(details.ErrorMessage) || details.SniErrorNumber != 0, "Empty error message received from SNI"); SqlClientEventSource.Log.TryAdvancedTraceEvent(" Empty error message received from SNI. Error Message = {0}, SNI Error Number ={1}", details.ErrorMessage, details.SniErrorNumber); @@ -1538,7 +1546,7 @@ internal SqlError ProcessSNIError(TdsParserStateObject stateObj) } else { - if (TdsParserStateObjectFactory.UseManagedSNI) + if (LocalAppContextSwitches.UseManagedNetworking) { // SNI error. Append additional error message info if available and hasn't been included. string sniLookupMessage = SQL.GetSNIErrorMessage(details.SniErrorNumber); @@ -7653,7 +7661,7 @@ internal static SqlDecimal AdjustSqlDecimalScale(SqlDecimal d, int newScale) { if (d.Scale != newScale) { - bool round = !EnableTruncateSwitch; + bool round = !LocalAppContextSwitches.TruncateScaledDecimal; return SqlDecimal.AdjustScale(d, newScale - d.Scale, round); } @@ -7666,7 +7674,7 @@ internal static decimal AdjustDecimalScale(decimal value, int newScale) if (newScale != oldScale) { - bool round = !EnableTruncateSwitch; + bool round = !LocalAppContextSwitches.TruncateScaledDecimal; SqlDecimal num = new SqlDecimal(value); num = SqlDecimal.AdjustScale(num, newScale - oldScale, round); return num.Value; @@ -9943,7 +9951,6 @@ private Task TDSExecuteRPCAddParameter(TdsParserStateObject stateObj, SqlParamet { int maxSupportedSize = Is2008OrNewer ? int.MaxValue : short.MaxValue; byte[] udtVal = null; - Format format = Format.Native; if (string.IsNullOrEmpty(param.UdtTypeName)) { @@ -9977,7 +9984,7 @@ private Task TDSExecuteRPCAddParameter(TdsParserStateObject stateObj, SqlParamet } else { - udtVal = _connHandler.Connection.GetBytes(value, out format, out maxsize); + udtVal = _connHandler.Connection.GetBytes(value, out maxsize); } Debug.Assert(udtVal != null, "GetBytes returned null instance. Make sure that it always returns non-null value"); @@ -13128,11 +13135,10 @@ internal TdsOperationStatus TryReadPlpUnicodeCharsWithContinue(TdsParserStateObj if (canContinue) { - if (isContinuing || isStarting) - { - temp = stateObj.TryTakeSnapshotStorage() as char[]; - Debug.Assert(temp == null || length == int.MaxValue || temp.Length == length, "stored buffer length must be null or must have been created with the correct length"); - } + temp = stateObj.TryTakeSnapshotStorage() as char[]; + Debug.Assert(temp != null || !isContinuing, "if continuing stored buffer must be present to contain previous data to continue from"); + Debug.Assert(temp == null || length == int.MaxValue || temp.Length == length, "stored buffer length must be null or must have been created with the correct length"); + if (temp != null) { startOffset = stateObj.GetSnapshotTotalSize(); @@ -13148,7 +13154,7 @@ internal TdsOperationStatus TryReadPlpUnicodeCharsWithContinue(TdsParserStateObj supportRentedBuff: !canContinue, // do not use the arraypool if we are going to keep the buffer in the snapshot rentedBuff: ref buffIsRented, startOffset, - isStarting || isContinuing + canContinue ); if (result == TdsOperationStatus.Done) @@ -13175,7 +13181,7 @@ internal TdsOperationStatus TryReadPlpUnicodeCharsWithContinue(TdsParserStateObj } else if (result == TdsOperationStatus.NeedMoreData) { - if (isStarting || isContinuing) + if (canContinue) { stateObj.SetSnapshotStorage(temp); } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs index d7dc8b62f4..b802689b24 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs @@ -93,8 +93,6 @@ internal abstract void CreatePhysicalSNIHandle( internal abstract uint EnableSsl(ref uint info, bool tlsFirst, string serverCertificateFilename); - internal abstract uint WaitForSSLHandShakeToComplete(out int protocolVersion); - internal abstract void Dispose(); internal abstract uint CheckConnection(); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs index ef4523eafd..2b173fb087 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Security.Authentication; using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Common; @@ -365,6 +366,8 @@ internal override uint EnableMars(ref uint info) return TdsEnums.SNI_ERROR; } + internal override uint PostReadAsyncForMars(TdsParserStateObject physicalStateObject) => TdsEnums.SNI_SUCCESS_IO_PENDING; + internal override uint EnableSsl(ref uint info, bool tlsFirst, string serverCertificateFilename) { SniHandle sessionHandle = GetSessionSNIHandleHandleOrThrow(); @@ -386,10 +389,10 @@ internal override uint SetConnectionBufferSize(ref uint unsignedPacketSize) return TdsEnums.SNI_SUCCESS; } - internal override uint WaitForSSLHandShakeToComplete(out int protocolVersion) + internal override uint WaitForSSLHandShakeToComplete(out SslProtocols protocolVersion) { protocolVersion = GetSessionSNIHandleHandleOrThrow().ProtocolVersion; - return 0; + return TdsEnums.SNI_SUCCESS; } internal override SniErrorDetails GetErrorDetails() diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs index 4f2ccaab83..6f3bdb915a 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs @@ -385,6 +385,43 @@ internal override uint DisableSsl() internal override uint EnableMars(ref uint info) => SniNativeWrapper.SniAddProvider(Handle, Provider.SMUX_PROV, ref info); + internal override uint PostReadAsyncForMars(TdsParserStateObject physicalStateObject) + { + // HACK HACK HACK - for Async only + // Have to post read to initialize MARS - will get callback on this when connection goes + // down or is closed. + + PacketHandle temp = default; + uint error = TdsEnums.SNI_SUCCESS; + +#if NETFRAMEWORK + RuntimeHelpers.PrepareConstrainedRegions(); +#endif + try + { } + finally + { + IncrementPendingCallbacks(); + SessionHandle handle = SessionHandle; + // we do not need to consider partial packets when making this read because we + // expect this read to pend. a partial packet should not exist at setup of the + // parser + Debug.Assert(physicalStateObject.PartialPacket == null); + temp = ReadAsync(handle, out error); + + Debug.Assert(temp.Type == PacketHandle.NativePointerType, "unexpected packet type when requiring NativePointer"); + + if (temp.NativePointer != IntPtr.Zero) + { + // Be sure to release packet, otherwise it will be leaked by native. + ReleasePacket(temp); + } + } + + Debug.Assert(IntPtr.Zero == temp.NativePointer, "unexpected syncReadPacket without corresponding SNIPacketRelease"); + return error; + } + internal override uint EnableSsl(ref uint info, bool tlsFirst, string serverCertificateFilename) { AuthProviderInfo authInfo = new AuthProviderInfo(); @@ -399,7 +436,7 @@ internal override uint EnableSsl(ref uint info, bool tlsFirst, string serverCert internal override uint SetConnectionBufferSize(ref uint unsignedPacketSize) => SniNativeWrapper.SniSetInfo(Handle, QueryType.SNI_QUERY_CONN_BUFSIZE, ref unsignedPacketSize); - internal override uint WaitForSSLHandShakeToComplete(out int protocolVersion) + internal override uint WaitForSSLHandShakeToComplete(out SslProtocols protocolVersion) { uint returnValue = SniNativeWrapper.SniWaitForSslHandshakeToComplete(Handle, GetTimeoutRemaining(), out uint nativeProtocolVersion); var nativeProtocol = (NativeProtocols)nativeProtocolVersion; @@ -407,35 +444,35 @@ internal override uint WaitForSSLHandShakeToComplete(out int protocolVersion) #pragma warning disable CA5398 // Avoid hardcoded SslProtocols values if (nativeProtocol.HasFlag(NativeProtocols.SP_PROT_TLS1_2_CLIENT) || nativeProtocol.HasFlag(NativeProtocols.SP_PROT_TLS1_2_SERVER)) { - protocolVersion = (int)SslProtocols.Tls12; + protocolVersion = SslProtocols.Tls12; } else if (nativeProtocol.HasFlag(NativeProtocols.SP_PROT_TLS1_3_CLIENT) || nativeProtocol.HasFlag(NativeProtocols.SP_PROT_TLS1_3_SERVER)) { /* The SslProtocols.Tls13 is supported by netcoreapp3.1 and later */ - protocolVersion = (int)SslProtocols.Tls13; + protocolVersion = SslProtocols.Tls13; } else if (nativeProtocol.HasFlag(NativeProtocols.SP_PROT_TLS1_1_CLIENT) || nativeProtocol.HasFlag(NativeProtocols.SP_PROT_TLS1_1_SERVER)) { - protocolVersion = (int)SslProtocols.Tls11; + protocolVersion = SslProtocols.Tls11; } else if (nativeProtocol.HasFlag(NativeProtocols.SP_PROT_TLS1_0_CLIENT) || nativeProtocol.HasFlag(NativeProtocols.SP_PROT_TLS1_0_SERVER)) { - protocolVersion = (int)SslProtocols.Tls; + protocolVersion = SslProtocols.Tls; } else if (nativeProtocol.HasFlag(NativeProtocols.SP_PROT_SSL3_CLIENT) || nativeProtocol.HasFlag(NativeProtocols.SP_PROT_SSL3_SERVER)) { // SSL 2.0 and 3.0 are only referenced to log a warning, not explicitly used for connections #pragma warning disable CS0618, CA5397 - protocolVersion = (int)SslProtocols.Ssl3; + protocolVersion = SslProtocols.Ssl3; } else if (nativeProtocol.HasFlag(NativeProtocols.SP_PROT_SSL2_CLIENT) || nativeProtocol.HasFlag(NativeProtocols.SP_PROT_SSL2_SERVER)) { - protocolVersion = (int)SslProtocols.Ssl2; + protocolVersion = SslProtocols.Ssl2; #pragma warning restore CS0618, CA5397 } else //if (nativeProtocol.HasFlag(NativeProtocols.SP_PROT_NONE)) { - protocolVersion = (int)SslProtocols.None; + protocolVersion = SslProtocols.None; } #pragma warning restore CA5398 // Avoid hardcoded SslProtocols values return returnValue; diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs index a3bb3b46c2..636c39c932 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs @@ -2425,7 +2425,7 @@ public SqlVector(System.ReadOnlyMemory memory) { } /// public bool IsNull => throw null; /// - public static SqlVector Null => throw null; + public static SqlVector? Null => throw null; /// public int Length { get { throw null; } } /// diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj index 6b507b5a0a..445fcca7a7 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj @@ -32,6 +32,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index 8d178e11b6..87e865c4ed 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -285,9 +285,6 @@ Microsoft\Data\ProviderBase\DbConnectionClosed.cs - - Microsoft\Data\ProviderBase\DbConnectionFactory.cs - Microsoft\Data\ProviderBase\DbConnectionInternal.cs @@ -903,6 +900,12 @@ Microsoft\Data\SqlTypes\SqlVector.cs + + Microsoft\Data\SqlClient\UserAgent\UserAgentInfo.cs + + + Microsoft\Data\SqlClient\UserAgent\UserAgentInfoDto.cs + Resources\ResDescriptionAttribute.cs @@ -954,6 +957,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs index 97212843f5..d65a0b7522 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -30,17 +30,15 @@ namespace Microsoft.Data.SqlClient { /// - [ - DefaultEvent("RecordsAffected"), - ToolboxItem(true), - DesignerCategory("") + [DefaultEvent("RecordsAffected")] + [ToolboxItem(true)] + [DesignerCategory("")] // TODO: Add designer attribute when Microsoft.VSDesigner.Data.VS.SqlCommandDesigner uses Microsoft.Data.SqlClient - ] public sealed class SqlCommand : DbCommand, ICloneable { private static int _objectTypeCount; // EventSource Counter private const int MaxRPCNameLength = 1046; - internal readonly int ObjectID = System.Threading.Interlocked.Increment(ref _objectTypeCount); + internal readonly int ObjectID = Interlocked.Increment(ref _objectTypeCount); internal sealed class ExecuteReaderAsyncCallContext : AAsyncCallContext { @@ -166,11 +164,11 @@ protected override void AfterCleared(SqlCommand owner) #endif internal static readonly Action s_cancelIgnoreFailure = CancelIgnoreFailureCallback; - // devnote: Prepare - // Against 7.0 Server a prepare/unprepare requires an extra roundtrip to the server. - // - // From 8.0 (2000) and above the preparation can be done as part of the command execution. + // Prepare + // Against 7.0 Serve a prepare/unprepare requires an extra roundtrip to the server. // + // From 8.0 and above, the preparation can be done as part of the command execution. + private enum EXECTYPE { UNPREPARED, // execute unprepared commands, all server versions (results in sp_execsql call) @@ -189,8 +187,9 @@ private enum EXECTYPE // // _prepareHandle - the handle of a prepared command. Apparently there can be multiple prepared commands at a time - a feature that we do not support yet. + private static readonly object s_cachedInvalidPrepareHandle = (object)-1; private bool _inPrepare = false; - private int _prepareHandle = -1; + private object _prepareHandle = s_cachedInvalidPrepareHandle; // this is an int which is used in the object typed SqlParameter.Value field, avoid repeated boxing by storing in a box private bool _hiddenPrepare = false; private int _preparedConnectionCloseCount = -1; private int _preparedConnectionReconnectCount = -1; @@ -205,13 +204,26 @@ private enum EXECTYPE // cut down on object creation and cache all these // cached metadata private _SqlMetaDataSet _cachedMetaData; + + // @TODO: Make properties + internal ConcurrentDictionary keysToBeSentToEnclave; + internal bool requiresEnclaveComputations = false; + + private bool ShouldCacheEncryptionMetadata + { + get + { + return !requiresEnclaveComputations || _activeConnection.Parser.AreEnclaveRetriesSupported; + } + } + internal EnclavePackage enclavePackage = null; private SqlEnclaveAttestationParameters enclaveAttestationParameters = null; private byte[] customData = null; private int customDataLength = 0; // Last TaskCompletionSource for reconnect task - use for cancellation only - TaskCompletionSource _reconnectionCompletionSource = null; + private TaskCompletionSource _reconnectionCompletionSource = null; #if DEBUG internal static int DebugForceAsyncWriteDelay { get; set; } @@ -245,15 +257,6 @@ internal bool IsColumnEncryptionEnabled (!string.IsNullOrWhiteSpace(_activeConnection.EnclaveAttestationUrl) || Connection.AttestationProtocol == SqlConnectionAttestationProtocol.None) && IsColumnEncryptionEnabled; - internal ConcurrentDictionary keysToBeSentToEnclave; - internal bool requiresEnclaveComputations = false; - private bool ShouldCacheEncryptionMetadata - { - get - { - return !requiresEnclaveComputations || _activeConnection.Parser.AreEnclaveRetriesSupported; - } - } /// /// Per-command custom providers. It can be provided by the user and can be set more than once. /// @@ -263,8 +266,9 @@ private bool ShouldCacheEncryptionMetadata _customColumnEncryptionKeyStoreProviders is not null && _customColumnEncryptionKeyStoreProviders.Count > 0; // Cached info for async executions - private sealed class CachedAsyncState + private sealed class AsyncState { + // @TODO: Autoproperties private int _cachedAsyncCloseCount = -1; // value of the connection's CloseCount property when the asyncResult was set; tracks when connections are closed after an async operation private TaskCompletionSource _cachedAsyncResult = null; private SqlConnection _cachedAsyncConnection = null; // Used to validate that the connection hasn't changed when end the connection; @@ -273,7 +277,7 @@ private sealed class CachedAsyncState private string _cachedSetOptions = null; private string _cachedEndMethod = null; - internal CachedAsyncState() + internal AsyncState() { } @@ -291,7 +295,7 @@ internal string CachedSetOptions } internal bool PendingAsyncOperation { - get { return (_cachedAsyncResult != null); } + get { return _cachedAsyncResult != null; } } internal string EndMethodName { @@ -353,16 +357,14 @@ internal void SetAsyncReaderState(SqlDataReader ds, RunBehavior runBehavior, str } } - private CachedAsyncState _cachedAsyncState = null; + private AsyncState _cachedAsyncState = null; - private CachedAsyncState cachedAsyncState + // @TODO: This is never null, so we can remove the null checks from usages of it. + private AsyncState CachedAsyncState { get { - if (_cachedAsyncState == null) - { - _cachedAsyncState = new CachedAsyncState(); - } + _cachedAsyncState ??= new AsyncState(); return _cachedAsyncState; } } @@ -400,7 +402,7 @@ private CachedAsyncState cachedAsyncState /// /// This variable is used to keep track of which RPC batch's results are being read when reading the results of - /// describe parameter encryption RPC requests in _batchRPCMode. + /// describe parameter encryption RPC requests in BatchRPCMode. /// private int _currentlyExecutingDescribeParameterEncryptionRPC; @@ -474,12 +476,10 @@ private SqlCommand(SqlCommand from) : this() } } - /// - [ - DefaultValue(null), - ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Data), - ResDescriptionAttribute(StringsHelper.ResourceNames.DbCommand_Connection), - ] + /// + [DefaultValue(null)] + [ResCategory(StringsHelper.ResourceNames.DataCategory_Data)] + [ResDescription(StringsHelper.ResourceNames.DbCommand_Connection)] new public SqlConnection Connection { get @@ -490,10 +490,12 @@ private SqlCommand(SqlCommand from) : this() { // Don't allow the connection to be changed while in an async operation. if (_activeConnection != value && _activeConnection != null) - { // If new value... - if (_cachedAsyncState != null && _cachedAsyncState.PendingAsyncOperation) - { // If in pending async state, throw. - throw SQL.CannotModifyPropertyAsyncOperationInProgress(SQL.Connection); + { + // If new value... + if (CachedAsyncState != null && CachedAsyncState.PendingAsyncOperation) + { + // If in pending async state, throw. + throw SQL.CannotModifyPropertyAsyncOperationInProgress(); } } @@ -532,7 +534,7 @@ private SqlCommand(SqlCommand from) : this() } catch (Exception) { - // we do not really care about errors in unprepare (may be the old connection went bad) + // we do not really care about errors in unprepare (may be the old connection went bad) } finally { @@ -571,10 +573,8 @@ private SqlInternalConnectionTds InternalTdsConnection private bool IsProviderRetriable => SqlConfigurableRetryFactory.IsRetriable(RetryLogicProvider); /// - [ - Browsable(false), - DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden) - ] + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public SqlRetryLogicBaseProvider RetryLogicProvider { get @@ -592,11 +592,9 @@ public SqlRetryLogicBaseProvider RetryLogicProvider } /// - [ - DefaultValue(true), - ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Notification), - ResDescriptionAttribute(StringsHelper.ResourceNames.SqlCommand_NotificationAutoEnlist), - ] + [DefaultValue(true)] + [ResCategory(StringsHelper.ResourceNames.DataCategory_Notification)] + [ResDescription(StringsHelper.ResourceNames.SqlCommand_NotificationAutoEnlist)] public bool NotificationAutoEnlist { get @@ -610,12 +608,10 @@ public bool NotificationAutoEnlist } /// - [ - Browsable(false), - DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), // MDAC 90471 - ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Notification), - ResDescriptionAttribute(StringsHelper.ResourceNames.SqlCommand_Notification), - ] + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] // MDAC 90471 + [ResCategory(StringsHelper.ResourceNames.DataCategory_Notification)] + [ResDescription(StringsHelper.ResourceNames.SqlCommand_Notification)] public SqlNotificationRequest Notification { get @@ -646,11 +642,9 @@ internal SqlStatistics Statistics } /// - [ - Browsable(false), - DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), - ResDescriptionAttribute(StringsHelper.ResourceNames.DbCommand_Transaction), - ] + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [ResDescription(StringsHelper.ResourceNames.DbCommand_Transaction)] new public SqlTransaction Transaction { get @@ -666,16 +660,16 @@ internal SqlStatistics Statistics { // Don't allow the transaction to be changed while in an async operation. if (_transaction != value && _activeConnection != null) - { // If new value... - if (cachedAsyncState.PendingAsyncOperation) - { // If in pending async state, throw - throw SQL.CannotModifyPropertyAsyncOperationInProgress(SQL.Transaction); + { + // If new value... + if (CachedAsyncState.PendingAsyncOperation) + { + // If in pending async state, throw + throw SQL.CannotModifyPropertyAsyncOperationInProgress(); } } - - // TODO: Add objid here - SqlClientEventSource.Log.TryTraceEvent(" {0}", ObjectID); _transaction = value; + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.Set_Transaction | API | Object Id {0}, Internal Transaction Id {1}, Client Connection Id {2}", ObjectID, value?.InternalTransaction?.TransactionId, Connection?.ClientConnectionId); } } @@ -689,45 +683,39 @@ protected override DbTransaction DbTransaction set { Transaction = (SqlTransaction)value; + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.Set_DbTransaction | API | Object Id {0}, Client Connection Id {1}", ObjectID, Connection?.ClientConnectionId); } } /// - [ - DefaultValue(""), - RefreshProperties(RefreshProperties.All), // MDAC 67707 - ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Data), - ResDescriptionAttribute(StringsHelper.ResourceNames.DbCommand_CommandText), - ] + [DefaultValue("")] + [RefreshProperties(RefreshProperties.All)] // MDAC 67707 + [ResCategory(StringsHelper.ResourceNames.DataCategory_Data)] + [ResDescription(StringsHelper.ResourceNames.DbCommand_CommandText)] public override string CommandText { get => _commandText ?? ""; set { - SqlClientEventSource.Log.TryTraceEvent(" {0}, String Value = '{1}'", ObjectID, value); - if (_commandText != value) { PropertyChanging(); _commandText = value; } + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.Set_CommandText | API | Object Id {0}, String Value = '{1}', Client Connection Id {2}", ObjectID, value, Connection?.ClientConnectionId); } } /// - [ - Browsable(false), - DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), - ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Data), - ResDescriptionAttribute(StringsHelper.ResourceNames.TCE_SqlCommand_ColumnEncryptionSetting), - ] + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [ResCategory(StringsHelper.ResourceNames.DataCategory_Data)] + [ResDescription(StringsHelper.ResourceNames.TCE_SqlCommand_ColumnEncryptionSetting)] public SqlCommandColumnEncryptionSetting ColumnEncryptionSetting => _columnEncryptionSetting; /// - [ - ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Data), - ResDescriptionAttribute(StringsHelper.ResourceNames.DbCommand_CommandTimeout), - ] + [ResCategory(StringsHelper.ResourceNames.DataCategory_Data)] + [ResDescription(StringsHelper.ResourceNames.DbCommand_CommandTimeout)] public override int CommandTimeout { get @@ -736,7 +724,6 @@ public override int CommandTimeout } set { - SqlClientEventSource.Log.TryTraceEvent(" {0}, {1}", ObjectID, value); if (value < 0) { throw ADP.InvalidCommandTimeout(value, nameof(CommandTimeout)); @@ -747,6 +734,8 @@ public override int CommandTimeout PropertyChanging(); _commandTimeout = value; } + + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.Set_CommandTimeout | API | ObjectId {0}, Command Timeout value {1}, Client Connection Id {2}", ObjectID, value, Connection?.ClientConnectionId); } } @@ -769,12 +758,10 @@ private int DefaultCommandTimeout } /// - [ - DefaultValue(System.Data.CommandType.Text), - RefreshProperties(RefreshProperties.All), - ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Data), - ResDescriptionAttribute(StringsHelper.ResourceNames.DbCommand_CommandType), - ] + [DefaultValue(CommandType.Text)] + [RefreshProperties(RefreshProperties.All)] + [ResCategory(StringsHelper.ResourceNames.DataCategory_Data)] + [ResDescription(StringsHelper.ResourceNames.DbCommand_CommandType)] public override CommandType CommandType { get @@ -784,7 +771,6 @@ public override CommandType CommandType } set { - SqlClientEventSource.Log.TryTraceEvent(" {0}, {1}{2}", ObjectID, (int)value, _commandType); if (_commandType != value) { switch (value) @@ -799,6 +785,8 @@ public override CommandType CommandType default: throw ADP.InvalidCommandType(value); } + + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.Set_CommandType | API | ObjectId {0}, Command type value {1}, Client Connection Id {2}", ObjectID, (int)value, Connection?.ClientConnectionId); } } } @@ -808,12 +796,10 @@ public override CommandType CommandType // when the DataAdapter design wizard generates the insert/update/delete commands it will // set the DesignTimeVisible property to false so that cmds won't appear as individual objects /// - [ - DefaultValue(true), - DesignOnly(true), - Browsable(false), - EditorBrowsableAttribute(EditorBrowsableState.Never), - ] + [DefaultValue(true)] + [DesignOnly(true)] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] public override bool DesignTimeVisible { get @@ -831,11 +817,9 @@ public override bool DesignTimeVisible public bool EnableOptimizedParameterBinding { get; set; } /// - [ - DesignerSerializationVisibility(DesignerSerializationVisibility.Content), - ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Data), - ResDescriptionAttribute(StringsHelper.ResourceNames.DbCommand_Parameters), - ] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] + [ResCategory(StringsHelper.ResourceNames.DataCategory_Data)] + [ResDescription(StringsHelper.ResourceNames.DbCommand_Parameters)] new public SqlParameterCollection Parameters { get @@ -859,35 +843,10 @@ protected override DbParameterCollection DbParameterCollection } } - internal static void CancelIgnoreFailureCallback(object state) - { - SqlCommand command = (SqlCommand)state; - command.CancelIgnoreFailure(); - } - - internal void CancelIgnoreFailure() - { - // This method is used to route CancellationTokens to the Cancel method. - // Cancellation is a suggestion, and exceptions should be ignored - // rather than allowed to be unhandled, as there is no way to route - // them to the caller. It would be expected that the error will be - // observed anyway from the regular method. An example is cancelling - // an operation on a closed connection. - try - { - Cancel(); - } - catch (Exception) - { - } - } - /// - [ - DefaultValue(System.Data.UpdateRowSource.Both), - ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_Update), - ResDescriptionAttribute(StringsHelper.ResourceNames.DbCommand_UpdatedRowSource), - ] + [DefaultValue(UpdateRowSource.Both)] + [ResCategory(StringsHelper.ResourceNames.DataCategory_Update)] + [ResDescription(StringsHelper.ResourceNames.DbCommand_UpdatedRowSource)] public override UpdateRowSource UpdatedRowSource { get @@ -907,14 +866,14 @@ public override UpdateRowSource UpdatedRowSource default: throw ADP.InvalidUpdateRowSource(value); } + + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.UpdatedRowSource | API | ObjectId {0}, Updated row source value {1}, Client Connection Id {2}", ObjectID, (int)value, Connection?.ClientConnectionId); } } /// - [ - ResCategoryAttribute(StringsHelper.ResourceNames.DataCategory_StatementCompleted), - ResDescriptionAttribute(StringsHelper.ResourceNames.DbCommand_StatementCompleted), - ] + [ResCategory(StringsHelper.ResourceNames.DataCategory_StatementCompleted)] + [ResDescription(StringsHelper.ResourceNames.DbCommand_StatementCompleted)] public event StatementCompletedEventHandler StatementCompleted { add @@ -936,7 +895,7 @@ internal void OnStatementCompleted(int recordCount) { try { - SqlClientEventSource.Log.TryTraceEvent(" {0}, recordCount={1}", ObjectID, recordCount); + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.OnStatementCompleted | Info | ObjectId {0}, Record Count {1}, Client Connection Id {2}", ObjectID, recordCount, Connection?.ClientConnectionId); handler(this, new StatementCompletedEventArgs(recordCount)); } catch (Exception e) @@ -953,7 +912,8 @@ internal void OnStatementCompleted(int recordCount) } private void PropertyChanging() - { // also called from SqlParameterCollection + { + // also called from SqlParameterCollection this.IsDirty = true; } @@ -966,7 +926,7 @@ public override void Prepare() // between entry into Execute* API and the thread obtaining the stateObject. _pendingCancel = false; - using (TryEventScope.Create(" {0}", ObjectID)) + using (TryEventScope.Create("SqlCommand.Prepare | API | Object Id {0}", ObjectID)) { SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); @@ -989,7 +949,7 @@ public override void Prepare() else { // Validate the command outside of the try\catch to avoid putting the _stateObj on error - ValidateCommand(nameof(Prepare), false /*not async*/); + ValidateCommand(isAsync: false); bool processFinallyBlock = true; TdsParser bestEffortCleanupTarget = null; @@ -1089,16 +1049,10 @@ internal void Unprepare() Debug.Assert(true == IsPrepared, "Invalid attempt to Unprepare a non-prepared command!"); Debug.Assert(_activeConnection != null, "must have an open connection to UnPrepare"); Debug.Assert(false == _inPrepare, "_inPrepare should be false!"); - - // @devnote: we're always falling back to Prepare pending - // @devnote: This seems broken because once the command is prepared it will - always - be a - // @devnote: prepared execution. - // @devnote: Even replacing the parameterlist with something completely different or - // @devnote: changing the commandtext to a non-parameterized query will result in prepared execution - // @devnote: - // @devnote: We need to keep the behavior for backward compatibility though (non-breaking change) - // _execType = EXECTYPE.PREPAREPENDING; + + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.UnPrepare | Info | Object Id {0}, Current Prepared Handle {1}", ObjectID, _prepareHandle); + // Don't zero out the handle because we'll pass it in to sp_prepexec on the next prepare // Unless the close count isn't the same as when we last prepared if ((_activeConnection.CloseCount != _preparedConnectionCloseCount) || (_activeConnection.ReconnectCount != _preparedConnectionReconnectCount)) @@ -1108,7 +1062,7 @@ internal void Unprepare() } _cachedMetaData = null; - SqlClientEventSource.Log.TryTraceEvent(" {0}, Command unprepared.", ObjectID); + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.UnPrepare | Info | Object Id {0}, Command unprepared.", ObjectID); } // Cancel is supposed to be multi-thread safe. @@ -1118,9 +1072,9 @@ internal void Unprepare() /// public override void Cancel() { - using (TryEventScope.Create(" {0}", ObjectID)) + using (TryEventScope.Create("SqlCommand.Cancel | API | Object Id {0}", ObjectID)) { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.Cancel | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); SqlStatistics statistics = null; try @@ -1159,7 +1113,8 @@ public override void Cancel() lock (connection) { if (connection != (_activeConnection.InnerConnection as SqlInternalConnectionTds)) - { // make sure the connection held on the active connection is what we have stored in our temp connection variable, if not between getting "connection" and taking the lock, the connection has been closed + { + // make sure the connection held on the active connection is what we have stored in our temp connection variable, if not between getting "connection" and taking the lock, the connection has been closed return; } @@ -1176,7 +1131,8 @@ public override void Cancel() bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_activeConnection); if (!_pendingCancel) - { // Do nothing if aleady pending. + { + // Do nothing if aleady pending. // Before attempting actual cancel, set the _pendingCancel flag to false. // This denotes to other thread before obtaining stateObject from the // session pool that there is another thread wishing to cancel. @@ -1240,18 +1196,27 @@ protected override DbParameter CreateDbParameter() protected override void Dispose(bool disposing) { if (disposing) - { // release managed objects + { + // release managed objects _cachedMetaData = null; // reset async cache information to allow a second async execute - _cachedAsyncState?.ResetAsyncState(); + CachedAsyncState?.ResetAsyncState(); } // release unmanaged objects base.Dispose(disposing); } - private SqlDataReader RunExecuteReaderWithRetry(CommandBehavior cmdBehavior, RunBehavior runBehavior, bool returnStream, string method) - => RetryLogicProvider.Execute(this, () => RunExecuteReader(cmdBehavior, runBehavior, returnStream, method)); + private SqlDataReader RunExecuteReaderWithRetry( + CommandBehavior cmdBehavior, + RunBehavior runBehavior, + bool returnStream, + [CallerMemberName] string method = "") + { + return RetryLogicProvider.Execute( + this, + () => RunExecuteReader(cmdBehavior, runBehavior, returnStream, method)); + } /// public override object ExecuteScalar() @@ -1263,21 +1228,19 @@ public override object ExecuteScalar() _pendingCancel = false; SqlStatistics statistics = null; - using (TryEventScope.Create(" {0}", ObjectID)) + using (TryEventScope.Create("SqlCommand.ExecuteScalar | API | ObjectId {0}", ObjectID)) { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID{0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); - bool success = false; int? sqlExceptionNumber = null; + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.ExecuteScalar | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); try { statistics = SqlStatistics.StartTimer(Statistics); WriteBeginExecuteEvent(); - SqlDataReader ds; - ds = IsProviderRetriable ? - RunExecuteReaderWithRetry(0, RunBehavior.ReturnImmediately, true, nameof(ExecuteScalar)) : - RunExecuteReader(0, RunBehavior.ReturnImmediately, true, nameof(ExecuteScalar)); + SqlDataReader ds = IsProviderRetriable + ? RunExecuteReaderWithRetry(0, RunBehavior.ReturnImmediately, returnStream: true) + : RunExecuteReader(0, RunBehavior.ReturnImmediately, returnStream: true); object result = CompleteExecuteScalar(ds, _batchRPCMode); success = true; return result; @@ -1295,86 +1258,6 @@ public override object ExecuteScalar() } } - internal Task ExecuteScalarBatchAsync(CancellationToken cancellationToken) - { - return ExecuteReaderAsync(cancellationToken).ContinueWith((executeTask) => - { - TaskCompletionSource source = new TaskCompletionSource(); - if (executeTask.IsCanceled) - { - source.SetCanceled(); - } - else if (executeTask.IsFaulted) - { - source.SetException(executeTask.Exception.InnerException); - } - else - { - SqlDataReader reader = executeTask.Result; - ExecuteScalarUntilEndAsync(reader, cancellationToken).ContinueWith( - (readTask) => - { - try - { - if (readTask.IsCanceled) - { - reader.Dispose(); - source.SetCanceled(); - } - else if (readTask.IsFaulted) - { - reader.Dispose(); - source.SetException(readTask.Exception.InnerException); - } - else - { - Exception exception = null; - object result = null; - try - { - result = readTask.Result; - } - finally - { - reader.Dispose(); - } - if (exception != null) - { - source.SetException(exception); - } - else - { - source.SetResult(result); - } - } - } - catch (Exception e) - { - // exception thrown by Dispose... - source.SetException(e); - } - }, - TaskScheduler.Default - ); - } - return source.Task; - }, TaskScheduler.Default).Unwrap(); - } - - private async Task ExecuteScalarUntilEndAsync(SqlDataReader reader, CancellationToken cancellationToken) - { - object retval = null; - do - { - if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false) && reader.FieldCount > 0) - { - retval = reader.GetValue(0); // no async untyped value getter, this will work ok as long as the value is in the current packet - } - } - while (_batchRPCMode && !cancellationToken.IsCancellationRequested && await reader.NextResultAsync(cancellationToken).ConfigureAwait(false)); - return retval; - } - private object CompleteExecuteScalar(SqlDataReader ds, bool returnLastResult) { object retResult = null; @@ -1401,10 +1284,25 @@ private object CompleteExecuteScalar(SqlDataReader ds, bool returnLastResult) return retResult; } - private Task InternalExecuteNonQueryWithRetry(string methodName, bool sendToPipe, int timeout, out bool usedCache, bool asyncWrite, bool inRetry) + private Task InternalExecuteNonQueryWithRetry( + bool sendToPipe, + int timeout, + out bool usedCache, + bool asyncWrite, + bool isRetry, + [CallerMemberName] string methodName = "") { bool innerUsedCache = false; - Task result = RetryLogicProvider.Execute(this, () => InternalExecuteNonQuery(completion: null, methodName, sendToPipe, timeout, out innerUsedCache, asyncWrite, inRetry)); + Task result = RetryLogicProvider.Execute( + this, + () => InternalExecuteNonQuery( + completion: null, + sendToPipe, + timeout, + out innerUsedCache, + asyncWrite, + isRetry, + methodName)); usedCache = innerUsedCache; return result; } @@ -1422,21 +1320,26 @@ public override int ExecuteNonQuery() using (TryEventScope.Create(" {0}", ObjectID)) { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); - bool success = false; int? sqlExceptionNumber = null; + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.ExecuteNonQuery | API | Correlation | Object Id {0}, ActivityID {1}, Client Connection Id {2}, Command Text {3}", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); + try { statistics = SqlStatistics.StartTimer(Statistics); WriteBeginExecuteEvent(); if (IsProviderRetriable) { - InternalExecuteNonQueryWithRetry(nameof(ExecuteNonQuery), sendToPipe: false, CommandTimeout, out _, asyncWrite: false, inRetry: false); + InternalExecuteNonQueryWithRetry( + sendToPipe: false, + CommandTimeout, + out _, + asyncWrite: false, + isRetry: false); } else { - InternalExecuteNonQuery(null, nameof(ExecuteNonQuery), sendToPipe: false, CommandTimeout, out _); + InternalExecuteNonQuery(completion: null, sendToPipe: false, CommandTimeout, out _); } success = true; return _rowsAffected; @@ -1456,11 +1359,8 @@ public override int ExecuteNonQuery() /// [System.Security.Permissions.HostProtectionAttribute(ExternalThreading = true)] - public IAsyncResult BeginExecuteNonQuery() - { - // BeginExecuteNonQuery will track ExecutionTime for us - return BeginExecuteNonQuery(null, null); - } + public IAsyncResult BeginExecuteNonQuery() => + BeginExecuteNonQuery(null, null); /// [System.Security.Permissions.HostProtectionAttribute(ExternalThreading = true)] @@ -1468,20 +1368,21 @@ public IAsyncResult BeginExecuteNonQuery(AsyncCallback callback, object stateObj { SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); SqlConnection.ExecutePermission.Demand(); - return BeginExecuteNonQueryInternal(0, callback, stateObject, 0, inRetry: false); + return BeginExecuteNonQueryInternal(0, callback, stateObject, 0, isRetry: false); } private IAsyncResult BeginExecuteNonQueryAsync(AsyncCallback callback, object stateObject) { - return BeginExecuteNonQueryInternal(0, callback, stateObject, CommandTimeout, inRetry: false, asyncWrite: true); + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.BeginExecuteNonQueryAsync | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); + return BeginExecuteNonQueryInternal(0, callback, stateObject, CommandTimeout, isRetry: false, asyncWrite: true); } - private IAsyncResult BeginExecuteNonQueryInternal(CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool inRetry, bool asyncWrite = false) + private IAsyncResult BeginExecuteNonQueryInternal(CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool isRetry, bool asyncWrite = false) { TaskCompletionSource globalCompletion = new TaskCompletionSource(stateObject); TaskCompletionSource localCompletion = new TaskCompletionSource(stateObject); - if (!inRetry) + if (!isRetry) { // Reset _pendingCancel upon entry into any Execute - used to synchronize state // between entry into Execute* API and the thread obtaining the stateObject. @@ -1494,7 +1395,7 @@ private IAsyncResult BeginExecuteNonQueryInternal(CommandBehavior behavior, Asyn SqlStatistics statistics = null; try { - if (!inRetry) + if (!isRetry) { statistics = SqlStatistics.StartTimer(Statistics); WriteBeginExecuteEvent(); @@ -1502,8 +1403,17 @@ private IAsyncResult BeginExecuteNonQueryInternal(CommandBehavior behavior, Asyn bool usedCache; try - { // InternalExecuteNonQuery already has reliability block, but if failure will not put stateObj back into pool. - Task execNQ = InternalExecuteNonQuery(localCompletion, nameof(BeginExecuteNonQuery), false, timeout, out usedCache, asyncWrite, inRetry: inRetry); + { + // InternalExecuteNonQuery already has reliability block, but if failure will not put stateObj back into pool. + Task execNQ = InternalExecuteNonQuery( + localCompletion, + sendToPipe: false, + timeout, + out usedCache, + asyncWrite, + isRetry, + methodName: nameof(BeginExecuteNonQuery)); + if (execNQ != null) { AsyncHelper.ContinueTaskWithState(execNQ, localCompletion, this, (object state) => ((SqlCommand)state).BeginExecuteNonQueryInternalReadStage(localCompletion)); @@ -1528,7 +1438,25 @@ private IAsyncResult BeginExecuteNonQueryInternal(CommandBehavior behavior, Asyn // When we use query caching for parameter encryption we need to retry on specific errors. // In these cases finalize the call internally and trigger a retry when needed. - if (!TriggerInternalEndAndRetryIfNecessary(behavior, stateObject, timeout, nameof(EndExecuteNonQuery), usedCache, inRetry, asyncWrite, globalCompletion, localCompletion, InternalEndExecuteNonQuery, BeginExecuteNonQueryInternal)) + if ( + !TriggerInternalEndAndRetryIfNecessary( + behavior, + stateObject, + timeout, + usedCache, + isRetry, + asyncWrite, + globalCompletion, + localCompletion, + endFunc: static (SqlCommand command, IAsyncResult asyncResult, bool isInternal, string endMethod) => + { + return command.InternalEndExecuteNonQuery(asyncResult, isInternal, endMethod); + }, + retryFunc: static (SqlCommand command, CommandBehavior commandBehavior, AsyncCallback callback, object stateObject, int timeout, bool isRetry, bool asyncWrite) => + { + return command.BeginExecuteNonQueryInternal(commandBehavior, callback, stateObject, timeout, isRetry, asyncWrite); + }, + nameof(EndExecuteNonQuery))) { globalCompletion = localCompletion; } @@ -1556,7 +1484,7 @@ private void BeginExecuteNonQueryInternalReadStage(TaskCompletionSource { bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_activeConnection); // must finish caching information before ReadSni which can activate the callback before returning - cachedAsyncState.SetActiveConnectionAndResult(completion, nameof(EndExecuteNonQuery), _activeConnection); + CachedAsyncState.SetActiveConnectionAndResult(completion, nameof(EndExecuteNonQuery), _activeConnection); _stateObj.ReadSni(completion); } catch (System.OutOfMemoryException e) @@ -1579,9 +1507,9 @@ private void BeginExecuteNonQueryInternalReadStage(TaskCompletionSource { // Similarly, if an exception occurs put the stateObj back into the pool. // and reset async cache information to allow a second async execute - if (_cachedAsyncState != null) + if (CachedAsyncState != null) { - _cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); } ReliablePutStateObject(); throw; @@ -1627,15 +1555,15 @@ private void VerifyEndExecuteState(Task completionTask, string endMethod, bool f return; } - if (cachedAsyncState.EndMethodName == null) + if (CachedAsyncState.EndMethodName == null) { throw ADP.MethodCalledTwice(endMethod); } - if (endMethod != cachedAsyncState.EndMethodName) + if (endMethod != CachedAsyncState.EndMethodName) { - throw ADP.MismatchedAsyncResult(cachedAsyncState.EndMethodName, endMethod); + throw ADP.MismatchedAsyncResult(CachedAsyncState.EndMethodName, endMethod); } - if ((_activeConnection.State != ConnectionState.Open) || (!cachedAsyncState.IsActiveConnectionValid(_activeConnection))) + if ((_activeConnection.State != ConnectionState.Open) || (!CachedAsyncState.IsActiveConnectionValid(_activeConnection))) { // If the connection is not 'valid' then it was closed while we were executing throw ADP.ClosedConnectionError(); @@ -1675,7 +1603,7 @@ public int EndExecuteNonQuery(IAsyncResult asyncResult) } finally { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.EndExecuteNonQuery | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); } } @@ -1693,14 +1621,14 @@ private void ThrowIfReconnectionHasBeenCanceled() private int EndExecuteNonQueryAsync(IAsyncResult asyncResult) { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.EndExecuteNonQueryAsync | Info | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); Debug.Assert(!_internalEndExecuteInitiated || _stateObj == null); Exception asyncException = ((Task)asyncResult).Exception; if (asyncException != null) { // Leftover exception from the Begin...InternalReadStage - cachedAsyncState?.ResetAsyncState(); + CachedAsyncState?.ResetAsyncState(); ReliablePutStateObject(); throw asyncException.InnerException; } @@ -1726,19 +1654,20 @@ private int EndExecuteNonQueryAsync(IAsyncResult asyncResult) private int EndExecuteNonQueryInternal(IAsyncResult asyncResult) { SqlStatistics statistics = null; - bool success = false; int? sqlExceptionNumber = null; + bool success = false; + try { statistics = SqlStatistics.StartTimer(Statistics); - int result = (int)InternalEndExecuteNonQuery(asyncResult, nameof(EndExecuteNonQuery), isInternal: false); + int result = (int)InternalEndExecuteNonQuery(asyncResult, isInternal: false, endMethod: nameof(EndExecuteNonQuery)); success = true; return result; } catch (SqlException e) { sqlExceptionNumber = e.Number; - _cachedAsyncState?.ResetAsyncState(); + CachedAsyncState?.ResetAsyncState(); // SqlException is always catchable ReliablePutStateObject(); @@ -1746,7 +1675,7 @@ private int EndExecuteNonQueryInternal(IAsyncResult asyncResult) } catch (Exception e) { - _cachedAsyncState?.ResetAsyncState(); + CachedAsyncState?.ResetAsyncState(); if (ADP.IsCatchableExceptionType(e)) { ReliablePutStateObject(); @@ -1760,7 +1689,10 @@ private int EndExecuteNonQueryInternal(IAsyncResult asyncResult) } } - private object InternalEndExecuteNonQuery(IAsyncResult asyncResult, string endMethod, bool isInternal) + private object InternalEndExecuteNonQuery( + IAsyncResult asyncResult, + bool isInternal, + [CallerMemberName] string endMethod = "") { SqlClientEventSource.Log.TryTraceEvent("SqlCommand.InternalEndExecuteNonQuery | INFO | ObjectId {0}, Client Connection Id {1}, MARS={2}, AsyncCommandInProgress={3}", _activeConnection?.ObjectID, _activeConnection?.ClientConnectionId, @@ -1795,7 +1727,7 @@ private object InternalEndExecuteNonQuery(IAsyncResult asyncResult, string endMe Debug.Assert(_stateObj == null); // Reset the state since we exit early. - cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); return _rowsAffected; } @@ -1820,12 +1752,13 @@ private object InternalEndExecuteNonQuery(IAsyncResult asyncResult, string endMe // Don't reset the state for internal End. The user End will do that eventually. if (!isInternal) { - cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); } } } else - { // otherwise, use a full-fledged execute that can handle params and stored procs + { + // otherwise, use a full-fledged execute that can handle params and stored procs SqlDataReader reader = CompleteAsyncExecuteReader(isInternal); if (reader != null) { @@ -1867,11 +1800,18 @@ private object InternalEndExecuteNonQuery(IAsyncResult asyncResult, string endMe } } - private Task InternalExecuteNonQuery(TaskCompletionSource completion, string methodName, bool sendToPipe, int timeout, out bool usedCache, bool asyncWrite = false, bool inRetry = false) + private Task InternalExecuteNonQuery( + TaskCompletionSource completion, + bool sendToPipe, + int timeout, + out bool usedCache, + bool asyncWrite = false, + bool isRetry = false, + [CallerMemberName] string methodName = "") { SqlClientEventSource.Log.TryTraceEvent("SqlCommand.InternalExecuteNonQuery | INFO | ObjectId {0}, Client Connection Id {1}, AsyncCommandInProgress={2}", _activeConnection?.ObjectID, _activeConnection?.ClientConnectionId, _activeConnection?.AsyncCommandInProgress); - bool async = completion != null; + bool isAsync = completion != null; usedCache = false; SqlStatistics statistics = Statistics; @@ -1884,16 +1824,16 @@ private Task InternalExecuteNonQuery(TaskCompletionSource completion, st bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_activeConnection); // @devnote: this function may throw for an invalid connection // @devnote: returns false for empty command text - if (!inRetry) + if (!isRetry) { - ValidateCommand(methodName, async); + ValidateCommand(isAsync, methodName); } CheckNotificationStateAndAutoEnlist(); // Only call after validate - requires non null connection! Task task = null; - //Always Encrypted generally operates only on parameterized queries. However enclave based Always encrypted also supports unparameterized queries - //We skip this block for enclave based always encrypted so that we can make a call to SQL Server to get the encryption information + // Always Encrypted generally operates only on parameterized queries. However enclave based Always encrypted also supports unparameterized queries + // We skip this block for enclave based always encrypted so that we can make a call to SQL Server to get the encryption information if (!ShouldUseEnclaveBasedWorkflow && !_batchRPCMode && CommandType == CommandType.Text && GetParameterCount(_parameters) == 0) { Debug.Assert(!sendToPipe, "trying to send non-context command to pipe"); @@ -1910,17 +1850,28 @@ private Task InternalExecuteNonQuery(TaskCompletionSource completion, st } // We should never get here for a retry since we only have retries for parameters. - Debug.Assert(!inRetry); + Debug.Assert(!isRetry); - task = RunExecuteNonQueryTds(methodName, async, timeout, asyncWrite); + task = RunExecuteNonQueryTds(methodName, isAsync, timeout, asyncWrite); } else { // otherwise, use a full-fledged execute that can handle params and stored procs - Debug.Assert(!sendToPipe, "trying to send non-context command to pipe"); - SqlClientEventSource.Log.TryTraceEvent(" {0}, Command executed as RPC.", ObjectID); - - SqlDataReader reader = RunExecuteReader(0, RunBehavior.UntilDone, false, methodName, completion, timeout, out task, out usedCache, asyncWrite, inRetry); + Debug.Assert(!sendToPipe, "Trying to send non-context command to pipe"); + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.InternalExecuteNonQuery | INFO | Object Id {0}, RPC execute method name {1}, isAsync {2}, isRetry {3}", ObjectID, methodName, isAsync, isRetry); + + SqlDataReader reader = RunExecuteReader( + CommandBehavior.Default, + RunBehavior.UntilDone, + returnStream: false, + completion, + timeout, + out task, + out usedCache, + asyncWrite, + isRetry, + methodName); + if (reader != null) { if (task != null) @@ -1933,7 +1884,7 @@ private Task InternalExecuteNonQuery(TaskCompletionSource completion, st } } } - Debug.Assert(async || _stateObj == null, "non-null state object in InternalExecuteNonQuery"); + Debug.Assert(isAsync || _stateObj == null, "non-null state object in InternalExecuteNonQuery"); return task; } catch (System.OutOfMemoryException e) @@ -1965,22 +1916,21 @@ public XmlReader ExecuteXmlReader() SqlStatistics statistics = null; - using (TryEventScope.Create(" {0}", ObjectID)) + using (TryEventScope.Create("SqlCommand.ExecuteXmlReader | API | Object Id {0}", ObjectID)) { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); - bool success = false; int? sqlExceptionNumber = null; + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.ExecuteXmlReader | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); + try { statistics = SqlStatistics.StartTimer(Statistics); WriteBeginExecuteEvent(); // use the reader to consume metadata - SqlDataReader ds; - ds = IsProviderRetriable ? - RunExecuteReaderWithRetry(CommandBehavior.SequentialAccess, RunBehavior.ReturnImmediately, true, nameof(ExecuteXmlReader)) : - RunExecuteReader(CommandBehavior.SequentialAccess, RunBehavior.ReturnImmediately, true, nameof(ExecuteXmlReader)); + SqlDataReader ds = IsProviderRetriable + ? RunExecuteReaderWithRetry(CommandBehavior.SequentialAccess, RunBehavior.ReturnImmediately, returnStream: true) + : RunExecuteReader(CommandBehavior.SequentialAccess, RunBehavior.ReturnImmediately, returnStream: true); XmlReader result = CompleteXmlReader(ds); success = true; return result; @@ -2010,35 +1960,36 @@ public IAsyncResult BeginExecuteXmlReader() [System.Security.Permissions.HostProtectionAttribute(ExternalThreading = true)] public IAsyncResult BeginExecuteXmlReader(AsyncCallback callback, object stateObject) { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.BeginExecuteXmlReader | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); SqlConnection.ExecutePermission.Demand(); - return BeginExecuteXmlReaderInternal(CommandBehavior.SequentialAccess, callback, stateObject, 0, inRetry: false); + return BeginExecuteXmlReaderInternal(CommandBehavior.SequentialAccess, callback, stateObject, 0, isRetry: false); } private IAsyncResult BeginExecuteXmlReaderAsync(AsyncCallback callback, object stateObject) { - return BeginExecuteXmlReaderInternal(CommandBehavior.SequentialAccess, callback, stateObject, CommandTimeout, inRetry: false, asyncWrite: true); + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.BeginExecuteXmlReaderAsync | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); + return BeginExecuteXmlReaderInternal(CommandBehavior.SequentialAccess, callback, stateObject, CommandTimeout, isRetry: false, asyncWrite: true); } - private IAsyncResult BeginExecuteXmlReaderInternal(CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool inRetry, bool asyncWrite = false) + private IAsyncResult BeginExecuteXmlReaderInternal(CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool isRetry, bool asyncWrite = false) { TaskCompletionSource globalCompletion = new TaskCompletionSource(stateObject); TaskCompletionSource localCompletion = new TaskCompletionSource(stateObject); - if (!inRetry) + if (!isRetry) { // Reset _pendingCancel upon entry into any Execute - used to synchronize state // between entry into Execute* API and the thread obtaining the stateObject. _pendingCancel = false; - ValidateAsyncCommand(); // Special case - done outside of try/catches to prevent putting a stateObj - // back into pool when we should not. + // Special case - done outside of try/catches to prevent putting a stateObj back into pool when we should not. + ValidateAsyncCommand(); } SqlStatistics statistics = null; try { - if (!inRetry) + if (!isRetry) { statistics = SqlStatistics.StartTimer(Statistics); WriteBeginExecuteEvent(); @@ -2047,8 +1998,18 @@ private IAsyncResult BeginExecuteXmlReaderInternal(CommandBehavior behavior, Asy bool usedCache; Task writeTask; try - { // InternalExecuteNonQuery already has reliability block, but if failure will not put stateObj back into pool. - RunExecuteReader(behavior, RunBehavior.ReturnImmediately, true, nameof(BeginExecuteXmlReader), localCompletion, timeout, out writeTask, out usedCache, asyncWrite, inRetry); + { + // InternalExecuteNonQuery already has reliability block, but if failure will not put stateObj back into pool. + RunExecuteReader( + behavior, + RunBehavior.ReturnImmediately, + returnStream: true, + localCompletion, + timeout, + out writeTask, + out usedCache, + asyncWrite, + isRetry); } catch (Exception e) { @@ -2074,12 +2035,30 @@ private IAsyncResult BeginExecuteXmlReaderInternal(CommandBehavior behavior, Asy // When we use query caching for parameter encryption we need to retry on specific errors. // In these cases finalize the call internally and trigger a retry when needed. - if (!TriggerInternalEndAndRetryIfNecessary(behavior, stateObject, timeout, nameof(EndExecuteXmlReader), usedCache, inRetry, asyncWrite, globalCompletion, localCompletion, InternalEndExecuteReader, BeginExecuteXmlReaderInternal)) + if ( + !TriggerInternalEndAndRetryIfNecessary( + behavior, + stateObject, + timeout, + usedCache, + isRetry, + asyncWrite, + globalCompletion, + localCompletion, + endFunc: static (SqlCommand command, IAsyncResult asyncResult, bool isInternal, string endMethod) => + { + return command.InternalEndExecuteReader(asyncResult, isInternal, endMethod); + }, + retryFunc: static (SqlCommand command, CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool isRetry, bool asyncWrite) => + { + return command.BeginExecuteXmlReaderInternal(behavior, callback, stateObject, timeout, isRetry, asyncWrite); + }, + endMethod: nameof(EndExecuteXmlReader))) { globalCompletion = localCompletion; } - // Add callback after work is done to avoid overlapping Begin\End methods + // Add callback after work is done to avoid overlapping Begin/End methods if (callback != null) { globalCompletion.Task.ContinueWith((t) => callback(t), TaskScheduler.Default); @@ -2102,7 +2081,7 @@ private void BeginExecuteXmlReaderInternalReadStage(TaskCompletionSource { bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_activeConnection); // must finish caching information before ReadSni which can activate the callback before returning - cachedAsyncState.SetActiveConnectionAndResult(completion, nameof(EndExecuteXmlReader), _activeConnection); + CachedAsyncState.SetActiveConnectionAndResult(completion, nameof(EndExecuteXmlReader), _activeConnection); _stateObj.ReadSni(completion); } catch (System.OutOfMemoryException e) @@ -2128,10 +2107,7 @@ private void BeginExecuteXmlReaderInternalReadStage(TaskCompletionSource { // Similarly, if an exception occurs put the stateObj back into the pool. // and reset async cache information to allow a second async execute - if (_cachedAsyncState != null) - { - _cachedAsyncState.ResetAsyncState(); - } + CachedAsyncState?.ResetAsyncState(); ReliablePutStateObject(); completion.TrySetException(e); } @@ -2146,20 +2122,19 @@ public XmlReader EndExecuteXmlReader(IAsyncResult asyncResult) } finally { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.EndExecuteXmlReader | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); } } private XmlReader EndExecuteXmlReaderAsync(IAsyncResult asyncResult) { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.EndExecuteXmlReaderAsync | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); Debug.Assert(!_internalEndExecuteInitiated || _stateObj == null); Exception asyncException = ((Task)asyncResult).Exception; if (asyncException != null) { - // Leftover exception from the Begin...InternalReadStage - cachedAsyncState?.ResetAsyncState(); + CachedAsyncState?.ResetAsyncState(); ReliablePutStateObject(); throw asyncException.InnerException; } @@ -2188,16 +2163,18 @@ private XmlReader EndExecuteXmlReaderInternal(IAsyncResult asyncResult) int? sqlExceptionNumber = null; try { - XmlReader result = CompleteXmlReader(InternalEndExecuteReader(asyncResult, nameof(EndExecuteXmlReader), isInternal: false), true); + XmlReader result = CompleteXmlReader( + InternalEndExecuteReader(asyncResult, isInternal: false, nameof(EndExecuteXmlReader)), + isAsync: true); success = true; return result; } catch (SqlException e) { sqlExceptionNumber = e.Number; - if (cachedAsyncState != null) + if (CachedAsyncState != null) { - cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); }; // SqlException is always catchable @@ -2206,9 +2183,9 @@ private XmlReader EndExecuteXmlReaderInternal(IAsyncResult asyncResult) } catch (Exception e) { - if (cachedAsyncState != null) + if (CachedAsyncState != null) { - cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); }; if (ADP.IsCatchableExceptionType(e)) { @@ -2222,7 +2199,7 @@ private XmlReader EndExecuteXmlReaderInternal(IAsyncResult asyncResult) } } - private XmlReader CompleteXmlReader(SqlDataReader ds, bool async = false) + private XmlReader CompleteXmlReader(SqlDataReader ds, bool isAsync = false) { XmlReader xr = null; @@ -2236,7 +2213,7 @@ private XmlReader CompleteXmlReader(SqlDataReader ds, bool async = false) try { SqlStream sqlBuf = new SqlStream(ds, true /*addByteOrderMark*/, (md[0].SqlDbType == SqlDbType.Xml) ? false : true /*process all rows*/); - xr = sqlBuf.ToXmlReader(async); + xr = sqlBuf.ToXmlReader(isAsync); } catch (Exception e) { @@ -2256,126 +2233,106 @@ private XmlReader CompleteXmlReader(SqlDataReader ds, bool async = false) } /// - [System.Security.Permissions.HostProtectionAttribute(ExternalThreading = true)] - public IAsyncResult BeginExecuteReader() - { - return BeginExecuteReader(null, null, CommandBehavior.Default); - } + [HostProtection(ExternalThreading = true)] + public IAsyncResult BeginExecuteReader() => + BeginExecuteReader(callback: null, stateObject: null, CommandBehavior.Default); /// - [System.Security.Permissions.HostProtectionAttribute(ExternalThreading = true)] - public IAsyncResult BeginExecuteReader(AsyncCallback callback, object stateObject) + [HostProtection(ExternalThreading = true)] + public IAsyncResult BeginExecuteReader(AsyncCallback callback, object stateObject) => + BeginExecuteReader(callback, stateObject, CommandBehavior.Default); + + /// + [HostProtection(ExternalThreading = true)] + public IAsyncResult BeginExecuteReader(CommandBehavior behavior) => + BeginExecuteReader(callback: null, stateObject: null, behavior); + + /// + [HostProtection(ExternalThreading = true)] + public IAsyncResult BeginExecuteReader(AsyncCallback callback, object stateObject, CommandBehavior behavior) { - return BeginExecuteReader(callback, stateObject, CommandBehavior.Default); + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.BeginExecuteReader | API | Correlation | Object Id {0}, Behavior {1}, Activity Id {2}, Client Connection Id {3}, Command Text '{4}'", ObjectID, (int)behavior, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); + SqlConnection.ExecutePermission.Demand(); + return BeginExecuteReaderInternal(behavior, callback, stateObject, 0, isRetry: false); } /// protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.ExecuteDbDataReader | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); return ExecuteReader(behavior); } - private SqlDataReader ExecuteReaderWithRetry(CommandBehavior behavior, string method) - => RetryLogicProvider.Execute(this, () => ExecuteReader(behavior, method)); - /// new public SqlDataReader ExecuteReader() { SqlStatistics statistics = null; - using (TryEventScope.Create(" {0}", ObjectID)) + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.ExecuteReader | API | Correlation | ObjectID {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); + try { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); - try - { - statistics = SqlStatistics.StartTimer(Statistics); - return IsProviderRetriable ? - ExecuteReaderWithRetry(CommandBehavior.Default, nameof(ExecuteReader)) : - ExecuteReader(CommandBehavior.Default, nameof(ExecuteReader)); - } - finally - { - SqlStatistics.StopTimer(statistics); - } + statistics = SqlStatistics.StartTimer(Statistics); + return ExecuteReader(CommandBehavior.Default); } - } - - /// - new public SqlDataReader ExecuteReader(CommandBehavior behavior) - { - using (TryEventScope.Create(" {0}, behavior={1}", ObjectID, (int)behavior)) + finally { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, behavior={1}, ActivityID {2}", ObjectID, (int)behavior, ActivityCorrelator.Current); - - return IsProviderRetriable ? - ExecuteReaderWithRetry(behavior, nameof(ExecuteReader)) : - ExecuteReader(behavior, nameof(ExecuteReader)); + SqlStatistics.StopTimer(statistics); } } - /// - [System.Security.Permissions.HostProtectionAttribute(ExternalThreading = true)] - public IAsyncResult BeginExecuteReader(CommandBehavior behavior) - { - return BeginExecuteReader(null, null, behavior); - } - - /// - [System.Security.Permissions.HostProtectionAttribute(ExternalThreading = true)] - public IAsyncResult BeginExecuteReader(AsyncCallback callback, object stateObject, CommandBehavior behavior) + /// + new public SqlDataReader ExecuteReader(CommandBehavior behavior) { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID{0}, behavior={1}, ActivityID {2}", ObjectID, (int)behavior, ActivityCorrelator.Current); SqlConnection.ExecutePermission.Demand(); - return BeginExecuteReaderInternal(behavior, callback, stateObject, 0, inRetry: false); - } - - internal SqlDataReader ExecuteReader(CommandBehavior behavior, string method) - { - SqlConnection.ExecutePermission.Demand(); // TODO: Need to move this to public methods... // Reset _pendingCancel upon entry into any Execute - used to synchronize state // between entry into Execute* API and the thread obtaining the stateObject. _pendingCancel = false; - SqlStatistics statistics = null; + SqlStatistics statistics = null; TdsParser bestEffortCleanupTarget = null; RuntimeHelpers.PrepareConstrainedRegions(); bool success = false; int? sqlExceptionNumber = null; - try - { - WriteBeginExecuteEvent(); - bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_activeConnection); - statistics = SqlStatistics.StartTimer(Statistics); - SqlDataReader result = RunExecuteReader(behavior, RunBehavior.ReturnImmediately, true, method); - success = true; - return result; - } - catch (SqlException e) - { - sqlExceptionNumber = e.Number; - throw; - } - catch (System.OutOfMemoryException e) - { - _activeConnection.Abort(e); - throw; - } - catch (System.StackOverflowException e) - { - _activeConnection.Abort(e); - throw; - } - catch (System.Threading.ThreadAbortException e) - { - _activeConnection.Abort(e); - SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); - throw; - } - finally + + using (TryEventScope.Create("SqlCommand.ExecuteReader | API | Object Id {0}", ObjectID)) { - SqlStatistics.StopTimer(statistics); - WriteEndExecuteEvent(success, sqlExceptionNumber, synchronous: true); + try + { + WriteBeginExecuteEvent(); + bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_activeConnection); + statistics = SqlStatistics.StartTimer(Statistics); + SqlDataReader result = IsProviderRetriable ? + RunExecuteReaderWithRetry(behavior, RunBehavior.ReturnImmediately, returnStream: true) : + RunExecuteReader(behavior, RunBehavior.ReturnImmediately, true); + success = true; + return result; + } + catch (SqlException e) + { + sqlExceptionNumber = e.Number; + throw; + } + catch (System.OutOfMemoryException e) + { + _activeConnection.Abort(e); + throw; + } + catch (System.StackOverflowException e) + { + _activeConnection.Abort(e); + throw; + } + catch (System.Threading.ThreadAbortException e) + { + _activeConnection.Abort(e); + SqlInternalConnection.BestEffortCleanup(bestEffortCleanupTarget); + throw; + } + finally + { + SqlStatistics.StopTimer(statistics); + WriteEndExecuteEvent(success, sqlExceptionNumber, synchronous: true); + } } } @@ -2388,20 +2345,19 @@ public SqlDataReader EndExecuteReader(IAsyncResult asyncResult) } finally { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID{0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.EndExecuteReader | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); } } private SqlDataReader EndExecuteReaderAsync(IAsyncResult asyncResult) { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID{0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.EndExecuteReaderAsync | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); Debug.Assert(!_internalEndExecuteInitiated || _stateObj == null); Exception asyncException = ((Task)asyncResult).Exception; if (asyncException != null) { - // Leftover exception from the Begin...InternalReadStage - cachedAsyncState?.ResetAsyncState(); + CachedAsyncState?.ResetAsyncState(); ReliablePutStateObject(); throw asyncException.InnerException; } @@ -2409,7 +2365,6 @@ private SqlDataReader EndExecuteReaderAsync(IAsyncResult asyncResult) { ThrowIfReconnectionHasBeenCanceled(); // lock on _stateObj prevents races with close/cancel. - // If we have already initiate the End call internally, we have already done that, so no point doing it again. if (!_internalEndExecuteInitiated) { lock (_stateObj) @@ -2435,16 +2390,19 @@ private SqlDataReader EndExecuteReaderInternal(IAsyncResult asyncResult) try { statistics = SqlStatistics.StartTimer(Statistics); - SqlDataReader result = InternalEndExecuteReader(asyncResult, nameof(EndExecuteReader), isInternal: false); + SqlDataReader result = InternalEndExecuteReader( + asyncResult, + isInternal: false, + nameof(EndExecuteReader)); success = true; return result; } catch (SqlException e) { sqlExceptionNumber = e.Number; - if (cachedAsyncState != null) + if (CachedAsyncState != null) { - cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); }; // SqlException is always catchable @@ -2453,9 +2411,9 @@ private SqlDataReader EndExecuteReaderInternal(IAsyncResult asyncResult) } catch (Exception e) { - if (cachedAsyncState != null) + if (CachedAsyncState != null) { - cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); }; if (ADP.IsCatchableExceptionType(e)) { @@ -2492,15 +2450,15 @@ private void CleanupExecuteReaderAsync(Task task, TaskCompletionS private IAsyncResult BeginExecuteReaderAsync(CommandBehavior behavior, AsyncCallback callback, object stateObject) { - return BeginExecuteReaderInternal(behavior, callback, stateObject, CommandTimeout, inRetry: false, asyncWrite: true); + return BeginExecuteReaderInternal(behavior, callback, stateObject, CommandTimeout, isRetry: false, asyncWrite: true); } - private IAsyncResult BeginExecuteReaderInternal(CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool inRetry, bool asyncWrite = false) + private IAsyncResult BeginExecuteReaderInternal(CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool isRetry, bool asyncWrite = false) { TaskCompletionSource globalCompletion = new TaskCompletionSource(stateObject); TaskCompletionSource localCompletion = new TaskCompletionSource(stateObject); - if (!inRetry) + if (!isRetry) { // Reset _pendingCancel upon entry into any Execute - used to synchronize state // between entry into Execute* API and the thread obtaining the stateObject. @@ -2510,7 +2468,7 @@ private IAsyncResult BeginExecuteReaderInternal(CommandBehavior behavior, AsyncC SqlStatistics statistics = null; try { - if (!inRetry) + if (!isRetry) { statistics = SqlStatistics.StartTimer(Statistics); WriteBeginExecuteEvent(); @@ -2524,7 +2482,17 @@ private IAsyncResult BeginExecuteReaderInternal(CommandBehavior behavior, AsyncC try { // InternalExecuteNonQuery already has reliability block, but if failure will not put stateObj back into pool. - RunExecuteReader(behavior, RunBehavior.ReturnImmediately, true, nameof(BeginExecuteReader), localCompletion, timeout, out writeTask, out usedCache, asyncWrite, inRetry); + RunExecuteReader( + behavior, + RunBehavior.ReturnImmediately, + returnStream: true, + localCompletion, + timeout, + out writeTask, + out usedCache, + asyncWrite, + isRetry, + nameof(BeginExecuteReader)); } catch (Exception e) { @@ -2550,12 +2518,30 @@ private IAsyncResult BeginExecuteReaderInternal(CommandBehavior behavior, AsyncC // When we use query caching for parameter encryption we need to retry on specific errors. // In these cases finalize the call internally and trigger a retry when needed. - if (!TriggerInternalEndAndRetryIfNecessary(behavior, stateObject, timeout, nameof(EndExecuteReader), usedCache, inRetry, asyncWrite, globalCompletion, localCompletion, InternalEndExecuteReader, BeginExecuteReaderInternal)) + if ( + !TriggerInternalEndAndRetryIfNecessary( + behavior, + stateObject, + timeout, + usedCache, + isRetry, + asyncWrite, + globalCompletion, + localCompletion, + endFunc: static (SqlCommand command, IAsyncResult asyncResult, bool isInternal, string endMethod) => + { + return command.InternalEndExecuteReader(asyncResult, isInternal, endMethod); + }, + retryFunc: static (SqlCommand command, CommandBehavior behavior, AsyncCallback callback, object stateObject, int timeout, bool isRetry, bool asyncWrite) => + { + return command.BeginExecuteReaderInternal(behavior, callback, stateObject, timeout, isRetry, asyncWrite); + }, + nameof(EndExecuteReader))) { globalCompletion = localCompletion; } - // Add callback after work is done to avoid overlapping Begin\End methods + // Add callback after work is done to avoid overlapping Begin/End methods if (callback != null) { globalCompletion.Task.ContinueWith((t) => callback(t), TaskScheduler.Default); @@ -2569,14 +2555,25 @@ private IAsyncResult BeginExecuteReaderInternal(CommandBehavior behavior, AsyncC } } - private bool TriggerInternalEndAndRetryIfNecessary(CommandBehavior behavior, object stateObject, int timeout, string endMethod, bool usedCache, bool inRetry, bool asyncWrite, TaskCompletionSource globalCompletion, TaskCompletionSource localCompletion, Func endFunc, Func retryFunc) + private bool TriggerInternalEndAndRetryIfNecessary( + CommandBehavior behavior, + object stateObject, + int timeout, + bool usedCache, + bool isRetry, + bool asyncWrite, + TaskCompletionSource globalCompletion, + TaskCompletionSource localCompletion, + Func endFunc, + Func retryFunc, + string endMethod) { // We shouldn't be using the cache if we are in retry. - Debug.Assert(!usedCache || !inRetry); + Debug.Assert(!usedCache || !isRetry); // If column encryption is enabled and we used the cache, we want to catch any potential exceptions that were caused by the query cache and retry if the error indicates that we should. // So, try to read the result of the query before completing the overall task and trigger a retry if appropriate. - if ((IsColumnEncryptionEnabled && !inRetry && (usedCache || ShouldUseEnclaveBasedWorkflow)) + if ((IsColumnEncryptionEnabled && !isRetry && (usedCache || ShouldUseEnclaveBasedWorkflow)) #if DEBUG || _forceInternalEndQuery #endif @@ -2605,7 +2602,7 @@ private bool TriggerInternalEndAndRetryIfNecessary(CommandBehavior behavior, obj // lock on _stateObj prevents races with close/cancel. lock (_stateObj) { - endFunc(tsk, endMethod, true/*inInternal*/); + endFunc(this, tsk, /*isInternal:*/ true, endMethod); } globalCompletion.TrySetResult(tsk.Result); } @@ -2639,9 +2636,9 @@ private bool TriggerInternalEndAndRetryIfNecessary(CommandBehavior behavior, obj if (!shouldRetry) { // If we cannot retry, Reset the async state to make sure we leave a clean state. - if (_cachedAsyncState != null) + if (CachedAsyncState != null) { - _cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); } try { @@ -2656,7 +2653,7 @@ private bool TriggerInternalEndAndRetryIfNecessary(CommandBehavior behavior, obj } else { - // Remove the enrty from the cache since it was inconsistent. + // Remove the entry from the cache since it was inconsistent. SqlQueryMetadataCache.GetInstance().InvalidateCacheEntry(this); InvalidateEnclaveSession(); @@ -2665,7 +2662,14 @@ private bool TriggerInternalEndAndRetryIfNecessary(CommandBehavior behavior, obj { // Kick off the retry. _internalEndExecuteInitiated = false; - Task retryTask = (Task)retryFunc(behavior, null, stateObject, TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), true/*inRetry*/, asyncWrite); + Task retryTask = (Task)retryFunc( + this, + behavior, + null, + stateObject, + TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), + /*isRetry:*/ true, + asyncWrite); retryTask.ContinueWith( static (Task retryTask, object state) => @@ -2736,7 +2740,7 @@ private void BeginExecuteReaderInternalReadStage(TaskCompletionSource co { bestEffortCleanupTarget = SqlInternalConnection.GetBestEffortCleanupTarget(_activeConnection); // must finish caching information before ReadSni which can activate the callback before returning - cachedAsyncState.SetActiveConnectionAndResult(completion, nameof(EndExecuteReader), _activeConnection); + CachedAsyncState.SetActiveConnectionAndResult(completion, nameof(EndExecuteReader), _activeConnection); _stateObj.ReadSni(completion); } catch (System.OutOfMemoryException e) @@ -2762,20 +2766,17 @@ private void BeginExecuteReaderInternalReadStage(TaskCompletionSource co { // Similarly, if an exception occurs put the stateObj back into the pool. // and reset async cache information to allow a second async execute - if (_cachedAsyncState != null) - { - _cachedAsyncState.ResetAsyncState(); - } + CachedAsyncState?.ResetAsyncState(); ReliablePutStateObject(); completion.TrySetException(e); } } - private SqlDataReader InternalEndExecuteReader(IAsyncResult asyncResult, string endMethod, bool isInternal) + private SqlDataReader InternalEndExecuteReader(IAsyncResult asyncResult, bool isInternal, string endMethod) { SqlClientEventSource.Log.TryTraceEvent("SqlCommand.InternalEndExecuteReader | INFO | ObjectId {0}, Client Connection Id {1}, MARS={2}, AsyncCommandInProgress={3}", - _activeConnection?.ObjectID, _activeConnection?.ClientConnectionId, - _activeConnection?.Parser?.MARSOn, _activeConnection?.AsyncCommandInProgress); + _activeConnection?.ObjectID, _activeConnection?.ClientConnectionId, + _activeConnection?.Parser?.MARSOn, _activeConnection?.AsyncCommandInProgress); VerifyEndExecuteState((Task)asyncResult, endMethod); WaitForAsyncResults(asyncResult, isInternal); @@ -2815,18 +2816,21 @@ private SqlDataReader InternalEndExecuteReader(IAsyncResult asyncResult, string } } - private Task InternalExecuteNonQueryWithRetryAsync(CancellationToken cancellationToken) - => RetryLogicProvider.ExecuteAsync(this, () => InternalExecuteNonQueryAsync(cancellationToken), cancellationToken); - /// - public override Task ExecuteNonQueryAsync(CancellationToken cancellationToken) - => IsProviderRetriable ? - InternalExecuteNonQueryWithRetryAsync(cancellationToken) : - InternalExecuteNonQueryAsync(cancellationToken); + public override Task ExecuteNonQueryAsync(CancellationToken cancellationToken) => + IsProviderRetriable + ? InternalExecuteNonQueryWithRetryAsync(cancellationToken) + : InternalExecuteNonQueryAsync(cancellationToken); + + private Task InternalExecuteNonQueryWithRetryAsync(CancellationToken cancellationToken) => + RetryLogicProvider.ExecuteAsync( + sender: this, + () => InternalExecuteNonQueryAsync(cancellationToken), + cancellationToken); private Task InternalExecuteNonQueryAsync(CancellationToken cancellationToken) { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.InternalExecuteNonQueryAsync | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); SqlConnection.ExecutePermission.Demand(); Guid operationId = Guid.Empty; @@ -2926,36 +2930,34 @@ protected override Task ExecuteDbDataReaderAsync(CommandBehavior b ); } - private Task InternalExecuteReaderWithRetryAsync(CommandBehavior behavior, CancellationToken cancellationToken) - => RetryLogicProvider.ExecuteAsync(this, () => InternalExecuteReaderAsync(behavior, cancellationToken), cancellationToken); - /// - new public Task ExecuteReaderAsync() - => IsProviderRetriable ? - InternalExecuteReaderWithRetryAsync(CommandBehavior.Default, CancellationToken.None) : - InternalExecuteReaderAsync(CommandBehavior.Default, CancellationToken.None); + public new Task ExecuteReaderAsync() => + ExecuteReaderAsync(CommandBehavior.Default, CancellationToken.None); /// - new public Task ExecuteReaderAsync(CommandBehavior behavior) - => IsProviderRetriable ? - InternalExecuteReaderWithRetryAsync(behavior, CancellationToken.None) : - InternalExecuteReaderAsync(behavior, CancellationToken.None); + public new Task ExecuteReaderAsync(CommandBehavior behavior) => + ExecuteReaderAsync(behavior, CancellationToken.None); /// - new public Task ExecuteReaderAsync(CancellationToken cancellationToken) - => IsProviderRetriable ? - InternalExecuteReaderWithRetryAsync(CommandBehavior.Default, cancellationToken) : - InternalExecuteReaderAsync(CommandBehavior.Default, cancellationToken); + public new Task ExecuteReaderAsync(CancellationToken cancellationToken) => + ExecuteReaderAsync(CommandBehavior.Default, cancellationToken); /// - new public Task ExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) - => IsProviderRetriable ? - InternalExecuteReaderWithRetryAsync(behavior, cancellationToken) : - InternalExecuteReaderAsync(behavior, cancellationToken); + public new Task ExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) => + IsProviderRetriable + ? InternalExecuteReaderWithRetryAsync(behavior, cancellationToken) + : InternalExecuteReaderAsync(behavior, cancellationToken); + + private Task InternalExecuteReaderWithRetryAsync(CommandBehavior behavior, CancellationToken cancellationToken) => + RetryLogicProvider.ExecuteAsync( + sender: this, + () => InternalExecuteReaderAsync(behavior, cancellationToken), + cancellationToken); private Task InternalExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, behavior={1}, ActivityID {2}", ObjectID, (int)behavior, ActivityCorrelator.Current); + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.InternalExecuteReaderAsync | API | Correlation | Object Id {0}, Behavior {1}, Activity Id {2}, Client Connection Id {3}, Command Text '{4}'", ObjectID, (int)behavior, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.InternalExecuteReaderAsync | API> {0}, Client Connection Id {1}, Command Text = '{2}'", ObjectID, Connection?.ClientConnectionId, CommandText); SqlConnection.ExecutePermission.Demand(); Guid operationId = default(Guid); @@ -2995,7 +2997,7 @@ private Task InternalExecuteReaderAsync(CommandBehavior behavior, beginMethod: static (AsyncCallback callback, object stateObject) => { ExecuteReaderAsyncCallContext args = (ExecuteReaderAsyncCallContext)stateObject; - return args.Command.BeginExecuteReaderInternal(args.CommandBehavior, callback, stateObject, args.Command.CommandTimeout, inRetry: false, asyncWrite: true); + return args.Command.BeginExecuteReaderInternal(args.CommandBehavior, callback, stateObject, args.Command.CommandTimeout, isRetry: false, asyncWrite: true); }, endMethod: static (IAsyncResult asyncResult) => { @@ -3071,84 +3073,86 @@ private Task InternalExecuteScalarAsync(CancellationToken cancellationTo else { SqlDataReader reader = executeTask.Result; - reader.ReadAsync(cancellationToken).ContinueWith((readTask) => - { - try + reader.ReadAsync(cancellationToken) + .ContinueWith((Task readTask) => { - if (readTask.IsCanceled) - { - reader.Dispose(); - source.SetCanceled(); - } - else if (readTask.IsFaulted) - { - reader.Dispose(); - source.SetException(readTask.Exception.InnerException); - } - else + try { - Exception exception = null; - object result = null; - try - { - bool more = readTask.Result; - if (more && reader.FieldCount > 0) - { - try - { - result = reader.GetValue(0); - } - catch (Exception e) - { - exception = e; - } - } - } - finally + if (readTask.IsCanceled) { reader.Dispose(); + source.SetCanceled(); } - if (exception != null) + else if (readTask.IsFaulted) { - source.SetException(exception); + reader.Dispose(); + source.SetException(readTask.Exception.InnerException); } else { - source.SetResult(result); + Exception exception = null; + object result = null; + try + { + bool more = readTask.Result; + if (more && reader.FieldCount > 0) + { + try + { + result = reader.GetValue(0); + } + catch (Exception e) + { + exception = e; + } + } + } + finally + { + reader.Dispose(); + } + if (exception != null) + { + source.SetException(exception); + } + else + { + source.SetResult(result); + } } } - } - catch (Exception e) - { - // exception thrown by Dispose... - source.SetException(e); - } - }, - TaskScheduler.Default - ); + catch (Exception e) + { + // exception thrown by Dispose... + source.SetException(e); + } + }, + TaskScheduler.Default + ); } return source.Task; }, TaskScheduler.Default).Unwrap(); } /// - public Task ExecuteXmlReaderAsync() - { - return ExecuteXmlReaderAsync(CancellationToken.None); - } - - private Task InternalExecuteXmlReaderWithRetryAsync(CancellationToken cancellationToken) - => RetryLogicProvider.ExecuteAsync(this, () => InternalExecuteXmlReaderAsync(cancellationToken), cancellationToken); + public Task ExecuteXmlReaderAsync() => + ExecuteXmlReaderAsync(CancellationToken.None); /// - public Task ExecuteXmlReaderAsync(CancellationToken cancellationToken) - => IsProviderRetriable ? - InternalExecuteXmlReaderWithRetryAsync(cancellationToken) : - InternalExecuteXmlReaderAsync(cancellationToken); + public Task ExecuteXmlReaderAsync(CancellationToken cancellationToken) => + IsProviderRetriable + ? InternalExecuteXmlReaderWithRetryAsync(cancellationToken) + : InternalExecuteXmlReaderAsync(cancellationToken); + + private Task InternalExecuteXmlReaderWithRetryAsync(CancellationToken cancellationToken) => + RetryLogicProvider.ExecuteAsync( + sender: this, + () => InternalExecuteXmlReaderAsync(cancellationToken), + cancellationToken); private Task InternalExecuteXmlReaderAsync(CancellationToken cancellationToken) { - SqlClientEventSource.Log.TryCorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); + SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlCommand.InternalExecuteXmlReaderAsync | API | Correlation | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command Text '{3}'", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); SqlConnection.ExecutePermission.Demand(); Guid operationId = Guid.Empty; @@ -3336,14 +3340,17 @@ private static string UnquoteProcedureName(string name, out object groupNumber) if (sproc != null) { if (char.IsDigit(sproc[sproc.Length - 1])) - { // If last char is a digit, parse. + { + // If last char is a digit, parse. int semicolon = sproc.LastIndexOf(';'); if (semicolon != -1) - { // If we found a semicolon, obtain the integer. + { + // If we found a semicolon, obtain the integer. string part = sproc.Substring(semicolon + 1); int number = 0; if (int.TryParse(part, out number)) - { // No checking, just fail if this doesn't work. + { + // No checking, just fail if this doesn't work. groupNumber = number; sproc = sproc.Substring(0, semicolon); } @@ -3428,11 +3435,11 @@ internal void DeriveParameters() } // validate that we have a valid connection - ValidateCommand(nameof(DeriveParameters), false /*not async*/); + ValidateCommand(isAsync: false); // Use common parser for SqlClient and OleDb - parse into 4 parts - Server, Catalog, Schema, ProcedureName string[] parsedSProc = MultipartIdentifier.ParseMultipartIdentifier(CommandText, "[\"", "]\"", Strings.SQL_SqlCommandCommandText, false); - if (parsedSProc[3] == null || string.IsNullOrEmpty(parsedSProc[3])) + if (string.IsNullOrEmpty(parsedSProc[3])) { throw ADP.NoStoredProcedureExists(CommandText); } @@ -3504,7 +3511,8 @@ internal void DeriveParameters() } if (!string.IsNullOrEmpty(parsedSProc[2])) - { // SchemaName is 3rd element in parsed array + { + // SchemaName is 3rd element in parsed array SqlParameter param = paramsCmd.Parameters.Add(new SqlParameter("@procedure_schema", SqlDbType.NVarChar, 255)); param.Value = UnquoteProcedurePart(parsedSProc[2]); } @@ -3788,9 +3796,9 @@ static internal string SqlNotificationContext() } // Tds-specific logic for ExecuteNonQuery run handling - private Task RunExecuteNonQueryTds(string methodName, bool async, int timeout, bool asyncWrite) + private Task RunExecuteNonQueryTds(string methodName, bool isAsync, int timeout, bool asyncWrite) { - Debug.Assert(!asyncWrite || async, "AsyncWrite should be always accompanied by Async"); + Debug.Assert(!asyncWrite || isAsync, "AsyncWrite should be always accompanied by Async"); bool processFinallyBlock = true; try { @@ -3799,7 +3807,7 @@ private Task RunExecuteNonQueryTds(string methodName, bool async, int timeout, b if (reconnectTask != null) { long reconnectionStart = ADP.TimerCurrent(); - if (async) + if (isAsync) { TaskCompletionSource completion = new TaskCompletionSource(); _activeConnection.RegisterWaitingForReconnect(completion.Task); @@ -3815,7 +3823,7 @@ private Task RunExecuteNonQueryTds(string methodName, bool async, int timeout, b } Interlocked.CompareExchange(ref _reconnectionCompletionSource, null, completion); timeoutCTS.Cancel(); - Task subTask = RunExecuteNonQueryTds(methodName, async, TdsParserStaticMethods.GetRemainingTimeout(timeout, reconnectionStart), asyncWrite); + Task subTask = RunExecuteNonQueryTds(methodName, isAsync, TdsParserStaticMethods.GetRemainingTimeout(timeout, reconnectionStart), asyncWrite); if (subTask == null) { completion.SetResult(null); @@ -3850,12 +3858,12 @@ private Task RunExecuteNonQueryTds(string methodName, bool async, int timeout, b // no parameters are sent over // no data reader is returned // use this overload for "batch SQL" tds token type - SqlClientEventSource.Log.TryTraceEvent(" {0}, Command executed as SQLBATCH.", ObjectID); + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.RunExecuteNonQueryTds | Info | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command executed as SQLBATCH, Command Text '{3}' ", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); Task executeTask = _stateObj.Parser.TdsExecuteSQLBatch(this.CommandText, timeout, this.Notification, _stateObj, sync: true); Debug.Assert(executeTask == null, "Shouldn't get a task when doing sync writes"); NotifyDependency(); - if (async) + if (isAsync) { _activeConnection.GetOpenTdsConnection(methodName).IncrementAsyncCount(); } @@ -3876,7 +3884,7 @@ private Task RunExecuteNonQueryTds(string methodName, bool async, int timeout, b } finally { - if (processFinallyBlock && !async) + if (processFinallyBlock && !isAsync) { // When executing Async, we need to keep the _stateObj alive... PutStateObject(); @@ -3887,7 +3895,7 @@ private Task RunExecuteNonQueryTds(string methodName, bool async, int timeout, b /// /// Resets the encryption related state of the command object and each of the parameters. - /// BatchRPC doesn't need special handling to cleanup the state of each RPC object and its parameters since a new RPC object and + /// BatchRPC doesn't need special handling to cleanup the state of each RPC object and its parameters since a new RPC object and /// parameters are generated on every execution. /// private void ResetEncryptionState() @@ -3964,17 +3972,22 @@ private void PrepareTransparentEncryptionFinallyBlock(bool closeDataReader, /// Executes the reader after checking to see if we need to encrypt input parameters and then encrypting it if required. /// TryFetchInputParameterEncryptionInfo() -> ReadDescribeEncryptionParameterResults()-> EncryptInputParameters() ->RunExecuteReaderTds() /// - /// - /// - /// + /// /// /// /// /// /// - /// + /// /// - private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool returnStream, bool async, int timeout, TaskCompletionSource completion, out Task returnTask, bool asyncWrite, out bool usedCache, bool inRetry) + private void PrepareForTransparentEncryption( + bool isAsync, + int timeout, + TaskCompletionSource completion, + out Task returnTask, + bool asyncWrite, + out bool usedCache, + bool isRetry) { // Fetch reader with input params Task fetchInputParameterEncryptionInfoTask = null; @@ -3990,11 +4003,11 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r Debug.Assert(_columnEncryptionSetting == SqlCommandColumnEncryptionSetting.Enabled || (_columnEncryptionSetting == SqlCommandColumnEncryptionSetting.UseConnectionSetting && _activeConnection.IsColumnEncryptionSettingEnabled), "ColumnEncryption setting should be enabled for input parameter encryption."); - Debug.Assert(async == (completion != null), "completion should can be null if and only if mode is async."); + Debug.Assert(isAsync == (completion != null), "completion should can be null if and only if mode is async."); // If we are not in Batch RPC and not already retrying, attempt to fetch the cipher MD for each parameter from the cache. // If this succeeds then return immediately, otherwise just fall back to the full crypto MD discovery. - if (!_batchRPCMode && !inRetry && (this._parameters != null && this._parameters.Count > 0) && SqlQueryMetadataCache.GetInstance().GetQueryMetadataIfExists(this)) + if (!_batchRPCMode && !isRetry && (this._parameters != null && this._parameters.Count > 0) && SqlQueryMetadataCache.GetInstance().GetQueryMetadataIfExists(this)) { usedCache = true; return; @@ -4021,17 +4034,17 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r { // Fetch the encryption information that applies to any of the input parameters. describeParameterEncryptionDataReader = TryFetchInputParameterEncryptionInfo(timeout, - async, + isAsync, asyncWrite, out describeParameterEncryptionNeeded, out fetchInputParameterEncryptionInfoTask, out describeParameterEncryptionRpcOriginalRpcMap, - inRetry); + isRetry); Debug.Assert(describeParameterEncryptionNeeded || describeParameterEncryptionDataReader == null, "describeParameterEncryptionDataReader should be null if we don't need to request describe parameter encryption request."); - Debug.Assert(fetchInputParameterEncryptionInfoTask == null || async, + Debug.Assert(fetchInputParameterEncryptionInfoTask == null || isAsync, "Task returned by TryFetchInputParameterEncryptionInfo, when in sync mode, in PrepareForTransparentEncryption."); Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == _batchRPCMode, @@ -4050,7 +4063,7 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r } // If we are in async execution, we need to decrement our async count on exception. - decrementAsyncCountInFinallyBlock = async; + decrementAsyncCountInFinallyBlock = isAsync; Debug.Assert(describeParameterEncryptionDataReader != null, "describeParameterEncryptionDataReader should not be null, as it is required to get results of describe parameter encryption."); @@ -4089,7 +4102,7 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r ReadDescribeEncryptionParameterResults( describeParameterEncryptionDataReader, describeParameterEncryptionRpcOriginalRpcMap, - inRetry); + isRetry); #if DEBUG // Failpoint to force the thread to halt to simulate cancellation of SqlCommand. @@ -4116,9 +4129,9 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r }, onFailure: ((exception) => { - if (_cachedAsyncState != null) + if (CachedAsyncState != null) { - _cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); } if (exception != null) { @@ -4131,7 +4144,7 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r else { // If it was async, ending the reader is still pending. - if (async) + if (isAsync) { // Mark that we should not process the finally block since we have async execution pending. // Note that this should be done outside the task's continuation delegate. @@ -4162,7 +4175,7 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r Debug.Assert(_stateObj == null, "non-null state object in PrepareForTransparentEncryption."); // Read the results of describe parameter encryption. - ReadDescribeEncryptionParameterResults(describeParameterEncryptionDataReader, describeParameterEncryptionRpcOriginalRpcMap, inRetry); + ReadDescribeEncryptionParameterResults(describeParameterEncryptionDataReader, describeParameterEncryptionRpcOriginalRpcMap, isRetry); #if DEBUG // Failpoint to force the thread to halt to simulate cancellation of SqlCommand. if (_sleepAfterReadDescribeEncryptionParameterResults) @@ -4192,7 +4205,7 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r else { // For synchronous execution, read the results of describe parameter encryption here. - ReadDescribeEncryptionParameterResults(describeParameterEncryptionDataReader, describeParameterEncryptionRpcOriginalRpcMap, inRetry); + ReadDescribeEncryptionParameterResults(describeParameterEncryptionDataReader, describeParameterEncryptionRpcOriginalRpcMap, isRetry); } #if DEBUG @@ -4213,9 +4226,9 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r finally { // Free up the state only for synchronous execution. For asynchronous execution, free only if there was an exception. - PrepareTransparentEncryptionFinallyBlock(closeDataReader: (processFinallyBlock && !async) || exceptionCaught, + PrepareTransparentEncryptionFinallyBlock(closeDataReader: (processFinallyBlock && !isAsync) || exceptionCaught, decrementAsyncCount: decrementAsyncCountInFinallyBlock && exceptionCaught, - clearDataStructures: (processFinallyBlock && !async) || exceptionCaught, + clearDataStructures: (processFinallyBlock && !isAsync) || exceptionCaught, wasDescribeParameterEncryptionNeeded: describeParameterEncryptionNeeded, describeParameterEncryptionRpcOriginalRpcMap: describeParameterEncryptionRpcOriginalRpcMap, describeParameterEncryptionDataReader: describeParameterEncryptionDataReader); @@ -4239,9 +4252,9 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r } catch (Exception e) { - if (cachedAsyncState != null) + if (CachedAsyncState != null) { - cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); } if (ADP.IsCatchableExceptionType(e)) @@ -4258,20 +4271,21 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r /// the request to wire, it'll set the "task" parameter which can be used to create continuations. /// /// - /// + /// /// /// /// /// - /// Indicates if this is a retry from a failed call. + /// Indicates if this is a retry from a failed call. /// - private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, - bool async, - bool asyncWrite, - out bool inputParameterEncryptionNeeded, - out Task task, - out ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, - bool inRetry) + private SqlDataReader TryFetchInputParameterEncryptionInfo( + int timeout, + bool isAsync, + bool asyncWrite, + out bool inputParameterEncryptionNeeded, + out Task task, + out ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, + bool isRetry) { inputParameterEncryptionNeeded = false; task = null; @@ -4286,7 +4300,7 @@ private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, EnclaveSessionParameters enclaveSessionParameters = GetEnclaveSessionParameters(); SqlEnclaveSession sqlEnclaveSession = null; - EnclaveDelegate.Instance.GetEnclaveSession(attestationProtocol, enclaveType, enclaveSessionParameters, true, inRetry, out sqlEnclaveSession, out customData, out customDataLength); + EnclaveDelegate.Instance.GetEnclaveSession(attestationProtocol, enclaveType, enclaveSessionParameters, true, isRetry, out sqlEnclaveSession, out customData, out customDataLength); if (sqlEnclaveSession == null) { enclaveAttestationParameters = EnclaveDelegate.Instance.GetAttestationParameters(attestationProtocol, enclaveType, enclaveSessionParameters.AttestationUrl, customData, customDataLength); @@ -4302,7 +4316,7 @@ private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, for (int i = 0; i < _RPCList.Count; i++) { - // In _batchRPCMode, the actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-_batchRPCMode. + // In BatchRPCMode, the actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-BatchRPCMode. // So input parameters start at parameters[1]. parameters[0] is the actual T-SQL Statement. rpcName is sp_executesql. if (_RPCList[i].systemParams.Length > 1) { @@ -4375,16 +4389,17 @@ private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, #endif // Execute the RPC. - return RunExecuteReaderTds(CommandBehavior.Default, - runBehavior: RunBehavior.ReturnImmediately, // Other RunBehavior modes will skip reading rows. - returnStream: true, - async: async, - timeout: timeout, - task: out task, - asyncWrite: asyncWrite, - inRetry: false, - ds: null, - describeParameterEncryptionRequest: true); + return RunExecuteReaderTds( + CommandBehavior.Default, + runBehavior: RunBehavior.ReturnImmediately, + returnStream: true, + isAsync: isAsync, + timeout: timeout, + task: out task, + asyncWrite: asyncWrite, + isRetry: false, + ds: null, + describeParameterEncryptionRequest: true); } else { @@ -4419,7 +4434,7 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques // Construct the RPC request for sp_describe_parameter_encryption // sp_describe_parameter_encryption always has 2 parameters (stmt, paramlist). - //sp_describe_parameter_encryption can have an optional 3rd parameter (attestationParametes), used to identify and execute attestation protocol + // sp_describe_parameter_encryption can have an optional 3rd parameter (attestationParameters), used to identify and execute attestation protocol GetRPCObject(attestationParameters == null ? 2 : 3, 0, ref describeParameterEncryptionRequest, forSpDescribeParameterEncryption: true); describeParameterEncryptionRequest.rpcName = "sp_describe_parameter_encryption"; @@ -4460,13 +4475,13 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques } Debug.Assert(text != null, "@tsql parameter is null in PrepareDescribeParameterEncryptionRequest."); - string parameterList = null; - // In _batchRPCMode, the input parameters start at parameters[1]. parameters[0] is the T-SQL statement. rpcName is sp_executesql. - // And it is already in the format expected out of BuildParamList, which is not the case with Non-_batchRPCMode. + // In BatchRPCMode, the input parameters start at parameters[1]. parameters[0] is the T-SQL statement. rpcName is sp_executesql. + // And it is already in the format expected out of BuildParamList, which is not the case with Non-BatchRPCMode. if (_batchRPCMode) { + // systemParamCount == 2 when user parameters are supplied to BuildExecuteSql if (originalRpcRequest.systemParamCount > 1) { parameterList = (string)originalRpcRequest.systemParams[1].Value; @@ -4547,8 +4562,11 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques /// /// Resultset from calling to sp_describe_parameter_encryption /// Readonly dictionary with the map of parameter encryption rpc requests with the corresponding original rpc requests. - /// Indicates if this is a retry from a failed call. - private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, bool inRetry) + /// Indicates if this is a retry from a failed call. + private void ReadDescribeEncryptionParameterResults( + SqlDataReader ds, + ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, + bool isRetry) { _SqlRPC rpc = null; int currentOrdinal = -1; @@ -4558,7 +4576,7 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == _batchRPCMode, "describeParameterEncryptionRpcOriginalRpcMap should be non-null if and only if it is _batchRPCMode."); - // Indicates the current result set we are reading, used in _batchRPCMode, where we can have more than 1 result set. + // Indicates the current result set we are reading, used in BatchRPCMode, where we can have more than 1 result set. int resultSetSequenceNumber = 0; #if DEBUG @@ -4566,7 +4584,7 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi int rowsAffected = 0; #endif - // A flag that used in _batchRPCMode, to assert the result of lookup in to the dictionary maintaining the map of describe parameter encryption requests + // A flag that used in BatchRPCMode, to assert the result of lookup in to the dictionary maintaining the map of describe parameter encryption requests // and the corresponding original rpc requests. bool lookupDictionaryResult; @@ -4715,7 +4733,6 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi int userParamCount = rpc.userParams?.Count ?? 0; int receivedMetadataCount = 0; - if (!enclaveMetadataExists || ds.NextResult()) { // Iterate over the parameter names to read the encryption type info @@ -4786,7 +4803,7 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi SqlParameter sqlParameter = rpc.userParams[index]; if (!sqlParameter.HasReceivedMetadata && sqlParameter.Direction != ParameterDirection.ReturnValue) { - // Encryption MD wasn't sent by the server - we expect the metadata to be sent for all the parameters + // Encryption MD wasn't sent by the server - we expect the metadata to be sent for all the parameters // that were sent in the original sp_describe_parameter_encryption but not necessarily for return values, // since there might be multiple return values but server will only send for one of them. // For parameters that don't need encryption, the encryption type is set to plaintext. @@ -4832,7 +4849,7 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi enclaveAttestationParameters, customData, customDataLength, - inRetry); + isRetry); enclaveAttestationParameters = null; attestationInfoRead = true; } @@ -4866,18 +4883,41 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi } } - internal SqlDataReader RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, bool returnStream, string method) - { - Task unused; // sync execution - SqlDataReader reader = RunExecuteReader(cmdBehavior, runBehavior, returnStream, method, completion: null, timeout: CommandTimeout, task: out unused, out _); + internal SqlDataReader RunExecuteReader( + CommandBehavior cmdBehavior, + RunBehavior runBehavior, + bool returnStream, + [CallerMemberName] string method = "") + { + Task unused; // sync execution + SqlDataReader reader = RunExecuteReader( + cmdBehavior, + runBehavior, + returnStream, + completion: null, + timeout: CommandTimeout, + task: out unused, + usedCache: out _, + method: method); + Debug.Assert(unused == null, "returned task during synchronous execution"); return reader; } - // task is created in case of pending asynchronous write, returned SqlDataReader should not be utilized until that task is complete - internal SqlDataReader RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, bool returnStream, string method, TaskCompletionSource completion, int timeout, out Task task, out bool usedCache, bool asyncWrite = false, bool inRetry = false) + // task is created in case of pending asynchronous write, returned SqlDataReader should not be utilized until that task is complete + internal SqlDataReader RunExecuteReader( + CommandBehavior cmdBehavior, + RunBehavior runBehavior, + bool returnStream, + TaskCompletionSource completion, + int timeout, + out Task task, + out bool usedCache, + bool asyncWrite = false, + bool isRetry = false, + [CallerMemberName] string method = "") { - bool async = completion != null; + bool isAsync = completion != null; usedCache = false; task = null; @@ -4893,9 +4933,9 @@ internal SqlDataReader RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior // this function may throw for an invalid connection // returns false for empty command text - if (!inRetry) + if (!isRetry) { - ValidateCommand(method, async); + ValidateCommand(isAsync, method); } CheckNotificationStateAndAutoEnlist(); // Only call after validate - requires non null connection! @@ -4926,20 +4966,29 @@ internal SqlDataReader RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior if (IsColumnEncryptionEnabled) { Task returnTask = null; - PrepareForTransparentEncryption(cmdBehavior, returnStream, async, timeout, completion, out returnTask, asyncWrite && async, out usedCache, inRetry); - Debug.Assert(usedCache || (async == (returnTask != null)), @"if we didn't use the cache, returnTask should be null if and only if async is false."); + PrepareForTransparentEncryption(isAsync, timeout, completion, out returnTask, asyncWrite && isAsync, out usedCache, isRetry); + Debug.Assert(usedCache || (isAsync == (returnTask != null)), @"if we didn't use the cache, returnTask should be null if and only if async is false."); long firstAttemptStart = ADP.TimerCurrent(); try { - return RunExecuteReaderTdsWithTransparentParameterEncryption(cmdBehavior, runBehavior, returnStream, async, timeout, out task, asyncWrite && async, inRetry: inRetry, ds: null, - describeParameterEncryptionRequest: false, describeParameterEncryptionTask: returnTask); + return RunExecuteReaderTdsWithTransparentParameterEncryption( + cmdBehavior, + runBehavior, + returnStream, + isAsync, + timeout, + out task, + asyncWrite && isAsync, + isRetry: isRetry, + ds: null, + describeParameterEncryptionTask: returnTask); } catch (EnclaveDelegate.RetryableEnclaveQueryExecutionException) { - if (inRetry) + if (isRetry) { throw; } @@ -4950,14 +4999,24 @@ internal SqlDataReader RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior InvalidateEnclaveSession(); - return RunExecuteReader(cmdBehavior, runBehavior, returnStream, method, completion, TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), out task, out usedCache, async, inRetry: true); + return RunExecuteReader( + cmdBehavior, + runBehavior, + returnStream, + completion, + TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), + out task, + out usedCache, + isAsync, + isRetry: true, + method: method); } catch (SqlException ex) { // We only want to retry once, so don't retry if we are already in retry. // If we didn't use the cache, we don't want to retry. - if (inRetry || (!usedCache && !ShouldUseEnclaveBasedWorkflow)) + if (isRetry || (!usedCache && !ShouldUseEnclaveBasedWorkflow)) { throw; } @@ -4988,13 +5047,23 @@ internal SqlDataReader RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior InvalidateEnclaveSession(); - return RunExecuteReader(cmdBehavior, runBehavior, returnStream, method, completion, TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), out task, out usedCache, async, inRetry: true); + return RunExecuteReader( + cmdBehavior, + runBehavior, + returnStream, + completion, + TdsParserStaticMethods.GetRemainingTimeout(timeout, firstAttemptStart), + out task, + out usedCache, + isAsync, + isRetry: true, + method: method); } } } else { - return RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, async, timeout, out task, asyncWrite && async, inRetry: inRetry); + return RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, timeout, out task, asyncWrite && isAsync, isRetry: isRetry); } } catch (System.OutOfMemoryException e) @@ -5020,16 +5089,15 @@ private SqlDataReader RunExecuteReaderTdsWithTransparentParameterEncryption( CommandBehavior cmdBehavior, RunBehavior runBehavior, bool returnStream, - bool async, + bool isAsync, int timeout, out Task task, bool asyncWrite, - bool inRetry, + bool isRetry, SqlDataReader ds = null, - bool describeParameterEncryptionRequest = false, Task describeParameterEncryptionTask = null) { - Debug.Assert(!asyncWrite || async, "AsyncWrite should be always accompanied by Async"); + Debug.Assert(!asyncWrite || isAsync, "AsyncWrite should be always accompanied by Async"); if (ds == null && returnStream) { @@ -5046,7 +5114,7 @@ private SqlDataReader RunExecuteReaderTdsWithTransparentParameterEncryption( SqlCommand command = (SqlCommand)state; Task subTask = null; command.GenerateEnclavePackage(); - command.RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, async, TdsParserStaticMethods.GetRemainingTimeout(timeout, parameterEncryptionStart), out subTask, asyncWrite, inRetry, ds); + command.RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, TdsParserStaticMethods.GetRemainingTimeout(timeout, parameterEncryptionStart), out subTask, asyncWrite, isRetry, ds); if (subTask == null) { completion.SetResult(null); @@ -5058,13 +5126,13 @@ private SqlDataReader RunExecuteReaderTdsWithTransparentParameterEncryption( }, onFailure: static (Exception exception, object state) => { - ((SqlCommand)state)._cachedAsyncState?.ResetAsyncState(); + ((SqlCommand)state).CachedAsyncState?.ResetAsyncState(); if (exception != null) { throw exception; } }, - onCancellation: static (object state) => ((SqlCommand)state)._cachedAsyncState?.ResetAsyncState(), + onCancellation: static (object state) => ((SqlCommand)state).CachedAsyncState?.ResetAsyncState(), connectionToDoom: null, connectionToAbort: _activeConnection); task = completion.Task; @@ -5074,7 +5142,7 @@ private SqlDataReader RunExecuteReaderTdsWithTransparentParameterEncryption( { // Synchronous execution. GenerateEnclavePackage(); - return RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, async, timeout, out task, asyncWrite, inRetry, ds); + return RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, timeout, out task, asyncWrite, isRetry, ds); } } @@ -5123,9 +5191,19 @@ private void GenerateEnclavePackage() } } - private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, bool returnStream, bool async, int timeout, out Task task, bool asyncWrite, bool inRetry, SqlDataReader ds = null, bool describeParameterEncryptionRequest = false) + private SqlDataReader RunExecuteReaderTds( + CommandBehavior cmdBehavior, + RunBehavior runBehavior, + bool returnStream, + bool isAsync, + int timeout, + out Task task, + bool asyncWrite, + bool isRetry, + SqlDataReader ds = null, + bool describeParameterEncryptionRequest = false) { - Debug.Assert(!asyncWrite || async, "AsyncWrite should be always accompanied by Async"); + Debug.Assert(!asyncWrite || isAsync, "AsyncWrite should be always accompanied by Async"); if (ds == null && returnStream) { @@ -5137,7 +5215,7 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi if (reconnectTask != null) { long reconnectionStart = ADP.TimerCurrent(); - if (async) + if (isAsync) { TaskCompletionSource completion = new TaskCompletionSource(); _activeConnection.RegisterWaitingForReconnect(completion.Task); @@ -5154,7 +5232,7 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi Interlocked.CompareExchange(ref _reconnectionCompletionSource, null, completion); timeoutCTS.Cancel(); Task subTask; - RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, async, TdsParserStaticMethods.GetRemainingTimeout(timeout, reconnectionStart), out subTask, asyncWrite, inRetry, ds); + RunExecuteReaderTds(cmdBehavior, runBehavior, returnStream, isAsync, TdsParserStaticMethods.GetRemainingTimeout(timeout, reconnectionStart), out subTask, asyncWrite, isRetry, ds); if (subTask == null) { completion.SetResult(null); @@ -5192,7 +5270,7 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi bool processFinallyBlock = true; bool decrementAsyncCountOnFailure = false; - if (async) + if (isAsync) { _activeConnection.GetOpenTdsConnection().IncrementAsyncCount(); decrementAsyncCountOnFailure = true; @@ -5235,7 +5313,7 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi if (returnStream) { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Command executed as SQLBATCH.", ObjectID); + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.RunExecuteReaderTds | Info | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command executed as SQLBATCH, Command Text '{3}' ", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, CommandText); } string text = GetCommandText(cmdBehavior) + GetResetOptionsString(cmdBehavior); @@ -5275,7 +5353,7 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi if (_execType == EXECTYPE.PREPARED) { - Debug.Assert(this.IsPrepared && (_prepareHandle != -1), "invalid attempt to call sp_execute without a handle!"); + Debug.Assert(IsPrepared && _prepareHandle != s_cachedInvalidPrepareHandle, "invalid attempt to call sp_execute without a handle!"); rpc = BuildExecute(inSchema); } else if (_execType == EXECTYPE.PREPAREPENDING) @@ -5298,7 +5376,7 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi rpc.options = TdsEnums.RPC_NOMETADATA; if (returnStream) { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Command executed as RPC.", ObjectID); + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.RunExecuteReaderTds | Info | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command executed as RPC, RPC Name '{3}' ", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, rpc?.rpcName); } // TODO: Medusa: Unprepare only happens for SQL 7.0 which may be broken anyway (it's not re-prepared). Consider removing the reset here if we're really dropping 7.0 support. @@ -5317,7 +5395,7 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi optionSettings = GetSetOptionsString(cmdBehavior); if (returnStream) { - SqlClientEventSource.Log.TryTraceEvent(" {0}, Command executed as RPC.", ObjectID); + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.RunExecuteReaderTds | Info | Object Id {0}, Activity Id {1}, Client Connection Id {2}, Command executed as RPC, RPC Name '{3}' ", ObjectID, ActivityCorrelator.Current, Connection?.ClientConnectionId, rpc?.rpcName); } // turn set options ON @@ -5340,9 +5418,9 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi writeTask = _stateObj.Parser.TdsExecuteRPC(this, _rpcArrayOf1, timeout, inSchema, this.Notification, _stateObj, CommandType.StoredProcedure == CommandType, sync: !asyncWrite); } - Debug.Assert(writeTask == null || async, "Returned task in sync mode"); + Debug.Assert(writeTask == null || isAsync, "Returned task in sync mode"); - if (async) + if (isAsync) { decrementAsyncCountOnFailure = false; if (writeTask != null) @@ -5350,7 +5428,7 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi task = AsyncHelper.CreateContinuationTask(writeTask, () => { _activeConnection.GetOpenTdsConnection(); // it will throw if connection is closed - cachedAsyncState.SetAsyncReaderState(ds, runBehavior, optionSettings); + CachedAsyncState.SetAsyncReaderState(ds, runBehavior, optionSettings); }, onFailure: (exc) => { @@ -5359,7 +5437,7 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi } else { - cachedAsyncState.SetAsyncReaderState(ds, runBehavior, optionSettings); + CachedAsyncState.SetAsyncReaderState(ds, runBehavior, optionSettings); } } else @@ -5375,7 +5453,8 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi { SqlInternalConnectionTds innerConnectionTds = (_activeConnection.InnerConnection as SqlInternalConnectionTds); if (innerConnectionTds != null) - { // it may be closed + { + // it may be closed innerConnectionTds.DecrementAsyncCount(); } } @@ -5383,24 +5462,24 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi } finally { - if (processFinallyBlock && !async) + if (processFinallyBlock && !isAsync) { // When executing async, we need to keep the _stateObj alive... PutStateObject(); } } - Debug.Assert(async || _stateObj == null, "non-null state object in RunExecuteReader"); + Debug.Assert(isAsync || _stateObj == null, "non-null state object in RunExecuteReader"); return ds; } private SqlDataReader CompleteAsyncExecuteReader(bool isInternal = false, bool forDescribeParameterEncryption = false) { - SqlDataReader ds = cachedAsyncState.CachedAsyncReader; // should not be null + SqlDataReader ds = CachedAsyncState.CachedAsyncReader; // should not be null bool processFinallyBlock = true; try { - FinishExecuteReader(ds, cachedAsyncState.CachedRunBehavior, cachedAsyncState.CachedSetOptions, isInternal, forDescribeParameterEncryption, shouldCacheForAlwaysEncrypted: !forDescribeParameterEncryption); + FinishExecuteReader(ds, CachedAsyncState.CachedRunBehavior, CachedAsyncState.CachedSetOptions, isInternal, forDescribeParameterEncryption, shouldCacheForAlwaysEncrypted: !forDescribeParameterEncryption); } catch (Exception e) { @@ -5414,7 +5493,7 @@ private SqlDataReader CompleteAsyncExecuteReader(bool isInternal = false, bool f // Don't reset the state for internal End. The user End will do that eventually. if (!isInternal) { - cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); } PutStateObject(); } @@ -5480,10 +5559,6 @@ private void FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, stri _stateObj = null; // the reader now owns this... ds.ResetOptionsString = resetOptionsString; - // UNDONE UNDONE BUGBUG - // In the future, with Mars, at this point we may have to turn the - // set options off, I am not sure how that is going to work as of yet. - // bind this reader to this connection now _activeConnection.AddWeakReference(ds, SqlReferenceCollection.DataReaderTag); @@ -5511,7 +5586,6 @@ private void FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, stri } catch (Exception e) { - // UNDONE - should not be catching all exceptions!!! if (ADP.IsCatchableExceptionType(e)) { if (_inPrepare) @@ -5520,7 +5594,7 @@ private void FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, stri // the handle unless command execution failed. If fail, move back to pending // state. _inPrepare = false; // reset the flag - IsDirty = true; // mark command as dirty so it will be prepared next time we're comming through + IsDirty = true; // mark command as dirty so it will be prepared next time we're coming through _execType = EXECTYPE.PREPAREPENDING; // reset execution type to pending } @@ -5532,26 +5606,16 @@ private void FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, stri } } - private void NotifyDependency() - { - if (_sqlDep != null) - { - _sqlDep.StartTimer(Notification); - } - } - /// public SqlCommand Clone() { SqlCommand clone = new SqlCommand(this); - SqlClientEventSource.Log.TryTraceEvent(" {0}, clone={1}", ObjectID, clone?.ObjectID); + SqlClientEventSource.Log.TryTraceEvent("SqlCommand.Clone | API | Object Id {0}, Clone Object Id {1}, Client Connection Id {2}", ObjectID, clone.ObjectID, Connection?.ClientConnectionId); return clone; } - object ICloneable.Clone() - { - return Clone(); - } + object ICloneable.Clone() => + Clone(); private Task RegisterForConnectionCloseNotification(Task outterTask) { @@ -5567,7 +5631,7 @@ private Task RegisterForConnectionCloseNotification(Task outterTask) // validates that a command has commandText and a non-busy open connection // throws exception for error case, returns false if the commandText is empty - private void ValidateCommand(string method, bool async) + private void ValidateCommand(bool isAsync, [CallerMemberName] string method = "") { if (_activeConnection == null) { @@ -5665,17 +5729,17 @@ private void ValidateCommand(string method, bool async) private void ValidateAsyncCommand() { - // TODO - UNDONE - move this if check below CloseDeadReader when that is fixed. - if (cachedAsyncState.PendingAsyncOperation) - { // Enforce only one pending async execute at a time. - if (cachedAsyncState.IsActiveConnectionValid(_activeConnection)) + if (CachedAsyncState.PendingAsyncOperation) + { + // Enforce only one pending async execute at a time. + if (CachedAsyncState.IsActiveConnectionValid(_activeConnection)) { throw SQL.PendingBeginXXXExists(); } else { _stateObj = null; // Session was re-claimed by session pool upon connection close. - cachedAsyncState.ResetAsyncState(); + CachedAsyncState.ResetAsyncState(); } } } @@ -5783,10 +5847,10 @@ internal void OnDoneProc(TdsParserStateObject stateObject) } } - private static void OnDone(TdsParserStateObject stateObj, int index, IList<_SqlRPC> array, int rowsAffected) + private static void OnDone(TdsParserStateObject stateObj, int index, IList<_SqlRPC> rpcList, int rowsAffected) { - _SqlRPC current = array[index]; - _SqlRPC previous = (index > 0) ? array[index - 1] : null; + _SqlRPC current = rpcList[index]; + _SqlRPC previous = (index > 0) ? rpcList[index - 1] : null; // track the records affected for the just completed rpc batch // _rowsAffected is cumulative for ExecuteNonQuery across all rpc batches @@ -5815,10 +5879,6 @@ private static void OnDone(TdsParserStateObject stateObj, int index, IList<_SqlR current.warnings = stateObj._warnings; } - // - // UNDONE: want to inherit, but hide from the user - // ICommandHandler - // internal void OnReturnStatus(int status) { if (_inPrepare) @@ -6100,7 +6160,6 @@ private SqlParameter GetParameterForOutputValueExtraction(SqlParameterCollection private void GetRPCObject(int systemParamCount, int userParamCount, ref _SqlRPC rpc, bool forSpDescribeParameterEncryption = false) { // Designed to minimize necessary allocations - if (rpc == null) { if (!forSpDescribeParameterEncryption) @@ -6176,7 +6235,7 @@ private void SetUpRPCParameters(_SqlRPC rpc, bool inSchema, SqlParameterCollecti parameter.Validate(index, CommandType.StoredProcedure == CommandType); // func will change type to that with a 4 byte length if the type has a two - // byte length and a parameter length > than that expressable in 2 bytes + // byte length and a parameter length > than that expressible in 2 bytes if ((!parameter.ValidateTypeLengths().IsPlp) && (parameter.Direction != ParameterDirection.Output)) { parameter.FixStreamDataForNonPLP(); @@ -6201,11 +6260,11 @@ private void SetUpRPCParameters(_SqlRPC rpc, bool inSchema, SqlParameterCollecti // set default value bit if (parameter.Direction != ParameterDirection.Output) { - // remember that null == Convert.IsEmpty, DBNull.Value is a database null! + // remember that Convert.IsEmpty is null, DBNull.Value is a database null! - // MDAC 62117, don't assume a default value exists for parameters in the case when - // the user is simply requesting schema - // SQLBUVSTS 179488 TVPs use DEFAULT and do not allow NULL, even for schema only. + // Don't assume a default value exists for parameters in the case when + // the user is simply requesting schema. + // TVPs use DEFAULT and do not allow NULL, even for schema only. if (parameter.Value == null && (!inSchema || SqlDbType.Structured == parameter.SqlDbType)) { options |= TdsEnums.RPC_PARAM_DEFAULT; @@ -6240,11 +6299,6 @@ private void SetUpRPCParameters(_SqlRPC rpc, bool inSchema, SqlParameterCollecti rpc.userParams = parameters; } - // - // 7.5 - // prototype for sp_prepexec is: - // sp_prepexec(@handle int IN/OUT, @batch_params ntext, @batch_text ntext, param1value,param2value...) - // private _SqlRPC BuildPrepExec(CommandBehavior behavior) { Debug.Assert(System.Data.CommandType.Text == this.CommandType, "invalid use of sp_prepexec for stored proc invocation!"); @@ -6287,7 +6341,6 @@ private _SqlRPC BuildPrepExec(CommandBehavior behavior) return rpc; } - // // returns true if the parameter is not a return value // and it's value is not DBNull (for a nullable parameter) @@ -6310,7 +6363,7 @@ private static bool ShouldSendParameter(SqlParameter p, bool includeReturnValue } } - private int CountSendableParameters(SqlParameterCollection parameters) + private static int CountSendableParameters(SqlParameterCollection parameters) { int cParams = 0; @@ -6343,11 +6396,12 @@ private void BuildRPC(bool inSchema, SqlParameterCollection parameters, ref _Sql int userParameterCount = CountSendableParameters(parameters); GetRPCObject(0, userParameterCount, ref rpc); + rpc.ProcID = 0; + // TDS Protocol allows rpc name with maximum length of 1046 bytes for ProcName // 4-part name 1 + 128 + 1 + 1 + 1 + 128 + 1 + 1 + 1 + 128 + 1 + 1 + 1 + 128 + 1 = 523 // each char takes 2 bytes. 523 * 2 = 1046 int commandTextLength = ADP.CharSize * CommandText.Length; - rpc.ProcID = 0; if (commandTextLength <= MaxRPCNameLength) { rpc.rpcName = CommandText; // just get the raw command text @@ -6368,7 +6422,7 @@ private void BuildRPC(bool inSchema, SqlParameterCollection parameters, ref _Sql // private _SqlRPC BuildExecute(bool inSchema) { - Debug.Assert(_prepareHandle != -1, "Invalid call to sp_execute without a valid handle!"); + Debug.Assert(_prepareHandle != s_cachedInvalidPrepareHandle, "Invalid call to sp_execute without a valid handle!"); const int systemParameterCount = 1; int userParameterCount = CountSendableParameters(_parameters); @@ -6382,8 +6436,8 @@ private _SqlRPC BuildExecute(bool inSchema) //@handle SqlParameter sqlParam = rpc.systemParams[0]; sqlParam.SqlDbType = SqlDbType.Int; - sqlParam.Value = _prepareHandle; sqlParam.Size = 4; + sqlParam.Value = _prepareHandle; sqlParam.Direction = ParameterDirection.Input; SetUpRPCParameters(rpc, inSchema, _parameters); @@ -6398,7 +6452,7 @@ private _SqlRPC BuildExecute(bool inSchema) private void BuildExecuteSql(CommandBehavior behavior, string commandText, SqlParameterCollection parameters, ref _SqlRPC rpc) { - Debug.Assert(_prepareHandle == -1, "This command has an existing handle, use sp_execute!"); + Debug.Assert(_prepareHandle == s_cachedInvalidPrepareHandle, "This command has an existing handle, use sp_execute!"); Debug.Assert(CommandType.Text == this.CommandType, "invalid use of sp_executesql for stored proc invocation!"); int systemParamCount; SqlParameter sqlParam; @@ -6511,7 +6565,7 @@ private SqlParameter BuildStoredProcedureStatementForColumnEncryption(string sto parameter = parameters[index]; // Possibility of a SQL Injection issue through parameter names and how to construct valid identifier for parameters. // Since the parameters comes from application itself, there should not be a security vulnerability. - // Also since the query is not executed, but only analyzed there is no possibility for elevation of priviledge, but only for + // Also since the query is not executed, but only analyzed there is no possibility for elevation of privilege, but only for // incorrect results which would only affect the user that attempts the injection. execStatement.Append(' '); SqlParameter.AppendPrefixedParameterName(execStatement, parameter.ParameterName); @@ -6519,10 +6573,8 @@ private SqlParameter BuildStoredProcedureStatementForColumnEncryption(string sto SqlParameter.AppendPrefixedParameterName(execStatement, parameter.ParameterName); // InputOutput and Output parameters need to be marked as such. - if ( - parameter.Direction == ParameterDirection.Output || - parameter.Direction == ParameterDirection.InputOutput - ) + if (parameter.Direction == ParameterDirection.Output || + parameter.Direction == ParameterDirection.InputOutput) { execStatement.AppendFormat(@" OUTPUT"); } @@ -6581,6 +6633,7 @@ internal string BuildParamList(TdsParser parser, SqlParameterCollection paramete { paramList.Append(','); } + SqlParameter.AppendPrefixedParameterName(paramList, sqlParam.ParameterName); MetaType mt = sqlParam.InternalMetaType; @@ -6590,7 +6643,7 @@ internal string BuildParamList(TdsParser parser, SqlParameterCollection paramete // paragraph above doesn't seem to be correct. Server won't find the type // if we don't provide a fully qualified name - paramList.Append(' '); + paramList.Append(" "); if (mt.SqlDbType == SqlDbType.Udt) { string fullTypeName = sqlParam.UdtTypeName; @@ -6614,7 +6667,7 @@ internal string BuildParamList(TdsParser parser, SqlParameterCollection paramete else { // func will change type to that with a 4 byte length if the type has a two - // byte length and a parameter length > than that expressable in 2 bytes + // byte length and a parameter length > than that expressible in 2 bytes mt = sqlParam.ValidateTypeLengths(); if ((!mt.IsPlp) && (sqlParam.Direction != ParameterDirection.Output)) { @@ -6652,6 +6705,9 @@ internal string BuildParamList(TdsParser parser, SqlParameterCollection paramete } else if (mt.SqlDbType == SqlDbTypeExtensions.Vector) { + // The validate function for SqlParameters would + // have already thrown InvalidCastException if an incompatible + // value is specified for SqlDbType Vector. var sqlVectorProps = (ISqlVector)sqlParam.Value; paramList.Append('('); paramList.Append(sqlVectorProps.Length); @@ -6952,17 +7008,14 @@ internal void SetBatchRPCModeReadyToExecute() /// private void SetColumnEncryptionSetting(SqlCommandColumnEncryptionSetting newColumnEncryptionSetting) { - if (!this._wasBatchModeColumnEncryptionSettingSetOnce) + if (!_wasBatchModeColumnEncryptionSettingSetOnce) { - this._columnEncryptionSetting = newColumnEncryptionSetting; - this._wasBatchModeColumnEncryptionSettingSetOnce = true; + _columnEncryptionSetting = newColumnEncryptionSetting; + _wasBatchModeColumnEncryptionSettingSetOnce = true; } - else + else if (_columnEncryptionSetting != newColumnEncryptionSetting) { - if (this._columnEncryptionSetting != newColumnEncryptionSetting) - { - throw SQL.BatchedUpdateColumnEncryptionSettingMismatch(); - } + throw SQL.BatchedUpdateColumnEncryptionSettingMismatch(); } } @@ -7049,6 +7102,37 @@ internal SqlException GetErrors(int commandIndex) return result; } + private static void CancelIgnoreFailureCallback(object state) + { + SqlCommand command = (SqlCommand)state; + command.CancelIgnoreFailure(); + } + + private void CancelIgnoreFailure() + { + // This method is used to route CancellationTokens to the Cancel method. + // Cancellation is a suggestion, and exceptions should be ignored + // rather than allowed to be unhandled, as there is no way to route + // them to the caller. It would be expected that the error will be + // observed anyway from the regular method. An example is cancelling + // an operation on a closed connection. + try + { + Cancel(); + } + catch (Exception) + { + } + } + + private void NotifyDependency() + { + if (_sqlDep != null) + { + _sqlDep.StartTimer(Notification); + } + } + private void WriteBeginExecuteEvent() { SqlClientEventSource.Log.TryBeginExecuteEvent(ObjectID, Connection?.DataSource, Connection?.Database, CommandText, Connection?.ClientConnectionId); @@ -7081,49 +7165,5 @@ private void WriteEndExecuteEvent(bool success, int? sqlExceptionNumber, bool sy SqlClientEventSource.Log.TryEndExecuteEvent(ObjectID, compositeState, sqlExceptionNumber.GetValueOrDefault(), Connection?.ClientConnectionId); } } - -#if DEBUG - internal void CompletePendingReadWithSuccess(bool resetForcePendingReadsToWait) - { - var stateObj = _stateObj; - if (stateObj != null) - { - stateObj.CompletePendingReadWithSuccess(resetForcePendingReadsToWait); - } - else - { - var tempCachedAsyncState = cachedAsyncState; - if (tempCachedAsyncState != null) - { - var reader = tempCachedAsyncState.CachedAsyncReader; - if (reader != null) - { - reader.CompletePendingReadWithSuccess(resetForcePendingReadsToWait); - } - } - } - } - - internal void CompletePendingReadWithFailure(int errorCode, bool resetForcePendingReadsToWait) - { - var stateObj = _stateObj; - if (stateObj != null) - { - stateObj.CompletePendingReadWithFailure(errorCode, resetForcePendingReadsToWait); - } - else - { - var tempCachedAsyncState = _cachedAsyncState; - if (tempCachedAsyncState != null) - { - var reader = tempCachedAsyncState.CachedAsyncReader; - if (reader != null) - { - reader.CompletePendingReadWithFailure(errorCode, resetForcePendingReadsToWait); - } - } - } - } -#endif } } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs index 3eac0d16ff..24cd7dbe28 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -89,6 +89,8 @@ private static readonly Dictionary private Func> _accessTokenCallback; + private SspiContextProvider _sspiContextProvider; + internal bool HasColumnEncryptionKeyStoreProvidersRegistered => _customColumnEncryptionKeyStoreProviders is not null && _customColumnEncryptionKeyStoreProviders.Count > 0; @@ -577,6 +579,16 @@ internal int ConnectRetryInterval get => ((SqlConnectionString)ConnectionOptions).ConnectRetryInterval; } + internal SspiContextProvider SspiContextProvider + { + get { return _sspiContextProvider; } + set + { + ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, _credential, accessToken: null, accessTokenCallback: null, sspiContextProvider: value)); + _sspiContextProvider = value; + } + } + /// [DefaultValue("")] #pragma warning disable 618 // ignore obsolete warning about RecommendedAsConfigurable to use SettingsBindableAttribute @@ -645,7 +657,7 @@ public override string ConnectionString CheckAndThrowOnInvalidCombinationOfConnectionOptionAndAccessTokenCallback(connectionOptions); } } - ConnectionString_Set(new SqlConnectionPoolKey(value, _credential, _accessToken, _accessTokenCallback)); + ConnectionString_Set(new SqlConnectionPoolKey(value, _credential, _accessToken, _accessTokenCallback, _sspiContextProvider)); _connectionString = value; // Change _connectionString value only after value is validated CacheConnectionStringProperties(); } @@ -705,7 +717,7 @@ public string AccessToken _accessToken = value; // Need to call ConnectionString_Set to do proper pool group check - ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, _credential, _accessToken, null)); + ConnectionString_Set(new SqlConnectionPoolKey(_connectionString, _credential, _accessToken, null, sspiContextProvider: null)); } } @@ -727,7 +739,7 @@ public Func @@ -1280,7 +1292,7 @@ public static void ClearPool(SqlConnection connection) if (connectionOptions != null) { connectionOptions.DemandPermission(); - SqlConnectionFactory.SingletonInstance.ClearPool(connection); + SqlConnectionFactory.Instance.ClearPool(connection); } } @@ -2184,9 +2196,9 @@ public static void ChangePassword(string connectionString, string newPassword) throw ADP.InvalidArgumentLength(nameof(newPassword), TdsEnums.MAXLEN_NEWPASSWORD); } - SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential: null, accessToken: null, accessTokenCallback: null); + SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential: null, accessToken: null, accessTokenCallback: null, sspiContextProvider: null); - SqlConnectionString connectionOptions = SqlConnectionFactory.FindSqlConnectionOptions(key); + SqlConnectionString connectionOptions = SqlConnectionFactory.Instance.FindSqlConnectionOptions(key); if (connectionOptions.IntegratedSecurity || connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated) { throw SQL.ChangePasswordConflictsWithSSPI(); @@ -2236,9 +2248,9 @@ public static void ChangePassword(string connectionString, SqlCredential credent throw ADP.InvalidArgumentLength(nameof(newSecurePassword), TdsEnums.MAXLEN_NEWPASSWORD); } - SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential, accessToken: null, accessTokenCallback: null); + SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential, accessToken: null, accessTokenCallback: null, sspiContextProvider: null); - SqlConnectionString connectionOptions = SqlConnectionFactory.FindSqlConnectionOptions(key); + SqlConnectionString connectionOptions = SqlConnectionFactory.Instance.FindSqlConnectionOptions(key); // Check for connection string values incompatible with SqlCredential if (!string.IsNullOrEmpty(connectionOptions.UserID) || !string.IsNullOrEmpty(connectionOptions.Password)) @@ -2277,9 +2289,9 @@ private static void ChangePassword(string connectionString, SqlConnectionString { con?.Dispose(); } - SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential, accessToken: null, accessTokenCallback: null); + SqlConnectionPoolKey key = new SqlConnectionPoolKey(connectionString, credential, accessToken: null, accessTokenCallback: null, sspiContextProvider: null); - SqlConnectionFactory.SingletonInstance.ClearPool(key); + SqlConnectionFactory.Instance.ClearPool(key); } internal Task RegisterForConnectionCloseNotification(Task outerTask, object value, int tag) @@ -2456,11 +2468,10 @@ internal object GetUdtValue(object value, SqlMetaDataPriv metaData, bool returnD } } - internal byte[] GetBytes(object o, out Format format, out int maxSize) + internal byte[] GetBytes(object o, out int maxSize) { SqlUdtInfo attr = GetInfoFromType(o.GetType()); maxSize = attr.MaxByteSize; - format = attr.SerializationFormat; if (maxSize < -1 || maxSize >= ushort.MaxValue) { diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionHelper.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionHelper.cs index eb5cb511be..68c7d24ea2 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionHelper.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionHelper.cs @@ -18,7 +18,7 @@ namespace Microsoft.Data.SqlClient { public sealed partial class SqlConnection : DbConnection { - private static readonly DbConnectionFactory _connectionFactory = SqlConnectionFactory.SingletonInstance; + private static readonly SqlConnectionFactory _connectionFactory = SqlConnectionFactory.Instance; internal static readonly System.Security.CodeAccessPermission ExecutePermission = SqlConnection.CreateExecutePermission(); private DbConnectionOptions _userConnectionOptions; @@ -69,7 +69,7 @@ internal int CloseCount } } - internal DbConnectionFactory ConnectionFactory + internal SqlConnectionFactory ConnectionFactory { get { @@ -203,8 +203,7 @@ override protected DbCommand CreateDbCommand() { using (TryEventScope.Create(" {0}", ObjectID)) { - DbProviderFactory providerFactory = ConnectionFactory.ProviderFactory; - DbCommand command = providerFactory.CreateCommand(); + DbCommand command = SqlClientFactory.Instance.CreateCommand(); command.Connection = this; return command; } @@ -212,8 +211,8 @@ override protected DbCommand CreateDbCommand() private static System.Security.CodeAccessPermission CreateExecutePermission() { - DBDataPermission p = (DBDataPermission)SqlConnectionFactory.SingletonInstance.ProviderFactory.CreatePermission(System.Security.Permissions.PermissionState.None); - p.Add(String.Empty, String.Empty, KeyRestrictionBehavior.AllowOnly); + DBDataPermission p = (DBDataPermission)SqlClientFactory.Instance.CreatePermission(System.Security.Permissions.PermissionState.None); + p.Add(string.Empty, string.Empty, KeyRestrictionBehavior.AllowOnly); return p; } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index c011863837..38118dda72 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -136,6 +136,7 @@ internal sealed class SqlInternalConnectionTds : SqlInternalConnection, IDisposa SqlFedAuthToken _fedAuthToken = null; internal byte[] _accessTokenInBytes; internal readonly Func> _accessTokenCallback; + internal readonly SspiContextProvider _sspiContextProvider; private readonly ActiveDirectoryAuthenticationTimeoutRetryHelper _activeDirectoryAuthTimeoutRetryHelper; @@ -472,8 +473,8 @@ internal SqlInternalConnectionTds( bool applyTransientFaultHandling = false, string accessToken = null, IDbConnectionPool pool = null, - Func> accessTokenCallback = null) + Func> accessTokenCallback = null, + SspiContextProvider sspiContextProvider = null) : base(connectionOptions) { #if DEBUG @@ -525,6 +526,7 @@ internal SqlInternalConnectionTds( } _accessTokenCallback = accessTokenCallback; + _sspiContextProvider = sspiContextProvider; _activeDirectoryAuthTimeoutRetryHelper = new ActiveDirectoryAuthenticationTimeoutRetryHelper(); @@ -3110,9 +3112,13 @@ internal bool ThreadHasParserLockForClose } } - internal override bool TryReplaceConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) + internal override bool TryReplaceConnection( + DbConnection outerConnection, + SqlConnectionFactory connectionFactory, + TaskCompletionSource retry, + DbConnectionOptions userOptions) { - return base.TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions); + return TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions); } } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index f89613204f..7aafe80082 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -11,9 +11,8 @@ using System.Diagnostics; using System.Globalization; using System.IO; -#if NET using System.Security.Authentication; -#else +#if NETFRAMEWORK using System.Runtime.CompilerServices; #endif using System.Text; @@ -69,7 +68,6 @@ internal sealed partial class TdsParser // Constants private const int constBinBufferSize = 4096; // Size of the buffer used to read input parameter of type Stream private const int constTextBufferSize = 4096; // Size of the buffer (in chars) user to read input parameter of type TextReader - private const string enableTruncateSwitch = "Switch.Microsoft.Data.SqlClient.TruncateScaledDecimal"; // for applications that need to maintain backwards compatibility with the previous behavior // State variables internal TdsParserState _state = TdsParserState.Closed; // status flag for connection @@ -205,16 +203,6 @@ internal SqlInternalConnectionTds Connection } } - private static bool EnableTruncateSwitch - { - get - { - bool value; - value = AppContext.TryGetSwitch(enableTruncateSwitch, out value) ? value : false; - return value; - } - } - internal SqlInternalTransaction CurrentTransaction { get @@ -411,7 +399,7 @@ internal void Connect(ServerInfo serverInfo, // AD Integrated behaves like Windows integrated when connecting to a non-fedAuth server if (integratedSecurity || authType == SqlAuthenticationMethod.ActiveDirectoryIntegrated) { - _authenticationProvider = _physicalStateObj.CreateSspiContextProvider(); + _authenticationProvider = Connection._sspiContextProvider ?? _physicalStateObj.CreateSspiContextProvider(); if (!string.IsNullOrEmpty(serverInfo.ServerSPN)) { @@ -694,31 +682,10 @@ internal void EnableMars() ThrowExceptionAndWarning(_physicalStateObj); } - // HACK HACK HACK - for Async only - // Have to post read to intialize MARS - will get callback on this when connection goes - // down or is closed. - - IntPtr temp = IntPtr.Zero; - - RuntimeHelpers.PrepareConstrainedRegions(); - try - { } - finally + error = _pMarsPhysicalConObj.PostReadAsyncForMars(_physicalStateObj); + if (error != TdsEnums.SNI_SUCCESS_IO_PENDING) { - _pMarsPhysicalConObj.IncrementPendingCallbacks(); - - error = SniNativeWrapper.SniReadAsync(_pMarsPhysicalConObj.Handle, ref temp); - - if (temp != IntPtr.Zero) - { - // Be sure to release packet, otherwise it will be leaked by native. - SniNativeWrapper.SniPacketRelease(temp); - } - } - Debug.Assert(IntPtr.Zero == temp, "unexpected syncReadPacket without corresponding SNIPacketRelease"); - if (TdsEnums.SNI_SUCCESS_IO_PENDING != error) - { - Debug.Assert(TdsEnums.SNI_SUCCESS != error, "Unexpected successful read async on physical connection before enabling MARS!"); + Debug.Assert(error != TdsEnums.SNI_SUCCESS, "Unexpected successful read async on physical connection before enabling MARS!"); _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); ThrowExceptionAndWarning(_physicalStateObj); } @@ -993,19 +960,25 @@ private void EnableSsl(uint info, SqlConnectionEncryptOption encrypt, bool integ ThrowExceptionAndWarning(_physicalStateObj); } + SslProtocols protocol = 0; + // in the case where an async connection is made, encryption is used and Windows Authentication is used, // wait for SSL handshake to complete, so that the SSL context is fully negotiated before we try to use its // Channel Bindings as part of the Windows Authentication context build (SSL handshake must complete // before calling SNISecGenClientContext). - error = SniNativeWrapper.SniWaitForSslHandshakeToComplete(_physicalStateObj.Handle, _physicalStateObj.GetTimeoutRemaining(), out uint protocolVersion); - - if (error != TdsEnums.SNI_SUCCESS) +#if NET + if (OperatingSystem.IsWindows()) +#endif { - _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); - ThrowExceptionAndWarning(_physicalStateObj); + error = _physicalStateObj.WaitForSSLHandShakeToComplete(out protocol); + if (error != TdsEnums.SNI_SUCCESS) + { + _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); + ThrowExceptionAndWarning(_physicalStateObj); + } } - string warningMessage = ((System.Security.Authentication.SslProtocols)protocolVersion).GetProtocolWarning(); + string warningMessage = protocol.GetProtocolWarning(); if (!string.IsNullOrEmpty(warningMessage)) { if (!encrypt && LocalAppContextSwitches.SuppressInsecureTlsWarning) @@ -7849,7 +7822,7 @@ internal static SqlDecimal AdjustSqlDecimalScale(SqlDecimal d, int newScale) { if (d.Scale != newScale) { - bool round = !EnableTruncateSwitch; + bool round = !LocalAppContextSwitches.TruncateScaledDecimal; return SqlDecimal.AdjustScale(d, newScale - d.Scale, round); } @@ -7862,7 +7835,7 @@ internal static decimal AdjustDecimalScale(decimal value, int newScale) if (newScale != oldScale) { - bool round = !EnableTruncateSwitch; + bool round = !LocalAppContextSwitches.TruncateScaledDecimal; SqlDecimal num = new SqlDecimal(value); num = SqlDecimal.AdjustScale(num, newScale - oldScale, round); return num.Value; @@ -10139,7 +10112,6 @@ private Task TDSExecuteRPCAddParameter(TdsParserStateObject stateObj, SqlParamet { int maxSupportedSize = Is2008OrNewer ? int.MaxValue : short.MaxValue; byte[] udtVal = null; - Format format = Format.Native; if (string.IsNullOrEmpty(param.UdtTypeName)) { @@ -10173,7 +10145,7 @@ private Task TDSExecuteRPCAddParameter(TdsParserStateObject stateObj, SqlParamet } else { - udtVal = _connHandler.Connection.GetBytes(value, out format, out maxsize); + udtVal = _connHandler.Connection.GetBytes(value, out maxsize); } Debug.Assert(udtVal != null, "GetBytes returned null instance. Make sure that it always returns non-null value"); @@ -13318,11 +13290,10 @@ internal TdsOperationStatus TryReadPlpUnicodeCharsWithContinue(TdsParserStateObj if (canContinue) { - if (isContinuing || isStarting) - { - temp = stateObj.TryTakeSnapshotStorage() as char[]; - Debug.Assert(temp == null || length == int.MaxValue || temp.Length == length, "stored buffer length must be null or must have been created with the correct length"); - } + temp = stateObj.TryTakeSnapshotStorage() as char[]; + Debug.Assert(temp != null || !isContinuing, "if continuing stored buffer must be present to contain previous data to continue from"); + Debug.Assert(temp == null || length == int.MaxValue || temp.Length == length, "stored buffer length must be null or must have been created with the correct length"); + if (temp != null) { startOffset = stateObj.GetSnapshotTotalSize(); @@ -13337,8 +13308,8 @@ internal TdsOperationStatus TryReadPlpUnicodeCharsWithContinue(TdsParserStateObj out length, supportRentedBuff: !canContinue, // do not use the arraypool if we are going to keep the buffer in the snapshot rentedBuff: ref buffIsRented, - startOffset, - isStarting || isContinuing + startOffset, + canContinue ); if (result == TdsOperationStatus.Done) @@ -13365,7 +13336,7 @@ internal TdsOperationStatus TryReadPlpUnicodeCharsWithContinue(TdsParserStateObj } else if (result == TdsOperationStatus.NeedMoreData) { - if (isStarting || isContinuing) + if (canContinue) { stateObj.SetSnapshotStorage(temp); } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs index 807474d98b..a77ab597d1 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Security.Authentication; using System.Threading.Tasks; using Interop.Windows.Sni; using Microsoft.Data.Common; @@ -172,9 +173,54 @@ internal override uint DisableSsl() internal override uint EnableMars(ref uint info) => SniNativeWrapper.SniAddProvider(Handle, Provider.SMUX_PROV, ref info); + internal override uint PostReadAsyncForMars(TdsParserStateObject physicalStateObject) + { + // HACK HACK HACK - for Async only + // Have to post read to initialize MARS - will get callback on this when connection goes + // down or is closed. + + PacketHandle temp = default; + uint error = TdsEnums.SNI_SUCCESS; + +#if NETFRAMEWORK + RuntimeHelpers.PrepareConstrainedRegions(); +#endif + try + { } + finally + { + IncrementPendingCallbacks(); + SessionHandle handle = SessionHandle; + // we do not need to consider partial packets when making this read because we + // expect this read to pend. a partial packet should not exist at setup of the + // parser + Debug.Assert(physicalStateObject.PartialPacket == null); + temp = ReadAsync(handle, out error); + + Debug.Assert(temp.Type == PacketHandle.NativePointerType, "unexpected packet type when requiring NativePointer"); + + if (temp.NativePointer != IntPtr.Zero) + { + // Be sure to release packet, otherwise it will be leaked by native. + ReleasePacket(temp); + } + } + + Debug.Assert(IntPtr.Zero == temp.NativePointer, "unexpected syncReadPacket without corresponding SNIPacketRelease"); + return error; + } + internal override uint SetConnectionBufferSize(ref uint unsignedPacketSize) => SniNativeWrapper.SniSetInfo(Handle, QueryType.SNI_QUERY_CONN_BUFSIZE, ref unsignedPacketSize); + internal override uint WaitForSSLHandShakeToComplete(out SslProtocols protocolVersion) + { + uint returnValue = SniNativeWrapper.SniWaitForSslHandshakeToComplete(Handle, GetTimeoutRemaining(), out uint nativeProtocolVersion); + + protocolVersion = (SslProtocols)nativeProtocolVersion; + return returnValue; + } + internal override SniErrorDetails GetErrorDetails() { SniNativeWrapper.SniGetLastError(out SniError sniError); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj index aef9ceeb7e..df4ca44714 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj @@ -8,8 +8,14 @@ + + + + + + @@ -17,13 +23,8 @@ - - - - - diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs index fc28c43938..35d232b4da 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs @@ -130,7 +130,18 @@ internal static Exception ExceptionWithStackTrace(Exception e) } } - internal static Timer UnsafeCreateTimer(TimerCallback callback, object state, int dueTime, int period) + internal static Timer UnsafeCreateTimer( + TimerCallback callback, + object state, + int dueTimeMilliseconds, + int periodMilliseconds) => + UnsafeCreateTimer( + callback, + state, + TimeSpan.FromMilliseconds(dueTimeMilliseconds), + TimeSpan.FromMilliseconds(periodMilliseconds)); + + internal static Timer UnsafeCreateTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) { // Don't capture the current ExecutionContext and its AsyncLocals onto // a global timer causing them to live forever @@ -154,6 +165,7 @@ internal static Timer UnsafeCreateTimer(TimerCallback callback, object state, in } } } + #region COM+ exceptions internal static ArgumentException Argument(string error) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionClosed.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionClosed.cs index d4ea183312..8549c48a8d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionClosed.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionClosed.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.Data.Common; using Microsoft.Data.Common.ConnectionString; +using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient.ConnectionPool; namespace Microsoft.Data.ProviderBase @@ -27,7 +28,7 @@ protected DbConnectionClosed(ConnectionState state, bool hidePassword, bool allo public override void ChangeDatabase(string database) => throw ADP.ClosedConnectionError(); - internal override void CloseConnection(DbConnection owningObject, DbConnectionFactory connectionFactory) + internal override void CloseConnection(DbConnection owningObject, SqlConnectionFactory connectionFactory) { // not much to do here... } @@ -36,13 +37,24 @@ internal override void CloseConnection(DbConnection owningObject, DbConnectionFa public override void EnlistTransaction(System.Transactions.Transaction transaction) => throw ADP.ClosedConnectionError(); - protected internal override DataTable GetSchema(DbConnectionFactory factory, DbConnectionPoolGroup poolGroup, DbConnection outerConnection, string collectionName, string[] restrictions) - => throw ADP.ClosedConnectionError(); + protected internal override DataTable GetSchema( + SqlConnectionFactory factory, + DbConnectionPoolGroup poolGroup, + DbConnection outerConnection, + string collectionName, + string[] restrictions) + { + throw ADP.ClosedConnectionError(); + } protected override DbReferenceCollection CreateReferenceCollection() => throw ADP.ClosedConnectionError(); - internal override bool TryOpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) - => base.TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions); + internal override bool TryOpenConnection( + DbConnection outerConnection, + SqlConnectionFactory connectionFactory, + TaskCompletionSource retry, + DbConnectionOptions userOptions) => + TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions); } internal abstract class DbConnectionBusy : DbConnectionClosed @@ -51,7 +63,11 @@ protected DbConnectionBusy(ConnectionState state) : base(state, true, false) { } - internal override bool TryOpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) + internal override bool TryOpenConnection( + DbConnection outerConnection, + SqlConnectionFactory connectionFactory, + TaskCompletionSource retry, + DbConnectionOptions userOptions) => throw ADP.ConnectionAlreadyOpen(State); } @@ -84,15 +100,23 @@ private DbConnectionClosedConnecting() : base(ConnectionState.Connecting) { } - internal override void CloseConnection(DbConnection owningObject, DbConnectionFactory connectionFactory) + internal override void CloseConnection(DbConnection owningObject, SqlConnectionFactory connectionFactory) { connectionFactory.SetInnerConnectionTo(owningObject, DbConnectionClosedPreviouslyOpened.SingletonInstance); } - internal override bool TryReplaceConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) - => TryOpenConnection(outerConnection, connectionFactory, retry, userOptions); - - internal override bool TryOpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) + internal override bool TryReplaceConnection( + DbConnection outerConnection, + SqlConnectionFactory connectionFactory, + TaskCompletionSource retry, + DbConnectionOptions userOptions) => + TryOpenConnection(outerConnection, connectionFactory, retry, userOptions); + + internal override bool TryOpenConnection( + DbConnection outerConnection, + SqlConnectionFactory connectionFactory, + TaskCompletionSource retry, + DbConnectionOptions userOptions) { if (retry == null || !retry.Task.IsCompleted) { @@ -137,7 +161,11 @@ private DbConnectionClosedPreviouslyOpened() : base(ConnectionState.Closed, true { } - internal override bool TryReplaceConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) - => TryOpenConnection(outerConnection, connectionFactory, retry, userOptions); + internal override bool TryReplaceConnection( + DbConnection outerConnection, + SqlConnectionFactory connectionFactory, + TaskCompletionSource retry, + DbConnectionOptions userOptions) => + TryOpenConnection(outerConnection, connectionFactory, retry, userOptions); } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs deleted file mode 100644 index 399b98435b..0000000000 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionFactory.cs +++ /dev/null @@ -1,678 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.Common; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Data.Common; -using Microsoft.Data.Common.ConnectionString; -using Microsoft.Data.SqlClient; -using Microsoft.Data.SqlClient.ConnectionPool; - -namespace Microsoft.Data.ProviderBase -{ - internal abstract class DbConnectionFactory - { - private Dictionary _connectionPoolGroups; - private readonly List _poolsToRelease; - private readonly List _poolGroupsToRelease; - private readonly Timer _pruningTimer; - - private const int PruningDueTime = 4 * 60 * 1000; // 4 minutes - private const int PruningPeriod = 30 * 1000; // thirty seconds - - private static int _objectTypeCount; // EventSource counter - internal int ObjectID { get; } = Interlocked.Increment(ref _objectTypeCount); - - // s_pendingOpenNonPooled is an array of tasks used to throttle creation of non-pooled connections to - // a maximum of Environment.ProcessorCount at a time. - private static uint s_pendingOpenNonPooledNext = 0; - private static Task[] s_pendingOpenNonPooled = new Task[Environment.ProcessorCount]; - private static Task s_completedTask; - - protected DbConnectionFactory() - { - _connectionPoolGroups = new Dictionary(); - _poolsToRelease = new List(); - _poolGroupsToRelease = new List(); - _pruningTimer = CreatePruningTimer(); - } - - public abstract DbProviderFactory ProviderFactory - { - get; - } - - public void ClearAllPools() - { - using (TryEventScope.Create(nameof(DbConnectionFactory))) - { - Dictionary connectionPoolGroups = _connectionPoolGroups; - foreach (KeyValuePair entry in connectionPoolGroups) - { - DbConnectionPoolGroup poolGroup = entry.Value; - if (poolGroup != null) - { - poolGroup.Clear(); - } - } - } - } - - public void ClearPool(DbConnection connection) - { - ADP.CheckArgumentNull(connection, nameof(connection)); - using (TryEventScope.Create(" {0}", GetObjectId(connection))) - { - DbConnectionPoolGroup poolGroup = GetConnectionPoolGroup(connection); - if (poolGroup != null) - { - poolGroup.Clear(); - } - } - } - - public void ClearPool(DbConnectionPoolKey key) - { - Debug.Assert(key != null, "key cannot be null"); - ADP.CheckArgumentNull(key.ConnectionString, $"{nameof(key)}.{nameof(key.ConnectionString)}"); - using (TryEventScope.Create(" connectionString")) - { - Dictionary connectionPoolGroups = _connectionPoolGroups; - if (connectionPoolGroups.TryGetValue(key, out DbConnectionPoolGroup poolGroup)) - { - poolGroup.Clear(); - } - } - } - - internal abstract DbConnectionPoolProviderInfo CreateConnectionPoolProviderInfo( - DbConnectionOptions connectionOptions); - - protected virtual DbMetaDataFactory CreateMetaDataFactory(DbConnectionInternal internalConnection, out bool cacheMetaDataFactory) - { - // providers that support GetSchema must override this with a method that creates a meta data - // factory appropriate for them. - cacheMetaDataFactory = false; - throw ADP.NotSupported(); - } - - internal DbConnectionInternal CreateNonPooledConnection(DbConnection owningConnection, DbConnectionPoolGroup poolGroup, DbConnectionOptions userOptions) - { - Debug.Assert(owningConnection != null, "null owningConnection?"); - Debug.Assert(poolGroup != null, "null poolGroup?"); - - DbConnectionOptions connectionOptions = poolGroup.ConnectionOptions; - DbConnectionPoolGroupProviderInfo poolGroupProviderInfo = poolGroup.ProviderInfo; - DbConnectionPoolKey poolKey = poolGroup.PoolKey; - - DbConnectionInternal newConnection = CreateConnection(connectionOptions, poolKey, poolGroupProviderInfo, null, owningConnection, userOptions); - if (newConnection != null) - { - SqlClientEventSource.Metrics.HardConnectRequest(); - newConnection.MakeNonPooledObject(owningConnection); - } - SqlClientEventSource.Log.TryTraceEvent(" {0}, Non-pooled database connection created.", ObjectID); - return newConnection; - } - - internal DbConnectionInternal CreatePooledConnection(IDbConnectionPool pool, DbConnection owningObject, DbConnectionOptions options, DbConnectionPoolKey poolKey, DbConnectionOptions userOptions) - { - Debug.Assert(pool != null, "null pool?"); - DbConnectionPoolGroupProviderInfo poolGroupProviderInfo = pool.PoolGroup.ProviderInfo; - DbConnectionInternal newConnection = CreateConnection(options, poolKey, poolGroupProviderInfo, pool, owningObject, userOptions); - - if (newConnection != null) - { - SqlClientEventSource.Metrics.HardConnectRequest(); - - newConnection.MakePooledConnection(pool); - } - SqlClientEventSource.Log.TryTraceEvent(" {0}, Pooled database connection created.", ObjectID); - return newConnection; - } - - internal abstract DbConnectionPoolGroupProviderInfo CreateConnectionPoolGroupProviderInfo( - DbConnectionOptions connectionOptions); - - private Timer CreatePruningTimer() => - ADP.UnsafeCreateTimer( - new TimerCallback(PruneConnectionPoolGroups), - null, - PruningDueTime, - PruningPeriod); - - protected DbConnectionOptions FindConnectionOptions(DbConnectionPoolKey key) - { - Debug.Assert(key != null, "key cannot be null"); - if (!string.IsNullOrEmpty(key.ConnectionString)) - { - DbConnectionPoolGroup connectionPoolGroup; - Dictionary connectionPoolGroups = _connectionPoolGroups; - if (connectionPoolGroups.TryGetValue(key, out connectionPoolGroup)) - { - return connectionPoolGroup.ConnectionOptions; - } - } - return null; - } - - private static Task GetCompletedTask() - { - Debug.Assert(Monitor.IsEntered(s_pendingOpenNonPooled), $"Expected {nameof(s_pendingOpenNonPooled)} lock to be held."); - return s_completedTask ?? (s_completedTask = Task.FromResult(null)); - } - - internal bool TryGetConnection(DbConnection owningConnection, TaskCompletionSource retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, out DbConnectionInternal connection) - { - Debug.Assert(owningConnection != null, "null owningConnection?"); - - DbConnectionPoolGroup poolGroup; - IDbConnectionPool connectionPool; - connection = null; - - // Work around race condition with clearing the pool between GetConnectionPool obtaining pool - // and GetConnection on the pool checking the pool state. Clearing the pool in this window - // will switch the pool into the ShuttingDown state, and GetConnection will return null. - // There is probably a better solution involving locking the pool/group, but that entails a major - // re-design of the connection pooling synchronization, so is postponed for now. - - // Use retriesLeft to prevent CPU spikes with incremental sleep - // start with one msec, double the time every retry - // max time is: 1 + 2 + 4 + ... + 2^(retries-1) == 2^retries -1 == 1023ms (for 10 retries) - int retriesLeft = 10; - int timeBetweenRetriesMilliseconds = 1; - - do - { - poolGroup = GetConnectionPoolGroup(owningConnection); - // Doing this on the callers thread is important because it looks up the WindowsIdentity from the thread. - connectionPool = GetConnectionPool(owningConnection, poolGroup); - if (connectionPool == null) - { - // If GetConnectionPool returns null, we can be certain that - // this connection should not be pooled via DbConnectionPool - // or have a disabled pool entry. - poolGroup = GetConnectionPoolGroup(owningConnection); // previous entry have been disabled - - if (retry != null) - { - Task newTask; - CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - lock (s_pendingOpenNonPooled) - { - // look for an available task slot (completed or empty) - int idx; - for (idx = 0; idx < s_pendingOpenNonPooled.Length; idx++) - { - Task task = s_pendingOpenNonPooled[idx]; - if (task == null) - { - s_pendingOpenNonPooled[idx] = GetCompletedTask(); - break; - } - else if (task.IsCompleted) - { - break; - } - } - - // if didn't find one, pick the next one in round-robin fashion - if (idx == s_pendingOpenNonPooled.Length) - { - idx = (int)(s_pendingOpenNonPooledNext % s_pendingOpenNonPooled.Length); - unchecked - { - s_pendingOpenNonPooledNext++; - } - } - - // now that we have an antecedent task, schedule our work when it is completed. - // If it is a new slot or a completed task, this continuation will start right away. - newTask = CreateReplaceConnectionContinuation(s_pendingOpenNonPooled[idx], owningConnection, retry, userOptions, oldConnection, poolGroup, cancellationTokenSource); - - // Place this new task in the slot so any future work will be queued behind it - s_pendingOpenNonPooled[idx] = newTask; - } - - // Set up the timeout (if needed) - if (owningConnection.ConnectionTimeout > 0) - { - int connectionTimeoutMilliseconds = owningConnection.ConnectionTimeout * 1000; - cancellationTokenSource.CancelAfter(connectionTimeoutMilliseconds); - } - - // once the task is done, propagate the final results to the original caller - newTask.ContinueWith( - continuationAction: TryGetConnectionCompletedContinuation, - state: Tuple.Create(cancellationTokenSource, retry), - scheduler: TaskScheduler.Default - ); - - return false; - } - - connection = CreateNonPooledConnection(owningConnection, poolGroup, userOptions); - - SqlClientEventSource.Metrics.EnterNonPooledConnection(); - } - else - { - if (((SqlClient.SqlConnection)owningConnection).ForceNewConnection) - { - Debug.Assert(!(oldConnection is DbConnectionClosed), "Force new connection, but there is no old connection"); - connection = connectionPool.ReplaceConnection(owningConnection, userOptions, oldConnection); - } - else - { - if (!connectionPool.TryGetConnection(owningConnection, retry, userOptions, out connection)) - { - return false; - } - } - - if (connection == null) - { - // connection creation failed on semaphore waiting or if max pool reached - if (connectionPool.IsRunning) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, GetConnection failed because a pool timeout occurred.", ObjectID); - // If GetConnection failed while the pool is running, the pool timeout occurred. - throw ADP.PooledOpenTimeout(); - } - else - { - // We've hit the race condition, where the pool was shut down after we got it from the group. - // Yield time slice to allow shut down activities to complete and a new, running pool to be instantiated - // before retrying. - System.Threading.Thread.Sleep(timeBetweenRetriesMilliseconds); - timeBetweenRetriesMilliseconds *= 2; // double the wait time for next iteration - } - } - } - } while (connection == null && retriesLeft-- > 0); - - if (connection == null) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, GetConnection failed because a pool timeout occurred and all retries were exhausted.", ObjectID); - // exhausted all retries or timed out - give up - throw ADP.PooledOpenTimeout(); - } - - return true; - } - - private Task CreateReplaceConnectionContinuation(Task task, DbConnection owningConnection, TaskCompletionSource retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionPoolGroup poolGroup, CancellationTokenSource cancellationTokenSource) - { - return task.ContinueWith( - (_) => - { - System.Transactions.Transaction originalTransaction = ADP.GetCurrentTransaction(); - try - { - ADP.SetCurrentTransaction(retry.Task.AsyncState as System.Transactions.Transaction); - var newConnection = CreateNonPooledConnection(owningConnection, poolGroup, userOptions); - if ((oldConnection != null) && (oldConnection.State == ConnectionState.Open)) - { - oldConnection.PrepareForReplaceConnection(); - oldConnection.Dispose(); - } - return newConnection; - } - finally - { - ADP.SetCurrentTransaction(originalTransaction); - } - }, - cancellationTokenSource.Token, - TaskContinuationOptions.LongRunning, - TaskScheduler.Default - ); - } - - private void TryGetConnectionCompletedContinuation(Task task, object state) - { - Tuple> parameters = (Tuple>)state; - CancellationTokenSource source = parameters.Item1; - source.Dispose(); - - TaskCompletionSource retryCompletionSource = parameters.Item2; - - if (task.IsCanceled) - { - retryCompletionSource.TrySetException(ADP.ExceptionWithStackTrace(ADP.NonPooledOpenTimeout())); - } - else if (task.IsFaulted) - { - retryCompletionSource.TrySetException(task.Exception.InnerException); - } - else - { - if (!retryCompletionSource.TrySetResult(task.Result)) - { - // The outer TaskCompletionSource was already completed - // Which means that we don't know if someone has messed with the outer connection in the middle of creation - // So the best thing to do now is to destroy the newly created connection - task.Result.DoomThisConnection(); - task.Result.Dispose(); - } - else - { - SqlClientEventSource.Metrics.EnterNonPooledConnection(); - } - } - } - - private IDbConnectionPool GetConnectionPool(DbConnection owningObject, DbConnectionPoolGroup connectionPoolGroup) - { - // if poolgroup is disabled, it will be replaced with a new entry - - Debug.Assert(owningObject != null, "null owningObject?"); - Debug.Assert(connectionPoolGroup != null, "null connectionPoolGroup?"); - - // It is possible that while the outer connection object has - // been sitting around in a closed and unused state in some long - // running app, the pruner may have come along and remove this - // the pool entry from the master list. If we were to use a - // pool entry in this state, we would create "unmanaged" pools, - // which would be bad. To avoid this problem, we automagically - // re-create the pool entry whenever it's disabled. - - // however, don't rebuild connectionOptions if no pooling is involved - let new connections do that work - if (connectionPoolGroup.IsDisabled && connectionPoolGroup.PoolGroupOptions != null) - { - SqlClientEventSource.Log.TryTraceEvent(" {0}, DisabledPoolGroup={1}", ObjectID, connectionPoolGroup?.ObjectID); - - // reusing existing pool option in case user originally used SetConnectionPoolOptions - DbConnectionPoolGroupOptions poolOptions = connectionPoolGroup.PoolGroupOptions; - - // get the string to hash on again - DbConnectionOptions connectionOptions = connectionPoolGroup.ConnectionOptions; - Debug.Assert(connectionOptions != null, "prevent expansion of connectionString"); - - connectionPoolGroup = GetConnectionPoolGroup(connectionPoolGroup.PoolKey, poolOptions, ref connectionOptions); - Debug.Assert(connectionPoolGroup != null, "null connectionPoolGroup?"); - SetConnectionPoolGroup(owningObject, connectionPoolGroup); - } - IDbConnectionPool connectionPool = connectionPoolGroup.GetConnectionPool(this); - return connectionPool; - } - - internal DbConnectionPoolGroup GetConnectionPoolGroup(DbConnectionPoolKey key, DbConnectionPoolGroupOptions poolOptions, ref DbConnectionOptions userConnectionOptions) - { - if (string.IsNullOrEmpty(key.ConnectionString)) - { - return (DbConnectionPoolGroup)null; - } - - DbConnectionPoolGroup connectionPoolGroup; - Dictionary connectionPoolGroups = _connectionPoolGroups; - if (!connectionPoolGroups.TryGetValue(key, out connectionPoolGroup) || (connectionPoolGroup.IsDisabled && connectionPoolGroup.PoolGroupOptions != null)) - { - // If we can't find an entry for the connection string in - // our collection of pool entries, then we need to create a - // new pool entry and add it to our collection. - - DbConnectionOptions connectionOptions = CreateConnectionOptions(key.ConnectionString, userConnectionOptions); - if (connectionOptions == null) - { - throw ADP.InternalConnectionError(ADP.ConnectionError.ConnectionOptionsMissing); - } - - if (userConnectionOptions == null) - { - // we only allow one expansion on the connection string - - userConnectionOptions = connectionOptions; - string expandedConnectionString = connectionOptions.Expand(); - - // if the expanded string is same instance (default implementation), then use the already created options - if ((object)expandedConnectionString != (object)key.ConnectionString) - { - // CONSIDER: caching the original string to reduce future parsing - DbConnectionPoolKey newKey = (DbConnectionPoolKey)((ICloneable)key).Clone(); - newKey.ConnectionString = expandedConnectionString; - return GetConnectionPoolGroup(newKey, null, ref userConnectionOptions); - } - } - - if (poolOptions == null) - { - if (connectionPoolGroup != null) - { - // reusing existing pool option in case user originally used SetConnectionPoolOptions - poolOptions = connectionPoolGroup.PoolGroupOptions; - } - else - { - // Note: may return null for non-pooled connections - poolOptions = CreateConnectionPoolGroupOptions(connectionOptions); - } - } - - lock (this) - { - connectionPoolGroups = _connectionPoolGroups; - if (!connectionPoolGroups.TryGetValue(key, out connectionPoolGroup)) - { - DbConnectionPoolGroup newConnectionPoolGroup = new DbConnectionPoolGroup(connectionOptions, key, poolOptions); - newConnectionPoolGroup.ProviderInfo = CreateConnectionPoolGroupProviderInfo(connectionOptions); - - // build new dictionary with space for new connection string - Dictionary newConnectionPoolGroups = new Dictionary(1 + connectionPoolGroups.Count); - foreach (KeyValuePair entry in connectionPoolGroups) - { - newConnectionPoolGroups.Add(entry.Key, entry.Value); - } - - // lock prevents race condition with PruneConnectionPoolGroups - newConnectionPoolGroups.Add(key, newConnectionPoolGroup); - - SqlClientEventSource.Metrics.EnterActiveConnectionPoolGroup(); - connectionPoolGroup = newConnectionPoolGroup; - _connectionPoolGroups = newConnectionPoolGroups; - } - else - { - Debug.Assert(!connectionPoolGroup.IsDisabled, "Disabled pool entry discovered"); - } - } - Debug.Assert(connectionPoolGroup != null, "how did we not create a pool entry?"); - Debug.Assert(userConnectionOptions != null, "how did we not have user connection options?"); - } - else if (userConnectionOptions == null) - { - userConnectionOptions = connectionPoolGroup.ConnectionOptions; - } - return connectionPoolGroup; - } - - internal DbMetaDataFactory GetMetaDataFactory(DbConnectionPoolGroup connectionPoolGroup, DbConnectionInternal internalConnection) - { - Debug.Assert(connectionPoolGroup != null, "connectionPoolGroup may not be null."); - - // get the matadatafactory from the pool entry. If it does not already have one - // create one and save it on the pool entry - DbMetaDataFactory metaDataFactory = connectionPoolGroup.MetaDataFactory; - - // consider serializing this so we don't construct multiple metadata factories - // if two threads happen to hit this at the same time. One will be GC'd - if (metaDataFactory == null) - { - bool allowCache = false; - metaDataFactory = CreateMetaDataFactory(internalConnection, out allowCache); - if (allowCache) - { - connectionPoolGroup.MetaDataFactory = metaDataFactory; - } - } - return metaDataFactory; - } - - private void PruneConnectionPoolGroups(object state) - { - // when debugging this method, expect multiple threads at the same time - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}", ObjectID); - - // First, walk the pool release list and attempt to clear each - // pool, when the pool is finally empty, we dispose of it. If the - // pool isn't empty, it's because there are active connections or - // distributed transactions that need it. - lock (_poolsToRelease) - { - if (0 != _poolsToRelease.Count) - { - IDbConnectionPool[] poolsToRelease = _poolsToRelease.ToArray(); - foreach (IDbConnectionPool pool in poolsToRelease) - { - if (pool != null) - { - pool.Clear(); - - if (0 == pool.Count) - { - _poolsToRelease.Remove(pool); - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, ReleasePool={1}", ObjectID, pool.Id); - - SqlClientEventSource.Metrics.ExitInactiveConnectionPool(); - } - } - } - } - } - - // Next, walk the pool entry release list and dispose of each - // pool entry when it is finally empty. If the pool entry isn't - // empty, it's because there are active pools that need it. - lock (_poolGroupsToRelease) - { - if (0 != _poolGroupsToRelease.Count) - { - DbConnectionPoolGroup[] poolGroupsToRelease = _poolGroupsToRelease.ToArray(); - foreach (DbConnectionPoolGroup poolGroup in poolGroupsToRelease) - { - if (poolGroup != null) - { - int poolsLeft = poolGroup.Clear(); // may add entries to _poolsToRelease - - if (0 == poolsLeft) - { - _poolGroupsToRelease.Remove(poolGroup); - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, ReleasePoolGroup={1}", ObjectID, poolGroup.ObjectID); - - SqlClientEventSource.Metrics.ExitInactiveConnectionPoolGroup(); - } - } - } - } - } - - // Finally, we walk through the collection of connection pool entries - // and prune each one. This will cause any empty pools to be put - // into the release list. - lock (this) - { - Dictionary connectionPoolGroups = _connectionPoolGroups; - Dictionary newConnectionPoolGroups = new Dictionary(connectionPoolGroups.Count); - - foreach (KeyValuePair entry in connectionPoolGroups) - { - if (entry.Value != null) - { - Debug.Assert(!entry.Value.IsDisabled, "Disabled pool entry discovered"); - - // entries start active and go idle during prune if all pools are gone - // move idle entries from last prune pass to a queue for pending release - // otherwise process entry which may move it from active to idle - if (entry.Value.Prune()) - { - // may add entries to _poolsToRelease - QueuePoolGroupForRelease(entry.Value); - } - else - { - newConnectionPoolGroups.Add(entry.Key, entry.Value); - } - } - } - _connectionPoolGroups = newConnectionPoolGroups; - } - } - - internal void QueuePoolForRelease(IDbConnectionPool pool, bool clearing) - { - // Queue the pool up for release -- we'll clear it out and dispose - // of it as the last part of the pruning timer callback so we don't - // do it with the pool entry or the pool collection locked. - Debug.Assert(pool != null, "null pool?"); - - // set the pool to the shutdown state to force all active - // connections to be automatically disposed when they - // are returned to the pool - pool.Shutdown(); - - lock (_poolsToRelease) - { - if (clearing) - { - pool.Clear(); - } - _poolsToRelease.Add(pool); - } - SqlClientEventSource.Metrics.EnterInactiveConnectionPool(); - SqlClientEventSource.Metrics.ExitActiveConnectionPool(); - } - - internal void QueuePoolGroupForRelease(DbConnectionPoolGroup poolGroup) - { - Debug.Assert(poolGroup != null, "null poolGroup?"); - SqlClientEventSource.Log.TryTraceEvent(" {0}, poolGroup={1}", ObjectID, poolGroup.ObjectID); - - lock (_poolGroupsToRelease) - { - _poolGroupsToRelease.Add(poolGroup); - } - - SqlClientEventSource.Metrics.EnterInactiveConnectionPoolGroup(); - SqlClientEventSource.Metrics.ExitActiveConnectionPoolGroup(); - } - - protected abstract DbConnectionInternal CreateConnection( - DbConnectionOptions options, - DbConnectionPoolKey poolKey, - DbConnectionPoolGroupProviderInfo poolGroupProviderInfo, - IDbConnectionPool pool, - DbConnection owningConnection, - DbConnectionOptions userOptions); - - abstract protected DbConnectionOptions CreateConnectionOptions(string connectionString, DbConnectionOptions previous); - - abstract protected DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions(DbConnectionOptions options); - - abstract internal DbConnectionPoolGroup GetConnectionPoolGroup(DbConnection connection); - - abstract internal DbConnectionInternal GetInnerConnection(DbConnection connection); - - abstract protected int GetObjectId(DbConnection connection); - - abstract internal void PermissionDemand(DbConnection outerConnection); - - abstract internal void SetConnectionPoolGroup(DbConnection outerConnection, DbConnectionPoolGroup poolGroup); - - abstract internal void SetInnerConnectionEvent(DbConnection owningObject, DbConnectionInternal to); - - abstract internal bool SetInnerConnectionFrom(DbConnection owningObject, DbConnectionInternal to, DbConnectionInternal from); - - abstract internal void SetInnerConnectionTo(DbConnection owningObject, DbConnectionInternal to); - - virtual internal void Unload() - { - _pruningTimer.Dispose(); - } - } -} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs index b4dfb4f214..31f207e5ce 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs @@ -398,7 +398,7 @@ internal void CleanupConnectionOnTransactionCompletion(Transaction transaction) pool?.TransactionEnded(transaction, this); } - internal virtual void CloseConnection(DbConnection owningObject, DbConnectionFactory connectionFactory) + internal virtual void CloseConnection(DbConnection owningObject, SqlConnectionFactory connectionFactory) { // The implementation here is the implementation required for the // "open" internal connections, since our own private "closed" @@ -708,7 +708,7 @@ internal void MakePooledConnection(IDbConnectionPool connectionPool) internal void NotifyWeakReference(int message) => ReferenceCollection?.Notify(message); - internal virtual void OpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory) + internal virtual void OpenConnection(DbConnection outerConnection, SqlConnectionFactory connectionFactory) { if (!TryOpenConnection(outerConnection, connectionFactory, null, null)) { @@ -814,7 +814,7 @@ internal void SetInStasis() /// internal virtual bool TryOpenConnection( DbConnection outerConnection, - DbConnectionFactory connectionFactory, + SqlConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) { @@ -823,7 +823,7 @@ internal virtual bool TryOpenConnection( internal virtual bool TryReplaceConnection( DbConnection outerConnection, - DbConnectionFactory connectionFactory, + SqlConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) { @@ -871,7 +871,7 @@ protected internal void DoomThisConnection() } protected internal virtual DataTable GetSchema( - DbConnectionFactory factory, + SqlConnectionFactory factory, DbConnectionPoolGroup poolGroup, DbConnection outerConnection, string collectionName, @@ -901,7 +901,11 @@ protected virtual void ReleaseAdditionalLocksForClose(bool lockToken) // No additional locks in default implementation } - protected bool TryOpenConnectionInternal(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource retry, DbConnectionOptions userOptions) + protected bool TryOpenConnectionInternal( + DbConnection outerConnection, + SqlConnectionFactory connectionFactory, + TaskCompletionSource retry, + DbConnectionOptions userOptions) { // ?->Connecting: prevent set_ConnectionString during Open if (connectionFactory.SetInnerConnectionFrom(outerConnection, DbConnectionClosedConnecting.SingletonInstance, this)) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceEnumerator.Windows.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceEnumerator.Windows.cs index 25caa8911b..f92e9beb32 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceEnumerator.Windows.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceEnumerator.Windows.cs @@ -15,7 +15,7 @@ private partial DataTable GetDataSourcesInternal() #if NETFRAMEWORK return SqlDataSourceEnumeratorNativeHelper.GetDataSources(); #else - return SqlClient.TdsParserStateObjectFactory.UseManagedSNI ? SqlDataSourceEnumeratorManagedHelper.GetDataSources() : SqlDataSourceEnumeratorNativeHelper.GetDataSources(); + return SqlClient.LocalAppContextSwitches.UseManagedNetworking ? SqlDataSourceEnumeratorManagedHelper.GetDataSources() : SqlDataSourceEnumeratorNativeHelper.GetDataSources(); #endif } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs index b374a550b3..9cbec9516d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs @@ -223,7 +223,11 @@ public override async Task AcquireTokenAsync(SqlAuthenti { if (!string.IsNullOrEmpty(parameters.UserId)) { + // The AcquireTokenByIntegratedWindowsAuth method is marked as obsolete in MSAL.NET + // but it is still a supported way to acquire tokens for Active Directory Integrated authentication. +#pragma warning disable CS0618 // Type or member is obsolete result = await app.AcquireTokenByIntegratedWindowsAuth(scopes) +#pragma warning restore CS0618 // Type or member is obsolete .WithCorrelationId(parameters.ConnectionId) .WithUsername(parameters.UserId) .ExecuteAsync(cancellationToken: cts.Token) @@ -231,7 +235,9 @@ public override async Task AcquireTokenAsync(SqlAuthenti } else { +#pragma warning disable CS0618 // Type or member is obsolete result = await app.AcquireTokenByIntegratedWindowsAuth(scopes) +#pragma warning restore CS0618 // Type or member is obsolete .WithCorrelationId(parameters.ConnectionId) .ExecuteAsync(cancellationToken: cts.Token) .ConfigureAwait(false); @@ -582,7 +588,28 @@ private static TokenCredentialData CreateTokenCredentialInstance(TokenCredential defaultAzureCredentialOptions.WorkloadIdentityClientId = tokenCredentialKey._clientId; } - return new TokenCredentialData(new DefaultAzureCredential(defaultAzureCredentialOptions), GetHash(secret)); + // SqlClient is a library and provides support to acquire access + // token using 'DefaultAzureCredential' on user demand when they + // specify 'Authentication = Active Directory Default' in + // connection string. + // + // Default Azure Credential is instantiated by the calling + // application when using "Active Directory Default" + // authentication code to connect to Azure SQL instance. + // SqlClient is a library, doesn't instantiate the credential + // without running application instructions. + // + // Note that CodeQL suppression support can only detect + // suppression comments that appear immediately above the + // flagged statement, or appended to the end of the statement. + // Multi-line justifications are not supported. + // + // https://eng.ms/docs/cloud-ai-platform/devdiv/one-engineering-system-1es/1es-docs/codeql/codeql-semmle#guidance-on-suppressions + // + // CodeQL [SM05137] See above for justification. + DefaultAzureCredential cred = new(defaultAzureCredentialOptions); + + return new TokenCredentialData(cred, GetHash(secret)); } TokenCredentialOptions tokenCredentialOptions = new() { AuthorityHost = new Uri(tokenCredentialKey._authority) }; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs index c4772bb736..8b0efe8763 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs @@ -25,7 +25,7 @@ internal sealed class ChannelDbConnectionPool : IDbConnectionPool public ConcurrentDictionary AuthenticationContexts => throw new NotImplementedException(); /// - public DbConnectionFactory ConnectionFactory => throw new NotImplementedException(); + public SqlConnectionFactory ConnectionFactory => throw new NotImplementedException(); /// public int Count => throw new NotImplementedException(); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolGroup.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolGroup.cs index 805beed2b5..226a5749b4 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolGroup.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolGroup.cs @@ -132,7 +132,7 @@ internal int Clear() IDbConnectionPool pool = entry.Value; if (pool != null) { - DbConnectionFactory connectionFactory = pool.ConnectionFactory; + SqlConnectionFactory connectionFactory = pool.ConnectionFactory; connectionFactory.QueuePoolForRelease(pool, true); } @@ -143,7 +143,7 @@ internal int Clear() return _poolCollection.Count; } - internal IDbConnectionPool GetConnectionPool(DbConnectionFactory connectionFactory) + internal IDbConnectionPool GetConnectionPool(SqlConnectionFactory connectionFactory) { // When this method returns null it indicates that the connection // factory should not use pooling. @@ -282,7 +282,7 @@ internal bool Prune() // to use it while we're processing and finally we put the // pool into a list of pools to be released when they // are completely empty. - DbConnectionFactory connectionFactory = pool.ConnectionFactory; + SqlConnectionFactory connectionFactory = pool.ConnectionFactory; connectionFactory.QueuePoolForRelease(pool, false); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolIdentity.Windows.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolIdentity.Windows.cs index 99783ff3c7..0c6fd07503 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolIdentity.Windows.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolIdentity.Windows.cs @@ -28,7 +28,7 @@ internal static WindowsIdentity GetCurrentWindowsIdentity() #else internal static DbConnectionPoolIdentity GetCurrent() { - return TdsParserStateObjectFactory.UseManagedSNI ? GetCurrentManaged() : GetCurrentNative(); + return LocalAppContextSwitches.UseManagedNetworking ? GetCurrentManaged() : GetCurrentNative(); } #endif diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/IDbConnectionPool.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/IDbConnectionPool.cs index d6647f832e..066318fce2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/IDbConnectionPool.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/IDbConnectionPool.cs @@ -28,7 +28,7 @@ internal interface IDbConnectionPool /// /// Gets the factory used to create database connections. /// - DbConnectionFactory ConnectionFactory { get; } + SqlConnectionFactory ConnectionFactory { get; } /// /// The number of connections currently managed by the pool. diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/SqlConnectionPoolKey.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/SqlConnectionPoolKey.cs index 207c0a8e1a..31da3521df 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/SqlConnectionPoolKey.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/SqlConnectionPoolKey.cs @@ -18,10 +18,12 @@ internal class SqlConnectionPoolKey : DbConnectionPoolKey private readonly SqlCredential _credential; private readonly string _accessToken; private Func> _accessTokenCallback; + private SspiContextProvider _sspiContextProvider; internal SqlCredential Credential => _credential; internal string AccessToken => _accessToken; internal Func> AccessTokenCallback => _accessTokenCallback; + internal SspiContextProvider SspiContextProvider => _sspiContextProvider; internal override string ConnectionString { @@ -33,12 +35,18 @@ internal override string ConnectionString } } - internal SqlConnectionPoolKey(string connectionString, SqlCredential credential, string accessToken, Func> accessTokenCallback) : base(connectionString) + internal SqlConnectionPoolKey( + string connectionString, + SqlCredential credential, + string accessToken, + Func> accessTokenCallback, + SspiContextProvider sspiContextProvider) : base(connectionString) { Debug.Assert(credential == null || accessToken == null || accessTokenCallback == null, "Credential, AccessToken, and Callback can't have a value at the same time."); _credential = credential; _accessToken = accessToken; _accessTokenCallback = accessTokenCallback; + _sspiContextProvider = sspiContextProvider; CalculateHashCode(); } @@ -47,6 +55,8 @@ private SqlConnectionPoolKey(SqlConnectionPoolKey key) : base(key) _credential = key.Credential; _accessToken = key.AccessToken; _accessTokenCallback = key._accessTokenCallback; + _sspiContextProvider = key._sspiContextProvider; + CalculateHashCode(); } @@ -61,7 +71,8 @@ public override bool Equals(object obj) && _credential == key._credential && ConnectionString == key.ConnectionString && _accessTokenCallback == key._accessTokenCallback - && string.CompareOrdinal(_accessToken, key._accessToken) == 0); + && string.CompareOrdinal(_accessToken, key._accessToken) == 0 + && _sspiContextProvider == key._sspiContextProvider); } public override int GetHashCode() @@ -94,6 +105,11 @@ private void CalculateHashCode() _hashValue = _hashValue * 17 + _accessTokenCallback.GetHashCode(); } } + + if (_sspiContextProvider != null) + { + _hashValue = _hashValue * 17 + _sspiContextProvider.GetHashCode(); + } } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs index 77d2b3bae1..bd6ebf708e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs @@ -403,7 +403,7 @@ internal WaitHandle[] GetHandles(bool withCreate) private readonly int _cleanupWait; private readonly DbConnectionPoolIdentity _identity; - private readonly DbConnectionFactory _connectionFactory; + private readonly SqlConnectionFactory _connectionFactory; private readonly DbConnectionPoolGroup _connectionPoolGroup; private readonly DbConnectionPoolGroupOptions _connectionPoolGroupOptions; private DbConnectionPoolProviderInfo _connectionPoolProviderInfo; @@ -439,10 +439,10 @@ internal WaitHandle[] GetHandles(bool withCreate) // only created by DbConnectionPoolGroup.GetConnectionPool internal WaitHandleDbConnectionPool( - DbConnectionFactory connectionFactory, - DbConnectionPoolGroup connectionPoolGroup, - DbConnectionPoolIdentity identity, - DbConnectionPoolProviderInfo connectionPoolProviderInfo) + SqlConnectionFactory connectionFactory, + DbConnectionPoolGroup connectionPoolGroup, + DbConnectionPoolIdentity identity, + DbConnectionPoolProviderInfo connectionPoolProviderInfo) { Debug.Assert(connectionPoolGroup != null, "null connectionPoolGroup"); @@ -492,7 +492,7 @@ private int CreationTimeout public int Count => _totalObjects; - public DbConnectionFactory ConnectionFactory => _connectionFactory; + public SqlConnectionFactory ConnectionFactory => _connectionFactory; public bool ErrorOccurred => _errorOccurred; @@ -746,7 +746,12 @@ private DbConnectionInternal CreateObject(DbConnection owningObject, DbConnectio try { - newObj = _connectionFactory.CreatePooledConnection(this, owningObject, _connectionPoolGroup.ConnectionOptions, _connectionPoolGroup.PoolKey, userOptions); + newObj = _connectionFactory.CreatePooledConnection( + owningObject, + this, + _connectionPoolGroup.PoolKey, + _connectionPoolGroup.ConnectionOptions, + userOptions); if (newObj == null) { throw ADP.InternalError(ADP.InternalErrorCode.CreateObjectReturnedNull); // CreateObject succeeded, but null object diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs index dfd94453f2..f546b0436b 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -15,14 +15,22 @@ private enum Tristate : byte True = 2 } - internal const string MakeReadAsyncBlockingString = @"Switch.Microsoft.Data.SqlClient.MakeReadAsyncBlocking"; - internal const string LegacyRowVersionNullString = @"Switch.Microsoft.Data.SqlClient.LegacyRowVersionNullBehavior"; - internal const string SuppressInsecureTlsWarningString = @"Switch.Microsoft.Data.SqlClient.SuppressInsecureTLSWarning"; - internal const string UseMinimumLoginTimeoutString = @"Switch.Microsoft.Data.SqlClient.UseOneSecFloorInTimeoutCalculationDuringLogin"; - internal const string LegacyVarTimeZeroScaleBehaviourString = @"Switch.Microsoft.Data.SqlClient.LegacyVarTimeZeroScaleBehaviour"; - internal const string UseCompatibilityProcessSniString = @"Switch.Microsoft.Data.SqlClient.UseCompatibilityProcessSni"; - internal const string UseCompatibilityAsyncBehaviourString = @"Switch.Microsoft.Data.SqlClient.UseCompatibilityAsyncBehaviour"; - internal const string UseConnectionPoolV2String = @"Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2"; + private const string MakeReadAsyncBlockingString = @"Switch.Microsoft.Data.SqlClient.MakeReadAsyncBlocking"; + private const string LegacyRowVersionNullString = @"Switch.Microsoft.Data.SqlClient.LegacyRowVersionNullBehavior"; + private const string SuppressInsecureTlsWarningString = @"Switch.Microsoft.Data.SqlClient.SuppressInsecureTLSWarning"; + private const string UseMinimumLoginTimeoutString = @"Switch.Microsoft.Data.SqlClient.UseOneSecFloorInTimeoutCalculationDuringLogin"; + private const string LegacyVarTimeZeroScaleBehaviourString = @"Switch.Microsoft.Data.SqlClient.LegacyVarTimeZeroScaleBehaviour"; + private const string UseCompatibilityProcessSniString = @"Switch.Microsoft.Data.SqlClient.UseCompatibilityProcessSni"; + private const string UseCompatibilityAsyncBehaviourString = @"Switch.Microsoft.Data.SqlClient.UseCompatibilityAsyncBehaviour"; + private const string UseConnectionPoolV2String = @"Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2"; + private const string TruncateScaledDecimalString = @"Switch.Microsoft.Data.SqlClient.TruncateScaledDecimal"; +#if NET + private const string GlobalizationInvariantModeString = @"System.Globalization.Invariant"; + private const string GlobalizationInvariantModeEnvironmentVariable = "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"; + private const string UseManagedNetworkingOnWindowsString = "Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows"; +#else + private const string DisableTnirByDefaultString = @"Switch.Microsoft.Data.SqlClient.DisableTNIRByDefaultInConnectionString"; +#endif // this field is accessed through reflection in tests and should not be renamed or have the type changed without refactoring NullRow related tests private static Tristate s_legacyRowVersionNullBehavior; @@ -34,6 +42,13 @@ private enum Tristate : byte private static Tristate s_useCompatibilityProcessSni; private static Tristate s_useCompatibilityAsyncBehaviour; private static Tristate s_useConnectionPoolV2; + private static Tristate s_truncateScaledDecimal; +#if NET + private static Tristate s_globalizationInvariantMode; + private static Tristate s_useManagedNetworking; +#else + private static Tristate s_disableTnirByDefault; +#endif #if NET static LocalAppContextSwitches() @@ -51,49 +66,12 @@ static LocalAppContextSwitches() } #endif -#if NETFRAMEWORK - internal const string DisableTnirByDefaultString = @"Switch.Microsoft.Data.SqlClient.DisableTNIRByDefaultInConnectionString"; - private static Tristate s_disableTnirByDefault; - /// - /// Transparent Network IP Resolution (TNIR) is a revision of the existing MultiSubnetFailover feature. - /// TNIR affects the connection sequence of the driver in the case where the first resolved IP of the hostname - /// doesn't respond and there are multiple IPs associated with the hostname. - /// - /// TNIR interacts with MultiSubnetFailover to provide the following three connection sequences: - /// 0: One IP is attempted, followed by all IPs in parallel - /// 1: All IPs are attempted in parallel - /// 2: All IPs are attempted one after another - /// - /// TransparentNetworkIPResolution is enabled by default. MultiSubnetFailover is disabled by default. - /// To disable TNIR, you can enable the app context switch. - /// - /// This app context switch defaults to 'false'. - /// - public static bool DisableTnirByDefault - { - get - { - if (s_disableTnirByDefault == Tristate.NotInitialized) - { - if (AppContext.TryGetSwitch(DisableTnirByDefaultString, out bool returnedValue) && returnedValue) - { - s_disableTnirByDefault = Tristate.True; - } - else - { - s_disableTnirByDefault = Tristate.False; - } - } - return s_disableTnirByDefault == Tristate.True; - } - } -#endif - /// - /// In TdsParser the ProcessSni function changed significantly when the packet + /// In TdsParser, the ProcessSni function changed significantly when the packet /// multiplexing code needed for high speed multi-packet column values was added. - /// In case of compatibility problems this switch will change TdsParser to use - /// the previous version of the function. + /// When this switch is set to true (the default), the old ProcessSni design is used. + /// When this switch is set to false, the new experimental ProcessSni behavior using + /// the packet multiplexer is enabled. /// public static bool UseCompatibilityProcessSni { @@ -101,7 +79,9 @@ public static bool UseCompatibilityProcessSni { if (s_useCompatibilityProcessSni == Tristate.NotInitialized) { - if (AppContext.TryGetSwitch(UseCompatibilityProcessSniString, out bool returnedValue) && returnedValue) + // Check if the switch has been set by the AppContext switch directly + // If it has not been set, we default to true. + if (!AppContext.TryGetSwitch(UseCompatibilityProcessSniString, out bool returnedValue) || returnedValue) { s_useCompatibilityProcessSni = Tristate.True; } @@ -115,12 +95,12 @@ public static bool UseCompatibilityProcessSni } /// - /// In TdsParser the async multi-packet column value fetch behaviour is capable of - /// using a continue snapshot state in addition to the original replay from start - /// logic. - /// This switch disables use of the continue snapshot state. This switch will always - /// return true if is enabled because the - /// continue state is not stable without the multiplexer. + /// In TdsParser, the async multi-packet column value fetch behavior can use a continue snapshot state + /// for improved efficiency. When this switch is enabled (the default), the driver preserves the legacy + /// compatibility behavior, which does not use the continue snapshot state. When disabled, the new behavior + /// using the continue snapshot state is enabled. This switch will always return true if + /// is enabled, because the continue state is not stable without + /// the multiplexer. /// public static bool UseCompatibilityAsyncBehaviour { @@ -137,7 +117,7 @@ public static bool UseCompatibilityAsyncBehaviour if (s_useCompatibilityAsyncBehaviour == Tristate.NotInitialized) { - if (AppContext.TryGetSwitch(UseCompatibilityAsyncBehaviourString, out bool returnedValue) && returnedValue) + if (!AppContext.TryGetSwitch(UseCompatibilityAsyncBehaviourString, out bool returnedValue) || returnedValue) { s_useCompatibilityAsyncBehaviour = Tristate.True; } @@ -296,5 +276,159 @@ public static bool UseConnectionPoolV2 return s_useConnectionPoolV2 == Tristate.True; } } + + /// + /// When set to true, TdsParser will truncate (rather than round) decimal and SqlDecimal values when scaling them. + /// + public static bool TruncateScaledDecimal + { + get + { + if (s_truncateScaledDecimal == Tristate.NotInitialized) + { + if (AppContext.TryGetSwitch(TruncateScaledDecimalString, out bool returnedValue) && returnedValue) + { + s_truncateScaledDecimal = Tristate.True; + } + else + { + s_truncateScaledDecimal = Tristate.False; + } + } + return s_truncateScaledDecimal == Tristate.True; + } + } + +#if NET + /// + /// .NET Core 2.0 and up supports Globalization Invariant mode, which reduces the size of the required libraries for + /// applications which don't need globalization support. SqlClient requires those libraries for core functionality, + /// and will throw exceptions later if they are not present. This switch allows SqlClient to detect this mode early. + /// + public static bool GlobalizationInvariantMode + { + get + { + if (s_globalizationInvariantMode == Tristate.NotInitialized) + { + // Check if invariant mode is has been set by the AppContext switch directly + if (AppContext.TryGetSwitch(GlobalizationInvariantModeString, out bool returnedValue) && returnedValue) + { + s_globalizationInvariantMode = Tristate.True; + } + else + { + // If the switch is not set, we check the environment variable as the first fallback + string envValue = Environment.GetEnvironmentVariable(GlobalizationInvariantModeEnvironmentVariable); + + if (string.Equals(envValue, bool.TrueString, StringComparison.OrdinalIgnoreCase) || string.Equals(envValue, "1", StringComparison.OrdinalIgnoreCase)) + { + s_globalizationInvariantMode = Tristate.True; + } + else + { + // If this hasn't been manually set, it could still apply if the OS doesn't have ICU libraries installed, + // or if the application is a native binary with ICU support trimmed away. + // .NET 3.1 to 5.0 do not throw in attempting to create en-US in invariant mode, but .NET 6+ does. In + // such cases, catch and infer invariant mode from the exception. + try + { + s_globalizationInvariantMode = System.Globalization.CultureInfo.GetCultureInfo("en-US").EnglishName.Contains("Invariant") + ? Tristate.True + : Tristate.False; + } + catch (System.Globalization.CultureNotFoundException) + { + // If the culture is not found, it means we are in invariant mode + s_globalizationInvariantMode = Tristate.True; + } + } + } + } + return s_globalizationInvariantMode == Tristate.True; + } + } + + /// + /// When set to true, .NET Core will use the managed SNI implementation instead of the native SNI implementation. + /// + /// + /// + /// Non-Windows platforms will always use the managed networking implementation. Windows platforms will use the native SNI + /// implementation by default, but this can be overridden by setting the AppContext switch. + /// + /// + /// ILLink.Substitutions.xml allows the unused SNI implementation to be trimmed away when the corresponding AppContext + /// switch is set at compile time. In such cases, this property will return a constant value, even if the AppContext switch is + /// set or reset at runtime. See the ILLink.Substitutions.Windows.xml and ILLink.Substitutions.Unix.xml resource files for details. + /// + /// + public static bool UseManagedNetworking + { + get + { + if (s_useManagedNetworking == Tristate.NotInitialized) + { + if (!OperatingSystem.IsWindows()) + { + s_useManagedNetworking = Tristate.True; + } + else if (AppContext.TryGetSwitch(UseManagedNetworkingOnWindowsString, out bool returnedValue) && returnedValue) + { + s_useManagedNetworking = Tristate.True; + } + else + { + s_useManagedNetworking = Tristate.False; + } + } + return s_useManagedNetworking == Tristate.True; + } + } +#else + /// + /// .NET Framework does not support Globalization Invariant mode, so this will always be false. + /// + public const bool GlobalizationInvariantMode = false; + + /// + /// .NET Framework does not support the managed SNI, so this will always be false. + /// + public const bool UseManagedNetworking = false; + + /// + /// Transparent Network IP Resolution (TNIR) is a revision of the existing MultiSubnetFailover feature. + /// TNIR affects the connection sequence of the driver in the case where the first resolved IP of the hostname + /// doesn't respond and there are multiple IPs associated with the hostname. + /// + /// TNIR interacts with MultiSubnetFailover to provide the following three connection sequences: + /// 0: One IP is attempted, followed by all IPs in parallel + /// 1: All IPs are attempted in parallel + /// 2: All IPs are attempted one after another + /// + /// TransparentNetworkIPResolution is enabled by default. MultiSubnetFailover is disabled by default. + /// To disable TNIR, you can enable the app context switch. + /// + /// This app context switch defaults to 'false'. + /// + public static bool DisableTnirByDefault + { + get + { + if (s_disableTnirByDefault == Tristate.NotInitialized) + { + if (AppContext.TryGetSwitch(DisableTnirByDefaultString, out bool returnedValue) && returnedValue) + { + s_disableTnirByDefault = Tristate.True; + } + else + { + s_disableTnirByDefault = Tristate.False; + } + } + return s_disableTnirByDefault == Tristate.True; + } + } +#endif } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniHandle.netcore.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniHandle.netcore.cs index 1f3ee01ab8..bcd814270c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniHandle.netcore.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniHandle.netcore.cs @@ -121,7 +121,7 @@ protected static void AuthenticateAsClient(SslStream sslStream, string serverNam /// /// Gets a value that indicates the security protocol used to authenticate this connection. /// - public virtual int ProtocolVersion { get; } = 0; + public virtual SslProtocols ProtocolVersion { get; } = 0; #if DEBUG /// diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniMarsConnection.netcore.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniMarsConnection.netcore.cs index 030dfb7452..3cef7f16df 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniMarsConnection.netcore.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniMarsConnection.netcore.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Security.Authentication; using System.Threading; namespace Microsoft.Data.SqlClient.ManagedSni @@ -32,7 +33,7 @@ internal class SniMarsConnection /// public Guid ConnectionId => _connectionId; - public int ProtocolVersion => _lowerHandle.ProtocolVersion; + public SslProtocols ProtocolVersion => _lowerHandle.ProtocolVersion; internal object DemuxerSync => _sync; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniMarsHandle.netcore.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniMarsHandle.netcore.cs index ed69c242bf..11e9abf9bc 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniMarsHandle.netcore.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniMarsHandle.netcore.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Security.Authentication; using System.Threading; namespace Microsoft.Data.SqlClient.ManagedSni @@ -49,7 +50,7 @@ internal sealed class SniMarsHandle : SniHandle public override int ReserveHeaderSize => SniSmuxHeader.HEADER_LENGTH; - public override int ProtocolVersion => _connection.ProtocolVersion; + public override SslProtocols ProtocolVersion => _connection.ProtocolVersion; /// /// Dispose object diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniNpHandle.netcore.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniNpHandle.netcore.cs index 7a1c2ec660..f19dc4a33e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniNpHandle.netcore.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniNpHandle.netcore.cs @@ -123,13 +123,13 @@ public SniNpHandle(string serverName, string pipeName, TimeoutTimer timeout, boo public override uint Status => _status; - public override int ProtocolVersion + public override SslProtocols ProtocolVersion { get { try { - return (int)_sslStream.SslProtocol; + return _sslStream.SslProtocol; } catch { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniTcpHandle.netcore.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniTcpHandle.netcore.cs index 8960801967..3dc8e36ff8 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniTcpHandle.netcore.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniTcpHandle.netcore.cs @@ -98,13 +98,13 @@ public override uint Status } } - public override int ProtocolVersion + public override SslProtocols ProtocolVersion { get { try { - return (int)_sslStream.SslProtocol; + return _sslStream.SslProtocol; } catch { @@ -953,9 +953,30 @@ public override uint Receive(out SniPacket packet, int timeoutInMilliseconds) } finally { - // Reset the socket timeout to Timeout.Infinite after the receive operation is done - // to avoid blocking the thread in case of a timeout error. - _socket.ReceiveTimeout = Timeout.Infinite; + const int resetTimeout = Timeout.Infinite; + + try + { + // Reset the socket timeout to Timeout.Infinite after + // the receive operation is done to avoid blocking the + // thread in case of a timeout error. + _socket.ReceiveTimeout = resetTimeout; + + } + catch (SocketException ex) + { + // We sometimes see setting the ReceiveTimeout fail + // on macOS. There's isn't much we can do about it + // though, so just log and move on. + SqlClientEventSource.Log.TrySNITraceEvent( + nameof(SniTcpHandle), + EventType.ERR, + "Connection Id {0}, Failed to reset socket " + + "receive timeout to {1}: {2}", + _connectionId, + resetTimeout, + ex.Message); + } } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NativeSspiContextProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NativeSspiContextProvider.cs index 5935b149c8..1cc4af3e9c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NativeSspiContextProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NativeSspiContextProvider.cs @@ -49,7 +49,7 @@ private void LoadSSPILibrary() } } - protected override bool GenerateSspiClientContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams) + protected override bool GenerateContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams) { #if NETFRAMEWORK SNIHandle handle = _physicalStateObj.Handle; @@ -62,7 +62,7 @@ protected override bool GenerateSspiClientContext(ReadOnlySpan incomingBlo var sendLength = s_maxSSPILength; var outBuff = outgoingBlobWriter.GetSpan((int)sendLength); - if (0 != SniNativeWrapper.SniSecGenClientContext(handle, incomingBlob, outBuff, ref sendLength, authParams.Resource)) + if (SniNativeWrapper.SniSecGenClientContext(handle, incomingBlob, outBuff, ref sendLength, authParams.Resource) != 0) { return false; } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NegotiateSspiContextProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NegotiateSspiContextProvider.cs index a74651cf2d..b9b6cbf36e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NegotiateSspiContextProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NegotiateSspiContextProvider.cs @@ -13,14 +13,14 @@ internal sealed class NegotiateSspiContextProvider : SspiContextProvider, IDispo { private NegotiateAuthentication? _negotiateAuth; - protected override bool GenerateSspiClientContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams) + protected override bool GenerateContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams) { var negotiateAuth = GetNegotiateAuthenticationForParams(authParams); var sendBuff = negotiateAuth.GetOutgoingBlob(incomingBlob, out var statusCode)!; // Log session id, status code and the actual SPN used in the negotiation SqlClientEventSource.Log.TryTraceEvent("{0}.{1} | Info | Session Id {2}, StatusCode={3}, SPN={4}", nameof(NegotiateSspiContextProvider), - nameof(GenerateSspiClientContext), _physicalStateObj.SessionId, statusCode, negotiateAuth.TargetName); + nameof(GenerateContext), _physicalStateObj.SessionId, statusCode, negotiateAuth.TargetName); if (statusCode == NegotiateAuthenticationStatusCode.Completed || statusCode == NegotiateAuthenticationStatusCode.ContinueNeeded) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs index f45ccee4fd..d23200410c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SspiContextProvider.cs @@ -16,6 +16,10 @@ internal abstract class SspiContextProvider private protected TdsParserStateObject _physicalStateObj = null!; + protected SspiContextProvider() + { + } + #if NET /// /// for details as to what and means and why there are two. @@ -58,7 +62,7 @@ private protected virtual void Initialize() { } - protected abstract bool GenerateSspiClientContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams); + protected abstract bool GenerateContext(ReadOnlySpan incomingBlob, IBufferWriter outgoingBlobWriter, SspiAuthenticationParameters authParams); internal void WriteSSPIContext(ReadOnlySpan receivedBuff, IBufferWriter outgoingBlobWriter) { @@ -94,9 +98,9 @@ private bool RunGenerateSspiClientContext(ReadOnlySpan incomingBlob, IBuff { try { - SqlClientEventSource.Log.TryTraceEvent("{0}.{1} | Info | SPN={1}", GetType().FullName, nameof(GenerateSspiClientContext), authParams.Resource); + SqlClientEventSource.Log.TryTraceEvent("{0}.{1} | Info | SPN={2}", GetType().FullName, nameof(GenerateContext), authParams.Resource); - return GenerateSspiClientContext(incomingBlob, outgoingBlobWriter, authParams); + return GenerateContext(incomingBlob, outgoingBlobWriter, authParams); } catch (Exception e) { @@ -105,7 +109,7 @@ private bool RunGenerateSspiClientContext(ReadOnlySpan incomingBlob, IBuff } } - protected void SSPIError(string error, string procedure) + private protected void SSPIError(string error, string procedure) { Debug.Assert(!string.IsNullOrEmpty(procedure), "TdsParser.SSPIError called with an empty or null procedure string"); Debug.Assert(!string.IsNullOrEmpty(error), "TdsParser.SSPIError called with an empty or null error string"); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/SqlNormalizer.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/SqlNormalizer.cs index 5acba813e2..c5f3721226 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/SqlNormalizer.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/SqlNormalizer.cs @@ -39,11 +39,6 @@ internal sealed class BinaryOrderedUdtNormalizer : Normalizer { private readonly FieldInfoEx[] _fieldsToNormalize; private int _size; - private readonly byte[] _padBuffer; - private readonly object _nullInstance; - //a boolean that tells us if a udt is a "top-level" udt, - //i.e. one that does not require a null byte header. - private readonly bool _isTopLevelUdt; #if NETFRAMEWORK [System.Security.Permissions.ReflectionPermission(System.Security.Permissions.SecurityAction.Assert, MemberAccess = true)] @@ -61,22 +56,8 @@ internal BinaryOrderedUdtNormalizer( #if NET [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] #endif - Type t, bool isTopLevelUdt) + Type t) { - _skipNormalize = false; - if (_skipNormalize) - { - // if skipping normalization, dont write the null - // byte header for IsNull - _isTopLevelUdt = true; - } - - // top level udt logic is disabled until we decide - // what to do about nested udts - _isTopLevelUdt = true; - // else - // this._isTopLevelUdt = isTopLevelUdt; - FieldInfo[] fields = GetFields(t); _fieldsToNormalize = new FieldInfoEx[fields.Length]; @@ -91,41 +72,8 @@ internal BinaryOrderedUdtNormalizer( //sort by offset Array.Sort(_fieldsToNormalize); - //if this is not a top-level udt, do setup for null values. - //null values need to compare less than all other values, - //so prefix a null byte indicator. - if (!_isTopLevelUdt) - { - //get the null value for this type, special case for sql types, which - //have a null field - if (typeof(INullable).IsAssignableFrom(t)) - { - PropertyInfo pi = t.GetProperty("Null", - BindingFlags.Public | BindingFlags.Static); - if (pi == null || pi.PropertyType != t) - { - FieldInfo fi = t.GetField("Null", BindingFlags.Public | BindingFlags.Static); - if (fi == null || fi.FieldType != t) - { - throw new Exception("could not find Null field/property in nullable type " + t); - } - else - { - _nullInstance = fi.GetValue(null); - } - } - else - { - _nullInstance = pi.GetValue(null, null); - } - - _padBuffer = new byte[Size - 1]; - } - } } - internal bool IsNullable => _nullInstance != null; - // Normalize the top-level udt internal void NormalizeTopObject(object udt, Stream s) => Normalize(null, udt, s); @@ -144,23 +92,8 @@ private object DeNormalizeInternal( #endif Type t, Stream s) { - object result = null; - //if nullable and not the top object, read the null marker - if (!_isTopLevelUdt && typeof(INullable).IsAssignableFrom(t)) - { - byte nullByte = (byte)s.ReadByte(); - if (nullByte == 0) - { - result = _nullInstance; - s.ReadExactly(_padBuffer, 0, _padBuffer.Length); + object result = Activator.CreateInstance(t); - return result; - } - } - if (result == null) - { - result = Activator.CreateInstance(t); - } foreach (FieldInfoEx field in _fieldsToNormalize) { field.Normalizer.DeNormalize(field.FieldInfo, result, s); @@ -180,21 +113,6 @@ internal override void Normalize(FieldInfo fi, object obj, Stream s) inner = GetValue(fi, obj); } - // If nullable and not the top object, write a null indicator - if (inner is INullable nullable && !_isTopLevelUdt) - { - if (nullable.IsNull) - { - s.WriteByte(0); - s.Write(_padBuffer, 0, _padBuffer.Length); - return; - } - else - { - s.WriteByte(1); - } - } - foreach (FieldInfoEx field in _fieldsToNormalize) { field.Normalizer.Normalize(field.FieldInfo, inner, s); @@ -211,10 +129,6 @@ internal override int Size { return _size; } - if (IsNullable && !_isTopLevelUdt) - { - _size = 1; - } foreach (FieldInfoEx field in _fieldsToNormalize) { _size += field.Normalizer.Size; @@ -226,8 +140,6 @@ internal override int Size internal abstract class Normalizer { - protected bool _skipNormalize; - internal static Normalizer GetNormalizer( #if NET [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] @@ -262,13 +174,12 @@ internal static Normalizer GetNormalizer( } else if (t.IsValueType) { - n = new BinaryOrderedUdtNormalizer(t, false); + n = new BinaryOrderedUdtNormalizer(t); } if (n == null) { throw new Exception(StringsHelper.GetString(Strings.SQL_CannotCreateNormalizer, t.FullName)); } - n._skipNormalize = false; return n; } @@ -322,20 +233,15 @@ internal override void Normalize(FieldInfo fi, object obj, Stream s) { b = (byte)sb; } - if (!_skipNormalize) - { - b ^= 0x80; //flip the sign bit - } + b ^= 0x80; //flip the sign bit s.WriteByte(b); } internal override void DeNormalize(FieldInfo fi, object recvr, Stream s) { byte b = (byte)s.ReadByte(); - if (!_skipNormalize) - { - b ^= 0x80; //flip the sign bit - } + + b ^= 0x80; //flip the sign bit sbyte sb; unchecked { @@ -369,11 +275,8 @@ internal sealed class ShortNormalizer : Normalizer internal override void Normalize(FieldInfo fi, object obj, Stream s) { byte[] b = BitConverter.GetBytes((short)GetValue(fi, obj)); - if (!_skipNormalize) - { - Array.Reverse(b); - b[0] ^= 0x80; - } + Array.Reverse(b); + b[0] ^= 0x80; s.Write(b, 0, b.Length); } @@ -381,11 +284,8 @@ internal override void DeNormalize(FieldInfo fi, object recvr, Stream s) { byte[] b = new byte[2]; s.ReadExactly(b, 0, b.Length); - if (!_skipNormalize) - { - b[0] ^= 0x80; - Array.Reverse(b); - } + b[0] ^= 0x80; + Array.Reverse(b); SetValue(fi, recvr, BitConverter.ToInt16(b, 0)); } @@ -397,10 +297,7 @@ internal sealed class UShortNormalizer : Normalizer internal override void Normalize(FieldInfo fi, object obj, Stream s) { byte[] b = BitConverter.GetBytes((ushort)GetValue(fi, obj)); - if (!_skipNormalize) - { - Array.Reverse(b); - } + Array.Reverse(b); s.Write(b, 0, b.Length); } @@ -408,10 +305,7 @@ internal override void DeNormalize(FieldInfo fi, object recvr, Stream s) { byte[] b = new byte[2]; s.ReadExactly(b, 0, b.Length); - if (!_skipNormalize) - { - Array.Reverse(b); - } + Array.Reverse(b); SetValue(fi, recvr, BitConverter.ToUInt16(b, 0)); } @@ -423,11 +317,8 @@ internal sealed class IntNormalizer : Normalizer internal override void Normalize(FieldInfo fi, object obj, Stream s) { byte[] b = BitConverter.GetBytes((int)GetValue(fi, obj)); - if (!_skipNormalize) - { - Array.Reverse(b); - b[0] ^= 0x80; - } + Array.Reverse(b); + b[0] ^= 0x80; s.Write(b, 0, b.Length); } @@ -435,11 +326,8 @@ internal override void DeNormalize(FieldInfo fi, object recvr, Stream s) { byte[] b = new byte[4]; s.ReadExactly(b, 0, b.Length); - if (!_skipNormalize) - { - b[0] ^= 0x80; - Array.Reverse(b); - } + b[0] ^= 0x80; + Array.Reverse(b); SetValue(fi, recvr, BitConverter.ToInt32(b, 0)); } @@ -451,10 +339,7 @@ internal sealed class UIntNormalizer : Normalizer internal override void Normalize(FieldInfo fi, object obj, Stream s) { byte[] b = BitConverter.GetBytes((uint)GetValue(fi, obj)); - if (!_skipNormalize) - { - Array.Reverse(b); - } + Array.Reverse(b); s.Write(b, 0, b.Length); } @@ -462,10 +347,7 @@ internal override void DeNormalize(FieldInfo fi, object recvr, Stream s) { byte[] b = new byte[4]; s.ReadExactly(b, 0, b.Length); - if (!_skipNormalize) - { - Array.Reverse(b); - } + Array.Reverse(b); SetValue(fi, recvr, BitConverter.ToUInt32(b, 0)); } @@ -477,11 +359,8 @@ internal sealed class LongNormalizer : Normalizer internal override void Normalize(FieldInfo fi, object obj, Stream s) { byte[] b = BitConverter.GetBytes((long)GetValue(fi, obj)); - if (!_skipNormalize) - { - Array.Reverse(b); - b[0] ^= 0x80; - } + Array.Reverse(b); + b[0] ^= 0x80; s.Write(b, 0, b.Length); } @@ -489,11 +368,8 @@ internal override void DeNormalize(FieldInfo fi, object recvr, Stream s) { byte[] b = new byte[8]; s.ReadExactly(b, 0, b.Length); - if (!_skipNormalize) - { - b[0] ^= 0x80; - Array.Reverse(b); - } + b[0] ^= 0x80; + Array.Reverse(b); SetValue(fi, recvr, BitConverter.ToInt64(b, 0)); } @@ -505,10 +381,7 @@ internal sealed class ULongNormalizer : Normalizer internal override void Normalize(FieldInfo fi, object obj, Stream s) { byte[] b = BitConverter.GetBytes((ulong)GetValue(fi, obj)); - if (!_skipNormalize) - { - Array.Reverse(b); - } + Array.Reverse(b); s.Write(b, 0, b.Length); } @@ -516,10 +389,7 @@ internal override void DeNormalize(FieldInfo fi, object recvr, Stream s) { byte[] b = new byte[8]; s.ReadExactly(b, 0, b.Length); - if (!_skipNormalize) - { - Array.Reverse(b); - } + Array.Reverse(b); SetValue(fi, recvr, BitConverter.ToUInt64(b, 0)); } @@ -532,26 +402,23 @@ internal override void Normalize(FieldInfo fi, object obj, Stream s) { float f = (float)GetValue(fi, obj); byte[] b = BitConverter.GetBytes(f); - if (!_skipNormalize) + Array.Reverse(b); + if ((b[0] & 0x80) == 0) { - Array.Reverse(b); - if ((b[0] & 0x80) == 0) - { - // This is a positive number. - // Flip the highest bit - b[0] ^= 0x80; - } - else - { - // This is a negative number. + // This is a positive number. + // Flip the highest bit + b[0] ^= 0x80; + } + else + { + // This is a negative number. - // If all zeroes, means it was a negative zero. - // Treat it same as positive zero, so that - // the normalized key will compare equal. - if (f < 0) - { - FlipAllBits(b); - } + // If all zeroes, means it was a negative zero. + // Treat it same as positive zero, so that + // the normalized key will compare equal. + if (f < 0) + { + FlipAllBits(b); } } s.Write(b, 0, b.Length); @@ -561,21 +428,18 @@ internal override void DeNormalize(FieldInfo fi, object recvr, Stream s) { byte[] b = new byte[4]; s.ReadExactly(b, 0, b.Length); - if (!_skipNormalize) + if ((b[0] & 0x80) > 0) { - if ((b[0] & 0x80) > 0) - { - // This is a positive number. - // Flip the highest bit - b[0] ^= 0x80; - } - else - { - // This is a negative number. - FlipAllBits(b); - } - Array.Reverse(b); + // This is a positive number. + // Flip the highest bit + b[0] ^= 0x80; } + else + { + // This is a negative number. + FlipAllBits(b); + } + Array.Reverse(b); SetValue(fi, recvr, BitConverter.ToSingle(b, 0)); } @@ -588,25 +452,22 @@ internal override void Normalize(FieldInfo fi, object obj, Stream s) { double d = (double)GetValue(fi, obj); byte[] b = BitConverter.GetBytes(d); - if (!_skipNormalize) + Array.Reverse(b); + if ((b[0] & 0x80) == 0) { - Array.Reverse(b); - if ((b[0] & 0x80) == 0) - { - // This is a positive number. - // Flip the highest bit - b[0] ^= 0x80; - } - else + // This is a positive number. + // Flip the highest bit + b[0] ^= 0x80; + } + else + { + // This is a negative number. + if (d < 0) { - // This is a negative number. - if (d < 0) - { - // If all zeroes, means it was a negative zero. - // Treat it same as positive zero, so that - // the normalized key will compare equal. - FlipAllBits(b); - } + // If all zeroes, means it was a negative zero. + // Treat it same as positive zero, so that + // the normalized key will compare equal. + FlipAllBits(b); } } s.Write(b, 0, b.Length); @@ -616,21 +477,18 @@ internal override void DeNormalize(FieldInfo fi, object recvr, Stream s) { byte[] b = new byte[8]; s.ReadExactly(b, 0, b.Length); - if (!_skipNormalize) + if ((b[0] & 0x80) > 0) { - if ((b[0] & 0x80) > 0) - { - // This is a positive number. - // Flip the highest bit - b[0] ^= 0x80; - } - else - { - // This is a negative number. - FlipAllBits(b); - } - Array.Reverse(b); + // This is a positive number. + // Flip the highest bit + b[0] ^= 0x80; + } + else + { + // This is a negative number. + FlipAllBits(b); } + Array.Reverse(b); SetValue(fi, recvr, BitConverter.ToDouble(b, 0)); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/SqlSer.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/SqlSer.cs index c10b5779f1..61546a1aeb 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/SqlSer.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Server/SqlSer.cs @@ -29,9 +29,6 @@ internal static int SizeInBytes( // Get the m_size of the serialized stream for this type, in bytes. internal static int SizeInBytes(object instance) { - Type t = instance.GetType(); - - _ = GetFormat(t); DummyStream stream = new DummyStream(); Serializer ser = GetSerializer(instance.GetType()); ser.Serialize(stream, instance); @@ -49,14 +46,10 @@ internal static object Deserialize(Stream s, #endif Type resultType) => GetSerializer(resultType).Deserialize(s); - private static Format GetFormat(Type t) => GetUdtAttribute(t).Format; // Cache the relationship between a type and its serializer. // This is expensive to compute since it involves traversing the // custom attributes of the type using reflection. - // - // Use a per-thread cache, so that there are no synchronization - // issues when accessing cache entries from multiple threads. private static ConcurrentDictionary s_types2Serializers; private static Serializer GetSerializer( @@ -169,8 +162,7 @@ internal NormalizedSerializer( #endif Type t) : base(t) { - _ = SerializationHelperSql9.GetUdtAttribute(t); - _normalizer = new BinaryOrderedUdtNormalizer(t, true); + _normalizer = new BinaryOrderedUdtNormalizer(t); } public override void Serialize(Stream s, object o) => _normalizer.NormalizeTopObject(o, s); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs index 7819e31bfe..80406c1e7e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs @@ -1641,7 +1641,7 @@ private object ConvertValue(object value, _SqlMetaData metadata, bool isNull, re // in byte[] form. if (!(value is byte[])) { - value = _connection.GetBytes(value, out _, out _); + value = _connection.GetBytes(value, out _); typeChanged = true; } break; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.Windows.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.Windows.cs index bb28a12d60..c47173c33c 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.Windows.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.Windows.cs @@ -301,6 +301,7 @@ private byte[] RSASignHashedData(byte[] dataToSign, RSACng rsaCngProvider) Debug.Assert((dataToSign != null) && (dataToSign.Length != 0)); Debug.Assert(rsaCngProvider != null); + // CodeQL [SM03796] Required for backwards compatibility with existing data and cross-driver compatability return rsaCngProvider.SignData(dataToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } @@ -317,6 +318,7 @@ private bool RSAVerifySignature(byte[] dataToVerify, byte[] signature, RSACng rs Debug.Assert((signature != null) && (signature.Length != 0)); Debug.Assert(rsaCngProvider != null); + // CodeQL [SM03796] Required for backwards compatibility with existing data and cross-driver compatability return rsaCngProvider.VerifyData(dataToVerify, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs index ae4c7bc98d..7a09bf6356 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs @@ -3,10 +3,14 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; +using System.Data; using System.Data.Common; using System.Diagnostics; using System.IO; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Data.Common; using Microsoft.Data.Common.ConnectionString; using Microsoft.Data.ProviderBase; @@ -18,26 +22,561 @@ namespace Microsoft.Data.SqlClient { - sealed internal class SqlConnectionFactory : DbConnectionFactory + // @TODO: Facade pattern (interface, use interface, add constructor overloads for providing non-default interface, reseal) + internal class SqlConnectionFactory { - public static readonly SqlConnectionFactory SingletonInstance = new SqlConnectionFactory(); + #region Member Variables - private SqlConnectionFactory() : base() + private static readonly TimeSpan PruningDueTime = TimeSpan.FromMinutes(4); + private static readonly TimeSpan PruningPeriod = TimeSpan.FromSeconds(30); + private static readonly Task CompletedTask = + Task.FromResult(null); + + // s_pendingOpenNonPooled is an array of tasks used to throttle creation of non-pooled + // connections to a maximum of Environment.ProcessorCount at a time. + private static readonly Task[] s_pendingOpenNonPooled = + new Task[Environment.ProcessorCount]; + + private static int s_objectTypeCount; + private static uint s_pendingOpenNonPooledNext = 0; + + private readonly List _poolGroupsToRelease; + private readonly List _poolsToRelease; + private readonly Timer _pruningTimer; + private Dictionary _connectionPoolGroups; + + #endregion + + #region Constructors + + private SqlConnectionFactory() { + _connectionPoolGroups = new Dictionary(); + _poolsToRelease = new List(); + _poolGroupsToRelease = new List(); + _pruningTimer = ADP.UnsafeCreateTimer( + PruneConnectionPoolGroups, + state: null, + PruningDueTime, + PruningPeriod); + #if NET SubscribeToAssemblyLoadContextUnload(); #endif } + + #endregion + + #region Properties + + internal static SqlConnectionFactory Instance { get; } = new SqlConnectionFactory(); + + internal int ObjectId { get; } = Interlocked.Increment(ref s_objectTypeCount); + + #endregion - override public DbProviderFactory ProviderFactory + #region Public Methods + + internal void ClearAllPools() { - get + using TryEventScope scope = TryEventScope.Create(nameof(SqlConnectionFactory)); + foreach (DbConnectionPoolGroup group in _connectionPoolGroups.Values) { - return SqlClientFactory.Instance; + group?.Clear(); } } - protected override DbConnectionInternal CreateConnection( + internal void ClearPool(DbConnection connection) + { + ADP.CheckArgumentNull(connection, nameof(connection)); + + using TryEventScope scope = TryEventScope.Create(" {0}", GetObjectId(connection)); + DbConnectionPoolGroup poolGroup = GetConnectionPoolGroup(connection); + poolGroup?.Clear(); + } + + internal void ClearPool(DbConnectionPoolKey key) + { + ADP.CheckArgumentNull(key.ConnectionString, $"{nameof(key)}.{nameof(key.ConnectionString)}"); + + using TryEventScope scope = TryEventScope.Create(" connectionString"); + if (_connectionPoolGroups.TryGetValue(key, out DbConnectionPoolGroup poolGroup)) + { + poolGroup?.Clear(); + } + } + + internal DbConnectionPoolProviderInfo CreateConnectionPoolProviderInfo(DbConnectionOptions connectionOptions) => + ((SqlConnectionString)connectionOptions).UserInstance + ? new SqlConnectionPoolProviderInfo() + : null; + + internal SqlInternalConnectionTds CreateNonPooledConnection( + DbConnection owningConnection, + DbConnectionPoolGroup poolGroup, + DbConnectionOptions userOptions) + { + Debug.Assert(owningConnection is not null, "null owningConnection?"); + Debug.Assert(poolGroup is not null, "null poolGroup?"); + + SqlInternalConnectionTds newConnection = CreateConnection( + poolGroup.ConnectionOptions, + poolGroup.PoolKey, + poolGroup.ProviderInfo, + pool: null, + owningConnection, + userOptions); + if (newConnection is not null) + { + SqlClientEventSource.Metrics.HardConnectRequest(); + newConnection.MakeNonPooledObject(owningConnection); + } + + SqlClientEventSource.Log.TryTraceEvent(" {0}, Non-pooled database connection created.", ObjectId); + return newConnection; + } + + internal SqlInternalConnectionTds CreatePooledConnection( + DbConnection owningConnection, + IDbConnectionPool pool, + DbConnectionPoolKey poolKey, + DbConnectionOptions options, + DbConnectionOptions userOptions) + { + Debug.Assert(pool != null, "null pool?"); + + SqlInternalConnectionTds newConnection = CreateConnection( + options, + poolKey, // @TODO: is pool.PoolGroup.Key the same thing? + pool.PoolGroup.ProviderInfo, + pool, + owningConnection, + userOptions); + if (newConnection is not null) + { + SqlClientEventSource.Metrics.HardConnectRequest(); + newConnection.MakePooledConnection(pool); + } + + SqlClientEventSource.Log.TryTraceEvent(" {0}, Pooled database connection created.", ObjectId); + return newConnection; + } + + internal DbConnectionPoolGroup GetConnectionPoolGroup( + DbConnectionPoolKey key, + DbConnectionPoolGroupOptions poolOptions, + ref DbConnectionOptions userConnectionOptions) + { + if (string.IsNullOrEmpty(key.ConnectionString)) + { + return null; + } + + if (!_connectionPoolGroups.TryGetValue(key, out DbConnectionPoolGroup connectionPoolGroup) || + (connectionPoolGroup.IsDisabled && connectionPoolGroup.PoolGroupOptions != null)) + { + // If we can't find an entry for the connection string in + // our collection of pool entries, then we need to create a + // new pool entry and add it to our collection. + + SqlConnectionString connectionOptions = new SqlConnectionString(key.ConnectionString); + + if (userConnectionOptions is null) + { + // We only allow one expansion on the connection string + userConnectionOptions = connectionOptions; + string expandedConnectionString = connectionOptions.Expand(); + + // if the expanded string is same instance (default implementation), then use the already created options + if ((object)expandedConnectionString != (object)key.ConnectionString) + { + // CONSIDER: caching the original string to reduce future parsing + DbConnectionPoolKey newKey = (DbConnectionPoolKey)key.Clone(); + newKey.ConnectionString = expandedConnectionString; + return GetConnectionPoolGroup(newKey, null, ref userConnectionOptions); + } + } + + if (poolOptions is null) + { + if (connectionPoolGroup is not null) + { + // reusing existing pool option in case user originally used SetConnectionPoolOptions + poolOptions = connectionPoolGroup.PoolGroupOptions; + } + else + { + // Note: may return null for non-pooled connections + poolOptions = CreateConnectionPoolGroupOptions(connectionOptions); + } + } + + lock (this) + { + if (!_connectionPoolGroups.TryGetValue(key, out connectionPoolGroup)) + { + DbConnectionPoolGroup newConnectionPoolGroup = + new DbConnectionPoolGroup(connectionOptions, key, poolOptions) + { + ProviderInfo = CreateConnectionPoolGroupProviderInfo(connectionOptions) + }; + + // build new dictionary with space for new connection string + Dictionary newConnectionPoolGroups = + new Dictionary(1 + _connectionPoolGroups.Count); + foreach (KeyValuePair entry in _connectionPoolGroups) + { + newConnectionPoolGroups.Add(entry.Key, entry.Value); + } + + // lock prevents race condition with PruneConnectionPoolGroups + newConnectionPoolGroups.Add(key, newConnectionPoolGroup); + + SqlClientEventSource.Metrics.EnterActiveConnectionPoolGroup(); + connectionPoolGroup = newConnectionPoolGroup; + _connectionPoolGroups = newConnectionPoolGroups; + } + else + { + Debug.Assert(!connectionPoolGroup.IsDisabled, "Disabled pool entry discovered"); + } + } + + Debug.Assert(connectionPoolGroup != null, "how did we not create a pool entry?"); + Debug.Assert(userConnectionOptions != null, "how did we not have user connection options?"); + } + else if (userConnectionOptions is null) + { + userConnectionOptions = connectionPoolGroup.ConnectionOptions; + } + + return connectionPoolGroup; + } + + internal DbMetaDataFactory GetMetaDataFactory( + DbConnectionPoolGroup poolGroup, + DbConnectionInternal internalConnection) + { + Debug.Assert(poolGroup is not null, "connectionPoolGroup may not be null."); + + // Get the matadatafactory from the pool entry. If it does not already have one + // create one and save it on the pool entry + DbMetaDataFactory metaDataFactory = poolGroup.MetaDataFactory; + + // CONSIDER: serializing this so we don't construct multiple metadata factories + // if two threads happen to hit this at the same time. One will be GC'd + if (metaDataFactory is null) + { + metaDataFactory = CreateMetaDataFactory(internalConnection, out bool allowCache); + if (allowCache) + { + poolGroup.MetaDataFactory = metaDataFactory; + } + } + + return metaDataFactory; + } + + internal void QueuePoolForRelease(IDbConnectionPool pool, bool clearing) + { + // Queue the pool up for release -- we'll clear it out and dispose of it as the last + // part of the pruning timer callback so we don't do it with the pool entry or the pool + // collection locked. + Debug.Assert(pool != null, "null pool?"); + + // Set the pool to the shutdown state to force all active connections to be + // automatically disposed when they are returned to the pool + pool.Shutdown(); + + lock (_poolsToRelease) + { + if (clearing) + { + pool.Clear(); + } + _poolsToRelease.Add(pool); + } + + SqlClientEventSource.Metrics.EnterInactiveConnectionPool(); + SqlClientEventSource.Metrics.ExitActiveConnectionPool(); + } + + internal void QueuePoolGroupForRelease(DbConnectionPoolGroup poolGroup) + { + Debug.Assert(poolGroup != null, "null poolGroup?"); + SqlClientEventSource.Log.TryTraceEvent(" {0}, poolGroup={1}", ObjectId, poolGroup.ObjectID); + + lock (_poolGroupsToRelease) + { + _poolGroupsToRelease.Add(poolGroup); + } + + SqlClientEventSource.Metrics.EnterInactiveConnectionPoolGroup(); + SqlClientEventSource.Metrics.ExitActiveConnectionPoolGroup(); + } + + internal bool TryGetConnection( + DbConnection owningConnection, + TaskCompletionSource retry, + DbConnectionOptions userOptions, + DbConnectionInternal oldConnection, + out DbConnectionInternal connection) + { + Debug.Assert(owningConnection is not null, "null owningConnection?"); + + connection = null; + + // Work around race condition with clearing the pool between GetConnectionPool obtaining pool + // and GetConnection on the pool checking the pool state. Clearing the pool in this window + // will switch the pool into the ShuttingDown state, and GetConnection will return null. + // There is probably a better solution involving locking the pool/group, but that entails a major + // re-design of the connection pooling synchronization, so is postponed for now. + + // Use retriesLeft to prevent CPU spikes with incremental sleep + // start with one msec, double the time every retry + // max time is: 1 + 2 + 4 + ... + 2^(retries-1) == 2^retries -1 == 1023ms (for 10 retries) + int retriesLeft = 10; + int timeBetweenRetriesMilliseconds = 1; + + do + { + DbConnectionPoolGroup poolGroup = GetConnectionPoolGroup(owningConnection); + + // Doing this on the callers thread is important because it looks up the WindowsIdentity from the thread. + IDbConnectionPool connectionPool = GetConnectionPool(owningConnection, poolGroup); + if (connectionPool == null) + { + // If GetConnectionPool returns null, we can be certain that this connection + // should not be pooled via DbConnectionPool or have a disabled pool entry. + poolGroup = GetConnectionPoolGroup(owningConnection); // previous entry have been disabled + + if (retry is not null) + { + Task newTask; + CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + lock (s_pendingOpenNonPooled) + { + // look for an available task slot (completed or empty) + int idx; + for (idx = 0; idx < s_pendingOpenNonPooled.Length; idx++) + { + Task task = s_pendingOpenNonPooled[idx]; + if (task is null) + { + s_pendingOpenNonPooled[idx] = CompletedTask; + break; + } + + if (task.IsCompleted) + { + break; + } + } + + // if didn't find one, pick the next one in round-robin fashion + if (idx == s_pendingOpenNonPooled.Length) + { + idx = (int)(s_pendingOpenNonPooledNext % s_pendingOpenNonPooled.Length); + unchecked + { + s_pendingOpenNonPooledNext++; + } + } + + // now that we have an antecedent task, schedule our work when it is completed. + // If it is a new slot or a completed task, this continuation will start right away. + newTask = CreateReplaceConnectionContinuation( + s_pendingOpenNonPooled[idx], + owningConnection, + retry, + userOptions, + oldConnection, + poolGroup, + cancellationTokenSource); + + // Place this new task in the slot so any future work will be queued behind it + s_pendingOpenNonPooled[idx] = newTask; + } + + // Set up the timeout (if needed) + if (owningConnection.ConnectionTimeout > 0) + { + int connectionTimeoutMilliseconds = owningConnection.ConnectionTimeout * 1000; + cancellationTokenSource.CancelAfter(connectionTimeoutMilliseconds); + } + + // once the task is done, propagate the final results to the original caller + newTask.ContinueWith( + continuationAction: TryGetConnectionCompletedContinuation, + state: Tuple.Create(cancellationTokenSource, retry), + scheduler: TaskScheduler.Default + ); + + return false; + } + + connection = CreateNonPooledConnection(owningConnection, poolGroup, userOptions); + + SqlClientEventSource.Metrics.EnterNonPooledConnection(); + } + else + { + if (((SqlConnection)owningConnection).ForceNewConnection) + { + Debug.Assert(oldConnection is not DbConnectionClosed, "Force new connection, but there is no old connection"); + + connection = connectionPool.ReplaceConnection(owningConnection, userOptions, oldConnection); + } + else + { + if (!connectionPool.TryGetConnection(owningConnection, retry, userOptions, out connection)) + { + return false; + } + } + + if (connection is null) + { + // connection creation failed on semaphore waiting or if max pool reached + if (connectionPool.IsRunning) + { + SqlClientEventSource.Log.TryTraceEvent(" {0}, GetConnection failed because a pool timeout occurred.", ObjectId); + // If GetConnection failed while the pool is running, the pool timeout occurred. + throw ADP.PooledOpenTimeout(); + } + + // We've hit the race condition, where the pool was shut down after we + // got it from the group. Yield time slice to allow shutdown activities + // to complete and a new, running pool to be instantiated before + // retrying. + Thread.Sleep(timeBetweenRetriesMilliseconds); + timeBetweenRetriesMilliseconds *= 2; // double the wait time for next iteration + } + } + } while (connection == null && retriesLeft-- > 0); + + if (connection == null) + { + SqlClientEventSource.Log.TryTraceEvent(" {0}, GetConnection failed because a pool timeout occurred and all retries were exhausted.", ObjectId); + // exhausted all retries or timed out - give up + throw ADP.PooledOpenTimeout(); + } + + return true; + } + + #endregion + + internal DbConnectionPoolGroupProviderInfo CreateConnectionPoolGroupProviderInfo( + DbConnectionOptions connectionOptions) => + new SqlConnectionPoolGroupProviderInfo((SqlConnectionString)connectionOptions); + + internal SqlConnectionString FindSqlConnectionOptions(SqlConnectionPoolKey key) + { + Debug.Assert(key is not null, "Key cannot be null"); + + DbConnectionOptions connectionOptions = null; + + if (!string.IsNullOrEmpty(key.ConnectionString) && + _connectionPoolGroups.TryGetValue(key, out DbConnectionPoolGroup poolGroup)) + { + connectionOptions = poolGroup.ConnectionOptions; + } + + if (connectionOptions is null) + { + connectionOptions = new SqlConnectionString(key.ConnectionString); + } + + if (connectionOptions.IsEmpty) + { + throw ADP.NoConnectionString(); + } + + return (SqlConnectionString)connectionOptions; + } + + // @TODO: All these methods seem redundant ... shouldn't we always have a SqlConnection? + internal DbConnectionPoolGroup GetConnectionPoolGroup(DbConnection connection) + { + SqlConnection c = (connection as SqlConnection); + if (c != null) + { + return c.PoolGroup; + } + return null; + } + + internal DbConnectionInternal GetInnerConnection(DbConnection connection) + { + SqlConnection c = (connection as SqlConnection); + if (c != null) + { + return c.InnerConnection; + } + return null; + } + + internal int GetObjectId(DbConnection connection) + { + SqlConnection c = (connection as SqlConnection); + if (c != null) + { + return c.ObjectID; + } + return 0; + } + + internal void PermissionDemand(DbConnection outerConnection) + { + SqlConnection c = (outerConnection as SqlConnection); + if (c != null) + { + c.PermissionDemand(); + } + } + + internal void SetConnectionPoolGroup(DbConnection outerConnection, DbConnectionPoolGroup poolGroup) + { + SqlConnection c = (outerConnection as SqlConnection); + if (c != null) + { + c.PoolGroup = poolGroup; + } + } + + internal void SetInnerConnectionEvent(DbConnection owningObject, DbConnectionInternal to) + { + SqlConnection c = (owningObject as SqlConnection); + if (c != null) + { + c.SetInnerConnectionEvent(to); + } + } + + internal bool SetInnerConnectionFrom(DbConnection owningObject, DbConnectionInternal to, DbConnectionInternal from) + { + SqlConnection c = (owningObject as SqlConnection); + if (c != null) + { + return c.SetInnerConnectionFrom(to, from); + } + return false; + } + + internal void SetInnerConnectionTo(DbConnection owningObject, DbConnectionInternal to) + { + SqlConnection c = (owningObject as SqlConnection); + if (c != null) + { + c.SetInnerConnectionTo(to); + } + } + + #region Private Methods + + // @TODO: I think this could be broken down into methods more specific to use cases above + private static SqlInternalConnectionTds CreateConnection( DbConnectionOptions options, DbConnectionPoolKey poolKey, DbConnectionPoolGroupProviderInfo poolGroupProviderInfo, @@ -70,10 +609,9 @@ protected override DbConnectionInternal CreateConnection( bool redirectedUserInstance = false; DbConnectionPoolIdentity identity = null; - // Pass DbConnectionPoolIdentity to SqlInternalConnectionTds if using integrated security - // or active directory integrated security. + // Pass DbConnectionPoolIdentity to SqlInternalConnectionTds if using integrated security. // Used by notifications. - if (opt.IntegratedSecurity || opt.Authentication is SqlAuthenticationMethod.ActiveDirectoryIntegrated) + if (opt.IntegratedSecurity || opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated) { if (pool != null) { @@ -103,7 +641,7 @@ protected override DbConnectionInternal CreateConnection( // This first connection is established to SqlExpress to get the instance name // of the UserInstance. SqlConnectionString sseopt = new SqlConnectionString(opt, opt.DataSource, userInstance: true, setEnlistValue: false); - sseConnection = new SqlInternalConnectionTds(identity, sseopt, key.Credential, null, "", null, false, applyTransientFaultHandling: applyTransientFaultHandling); + sseConnection = new SqlInternalConnectionTds(identity, sseopt, key.Credential, null, "", null, false, applyTransientFaultHandling: applyTransientFaultHandling, sspiContextProvider: key.SspiContextProvider); // NOTE: Retrieve here. This user instance name will be used below to connect to the Sql Express User Instance. instanceName = sseConnection.InstanceName; @@ -157,29 +695,11 @@ protected override DbConnectionInternal CreateConnection( applyTransientFaultHandling, key.AccessToken, pool, - key.AccessTokenCallback); - } - - protected override DbConnectionOptions CreateConnectionOptions(string connectionString, DbConnectionOptions previous) - { - Debug.Assert(!string.IsNullOrEmpty(connectionString), "empty connectionString"); - SqlConnectionString result = new SqlConnectionString(connectionString); - return result; + key.AccessTokenCallback, + key.SspiContextProvider); } - internal override DbConnectionPoolProviderInfo CreateConnectionPoolProviderInfo(DbConnectionOptions connectionOptions) - { - DbConnectionPoolProviderInfo providerInfo = null; - - if (((SqlConnectionString)connectionOptions).UserInstance) - { - providerInfo = new SqlConnectionPoolProviderInfo(); - } - - return providerInfo; - } - - protected override DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions(DbConnectionOptions connectionOptions) + private static DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions(SqlConnectionString connectionOptions) { SqlConnectionString opt = (SqlConnectionString)connectionOptions; @@ -224,123 +744,222 @@ protected override DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions return poolingOptions; } - internal override DbConnectionPoolGroupProviderInfo CreateConnectionPoolGroupProviderInfo( - DbConnectionOptions connectionOptions) + private static DbMetaDataFactory CreateMetaDataFactory( + DbConnectionInternal internalConnection, + out bool cacheMetaDataFactory) { - return new SqlConnectionPoolGroupProviderInfo((SqlConnectionString)connectionOptions); - } + Debug.Assert(internalConnection is not null, "internalConnection may not be null."); - internal static SqlConnectionString FindSqlConnectionOptions(SqlConnectionPoolKey key) - { - SqlConnectionString connectionOptions = (SqlConnectionString)SingletonInstance.FindConnectionOptions(key); - if (connectionOptions == null) - { - connectionOptions = new SqlConnectionString(key.ConnectionString); - } - if (connectionOptions.IsEmpty) - { - throw ADP.NoConnectionString(); - } - return connectionOptions; + Stream xmlStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Microsoft.Data.SqlClient.SqlMetaData.xml"); + Debug.Assert(xmlStream is not null, $"{nameof(xmlStream)} may not be null."); + + cacheMetaDataFactory = true; + return new SqlMetaDataFactory(xmlStream, + internalConnection.ServerVersion, + internalConnection.ServerVersion); } - - // @TODO: All these methods seem redundant ... shouldn't we always have a SqlConnection? - override internal DbConnectionPoolGroup GetConnectionPoolGroup(DbConnection connection) + + private Task CreateReplaceConnectionContinuation( + Task task, + DbConnection owningConnection, + TaskCompletionSource retry, + DbConnectionOptions userOptions, + DbConnectionInternal oldConnection, + DbConnectionPoolGroup poolGroup, + CancellationTokenSource cancellationTokenSource) { - SqlConnection c = (connection as SqlConnection); - if (c != null) - { - return c.PoolGroup; - } - return null; + return task.ContinueWith( + _ => + { + System.Transactions.Transaction originalTransaction = ADP.GetCurrentTransaction(); + try + { + ADP.SetCurrentTransaction(retry.Task.AsyncState as System.Transactions.Transaction); + + DbConnectionInternal newConnection = CreateNonPooledConnection(owningConnection, poolGroup, userOptions); + + if (oldConnection?.State == ConnectionState.Open) + { + oldConnection.PrepareForReplaceConnection(); + oldConnection.Dispose(); + } + + return newConnection; + } + finally + { + ADP.SetCurrentTransaction(originalTransaction); + } + }, + cancellationTokenSource.Token, + TaskContinuationOptions.LongRunning, + TaskScheduler.Default + ); } - override internal DbConnectionInternal GetInnerConnection(DbConnection connection) + private IDbConnectionPool GetConnectionPool( + DbConnection owningObject, + DbConnectionPoolGroup connectionPoolGroup) { - SqlConnection c = (connection as SqlConnection); - if (c != null) + // If poolgroup is disabled, it will be replaced with a new entry + + Debug.Assert(owningObject != null, "null owningObject?"); + Debug.Assert(connectionPoolGroup != null, "null connectionPoolGroup?"); + + // It is possible that while the outer connection object has been sitting around in a + // closed and unused state in some long-running app, the pruner may have come along and + // remove this the pool entry from the master list. If we were to use a pool entry in + // this state, we would create "unmanaged" pools, which would be bad. To avoid this + // problem, we automagically re-create the pool entry whenever it's disabled. + + // however, don't rebuild connectionOptions if no pooling is involved - let new connections do that work + if (connectionPoolGroup.IsDisabled && connectionPoolGroup.PoolGroupOptions != null) { - return c.InnerConnection; + SqlClientEventSource.Log.TryTraceEvent(" {0}, DisabledPoolGroup={1}", ObjectId, connectionPoolGroup.ObjectID); + + // reusing existing pool option in case user originally used SetConnectionPoolOptions + DbConnectionPoolGroupOptions poolOptions = connectionPoolGroup.PoolGroupOptions; + + // get the string to hash on again + DbConnectionOptions connectionOptions = connectionPoolGroup.ConnectionOptions; + Debug.Assert(connectionOptions != null, "prevent expansion of connectionString"); + + connectionPoolGroup = GetConnectionPoolGroup(connectionPoolGroup.PoolKey, poolOptions, ref connectionOptions); + Debug.Assert(connectionPoolGroup != null, "null connectionPoolGroup?"); + SetConnectionPoolGroup(owningObject, connectionPoolGroup); } - return null; + + IDbConnectionPool connectionPool = connectionPoolGroup.GetConnectionPool(this); + return connectionPool; } - override protected int GetObjectId(DbConnection connection) + private void PruneConnectionPoolGroups(object state) { - SqlConnection c = (connection as SqlConnection); - if (c != null) + // When debugging this method, expect multiple threads at the same time + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}", ObjectId); + + // First, walk the pool release list and attempt to clear each pool, when the pool is + // finally empty, we dispose of it. If the pool isn't empty, it's because there are + // active connections or distributed transactions that need it. + lock (_poolsToRelease) { - return c.ObjectID; + if (_poolsToRelease.Count != 0) + { + IDbConnectionPool[] poolsToRelease = _poolsToRelease.ToArray(); + foreach (IDbConnectionPool pool in poolsToRelease) + { + if (pool is not null) + { + pool.Clear(); + + if (pool.Count == 0) + { + _poolsToRelease.Remove(pool); + + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, ReleasePool={1}", ObjectId, pool.Id); + SqlClientEventSource.Metrics.ExitInactiveConnectionPool(); + } + } + } + } } - return 0; - } - override internal void PermissionDemand(DbConnection outerConnection) - { - SqlConnection c = (outerConnection as SqlConnection); - if (c != null) + // Next, walk the pool entry release list and dispose of each pool entry when it is + // finally empty. If the pool entry isn't empty, it's because there are active pools + // that need it. + lock (_poolGroupsToRelease) { - c.PermissionDemand(); + if (_poolGroupsToRelease.Count != 0) + { + DbConnectionPoolGroup[] poolGroupsToRelease = _poolGroupsToRelease.ToArray(); + foreach (DbConnectionPoolGroup poolGroup in poolGroupsToRelease) + { + if (poolGroup != null) + { + int poolsLeft = poolGroup.Clear(); // may add entries to _poolsToRelease + + if (poolsLeft == 0) + { + _poolGroupsToRelease.Remove(poolGroup); + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, ReleasePoolGroup={1}", ObjectId, poolGroup.ObjectID); + + SqlClientEventSource.Metrics.ExitInactiveConnectionPoolGroup(); + } + } + } + } } - } - override internal void SetConnectionPoolGroup(DbConnection outerConnection, DbConnectionPoolGroup poolGroup) - { - SqlConnection c = (outerConnection as SqlConnection); - if (c != null) + // Finally, we walk through the collection of connection pool entries and prune each + // one. This will cause any empty pools to be put into the release list. + lock (this) { - c.PoolGroup = poolGroup; + Dictionary connectionPoolGroups = _connectionPoolGroups; + Dictionary newConnectionPoolGroups = new Dictionary(connectionPoolGroups.Count); + + foreach (KeyValuePair entry in connectionPoolGroups) + { + if (entry.Value != null) + { + Debug.Assert(!entry.Value.IsDisabled, "Disabled pool entry discovered"); + + // entries start active and go idle during prune if all pools are gone + // move idle entries from last prune pass to a queue for pending release + // otherwise process entry which may move it from active to idle + if (entry.Value.Prune()) + { + // may add entries to _poolsToRelease + QueuePoolGroupForRelease(entry.Value); + } + else + { + newConnectionPoolGroups.Add(entry.Key, entry.Value); + } + } + } + _connectionPoolGroups = newConnectionPoolGroups; } } - - override internal void SetInnerConnectionEvent(DbConnection owningObject, DbConnectionInternal to) + + private void TryGetConnectionCompletedContinuation(Task task, object state) { - SqlConnection c = (owningObject as SqlConnection); - if (c != null) + // Decompose the state into the parameters we want + (CancellationTokenSource cts, TaskCompletionSource tcs) = + (Tuple>)state; + + cts.Dispose(); + + if (task.IsCanceled) { - c.SetInnerConnectionEvent(to); + tcs.TrySetException(ADP.ExceptionWithStackTrace(ADP.NonPooledOpenTimeout())); } - } - - override internal bool SetInnerConnectionFrom(DbConnection owningObject, DbConnectionInternal to, DbConnectionInternal from) - { - SqlConnection c = (owningObject as SqlConnection); - if (c != null) + else if (task.IsFaulted) { - return c.SetInnerConnectionFrom(to, from); + tcs.TrySetException(task.Exception.InnerException); } - return false; - } - - override internal void SetInnerConnectionTo(DbConnection owningObject, DbConnectionInternal to) - { - SqlConnection c = (owningObject as SqlConnection); - if (c != null) + else { - c.SetInnerConnectionTo(to); + if (!tcs.TrySetResult(task.Result)) + { + // The outer TaskCompletionSource was already completed + // Which means that we don't know if someone has messed with the outer connection in the middle of creation + // So the best thing to do now is to destroy the newly created connection + task.Result.DoomThisConnection(); + task.Result.Dispose(); + } + else + { + SqlClientEventSource.Metrics.EnterNonPooledConnection(); + } } } - - protected override DbMetaDataFactory CreateMetaDataFactory(DbConnectionInternal internalConnection, out bool cacheMetaDataFactory) - { - Debug.Assert(internalConnection != null, "internalConnection may not be null."); - - Stream xmlStream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("Microsoft.Data.SqlClient.SqlMetaData.xml"); - cacheMetaDataFactory = true; - - Debug.Assert(xmlStream != null, nameof(xmlStream) + " may not be null."); - - return new SqlMetaDataFactory(xmlStream, - internalConnection.ServerVersion, - internalConnection.ServerVersion); - } - + #if NET private void Unload(object sender, EventArgs e) { try { - Unload(); + _pruningTimer.Dispose(); } finally { @@ -359,6 +978,8 @@ private void SubscribeToAssemblyLoadContextUnload() SqlConnectionFactoryAssemblyLoadContext_Unloading; } #endif + + #endregion } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUdtInfo.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUdtInfo.cs index 4ac81a9844..1ca65d7ca2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUdtInfo.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUdtInfo.cs @@ -12,20 +12,12 @@ namespace Microsoft.Data.SqlClient internal class SqlUdtInfo { internal readonly Format SerializationFormat; - internal readonly bool IsByteOrdered; - internal readonly bool IsFixedLength; internal readonly int MaxByteSize; - internal readonly string Name; - internal readonly string ValidationMethodName; private SqlUdtInfo(SqlUserDefinedTypeAttribute attr) { SerializationFormat = attr.Format; - IsByteOrdered = attr.IsByteOrdered; - IsFixedLength = attr.IsFixedLength; MaxByteSize = attr.MaxByteSize; - Name = attr.Name; - ValidationMethodName = attr.ValidationMethodName; } internal static SqlUdtInfo GetFromType(Type target) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs index 06d679e22a..479bdd0ae7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Security; +using System.Security.Authentication; using System.Security.Cryptography; using System.Text; using System.Threading; @@ -533,12 +534,16 @@ internal long TimeoutTime internal abstract uint SniGetConnectionId(ref Guid clientConnectionId); + internal abstract uint WaitForSSLHandShakeToComplete(out SslProtocols protocolVersion); + internal abstract uint DisableSsl(); internal abstract SspiContextProvider CreateSspiContextProvider(); internal abstract uint EnableMars(ref uint info); + internal abstract uint PostReadAsyncForMars(TdsParserStateObject physicalStateObject); + internal abstract uint SetConnectionBufferSize(ref uint unsignedPacketSize); internal abstract void DisposePacketCache(); @@ -1529,11 +1534,10 @@ public TdsOperationStatus TryReadByteArrayWithContinue(int length, out byte[] by (bool canContinue, bool isStarting, bool isContinuing) = GetSnapshotStatuses(); if (canContinue) { - if (isContinuing || isStarting) - { - temp = TryTakeSnapshotStorage() as byte[]; - Debug.Assert(bytes == null || bytes.Length == length, "stored buffer length must be null or must have been created with the correct length"); - } + temp = TryTakeSnapshotStorage() as byte[]; + Debug.Assert(temp != null || !isContinuing, "if continuing stored buffer must be present to contain previous data to continue from"); + Debug.Assert(bytes == null || bytes.Length == length, "stored buffer length must be null or must have been created with the correct length"); + if (temp != null) { offset = GetSnapshotTotalSize(); @@ -1554,7 +1558,7 @@ public TdsOperationStatus TryReadByteArrayWithContinue(int length, out byte[] by } else if (result == TdsOperationStatus.NeedMoreData) { - if (isStarting || isContinuing) + if (canContinue) { SetSnapshotStorage(temp); } @@ -1983,11 +1987,10 @@ internal TdsOperationStatus TryReadStringWithEncoding(int length, System.Text.En int startOffset = 0; if (canContinue) { - if (isContinuing || isStarting) - { - buf = TryTakeSnapshotStorage() as byte[]; - Debug.Assert(buf == null || buf.Length == length, "stored buffer length must be null or must have been created with the correct length"); - } + buf = TryTakeSnapshotStorage() as byte[]; + Debug.Assert(buf != null || !isContinuing, "if continuing stored buffer must be present to contain previous data to continue from"); + Debug.Assert(buf == null || buf.Length == length, "stored buffer length must be null or must have been created with the correct length"); + if (buf != null) { startOffset = GetSnapshotTotalSize(); @@ -2005,7 +2008,7 @@ internal TdsOperationStatus TryReadStringWithEncoding(int length, System.Text.En { if (result == TdsOperationStatus.NeedMoreData) { - if (isStarting || isContinuing) + if (canContinue) { SetSnapshotStorage(buf); } @@ -3224,7 +3227,7 @@ internal void ReadSni(TaskCompletionSource completion) ReadAsyncCallback(IntPtr.Zero, readPacket, 0); // Only release packet for Managed SNI as for Native SNI packet is released in finally block. - if (TdsParserStateObjectFactory.UseManagedSNI && readFromNetwork && !IsPacketEmpty(readPacket)) + if (LocalAppContextSwitches.UseManagedNetworking && readFromNetwork && !IsPacketEmpty(readPacket)) { ReleasePacket(readPacket); } @@ -3260,7 +3263,7 @@ internal void ReadSni(TaskCompletionSource completion) } finally { - if (!TdsParserStateObjectFactory.UseManagedSNI) + if (!LocalAppContextSwitches.UseManagedNetworking) { if (readFromNetwork && !IsPacketEmpty(readPacket)) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Unix.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Unix.cs index f7cb55d451..1362e47b45 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Unix.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Unix.cs @@ -8,9 +8,6 @@ namespace Microsoft.Data.SqlClient { internal sealed class TdsParserStateObjectFactory { - - public static bool UseManagedSNI => true; - public static readonly TdsParserStateObjectFactory Singleton = new TdsParserStateObjectFactory(); public EncryptionOptions EncryptionOptions => ManagedSni.SniLoadHandle.SingletonInstance.Options; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Windows.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Windows.cs index 8b8fe9186b..53cbc4decc 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Windows.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Windows.cs @@ -13,28 +13,16 @@ internal sealed class TdsParserStateObjectFactory { public static readonly TdsParserStateObjectFactory Singleton = new TdsParserStateObjectFactory(); - private const string UseManagedNetworkingOnWindows = "Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows"; - -#if NET - private static bool s_shouldUseManagedSNI; - - // If the appcontext switch is set then Use Managed SNI based on the value. Otherwise Native SNI.dll will be used by default. - public static bool UseManagedSNI => - AppContext.TryGetSwitch(UseManagedNetworkingOnWindows, out s_shouldUseManagedSNI) ? s_shouldUseManagedSNI : false; -#else - public const bool UseManagedSNI = false; -#endif - public EncryptionOptions EncryptionOptions => #if NET - UseManagedSNI ? ManagedSni.SniLoadHandle.SingletonInstance.Options : SNILoadHandle.SingletonInstance.Options; + LocalAppContextSwitches.UseManagedNetworking ? ManagedSni.SniLoadHandle.SingletonInstance.Options : SNILoadHandle.SingletonInstance.Options; #else SNILoadHandle.SingletonInstance.Options; #endif public uint SNIStatus => #if NET - UseManagedSNI ? ManagedSni.SniLoadHandle.SingletonInstance.Status : SNILoadHandle.SingletonInstance.Status; + LocalAppContextSwitches.UseManagedNetworking ? ManagedSni.SniLoadHandle.SingletonInstance.Status : SNILoadHandle.SingletonInstance.Status; #else SNILoadHandle.SingletonInstance.Status; #endif @@ -44,7 +32,7 @@ internal sealed class TdsParserStateObjectFactory /// public bool ClientOSEncryptionSupport => #if NET - UseManagedSNI ? ManagedSni.SniLoadHandle.SingletonInstance.ClientOSEncryptionSupport : SNILoadHandle.SingletonInstance.ClientOSEncryptionSupport; + LocalAppContextSwitches.UseManagedNetworking ? ManagedSni.SniLoadHandle.SingletonInstance.ClientOSEncryptionSupport : SNILoadHandle.SingletonInstance.ClientOSEncryptionSupport; #else SNILoadHandle.SingletonInstance.ClientOSEncryptionSupport; #endif @@ -52,16 +40,14 @@ internal sealed class TdsParserStateObjectFactory public TdsParserStateObject CreateTdsParserStateObject(TdsParser parser) { #if NET - if (UseManagedSNI) + if (LocalAppContextSwitches.UseManagedNetworking) { - SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObjectFactory.CreateTdsParserStateObject | Info | Found AppContext switch '{0}' enabled, managed networking implementation will be used." - , UseManagedNetworkingOnWindows); + SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObjectFactory.CreateTdsParserStateObject | Info | Using managed networking implementation."); return new TdsParserStateObjectManaged(parser); } else { - SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObjectFactory.CreateTdsParserStateObject | Info | AppContext switch '{0}' not enabled, native networking implementation will be used." - , UseManagedNetworkingOnWindows); + SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObjectFactory.CreateTdsParserStateObject | Info | Using native networking implementation."); return new TdsParserStateObjectNative(parser); } #else @@ -72,7 +58,7 @@ public TdsParserStateObject CreateTdsParserStateObject(TdsParser parser) internal TdsParserStateObject CreateSessionObject(TdsParser tdsParser, TdsParserStateObject _pMarsPhysicalConObj, bool v) { #if NET - if (TdsParserStateObjectFactory.UseManagedSNI) + if (LocalAppContextSwitches.UseManagedNetworking) { return new TdsParserStateObjectManaged(tdsParser, _pMarsPhysicalConObj, true); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs new file mode 100644 index 0000000000..8ab6ac79f3 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfo.cs @@ -0,0 +1,357 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Data.Common; + +#nullable enable + +namespace Microsoft.Data.SqlClient.UserAgent; + +/// +/// Gathers driver + environment info, enforces size constraints, +/// and serializes into a UTF-8 JSON payload. +/// The spec document can be found at: https://microsoft.sharepoint-df.com/:w:/t/sqldevx/ERIWTt0zlCxLroNHyaPlKYwBI_LNSff6iy_wXZ8xX6nctQ?e=0hTJX7 +/// +internal static class UserAgentInfo +{ + /// + /// Maximum number of characters allowed for the system architecture. + /// + private const int ArchMaxChars = 16; + + /// + /// Maximum number of characters allowed for the driver name. + /// + internal const int DriverNameMaxChars = 16; + + /// + /// Maximum number of bytes allowed for the user agent json payload. + /// Payloads larger than this may be rejected by the server. + /// + internal const int JsonPayloadMaxBytes = 2047; + + /// + /// Maximum number of characters allowed for the operating system details. + /// + private const int OsDetailsMaxChars = 128; + + /// + /// Maximum number of characters allowed for the operating system type. + /// + internal const int OsTypeMaxChars = 16; + + /// + /// Maximum number of characters allowed for the driver runtime. + /// + private const int RuntimeMaxChars = 128; + + /// + /// Maximum number of characters allowed for the driver version. + /// + internal const int VersionMaxChars = 16; + + + internal const string DefaultJsonValue = "Unknown"; + internal const string DriverName = "MS-MDS"; + + private static readonly UserAgentInfoDto s_dto; + private static readonly byte[] s_userAgentCachedPayload; + + /// + /// Provides the UTF-8 encoded UserAgent JSON payload as a cached read-only memory buffer. + /// The value is computed once during process initialization and reused across all calls. + /// No re-encoding or recalculation occurs at access time, and the same memory is safely shared across all threads. + /// + public static ReadOnlyMemory UserAgentCachedJsonPayload => s_userAgentCachedPayload; + + private enum OsType + { + Windows, + Linux, + macOS, + FreeBSD, + Android, + Unknown + } + + static UserAgentInfo() + { + s_dto = BuildDto(); + s_userAgentCachedPayload = AdjustJsonPayloadSize(s_dto); + } + + /// + /// This function returns the appropriately sized json payload + /// We check the size of encoded json payload, if it is within limits we return the dto to be cached + /// other wise we drop some fields to reduce the size of the payload. + /// + /// Data Transfer Object for the json payload + /// Serialized UTF-8 encoded json payload version of DTO within size limit + internal static byte[] AdjustJsonPayloadSize(UserAgentInfoDto dto) + { + // Note: We serialize 6 fields in total: + // - 4 fields with up to 16 characters each + // - 2 fields with up to 128 characters each + // + // For estimating **on-the-wire UTF-8 size** of the serialized JSON: + // 1) For the 4 fields of 16 characters: + // - In worst case (all characters require escaping in JSON, e.g., quotes, backslashes, control chars), + // each character may expand to 2–6 bytes in the JSON string (e.g., \" = 2 bytes, \uXXXX = 6 bytes) + // - Assuming full escape with \uXXXX form (6 bytes per char): 4 × 16 × 6 = 384 bytes (extreme worst case) + // - For unescaped high-plane Unicode (e.g., emojis), UTF-8 uses up to 4 bytes per character: + // 4 × 16 × 4 = 256 bytes (UTF-8 max) + // + // Conservative max estimate for these fields = **384 bytes** + // + // 2) For the 2 fields of 128 characters: + // - Worst-case with \uXXXX escape sequences: 2 × 128 × 6 = 1,536 bytes + // - Worst-case with high Unicode: 2 × 128 × 4 = 1,024 bytes + // + // Conservative max estimate for these fields = **1,536 bytes** + // + // Combined worst-case for value content = 384 + 1536 = **1,920 bytes** + // + // 3) The rest of the serialized JSON payload (object braces, field names, quotes, colons, commas) is fixed. + // Based on measurements, it typically adds to about **81 bytes**. + // + // Final worst-case estimate for total payload on the wire (UTF-8 encoded): + // 1,920 + 81 = **2,001 bytes** + // + // This is still below our spec limit of 2,047 bytes. + // + // TDS Prelogin7 packets support up to 65,535 bytes (including headers), but many server versions impose + // stricter limits for prelogin payloads. + // + // As a safety measure: + // - If the serialized payload exceeds **10 KB**, we fallback to transmitting only essential fields: + // 'driver', 'version', and 'os.type' + // - If the payload exceeds 2,047 bytes but remains within sensible limits, we still send it, but note that + // some servers may silently drop or reject such packets — behavior we may use for future probing or diagnostics. + // - If payload exceeds 10KB even after dropping fields , we send an empty payload. + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = null, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false + }; + byte[] payload = JsonSerializer.SerializeToUtf8Bytes(dto, options); + + // We try to send the payload if it is within the limits. + // Otherwise we drop some fields to reduce the size of the payload and try one last time + // Note: server will reject payloads larger than 2047 bytes + // Try if the payload fits the max allowed bytes + if (payload.Length <= JsonPayloadMaxBytes) + { + return payload; + } + + dto.Runtime = null; // drop Runtime + dto.Arch = null; // drop Arch + if (dto.OS != null) + { + dto.OS.Details = null; // drop OS.Details + } + + payload = JsonSerializer.SerializeToUtf8Bytes(dto, options); + if (payload.Length <= JsonPayloadMaxBytes) + { + return payload; + } + + dto.OS = null; // drop OS entirely + // Last attempt to send minimal payload driver + version only + // As per the comment in AdjustJsonPayloadSize, we know driver + version cannot be larger than the max + return JsonSerializer.SerializeToUtf8Bytes(dto, options); + } + + internal static UserAgentInfoDto BuildDto() + { + // Instantiate DTO before serializing + return new UserAgentInfoDto + { + Driver = TruncateOrDefault(DriverName, DriverNameMaxChars), + Version = TruncateOrDefault(ADP.GetAssemblyVersion().ToString(), VersionMaxChars), + OS = new UserAgentInfoDto.OsInfo + { + Type = TruncateOrDefault(DetectOsType().ToString(), OsTypeMaxChars), + Details = TruncateOrDefault(DetectOsDetails(), OsDetailsMaxChars) + }, + Arch = TruncateOrDefault(DetectArchitecture(), ArchMaxChars), + Runtime = TruncateOrDefault(DetectRuntime(), RuntimeMaxChars) + }; + + } + + /// + /// Detects and reports whatever CPU architecture the guest OS exposes + /// + private static string DetectArchitecture() + { + try + { + // Returns the architecture of the current process (e.g., "X86", "X64", "Arm", "Arm64"). + // Note: This reflects the architecture of the running process, not the physical host system. + return RuntimeInformation.ProcessArchitecture.ToString(); + } + catch + { + // In case RuntimeInformation isn’t available or something unexpected happens + return DefaultJsonValue; + } + } + + /// + /// Retrieves the operating system details based on RuntimeInformation. + /// + private static string DetectOsDetails() + { + var osDetails = RuntimeInformation.OSDescription; + if (!string.IsNullOrWhiteSpace(osDetails)) + { + return osDetails; + } + + return DefaultJsonValue; + } + + /// + /// Detects the OS platform and returns the matching OsType enum. + /// + private static OsType DetectOsType() + { + try + { + // first we try with built-in checks (Android and FreeBSD also report Linux so they are checked first) +#if NET6_0_OR_GREATER + if (OperatingSystem.IsAndroid()) + { + return OsType.Android; + } + if (OperatingSystem.IsFreeBSD()) + { + return OsType.FreeBSD; + } + if (OperatingSystem.IsWindows()) + { + return OsType.Windows; + } + if (OperatingSystem.IsLinux()) + { + return OsType.Linux; + } + if (OperatingSystem.IsMacOS()) + { + return OsType.macOS; + } +#endif + +#if NET462 + if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("FREEBSD"))) + { + return OsType.FreeBSD; + } +#else + if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) + { + return OsType.FreeBSD; + } +#endif + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return OsType.Windows; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return OsType.Linux; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return OsType.macOS; + } + + // Final fallback is inspecting OSDecription + // Note: This is not based on any formal specification, + // that is why we use it as a last resort. + // The string values are based on trial and error. + var desc = RuntimeInformation.OSDescription?.ToLowerInvariant() ?? ""; + if (desc.Contains("android")) + { + return OsType.Android; + } + if (desc.Contains("freebsd")) + { + return OsType.FreeBSD; + } + if (desc.Contains("windows")) + { + return OsType.Windows; + } + if (desc.Contains("linux")) + { + return OsType.Linux; + } + if (desc.Contains("darwin") || desc.Contains("mac os")) + { + return OsType.macOS; + } + } + catch + { + // swallow any unexpected errors + return OsType.Unknown; + } + return OsType.Unknown; + } + + /// + /// Returns the framework description as a string. + /// + private static string DetectRuntime() + { + // FrameworkDescription is never null, but IsNullOrWhiteSpace covers it anyway + var desc = RuntimeInformation.FrameworkDescription; + if (string.IsNullOrWhiteSpace(desc)) + { + return DefaultJsonValue; + } + + // at this point, desc is non‑null, non‑empty (after trimming) + return desc.Trim(); + } + + /// + /// Truncates a string to the specified maximum length or returns a default value if input is null or empty. + /// + /// The string value to truncate + /// Maximum number of characters allowed + /// Truncated string or default value if input is invalid + internal static string TruncateOrDefault(string jsonStringVal, int maxChars) + { + try + { + if (string.IsNullOrEmpty(jsonStringVal)) + { + return DefaultJsonValue; + } + + if (jsonStringVal.Length <= maxChars) + { + return jsonStringVal; + } + + return jsonStringVal.Substring(0, maxChars); + } + catch + { + // Silently consume all exceptions + return DefaultJsonValue; + } + } + +} + diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfoDto.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfoDto.cs new file mode 100644 index 0000000000..2c61d1c4bb --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/UserAgent/UserAgentInfoDto.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Data.Common; + +#nullable enable + +namespace Microsoft.Data.SqlClient.UserAgent; +internal class UserAgentInfoDto +{ + // Note: JSON key names are defined as constants to avoid reflection during serialization. + // This allows us to calculate their UTF-8 encoded byte sizes efficiently without instantiating + // the DTO or relying on JsonProperty attribute resolution at runtime. The small overhead of + // maintaining constants is justified by the performance and allocation savings. + + // Note: These values reflect the order of the JSON fields defined in the spec. + // The order is maintained to match the JSON payload structure. + public const string DriverJsonKey = "driver"; + public const string VersionJsonKey = "version"; + public const string OsJsonKey = "os"; + public const string ArchJsonKey = "arch"; + public const string RuntimeJsonKey = "runtime"; + + [JsonPropertyName(DriverJsonKey)] + public string Driver { get; set; } = string.Empty; + + [JsonPropertyName(VersionJsonKey)] + public string Version { get; set; } = string.Empty; + + [JsonPropertyName(OsJsonKey)] + public OsInfo? OS { get; set; } + + [JsonPropertyName(ArchJsonKey)] + public string? Arch { get; set; } + + [JsonPropertyName(RuntimeJsonKey)] + public string? Runtime { get; set; } + + public class OsInfo + { + public const string TypeJsonKey = "type"; + public const string DetailsJsonKey = "details"; + + [JsonPropertyName(TypeJsonKey)] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName(DetailsJsonKey)] + public string? Details { get; set; } + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.Unix.xml b/src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.Unix.xml new file mode 100644 index 0000000000..4e105d25f0 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.Unix.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.Windows.xml b/src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.Windows.xml new file mode 100644 index 0000000000..e8577c7ae0 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.Windows.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.cs.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.cs.resx index 889e78d915..7fc79f1934 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.cs.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.cs.resx @@ -231,18 +231,12 @@ Nelze použít pověření (Credential) s klíčovým slovem Integrated Security, které může být součástí připojovacího řetězce. - - Nelze použít pověření (Credential) s klíčovým slovem Context Connection. - Pokud je v připojovacím řetězci zadané UserID, UID, Password nebo PWD, nejde vlastnost AccessToken nastavit. Pokud je klíčové slovo Integrated Security připojovacího řetězce nastavené na hodnotu true nebo SSPI, nejde vlastnost AccessToken nastavit. - - Vlastnost AccessToken nejde nastavit s klíčovým slovem Context Connection. - Pokud už je nastavená vlastnost Credential, nejde vlastnost AccessToken nastavit. @@ -2073,9 +2067,6 @@ Neplatný posun cílové vyrovnávací paměti (velikost: {0}, posun: {1}) - - Pro řádek nebo sloupec neexistují data. - Číselná hodnota je příliš velká. Nemůže ji obsahovat desítková 96bitová hodnota. @@ -2529,6 +2520,9 @@ Tento příkaz vyžaduje asynchronní připojení. Nastavte v připojovacím řetězci hodnotu Asynchronous Processing=true. + + Časový limit pokusu o připojení vypršel. + Časový limit vypršel. V důsledku toho bylo připojení přerušeno. @@ -2778,30 +2772,6 @@ Délka dat {0} je menší než 0. - - Neplatný pokus o volání metody {0}, pokud je zavřena třída SqlResultSet. - - - Operaci nelze dokončit, protože třída SqlResultSet je zavřena. - - - {0} nelze volat, pokud je záznam určen jen pro čtení. - - - Operaci nelze dokončit, protože záznam je jen pro čtení. - - - Neplatný pokus o volání metody {0}, pokud je aktuální řádek odstraněn. - - - Operaci nelze dokončit, protože aktuální řádek je odstraněn. - - - Operaci nelze dokončit, protože příkaz, který vytvořil objekt SqlResultSet, byl odpojen od původního připojení. Objekt SqlResultSet je uzavřen. - - - SqlResultSet nebylo možné vytvořit pro daný dotaz s požadovanými možnostmi. - {0} nelze volat, pokud je třída SqlDataRecord určena jen pro čtení. @@ -2871,51 +2841,21 @@ Aktuálně načtená knihovna System.Transactions.dll nepodporuje globální transakce. Upgradujte prosím na .NET Framework 4.6.2 nebo novější. - - Dávkové aktualizace nejsou u připojení kontextu podporovány. - - - Jediné další klíčové slovo řetězce připojení, které by bylo možné používat při požadavku na připojení kontextu, je klíčové slovo Type System Version. - - - Připojení kontextu nepodporuje nastavení Type System Version=SQL Server 2000. - Připojení ke kontextu pomocí Microsoft.Data.SqlClient není podporováno. - - Připojení kontextu se už používá. - - - Požadovaná operace vyžaduje kontext SqlClr, který je k dispozici pouze při spuštění v procesu systému Sql Server. - - - Požadovaná operace vyžaduje vlákno prováděného procesu systému Sql Server. Aktuální vlákno bylo spuštěno uživatelským kódem či jiným kódem, který není kódem modulu systému Sql Server. - Vnořené objekty TransactionScopes nejsou podporovány. - - Požadovaná operace není dostupná u připojení kontextu. - - - Upozornění nejsou k dispozici u připojení kontextu. - Neočekávaná událost serveru: {0} - - Uživatelské instance nejsou povoleny při spuštění v procesu systému Sql Server. - Délka argumentu {0} se musí shodovat s délkou argumentu {1}. Hodnota SqlDbType {0} je pro {1} neplatná. Podporuje se pouze {2}. - - U předcházejícího řádku odeslaného do SqlPipe došlo k chybě. Před odesláním dalších dat se musí volat metoda SendResultsEnd. - Příliš mnoho hodnot @@ -3663,9 +3603,6 @@ Tento objekt SqlCommand je již spojen s jiným objektem SqlDependency. - - Objekt SqlDependency nelze vytvořit při spuštění z procesu systému SQL Server. - SQL Server Service Broker pro aktuální databázi není povolen. Nejsou proto podporována upozornění na výsledky dotazů. Chcete-li používat upozornění, povolte zprostředkovatele Service Broker pro tuto databázi. @@ -3726,21 +3663,6 @@ Ve výčtu SqlDataRecord nejsou žádné záznamy. Pokud chcete odeslat parametr vracející tabulku bez řádků, použijte pro hodnotu odkaz s hodnotou null. - - Třída SqlPipe nepodporuje provádění příkazu s připojením, které není připojením kontextu. - - - Délka zprávy {0} překračuje maximální podporovanou délku 4000. - - - Kanál nelze používat, pokud zpracovává jinou operaci. - - - Sada výsledků dotazu je momentálně odesílána do kanálu. Před voláním metody {0} ukončete aktuální sadu výsledků dotazu. - - - Sada výsledků dotazu nebyla inicializována. Před voláním metody {0} volejte metodu SendResultSetStart. - Zprostředkovatel HTTP: @@ -4743,4 +4665,19 @@ Seznam SqlBatchCommand nebyl inicializován. + + Pokus o načtení vektorových dat ze sloupce {0} byl neplatný. Vektory jsou podporovány pouze pro sloupce typu vektor. + + + Nepodporovaný typ vektoru „{0}“. + + + Hodnota null není podporována pro výstupní parametr „{0}“ vektoru SqlDbtype. + + + Byla přijata neplatná hlavička vektoru. + + + {0}: Neplatný řetězec JSON pro vektor. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.de.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.de.resx index 17adb6d62a..07411316df 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.de.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.de.resx @@ -231,18 +231,12 @@ Credential kann nicht mit dem Schlüsselwort für Verbindungszeichenfolgen "Integrated Security" verwendet werden. - - Credential kann nicht mit dem Schlüsselwort "Context Connection" verwendet werden. - AccessToken-Eigenschaft kann nicht festgelegt werden, wenn 'UserID', 'UID', 'Password' oder 'PWD' in der Verbindungszeichenfolge angegeben wurde. AccessToken-Eigenschaft kann nicht festgelegt werden, wenn das Schlüsselwort für Verbindungszeichenfolgen 'Integrated Security' auf 'true' oder 'SSPI' gesetzt wurde. - - AccessToken-Eigenschaft kann nicht mit dem Schlüsselwort 'Context Connection' festgelegt werden. - AccessToken-Eigenschaft kann nicht festgelegt werden, wenn bereits die Credential-Eigenschaft festgelegt wurde. @@ -2073,9 +2067,6 @@ Ungültiger Zielpufferoffset (Größe von {0}): {1} - - Keine Daten für die Zeile/Spalte. - Der numerische Wert ist zu groß, um in eine Dezimalzahl mit 96 Bits zu passen. @@ -2529,6 +2520,9 @@ Dieser Befehl erfordert eine asynchrone Verbindung. Legen Sie in der Verbindungszeichenfolge "Asynchronous Processing=true" fest. + + Timeout beim Versuch, eine Verbindung herzustellen. + Timeout abgelaufen. Die Verbindung wurde aus diesem Grund unterbrochen. @@ -2778,30 +2772,6 @@ Datenlänge '{0}' ist kleiner als 0. - - Ungültiger Versuch des Aufrufs von Methode {0}, wenn SqlResultSet geschlossen ist. - - - Operation kann nicht abgeschlossen werden, da SqlResultSet geschlossen ist. - - - '{0}' kann nicht aufgerufen werden, wenn der Datensatz schreibgeschützt ist. - - - Operation kann nicht abgeschlossen werden, da der Datensatz schreibgeschützt ist. - - - Ungültiger Versuch des Aufrufs von Methode {0}, wenn die aktuelle Zeile gelöscht wurde - - - Operation kann nicht abgeschlossen werden, da die aktuelle Zeile gelöscht wurde - - - Operation kann nicht abgeschlossen werden, da der Befehl, der SqlResultSet erstellt hat, von der ursprünglichen Verbindung getrennt wurde. SqlResultSet ist geschlossen. - - - SqlResultSet konnte für die angegebene Abfrage nicht mit den gewünschten Optionen erstellt werden. - '{0}' kann nicht aufgerufen werden, wenn SqlDataRecord schreibgeschützt ist. @@ -2871,51 +2841,21 @@ Die aktuell geladene System.Transactions.dll unterstützt keine globalen Transaktionen. Bitte aktualisieren Sie auf .NET Framework 4.6.2 oder höher. - - Batchupdates werden bei der Kontextverbindung nicht unterstützt. - - - Das einzige zusätzliche Schlüsselwort für die Verbindungszeichenfolge, das beim Anfordern der Kontextverbindung verwendet werden darf, lautet 'Type System Version'. - - - Die Kontextverbindung unterstützt nicht Type System Version=SQL Server 2000. - Das Herstellen einer Verbindung mit der Kontextverbindung über Microsoft.Data.SqlClient wird nicht unterstützt. - - Die Kontextverbindung wird bereits verwendet. - - - Die angeforderte Operation erfordert einen SqlClr -Kontext, der nur bei Ausführung im Sql Server-Prozess verfügbar ist. - - - Für die angeforderte Operation ist ein SQL Server-Ausführungsthread erforderlich. Der aktuelle Thread wurde durch Benutzercode oder einen anderen Nicht-Sql Server-Modulcode gestartet. - Geschachtelte 'TransactionScopes' werden nicht unterstützt. - - Die angeforderte Operation ist für die Kontextverbindung nicht verfügbar. - - - Die Benachrichtigungen sind für die Kontextverbindung nicht verfügbar. - Unerwartetes Serverereignis: {0}. - - Benutzerinstanzen sind bei der Ausführung im Sql Server-Prozess nicht zulässig. - Die Länge von '{0}' muss mit der Länge von '{1}' übereinstimmen. Der 'SqlDbType' '{0}' ist für {1} unzulässig. Unterstützt wird nur {2}. - - Fehler bei einer vorherigen Zeile, die an die SqlPipe gesendet wurde. 'SendResultsEnd' muss aufgerufen werden, bevor eine andere Zeile gesendet werden kann. - Zu viele Werte. @@ -3663,9 +3603,6 @@ Dieses SqlCommand-Objekt ist bereits mit einem anderen SqlDependency-Objekt verknüpft. - - Das 'SqlDependency'-Objekt kann nicht erstellt werden, wenn es im Rahmen des SQL Server-Prozesses ausgeführt wird. - Der SQL Server Service Broker für die aktuelle Datenbank ist nicht aktiviert. Als Folge davon werden keine Abfragebenachrichtigungen unterstützt. Aktivieren Sie den Service Broker für diese Datenbank, wenn Sie Benachrichtigungen verwenden möchten. @@ -3726,21 +3663,6 @@ Es sind keine Datensätze in der 'SqlDataRecord'-Enumeration vorhanden. Verwenden Sie zum Senden eines table-valued parameters ohne Zeilen stattdessen einen null-Verweis für den Wert. - - 'SqlPipe' unterstützt nicht die Ausführung eines Befehls bei einer Nicht-Kontextverbindung. - - - Die Nachrichtenlänge {0} überschreitet die zulässige Länge von 4.000. - - - Die Pipe konnte nicht verwendet werden, da sie zurzeit eine andere Operation ausführt. - - - Momentan wird ein Resultset an die Pipe gesendet. Beenden Sie das aktuelle Resultset vor dem Aufruf von {0}. - - - Das Resultset wurde nicht initiiert. Rufen Sie 'SendResultSetStart' vor dem Aufruf von {0} auf. - HTTP-Anbieter @@ -4743,4 +4665,19 @@ Die SqlBatchCommand-Liste wurde nicht initialisiert. + + Ungültiger Versuch, Vektordaten aus Spalte „{0}“ abzurufen. Vektoren werden nur für Spalten vom Typ Vektor unterstützt. + + + Nicht unterstützter Vektortyp „{0}“. + + + Der Nullwert wird für den Ausgabeparameter „{0}“ des SqlDbtype-Vektors nicht unterstützt. + + + Ungültiger Vektorheader empfangen. + + + {0} Ungültige JSON-Zeichenfolge für Vektor. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.es.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.es.resx index c27bac437e..9b537b9e6c 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.es.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.es.resx @@ -231,18 +231,12 @@ No se pueden utilizar Credential con la palabra clave de cadena de conexión de Integrated Security. - - No se pueden utilizar Credential con la palabra clave de Context Connection. - No se puede establecer la propiedad AccessToken si en la cadena de conexión se han especificado "UserID", "UID", "Password" o "PWD". No se puede establecer la propiedad AccessToken si la palabra clave de la cadena de conexión "Integrated Security" se ha establecido en "true" o "SSPI". - - No se puede establecer la propiedad AccessToken con la palabra clave "Context Connection". - No se puede establecer la propiedad AccessToken si la propiedad Credential ya está establecida. @@ -2073,9 +2067,6 @@ Desplazamiento del búfer de destino (tamaño de {0}) no válido: {1} - - No hay ningún dato disponible para la fila o columna. - El valor numérico es demasiado grande para ajustarlo a un valor decimal de 96 bits. @@ -2529,6 +2520,9 @@ Este comando requiere una conexión asincrónica. Establezca "Asynchronous Processing=true" en la cadena de conexión. + + Se agotó el tiempo de espera del intento de conexión. + Valor de tiempo de espera caducado. Por ello, se ha interrumpido la conexión. @@ -2778,30 +2772,6 @@ La longitud de los datos '{0}' es menor que 0. - - Intento no válido de llamar al método {0} cuando SqlResultSet está cerrado. - - - No se puede completar la operación porque SqlResultSet está cerrado. - - - No se puede llamar a '{0}' cuando el registro es de solo lectura. - - - No se puede completar la operación porque el registro es de solo lectura. - - - Intento no válido de llamar al método {0} cuando se ha eliminado la fila actual - - - No se puede completar la operación porque se ha eliminado la fila actual - - - No se puede completar la operación porque el comando que creó SqlResultSet se ha disociado de la conexión original. SqlResultSet está cerrado. - - - No se puede crear SqlResultSet para la consulta proporcionada con las opciones deseadas. - No se puede llamar a '{0}' cuando SqlDataRecord es de solo lectura. @@ -2871,51 +2841,21 @@ El archivo System.Transactions.dll cargado actualmente no es compatible con las transacciones globales. Actualice a .NET Framework 4.6.2 o posterior. - - La conexión de contexto no admite la actualización de procesamiento por lotes. - - - La palabra clave Type System Version es la única palabra clave de cadena de conexión adicional que se puede emplear al solicitar la conexión de contexto. - - - La conexión de contexto no admite Type System Version=SQL Server 2000. - No se admite la conexión a la conexión de contexto mediante Microsoft.Data.SqlClient. - - La conexión de contexto ya está en uso. - - - La operación solicitada necesita un contexto SqlClr que sólo está disponible cuando se utiliza con el proceso de Sql Server. - - - La operación solicitada necesita un proceso de ejecución de Sql Server. El proceso de ejecución actual comenzó por un código de usuario u otro código de motor de servidor que no es Sql. - No se admiten las TransactionScopes anidadas. - - La operación solicitada no está disponible en la conexión de contexto. - - - Las notificaciones no están disponibles en la conexión de contexto. - Evento de servidor inesperado: {0}. - - No se permiten las instancias de usuario al funcionar dentro de proceso del Sql Server. - La longitud de '{0}' debe coincidir con la longitud de '{1}'. SqlDbType '{0}' no es válido para {1}. Sólo admite {2}. - - Error al enviar la fila anterior a SqlPipe. Se debe llamar a SendResultsEnd antes de poder enviar algo más. - Demasiados valores. @@ -3663,9 +3603,6 @@ Este objeto SqlCommand ya está asociado a otro objeto SqlDependency. - - No se puede crear el objeto SqlDependency al funcionar dentro del proceso de SQL Server. - No está habilitado SQL Server Service Broker para la base de datos actual y, en consecuencia, no se admiten las notificaciones de consulta. Habilite Service Broker en esta base de datos si desea utilizar las notificaciones. @@ -3726,21 +3663,6 @@ No hay registros en la enumeración SqlDataRecord. Para enviar un table-valued parameter sin filas, utilice en su lugar una referencia null para el valor. - - SqlPipe no admite ejecutar un comando con una conexión que no es de contexto. - - - La longitud del mensaje {0} excede la longitud máxima admitida de 4000. - - - No se puede usar la canalización mientras esté ocupado con otra operación. - - - Se está enviando un conjunto de resultados a la canalización. Finalice el conjunto de resultados actual antes de llamar {0}. - - - No se ha iniciado el conjunto de resultados. Llame a SendResultSetStart antes de llamar {0}. - Proveedor HTTP @@ -4743,4 +4665,19 @@ No se ha inicializado la lista SqlBatchCommand. + + Intento no válido de obtener datos vectoriales de la columna '{0}'. Los vectores solo son compatibles con columnas del tipo vector. + + + El tipo de vector "{0}" no es compatible. + + + El valor 'null' no es compatible con el parámetro de salida '{0}' de SqlDbType.Vector. + + + Se recibió un encabezado de vector no válido. + + + Cadena JSON {0} no válida para el vector. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.fr.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.fr.resx index 3dc712fc5a..108f0a1f0f 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.fr.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.fr.resx @@ -231,18 +231,12 @@ Impossible d'utiliser de Credential avec le mot-clé de chaîne de connexion de Integrated Security. - - Impossible d'utiliser de Credential avec le mot clé de Context Connection. - Impossible de définir la propriété AccessToken si « UserID », « UID », « Password » ou « PWD » a été spécifié dans la chaîne de connexion. Impossible de définir la propriété AccessToken si le mot clé de chaîne de connexion « Integrated Security » a la valeur « true » ou « SSPI ». - - Impossible de définir la propriété AccessToken avec le mot clé « Context Connection ». - Impossible de définir la propriété AccessToken si la propriété Credential est déjà définie. @@ -2073,9 +2067,6 @@ Mémoire tampon de destination non valide (taille de {0}) offset : {1} - - Aucune donnée n'existe pour la ligne/colonne. - La valeur numérique est trop élevée pour tenir dans un décimal à 96 bits. @@ -2529,6 +2520,9 @@ Cette commande requiert une connexion asynchrone. Définissez "Asynchronous Processing=true" dans la chaîne de connexion. + + La tentative de connexion a expiré. + Le délai d'attente est arrivé à expiration. Par conséquent, la connexion a été interrompue. @@ -2778,30 +2772,6 @@ La longueur des données « {0} » est inférieure à 0. - - Tentative d'appel de la méthode {0} non valide avec SqlResultSet fermé. - - - Opération impossible : le SqlResultSet est fermé. - - - '{0}' ne peut pas être appelé lorsque l'enregistrement est en lecture seule. - - - Opération impossible : l'enregistrement est en lecture seule. - - - Tentative d'appel de la méthode {0} non valide si la ligne actuelle est supprimée - - - Opération impossible : la ligne actuelle a été supprimée - - - Opération impossible : la commande qui a créé le SqlResultSet est dissociée de la connexion d'origine. Le SqlResultSet est fermé. - - - Impossible de créer le SqlResultSet pour la requête fournie avec les options souhaitées. - Impossible d'appeler '{0}' si le SqlDataRecord est en lecture seule. @@ -2871,51 +2841,21 @@ Le fichier System.Transactions.dll actuellement chargé ne prend pas en charge les transactions globales. Effectuez une mise à niveau vers .NET Framework 4.6.2 ou ultérieur. - - Le traitement par lots des mises à jour n'est pas pris en charge sur la connexion du contexte. - - - Le seul mot clé de chaîne de connexion supplémentaire qui peut être utilisé lors de la demande de la connexion du contexte est le mot clé Type System Version. - - - La connexion du contexte ne prend pas en charge Type System Version=SQL Server 2000. - La connexion à la connexion de contexte à l’aide de Microsoft.Data.SqlClient n’est pas prise en charge. - - La connexion du contexte est déjà en cours d'utilisation. - - - L'opération demandée nécessite un contexte SqlClr, qui n'est disponible qu'en cas d'exécution dans le processus Sql Server. - - - L'opération demandée nécessite un thread d'exécution Sql Server. Le thread en cours a été démarré par code d'utilisateur ou autre code de moteur non-Sql Server. - Les TransactionScopes imbriquées ne sont pas prises en charge. - - L'opération demandée n'est pas disponible sur la connexion du contexte. - - - Les notifications ne sont pas disponibles sur la connexion du contexte. - Événement serveur inattendu : {0}. - - Les instances d'utilisateur ne sont pas autorisées en cas d'exécution dans le processus Sql Server. - La longueur de '{0}' doit correspondre à celle de '{1}'. Le type SqlDbType '{0}' n'est pas valide pour {1}. Seul {2} est pris en charge. - - Une erreur s’est produite avec une ligne précédente envoyée au SqlPipe. SendResultsEnd doit être appelé avant que quoi que ce soit d’autre puisse être envoyé. - Trop de valeurs. @@ -3663,9 +3603,6 @@ Cet objet SqlCommand est déjà associé à un autre objet SqlDependency. - - L'objet SqlDependency ne peut pas être créé en cas d'exécution dans le processus SQL Server. - Le SQL Server Service Broker de la base de données actuelle n'est pas activé. Par conséquent, les notifications de requête ne sont pas prises en charge. Activez le Service Broker pour cette base de données si vous souhaitez utiliser des notifications. @@ -3726,21 +3663,6 @@ Il n'existe aucun enregistrement dans l'énumération SqlDataRecord. Pour envoyer un table-valued parameter sans ligne, utilisez plutôt une référence null pour la valeur. - - SqlPipe ne prend pas en charge l'exécution d'une commande avec une connexion qui n'est pas une connexion du contexte. - - - La longueur de message {0} dépasse la longueur maximale prise en charge de 4000. - - - Impossible d'utiliser le canal tant qu'il est occupé par une autre opération. - - - Une série de résultats est actuellement envoyée au canal. Terminez la série des résultats avant d'appeler {0}. - - - La série de résultats n'a pas été initialisée. Appelez SendResultSetStart avant d'appeler {0}. - Fournisseur HTTP @@ -4743,4 +4665,19 @@ La liste SqlBatchCommand n'a pas été initialisée. + + Tentative non valide d’obtention de données vectorielles à partir de la colonne « {0} ». Les vecteurs ne sont pris en charge que pour les colonnes de type vecteur. + + + Type de vecteur « {0} » non pris en charge. + + + Valeur « null » non prise en charge pour le paramètre de sortie « {0} » du vecteur SqlDbtype. + + + En-tête de vecteur non valide reçu. + + + Chaîne JSON {0} non valide pour le vecteur. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.it.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.it.resx index 65271aee82..d56314dceb 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.it.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.it.resx @@ -231,18 +231,12 @@ Impossibile utilizzare Credential con la parola chiave per la stringa di connessione Integrated Security. - - Impossibile utilizzare Credential con la parola chiave Context Connection. - Non è possibile impostare la proprietà AccessToken se nella stringa di connessione è stato specificato 'UserID', 'UID', 'Password' o 'PWD'. Non è possibile impostare la proprietà AccessToken se la parola chiave della stringa di connessione 'Integrated Security' è stata impostata su 'true' o 'SSPI'. - - Non è possibile impostare la proprietà AccessToken con la parola chiave 'Context Connection'. - Non è possibile impostare la proprietà AccessToken se la proprietà Credential è già impostata. @@ -2073,9 +2067,6 @@ Offset buffer di destinazione (dimensione {0}) non valido: {1} - - Nessun dato esistente per la riga/colonna. - Il valore numerico non è contenibile in un numero decimale a 96 bit. @@ -2529,6 +2520,9 @@ Il comando richiede una connessione asincrona. Impostare "Asynchronous Processing=true" nella stringa di connessione. + + Timeout del tentativo di connessione. + Timeout scaduto. La connessione è stata interrotta. @@ -2778,30 +2772,6 @@ Lunghezza dei dati '{0}' minore di 0. - - Tentativo non valido di chiamare il metodo {0} quando SqlResultSet è chiuso. - - - Impossibile completare l'operazione, perché SqlResultSet è chiuso. - - - Impossibile chiamare "{0}" se il record è di sola lettura. - - - Impossibile completare l'operazione. Il record è di sola lettura. - - - Tentativo non valido di chiamare il metodo {0} quando la riga corrente è stata eliminata - - - Impossibile completare l'operazione perché la riga corrente è stata eliminata - - - Impossibile completare l'operazione. Il comando che ha creato SqlResultSet è stato dissociato dalla connessione originale. SqlResultSet è chiuso. - - - Impossibile creare SqlResultSet con le opzioni desiderate per la query specificata. - Impossibile chiamare "{0}" quando SqlDataRecord è di sola lettura. @@ -2871,51 +2841,21 @@ Il file System.Transactions.dll attualmente caricato non supporta le transazioni globali. Eseguire l'aggiornamento a .NET Framework 4.6.2 o versione successiva. - - L'esecuzione di batch di aggiornamenti non è supportata nella connessione contesto. - - - L'unica parola chiave per la stringa di connessione supplementare che può essere utilizzata quando si richiede la connessione contesto è Type System Version. - - - La connessione contesto non supporta Type System Version=SQL Server 2000. - La connessione alla connessione di contesto tramite Microsoft.Data.SqlClient non è supportata. - - La connessione contesto è giù in uso. - - - L'operazione richiesta richiede un contesto SqlClr che è disponibile solo quando si opera nel processo Sql Server. - - - L'operazione richiesta richiede un thread di esecuzione Sql Server Il thread corrente è stato avviato da un codice utente o da un altro codice motore diverso da Sql Server. - Non sono supportati TransactionScopes annidati. - - L'operazione richiesta non è disponibile nella connessione contesto. - - - Le notifiche non sono disponibili nella connessione contesto. - Evento server imprevisto: {0}. - - Le istanze utente non sono consentite quando è in funzione il processo Sql Server. - La lunghezza di '{0}' deve corrispondere alla lunghezza di '{1}'. SqlDbType '{0}' non è valido per {1}. Solo {2} è supportato. - - Si è verificato un errore con una riga precedente inviata alla SqlPipe. Prima di poter eseguire altri invii, è necessario chiamare SendResultsEnd. - Troppi valori. @@ -3663,9 +3603,6 @@ L'oggetto SqlCommand è già associato a un altro oggetto SqlDependency. - - Impossibile creare un oggetto SqlDependency durante l'esecuzione nel processo SQL Server. - SQL Server Service Broker non è attivato per il database corrente, pertanto non sono supportate le notifiche di query. Attivare il servizio Service Broker per questo database se si desidera utilizzare le notifiche. @@ -3726,21 +3663,6 @@ Non sono disponibili record nell'enumerazione SqlDataRecord. Utilizzare un riferimento con valore null, per inviare un table-valued parameter senza righe. - - SqlPipe non supporta l'esecuzione di un comando con una connessione diversa da una connessione contesto. - - - La lunghezza del messaggio {0} supera la lunghezza massima supportata di 4000. - - - Impossibile utilizzare la pipe se è occupata con un'altra operazione. - - - Si sta inviando alla pipe un set di risultati. Terminare il set di risultati corrente prima di chiamare {0}. - - - Il set di risultati non è stato iniziato. Chiamare SendResultSetStart prima di chiamare {0}. - Provider HTTP @@ -4743,4 +4665,19 @@ L'elenco SqlBatchCommand non è stato inizializzato. + + Tentativo non valido di ottenere dati vettoriali dalla colonna '{0}'. I vettori sono supportati solo per le colonne di tipo vettore. + + + Tipo di vettore '{0}' non supportato. + + + Il valore 'null' non è supportato per il parametro di output '{0}' di SqlDbtype Vector. + + + Intestazione del vettore non valida ricevuta. + + + {0} Stringa JSON non valida per il vettore. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ja.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ja.resx index 11ffabacb8..311e026244 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ja.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ja.resx @@ -231,18 +231,12 @@ Integrated Security 接続文字列キーワードを含む Credential は使用できません。 - - Context Connection キーワードを含む Credential は使用できません。 - 接続文字列で 'UserID'、'UID'、'Password'、または 'PWD' が指定されている場合は、AccessToken プロパティを設定できません。 'Integrated Security' 接続文字列キーワードが 'true' または 'SSPI' に設定されている場合は、AccessToken プロパティを設定できません。 - - 'Context Connection' キーワードを含む AccessToken プロパティは設定できません。 - Credential プロパティが既に設定されている場合は、AccessToken プロパティを設定できません。 @@ -2073,9 +2067,6 @@ ターゲット バッファー (サイズ {0}) オフセット : {1} が無効です。 - - 行および列にデータが存在しません。 - この数値は 10 進数の 96 ビットに適合させるには大きすぎます。 @@ -2529,6 +2520,9 @@ このコマンドには非同期接続が必要です。接続文字列に "Asynchronous Processing=true" を設定してください。 + + 接続試行がタイムアウトしました。 + タイムアウトに達しました。その結果、接続が切断されました。 @@ -2778,30 +2772,6 @@ データの長さ '{0}' が 0 未満です。 - - SqlResultSet が閉じている場合は、メソッド {0} の呼び出しは無効です。 - - - SqlResultSet が閉じているため、操作を完了できません。 - - - レコードが読み取り専用の場合は、'{0}' を呼び出すことはできません。 - - - レコードが読み取り専用のため、操作を完了できません。 - - - 現在の行が削除されている場合は、メソッド {0} の呼び出しは無効です。 - - - 現在の行が削除されているため、操作を完了できません。 - - - SqlResultSet を作成したコマンドが元の接続から切断されているため、操作を完了できません。SqlResultSet は閉じています。 - - - 要求されたオプションを使用して指定されたクエリの SqlResultSet を作成できません。 - SqlDataRecord が読み取り専用の場合は、'{0}' を呼び出すことはできません。 @@ -2871,51 +2841,21 @@ 現在読み込まれている System.Transactions.dll では、グローバル トランザクションはサポートされていません。.NET Framework 4.6.2 またはそれ以降にアップグレードしてください。 - - コンテキスト接続では、更新のバッチ処理はサポートされていません。 - - - コンテキスト接続を要求する際に、この他に使用される唯一の接続文字列キーワードは Type System Version キーワードです。 - - - コンテキスト接続は Type System Version=SQL Server 2000 をサポートしていません。 - Microsoft.Data.SqlClient を使用したコンテキスト接続への接続はサポートされていません。 - - コンテキスト接続は既に使用されています。 - - - 要求された操作には、SqlClr コンテキストが必要です。このコンテキストは、Sql Server プロセス内で実行する場合にのみ使用できます。 - - - 要求された操作には、Sql Server の実行スレッドが必要です。現在のスレッドはユーザー コードまたはその他の non-Sql Server エンジン コードによって開始されました。 - TransactionScopes を入れ子にすることはできません。 - - 要求された操作をコンテキスト接続で行うことはできません。 - - - コンテキスト接続で通知を使用することはできません。 - サーバーの予期しないイベント : {0}。 - - Sql Server プロセスで実行している場合は、ユーザー インスタンスは許可されません。 - '{0}' と '{1}' の長さは同じである必要があります。 {1} に対し、SqlDbType '{0}' は無効です。サポートしているのは {2} のみです。 - - SqlPipe に送信された前の行でエラーが発生しました。他のものを送信する前に SendResultsEnd を呼び出す必要があります。 - 値が多すぎます。 @@ -3663,9 +3603,6 @@ この SqlCommand オブジェクトは、既に別の SqlDependency オブジェクトに関連付けられています。 - - SQL Server プロセス内で実行されている場合、SqlDependency オブジェクトは作成できません。 - 現在のデータベースの SQL Server Service Broker は有効になっていません。このため、クエリ通知はサポートされません。通知を利用する場合は、このデータベースの Service Broker を有効にしてください。 @@ -3726,21 +3663,6 @@ SqlDataRecord 列挙にレコードがありません。行のない table-valued parameter を送信したい場合は、代わりにその値の null 参照を使用します。 - - SqlPipe では、コンテキスト接続ではない接続を使用してコマンドを実行することはできません。 - - - メッセージの長さ {0} が、サポートしている最大長である 4000 を超えています。 - - - 別の操作でパイプがビジー状態である場合、このパイプを使用できない可能性があります。 - - - 結果セットをパイプに送信しています。{0} を呼び出す前に現在の結果セットを終了してください。 - - - 結果セットが初期化されていません。{0} を呼び出す前に SendResultSetStart を呼び出してください。 - HTTP プロバイダー @@ -4743,4 +4665,19 @@ SqlBatchCommand リストが初期化されていません。 + + 列 '{0}' からベクター データを取得しようとしましたが無効です。ベクターは、ベクター型の列でのみサポートされます。 + + + サポートされていないベクター型 '{0}'。 + + + SqlDbtype ベクターの出力パラメーター '{0}' では、'null' 値はサポートされていません。 + + + 無効なベクター ヘッダーを受信しました。 + + + {0} ベクターの JSON 文字列が無効です。 + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ko.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ko.resx index 5d37a7b8ec..8ed8543b04 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ko.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ko.resx @@ -231,18 +231,12 @@ Integrated Security 연결 문자열 키워드가 포함된 Credential을 사용할 수 없습니다. - - Context Connection 키워드가 포함된 Credential을 사용할 수 없습니다. - 연결 문자열에서 'UserID', 'UID', 'Password' 또는 'PWD'를 지정한 경우 AccessToken 속성을 설정할 수 없습니다. 'Integrated Security' 연결 문자열 키워드가 'true' 또는 'SSPI'로 설정된 경우 AccessToken 속성을 설정할 수 없습니다. - - 'Context Connection' 키워드를 사용하여 AccessToken 속성을 지정할 수 없습니다. - Credential 속성이 이미 설정된 경우 AccessToken 속성을 설정할 수 없습니다. @@ -2073,9 +2067,6 @@ 대상 버퍼(크기: {0}) 오프셋이 잘못되었습니다. {1} - - 행/열에 대한 데이터가 없습니다. - 숫자 값이 너무 커서 96비트 10진수에 맞지 않습니다. @@ -2529,6 +2520,9 @@ 이 명령을 사용하려면 비동기 연결이 필요합니다. 연결 문자열에서 "Asynchronous Processing=true"로 설정하십시오. + + 연결 시도 시간이 초과되었습니다. + 제한 시간이 만료되었으므로 연결이 끊겼습니다. @@ -2778,30 +2772,6 @@ 데이터 길이 '{0}'은(는) 0보다 작습니다. - - SqlResultSet이 닫혀 있으면 메서드 {0}을(를) 호출할 수 없습니다. - - - SqlResultSet이 닫혀 있으므로 작업을 완료할 수 없습니다. - - - 레코드가 읽기 전용이면 '{0}'을(를) 호출할 수 없습니다. - - - 레코드가 읽기 전용이므로 작업을 완료할 수 없습니다. - - - 현재 행이 삭제되면 메서드 {0}을(를) 호출할 수 없습니다. - - - 현재 행이 삭제되었으므로 작업을 완료할 수 없습니다. - - - SqlResultSet을 만든 명령이 원래 연결에서 분리되었기 때문에 작업을 완료할 수 없습니다. SqlResultSet이 닫혀 있습니다. - - - 지정된 쿼리에 대한 SqlResultSet을 원하는 옵션으로 만들 수 없습니다. - SqlDataRecord가 읽기 전용이면 '{0}'을(를) 호출할 수 없습니다. @@ -2871,51 +2841,21 @@ 현재 로드된 System.Transactions.dll은 전역 트랜잭션을 지원하지 않습니다. .NET Framework 4.6.2 이상으로 업그레이드하세요. - - 컨텍스트 연결에서는 일괄 처리 업데이트가 지원되지 않습니다. - - - 컨텍스트 연결을 요청할 때 유일하게 추가할 수 있는 연결 문자열 키워드는 Type System Version 키워드입니다. - - - 컨텍스트 연결은 Type System Version=SQL Server 2000을 지원하지 않습니다. - Microsoft.Data.SqlClient를 사용하여 컨텍스트 연결에 연결할 수 없습니다. - - 컨텍스트 연결이 이미 사용 중입니다. - - - 요청한 작업을 수행하려면 Sql Server 프로세스에서 실행 중인 경우에만 사용할 수 있는 SqlClr 컨텍스트가 필요합니다. - - - 요청한 작업을 수행하려면 Sql Server 실행 스레드가 필요합니다. 현재 스레드는 사용자 코드 또는 Sql Server가 아닌 다른 엔진 코드에서 시작했습니다. - 중첩 TransactionScopes는 지원되지 않습니다. - - 요청한 작업은 컨텍스트 연결에서 사용할 수 없습니다. - - - 알림은 컨텍스트 연결에서 사용할 수 없습니다. - 예기치 않은 서버 이벤트가 발생했습니다. {0} - - Sql Server 프로세스에서 실행할 때는 사용자 인스턴스가 허용되지 않습니다. - '{0}'의 길이는 '{1}'의 길이와 일치해야 합니다. SqlDbType '{0}'은(는) {1}에 사용할 수 없습니다. {2}만 지원됩니다. - - SqlPipe로 보낸 이전 행에서 오류가 발생했습니다. 다른 행을 보내려면 SendResultsEnd를 호출해야 합니다. - 값이 너무 많습니다. @@ -3663,9 +3603,6 @@ 이 SqlCommand 개체는 이미 다른 SqlDependency 개체에 연결되어 있습니다. - - SQL Server 프로세스 내에서 실행할 때는 SqlDependency 개체를 만들 수 없습니다. - 현재 데이터베이스에 대해 SQL Server Service Broker가 설정되어 있지 않으므로 쿼리 알림을 사용할 수 없습니다. 알림을 사용하려면 이 데이터베이스에 대해 Service Broker를 설정하십시오. @@ -3726,21 +3663,6 @@ SqlDataRecord 열거형에 레코드가 없습니다. 행이 없는 table-valued parameter를 보내려면 대신 값에 null 참조를 사용하십시오. - - SqlPipe는 컨텍스트 연결이 아닌 다른 연결 상태에서 명령을 실행할 수 없습니다. - - - 메시지 길이 {0}이(가) 지원되는 최대 길이 4000자를 초과했습니다. - - - 다른 작업을 수행 중인 동안에는 파이프를 사용할 수 없습니다. - - - 현재 결과 집합을 파이프로 보내고 있습니다. {0}을(를) 호출하기 전에 현재 결과 집합을 종료하십시오. - - - 결과 집합이 시작되지 않았습니다. {0}을(를) 호출하기 전에 SendResultSetStart를 호출하십시오. - HTTP 공급자 @@ -4743,4 +4665,19 @@ SqlBatchCommand 목록이 초기화되지 않았습니다. + + 열 '{0}'에서 벡터 데이터를 가져오려는 시도가 잘못되었습니다. 벡터는 벡터 형식의 열에서만 지원됩니다. + + + 지원되지 않는 벡터 형식 '{0}'입니다. + + + SqlDbType Vector의 출력 매개변수 '{0}'에는 'null' 값이 지원되지 않습니다. + + + 잘못된 벡터 헤더가 수신되었습니다. + + + {0} 벡터에 대한 JSON 문자열이 잘못되었습니다. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.pl.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.pl.resx index 8abe8236a1..fd8e76afb9 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.pl.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.pl.resx @@ -231,18 +231,12 @@ Nie można użyć elementu Credential ze słowem kluczowym parametrów połączenia Integrated Security. - - Nie można użyć elementu Credential ze słowem kluczowym Context Connection. - Nie można ustawić właściwości AccessToken, jeśli w parametrach połączenia określono element UserID, UID, Password lub PWD. Nie można ustawić właściwości AccessToken, jeśli dla słowa kluczowego „Integrated Security” w parametrach połączenia ustawiono wartość true lub SSPI. - - Nie można ustawić właściwości AccessToken ze słowem kluczowym „Context Connection”. - Nie można ustawić właściwości AccessToken, jeśli ustawiono już właściwość Credential. @@ -2073,9 +2067,6 @@ Nieprawidłowe przesunięcie buforu docelowego (rozmiar {0}): {1} - - Nie istnieją dane dla wiersza/kolumny. - Wartość numeryczna jest za duża – nie mieści się w 96-bitowej zmiennej dziesiętnej. @@ -2529,6 +2520,9 @@ To polecenie wymaga połączenia asynchronicznego. Ustaw wartość „Asynchronous Processing=true” w parametrach połączenia. + + Upłynął limit czasu próby połączenia. + Upłynął limit czasu. W wyniku tego połączenie zostało przerwane. @@ -2778,30 +2772,6 @@ Długość danych „{0}” ma wartość mniejszą niż 0. - - Nieprawidłowa próba wywołania metody {0} podjęta, gdy element SqlResultSet jest zamknięty. - - - Nie można ukończyć operacji, ponieważ element SqlResultSet jest zamknięty. - - - Nie można wywołać elementu „{0}”, jeśli rekord jest tylko do odczytu. - - - Nie można ukończyć operacji, ponieważ rekord jest tylko do odczytu. - - - Nieprawidłowa próba wywołania {0} podjęta, gdy bieżący wiersz został usunięty - - - Nie można ukończyć operacji, ponieważ bieżący wiersz został usunięty - - - Nie można ukończyć operacji, ponieważ polecenie, przy użyciu którego został utworzony element SqlResultSet, nie ma już skojarzenia z oryginalnym połączeniem. Element SqlResultSet jest zamknięty. - - - Nie można utworzyć elementu SqlResultSet dla podanego zapytania z żądanymi opcjami. - Nie można wywołać elementu {0}, jeśli element SqlDataRecord jest tylko do odczytu. @@ -2871,51 +2841,21 @@ Aktualnie załadowany System.Transactions.dll nie obsługuje transakcji globalnych. Uaktualnij do programu .NET Framework 4.6.2 lub nowszego. - - Przetwarzanie wsadowe aktualizacji nie jest obsługiwane dla połączenia kontekstu. - - - Jedynym dodatkowym słowem kluczowym parametrach połączenia, które może być używane podczas żądania połączenia kontekstu, jest słowo kluczowe dotyczące wersji systemu (Type System Version). - - - Połączenie kontekstu nie obsługuje wersji systemu Type System Version=SQL Server 2000. - Nawiązywanie połączenia kontekstowego przy użyciu Microsoft.Data.SqlClient nie jest obsługiwane. - - Połączenie kontekstu jest już używane. - - - Żądana operacja wymaga kontekstu SqlClr, który jest dostępny tylko podczas uruchamiania procesu programu Sql Server. - - - Żądana operacja wymaga wątku wykonywania programu Sql Server. Bieżący wątek został uruchomiony przez kod użytkownika lub inny kod aparatu programu innego niż Sql Server. - Zagnieżdżone elementy TransactionScopes nie są obsługiwane. - - Żądana operacja jest niedostępna dla połączenia kontekstu. - - - Powiadomienia nie są dostępne dla połączenia kontekstu. - Nieoczekiwane zdarzenie serwera: {0}. - - Wystąpienia użytkownika nie są dozwolone podczas uruchomienia procesu serwera Sql Server. - Długość {0} musi być zgodna z długością {1}. Element SqlDbType „{0}” jest nieprawidłowy dla {1}. Obsługiwany jest tylko element {2}. - - Wystąpił błąd z poprzednim wierszem wysłanym do elementu SqlPipe. Aby można było wysłać dowolne inne elementy, należy wywołać funkcję SendResultsEnd. - Zbyt wiele wartości. @@ -3663,9 +3603,6 @@ Ten obiekt SqlCommand został już skojarzony z innym obiektem SqlDependency. - - Obiektu SqlDependency nie można utworzyć podczas uruchomienia wewnątrz procesu programu SQL Server. - Usługa Microsoft SQL Server Service Broker dla bieżącej bazy danych nie jest włączony i w wyniku tego powiadomienia o zapytaniach nie są obsługiwane. Włącz usługę Service Broker dla tej bazy danych, jeśli chcesz używać powiadomień. @@ -3726,21 +3663,6 @@ Brak rekordów w wyliczeniu SqlDataRecord. Aby wysłać parametr z wartościami przechowywanymi w tabeli bez wierszy, użyj odwołania o wartości null. - - Element SqlPipe nie obsługuje wykonywania polecenia dla połączenia, które nie jest połączeniem kontekstu. - - - Długość komunikatu {0} przekracza maksymalną obsługiwaną długość wynoszącą 4000. - - - Nie można użyć potoku, gdy jest zajęty inną operacją. - - - Zestaw wyników jest obecnie wysyłany do potoku. Zakończ bieżący zestaw wyników przed wywołaniem {0}. - - - Zestaw wyników nie został zainicjowany. Wywołaj metodę SendResultSetStart przed wywołaniem {0}. - Dostawca HTTP @@ -4743,4 +4665,19 @@ Lista SqlBatchCommand nie została zainicjowana. + + Nieprawidłowa próba pobrania danych wektorowych z kolumny „{0}”. Wektory są obsługiwane tylko dla kolumn wektora typu. + + + Nieobsługiwany typ wektora „{0}”. + + + Wartość „null” nie jest obsługiwana dla parametru wyjściowego „{0}” wektora SqlDbtype. + + + Odebrano nieprawidłowy nagłówek wektora. + + + {0} Nieprawidłowy ciąg JSON wektora. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.pt-BR.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.pt-BR.resx index e057491201..03c8087d35 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.pt-BR.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.pt-BR.resx @@ -231,18 +231,12 @@ Não é possível usar a Credential com palavra-chave de cadeia de conexão de Integrated Security. - - Não é possível usar a Credential com palavra-chave de Context Connection. - Não é possível definir a propriedade AccessToken se 'UserID', 'UID', 'Password' ou 'PWD' tiver sido especificado na cadeia de conexão. Não é possível definir a propriedade AccessToken se a palavra-chave da cadeia de conexão 'Integrated Security' estiver sido definida para 'true' ou 'SSPI'. - - Não é possível definir a propriedade AccessToken com a palavra-chave 'Context Connection'. - Não é possível definir a propriedade AccessToken se a propriedade Credential já estiver definida. @@ -2073,9 +2067,6 @@ Deslocamento de buffer de destino inválido (tamanho de {0}): {1} - - Não existem dados para a linha ou coluna. - Valor numérico muito grande para um decimal de 96 bits. @@ -2529,6 +2520,9 @@ Este comando requer uma conexão assíncrona. Defina "Asynchronous Processing=true" na cadeia de conexão. + + A tentativa de conexão atingiu o tempo limite. + Tempo limite expirado. Como resultado a conexão foi quebrada. @@ -2778,30 +2772,6 @@ O comprimento de dados '{0}' é menor que 0. - - Tentativa inválida para chamar o método {0} quando o SqlResultSet está fechado. - - - A operação não pode ser concluída, pois o SqlResultSet está fechado. - - - '{0}' não pode ser chamado quando o registro é somente leitura. - - - A operação não pode ser concluída, pois o registro é somente leitura. - - - Tentativa inválida para chamar o método {0} quando a linha atual foi excluída - - - A operação não foi concluída, pois a linha atual foi excluída - - - A operação não pode ser concluída, pois o comando que criou o SqlResultSet foi dissociado da conexão original. O SqlResultSet está fechado. - - - O SqlResultSet não pôde ser criado para a consulta fornecida com as opções desejadas. - '{0}' não pode ser chamado quando o SqlDataRecord é somente leitura. @@ -2871,51 +2841,21 @@ O System.Transactions.dll carregado no momento não dá suporte a Transações Globais. Atualize para o .NET Framework 4.6.2 ou posterior. - - Não há suporte para as atualizações em lote na conexão de contexto. - - - A única palavra-chave da cadeia de conexão adicional que deve ser usada ao solicitar a conexão de contexto é a palavra-chave Type System Version. - - - Não há suporte para a conexão de contexto Type System Version=SQL Server 2000. - Não há suporte para a conexão de contexto usando Microsoft.Data.SqlClient. - - A conexão de contexto já está em uso. - - - A operação solicitada requer um contexto SqlClr que somente está disponível durante a execução no processo Sql Server. - - - A operação solicitada requer um thread de execução Sql Server. O thread atual foi iniciado pelo código do usuário ou outro código de mecanismo sem ser o Sql Server. - Não há suporte para TransactionScopes aninhados. - - A operação solicitada não está disponível na conexão de contexto. - - - Notificações não estão disponíveis na conexão de contexto. - Evento de servidor inesperado: {0}. - - Instâncias do usuário não são permitidas durante a execução no processo Sql Server. - O comprimento de '{0}' deve corresponder ao comprimento de '{1}'. A SqlDbType '{0}' é inválida para {1}. Há suporte apenas para {2}. - - Ocorreu um erro com uma linha anteriormente enviada para o SqlPipe. O SendResultsEnd deve ser chamado antes de qualquer outro envio. - Excesso de valores. @@ -3663,9 +3603,6 @@ Esse objeto SqlCommand já está associado a outro objeto SqlDependency. - - Não é possível criar o objeto SqlDependency ao executar dentro do processo do SQL Server. - O SQL Server Service Broker do banco de dados atual não está habilitado e, como resultado, não há suporte a notificações de consultas. Habilite o Service Broker para este banco de dados se desejar usar notificações. @@ -3726,21 +3663,6 @@ Não há registros na enumeração SqlDataRecord. Para enviar um table-valued parameter sem linhas, use uma referência null para o valor. - - O SqlPipe não oferece suporte à execução de um comando com uma conexão que não seja conexão de contexto. - - - {0} ultrapassa o tamanho máximo de mensagens aceito de 4.000. - - - Não é possível usar o pipe enquanto ele está ocupado com outra operação. - - - Um conjunto de resultados está sendo enviado para o pipe. Encerre o conjunto de resultados atual antes de chamar {0}. - - - O conjunto de resultados não foi iniciado. Chame SendResultSetStart antes de chamar {0}. - Provedor de HTTP @@ -4743,4 +4665,19 @@ A lista SqlBatchCommand não foi inicializada. + + Tentativa inválida de obter dados vetoriais da coluna '{0}'. Vetores são suportados apenas para colunas do tipo vetor. + + + Tipo de vetor não suportado '{0}'. + + + Valor 'null' não suportado para o parâmetro de saída '{0}' do tipo SqlDbType Vector. + + + Cabeçalho de vetor inválido recebido. + + + {0} Cadeia de caracteres JSON inválida para vetor. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ru.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ru.resx index cedbe8e3cd..89ed2f634d 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.ru.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.ru.resx @@ -231,18 +231,12 @@ Нельзя использовать слово Credential с ключевым словом строки соединения Integrated Security. - - Нельзя использовать слово Credential с ключевым словом Context Connection. - Невозможно задать свойство AccessToken, если в строке подключения указаны ключевые слова "UserID", "UID", "Password" или "PWD". Невозможно задать свойство AccessToken, если для ключевого слова строки подключения "Integrated Security" заданы значения "true" или "SSPI". - - Невозможно задать свойство AccessToken с ключевым словом "Context Connection". - Невозможно задать свойство AccessToken, если свойство Credential уже задано. @@ -2073,9 +2067,6 @@ Недопустимое смещение в конечном буфере (размер: {0}): {1} - - Отсутствуют данные для строки или столбца. - Численное значение не умещается в 96-разрядном десятичном числе. @@ -2529,6 +2520,9 @@ Для этой команды необходимо асинхронное подключение. Укажите в строке подключения «Asynchronous Processing=true». + + Истекло время ожидания для попытки подключения. + Истекло время ожидания. Подключение разорвано. @@ -2778,30 +2772,6 @@ Длина данных "{0}" меньше 0. - - Недопустимая попытка вызвать метод {0}, когда закрыт SqlResultSet. - - - Невозможно завершить операцию, поскольку SqlResultSet закрыт. - - - Нельзя вызвать "{0}", если запись доступна только для чтения. - - - Невозможно завершить операцию, поскольку запись доступна только для чтения. - - - Недопустимая попытка вызвать метод {0}, когда текущая строка удалена. - - - Невозможно завершить операцию, поскольку текущая строка удалена. - - - Невозможно завершить операцию, поскольку команда, создавшая SqlResultSet, отделена от исходного подключения. SqlResultSet закрыт. - - - Для данного запроса не удалось создать SqlResultSet с нужными параметрами. - Нельзя вызвать "{0}", если SqlDataRecord доступна только для чтения. @@ -2871,51 +2841,21 @@ Загруженная библиотека System.Transactions.dll не поддерживает глобальные транзакции. Перейдите на .NET Framework 4.6.2 или более позднюю версию. - - Группировка обновлений для контекстного подключения не поддерживается. - - - Единственным дополнительным ключевым словом строки подключения, которое может использоваться при запросе контекстного подключения, является ключевое слово Type System Version. - - - Контекстное подключение не поддерживает Type System Version=SQL Server 2000. - Подключение к контексту с помощью Microsoft.Data.SqlClient не поддерживается. - - Контекстное подключение уже используется. - - - Для затребованной операции необходим контекст SqlClr, который доступен только при выполнении в процессе Sql Server. - - - Для затребованной операции необходим поток выполнения Sql Server. Текущий поток был запущен кодом пользователя или другим кодом, который не является кодом модуля Sql Server. - Вложенные TransactionScopes не поддерживаются. - - Для контекстного подключения затребованная операция не доступна. - - - Для контекстного подключения уведомления не доступны. - Недопустимое серверное событие: {0}. - - Пользовательские экземпляры не разрешены при выполнении в процессе Sql Server. - Длина "{0}" должна соответствовать длине "{1}". SqlDbType "{0}" недопустим для {1}. Поддерживается только {2}. - - Произошла ошибка, связанная с предыдущей строкой, отправленной в SqlPipe. Перед отправкой других строк необходимо вызвать SendResultsEnd. - Слишком много значений. @@ -3663,9 +3603,6 @@ Этот объект SqlCommand уже связан с другим объектом SqlDependency. - - Объект SqlDependency не может быть создан при выполнении внутри процесса SQL Server. - SQL Server Service Broker для текущей базы данных не включен, поэтому оповещения не поддерживаются. Включите Service Broker для этой базы данных, чтобы использовать оповещения. @@ -3726,21 +3663,6 @@ В перечислении SqlDataRecord нет записей. Чтобы передать имеющий табличное значение параметр table-valued parameter, который не содержит строк, используйте неопределенную ссылку null на значение. - - SqlPipe не поддерживает выполнения команды с подключением, которое не является контекстным. - - - Длина сообщения {0} превышает максимальное поддерживаемое значение 4000. - - - Не удалось использовать канал, поскольку он занят другой операцией. - - - Результирующий набор данных сейчас отправляется в канал. Перед вызовом {0} необходимо завершить текущий результирующий набор данных. - - - Результирующий набор данных не инициализирован. Вызовите SendResultSetStart перед вызовом {0}. - Поставщик HTTP @@ -4743,4 +4665,19 @@ Список SqlBatchCommand не инициализирован. + + Недопустимая попытка получить векторные данные из столбца "{0}". Векторы поддерживаются только для столбцов векторного типа. + + + Неподдерживаемый тип вектора "{0}". + + + Значение null не поддерживается для выходного параметра "{0}" вектора SqlDbType. + + + Получен недопустимый заголовок вектора. + + + {0} Недопустимая строка JSON для вектора. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.tr.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.tr.resx index 15b19580eb..e8b2fcd99d 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.tr.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.tr.resx @@ -231,18 +231,12 @@ Integrated Security anahtar sözcüğüyle Credential kullanılamaz. - - Context Connection anahtar sözcüğüyle Credential kullanılamaz. - 'UserID', 'UID', 'Password' veya 'PWD' bağlantı dizesinde belirtilmişse AccessToken özelliği ayarlanamaz. 'Integrated Security' bağlantı dizesi anahtar sözcüğü 'true' veya 'SSPI' olarak ayarlanmışsa AccessToken özelliği ayarlanamaz. - - 'Context Connection' anahtar sözcüğü ile AccessToken özelliği ayarlanamaz. - Credential özelliği zaten ayarlanmışsa AccessToken özelliği ayarlanamaz. @@ -2073,9 +2067,6 @@ Geçersiz hedef arabellek (boyutu {0}) uzaklığı: {1} - - Satır/sütun için hiç veri yok. - Sayı değeri 96 bit ondalık basamağa sığmayacak kadar büyük. @@ -2529,6 +2520,9 @@ Bu komut, zaman uyumsuz bağlantı gerektiriyor. Bağlantı dizesinde “Asynchronous Processing=true” ayarını yapın. + + Bağlantı denemesi zaman aşımına uğradı. + Zaman aşımı süresi sona erdi. Sonuç olarak bağlantı kesildi. @@ -2778,30 +2772,6 @@ Veri uzunluğu '{0}' 0'dan küçük. - - SqlResultSet kapalıyken geçersiz bir {0} metodu çağırma girişimi. - - - SqlResultSet kapalı olduğundan işlem tamamlanamıyor. - - - Kayıt salt okunur olduğunda '{0}' çağrılamaz. - - - Kayıt salt okunur olduğundan işlem tamamlanamıyor. - - - Geçerli satır silinmişken geçersiz bir {0} metodunu çağırma girişimi - - - Geçerli satır silindiğinden işlem tamamlanamıyor - - - SqlResultSet'i oluşturan komutun özgün bağlantıyla ilişkisi kesildiği için işlem tamamlanamıyor. SqlResultSet kapalı. - - - SqlResultSet, verilen sorgu için istenen seçeneklerle oluşturulamadı. - SqlDataRecord salt okunur olduğunda '{0}' çağrılamaz. @@ -2871,51 +2841,21 @@ Şu anda yüklü olan System.Transactions.dll, Genel İşlemleri desteklemiyor. Lütfen .NET Framework 4.6.2 veya sonraki bir sürüme yükseltin. - - Güncelleştirmelerin toplu işlenmesi, içerik bağlantısında desteklenmiyor. - - - İçerik bağlantısı istenirken kullanılabilecek tek ek bağlantı dizesi anahtar sözcüğü, Type System Version anahtar sözcüğüdür. - - - İçerik bağlantısı, Type System Version=SQL Server 2000 desteklemiyor. - Microsoft.Data.SqlClient kullanarak bağlam bağlantısına bağlanma desteklenmiyor. - - İçerik bağlantısı zaten kullanılıyor. - - - İstenen işlem, yalnızca Sql Server işleminde çalışılırken kullanılabilen bir SqlClr içeriği gerektiriyor. - - - İstenen işlem bir Sql Server yürütme iş parçacığı gerektirir. Geçerli iş parçacığı kullanıcı kodu veya diğer Sql Server motoru olmayan kodlar tarafından başlatılmıştır. - İç içe TransactionScopes desteklenmiyor. - - İstenen işlem içerik bağlantısında kullanılamıyor. - - - İçerik bağlantısında bildirim kullanılamıyor. - Beklenmeyen sunucu olayı: {0}. - - Sql Server işleminde çalışırken kullanıcı örneklerine izin verilmez. - ‘{0}’ uzunluğu, ‘{1}’ uzunluğu ile aynı olmalıdır. SqlDbType '{0}' {1} için geçersiz. Yalnızca {2} desteklenir. - - SqlPipe'a gönderilen önceki satırla ilgili bir hata oluştu. Başka bir şey gönderilmeden önce SendResultsEnd çağrılmalıdır. - Çok fazla değer. @@ -3663,9 +3603,6 @@ Bu SqlCommand nesnesi, zaten başka bir SqlDependency nesnesiyle ilişkilendirildi. - - SqlDependency nesnesi, SQL Server işleminde çalışılırken oluşturulamaz. - Geçerli veritabanı için SQL Server Hizmet Aracısı etkin değil ve sonuç olarak sorgu bildirimleri desteklenmiyor. Bildirimleri kullanmak istiyorsanız lütfen bu veritabanı için Hizmet Aracısını etkinleştirin. @@ -3726,21 +3663,6 @@ SqlDataRecord numaralandırması içinde kayıt yok. Satır içermeyen tablo değerli bir parametre göndermek için değer olarak bunun yerine bir null başvuru kullanın. - - SqlPipe, içerik bağlantısı olmayan bir bağlantıyla komut yürütmeyi desteklemez. - - - İleti uzunluğu {0}, desteklenen en fazla uzunluk olan 4000'i aşıyor. - - - Kanal, başka bir işlemle meşgulken kullanılamadı. - - - Şu anda kanala bir sonuç kümesi gönderiliyor. {0}‘i çağırmadan önce geçerli sonuç kümesini sonlandırın. - - - Sonuç kümesi başlatılmadı. {0}'i çağırmadan önce SendResultSetStart'ı çağırın. - HTTP Sağlayıcısı @@ -4743,4 +4665,19 @@ SqlBatchCommand listesi başlatılmadı. + + '{0}' sütunundan vektör verilerini alma denemesi geçersiz. Vektörler yalnızca vektör türündeki sütunlar için desteklenir. + + + '{0}' vektör türü desteklenmiyor. + + + SqlDbType Vector'ın '{0}' çıkış parametresi için 'null' değeri desteklenmiyor. + + + Geçersiz vektör başlığı alındı. + + + Vektör JSON dizesi {0} geçersiz. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hans.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hans.resx index 8ea45f3b3a..d21b03bca3 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hans.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hans.resx @@ -231,18 +231,12 @@ 无法使用含有 Integrated Security 连接字符串关键字的 Credential。 - - 无法使用含有 Context Connection 关键字的 Credential。 - 如果在连接字符串中指定了 "UserID"、"UID"、"Password" 或 "PWD",则无法设置 AccessToken 属性。 如果 "Integrated Security" 连接字符串关键字设置为 "true" 或 "SSPI",则无法设置 AccessToken 属性。 - - 无法设置使用 "Context Connection" 关键字的 AccessToken 属性。 - 如果已设置 Credential 属性,则无法设置 AccessToken 属性。 @@ -2073,9 +2067,6 @@ 无效的目标缓冲区(大小为 {0})偏移量: {1} - - 不存在此行/列的数据。 - 枚举值太大,不能适应 96 位十进制。 @@ -2529,6 +2520,9 @@ 此命令要求异步连接。请在连接字符串中设置“Asynchronous Processing=true”。 + + 连接尝试超时。 + 超时时间已到。因此,连接被中断。 @@ -2778,30 +2772,6 @@ 数据长度“{0}”小于 0。 - - SqlResultSet 关闭时,调用方法 {0} 的尝试无效。 - - - 不能完成操作,因为 SqlResultSet 已关闭。 - - - 记录为只读时不能调用“{0}”。 - - - 不能完成操作,因为记录为只读。 - - - 当前列被删除时,调用方法 {0} 的尝试无效 - - - 不能完成操作,因为当前列已被删除。 - - - 不能完成操作,因为创建 SqlResultSet 的命令已经与原始连接取消关联。SqlResultSet 已关闭。 - - - 不能使用期望选项为给定查询创建 SqlResultSet。 - SqlDataRecord 为只读时,不能调用“{0}”。 @@ -2871,51 +2841,21 @@ 当前加载的 System.Transactions.dll 不支持全局事务。请升级到 .NET Framework 4.6.2 或更高版本。 - - 在上下文连接上不支持批处理更新。 - - - 在请求上下文连接时唯一可以使用的其他连接字符串关键字是 Type System Version 关键字。 - - - 上下文连接不支持 Type System Version=SQL Server 2000。 - 不支持使用 Microsoft.Data.SqlClient 连接到上下文连接。 - - 上下文连接已经在使用。 - - - 所请求的操作需要使用 SqlClr 上下文,该上下文只有在 Sql Server 进程中运行时才可用。 - - - 所请求的操作需要使用 Sql Server 执行线程。当前线程由用户代码或其他非 Sql Server 引擎代码启动。 - 不支持嵌套的 TransactionScopes。 - - 所请求的操作在上下文连接上不可用。 - - - 通知在上下文连接上不可用。 - 意外的服务器事件: {0}。 - - 在 Sql Server 进程中运行时不允许使用用户实例。 - “{0}”的长度必须与“{1}”的长度匹配。 SqlDbType“{0}”对于 {1} 无效。只支持 {2}。 - - 以前发送到 SqlPipe 的某行出错。必须先调用 SendResultsEnd,才能发送其他内容。 - 值太多。 @@ -3663,9 +3603,6 @@ 此 SqlCommand 对象已与另一 SqlDependency 对象关联。 - - 在 SQL Server 进程内运行时,无法创建 SqlDependency 对象。 - 未启用当前数据库的 SQL Server Service Broker,因此查询通知不受支持。如果希望使用通知,请为此数据库启用 Service Broker。 @@ -3726,21 +3663,6 @@ SqlDataRecord 枚举中没有记录。要发送不包含行的 table-valued parameter,请对该值改用 null 引用。 - - SqlPipe 不支持使用非上下文连接执行命令。 - - - 消息长度 {0} 超过支持的最大长度 4000。 - - - 无法使用该管道,该管道正在处理其他操作。 - - - 结果集当前正在发送到该管道。在调用 {0} 之前结束当前结果集。 - - - 结果集尚未启动。在调用 {0} 之前调用 SendResultSetStart。 - HTTP 提供程序 @@ -4743,4 +4665,19 @@ SqlBatchCommand 列表尚未初始化。 + + 从列“{0}”获取矢量数据的尝试无效。仅矢量类型的列支持矢量。 + + + 矢量类型“{0}”不受支持。 + + + SqlDbtype 向量的输出参数“{0}”不支持 "null" 值。 + + + 收到的矢量标头无效。 + + + {0} 矢量的 JSON 字符串无效。 + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hant.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hant.resx index 545ea55fa4..2f41424cf7 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hant.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.zh-Hant.resx @@ -231,18 +231,12 @@ 無法將 Credential 與 Integrated Security 連接字串關鍵字搭配使用。 - - 無法將 Credential 與 Context Connection 關鍵字搭配使用。 - 如果連接字串中指定了 'UserID'、'UID'、'Password' 或 'PWD',就無法設定 AccessToken 屬性。 如果 'Integrated Security' 連接字串關鍵字已設為 'true' 或 'SSPI',就無法設定 AccessToken 屬性。 - - 無法使用 'Context Connection' 關鍵字設定 AccessToken 屬性。 - 如果已設定 Credential 屬性,就無法設定 AccessToken 屬性。 @@ -2073,9 +2067,6 @@ 無效的目的緩衝區 ({0} 的大小) 位移: {1} - - 資料列/資料行沒有資料。 - 數值太大,超過 96 位元小數。 @@ -2529,6 +2520,9 @@ 此命令需要非同步連接。請在連接字串中設定 "Asynchronous Processing=true"。 + + 連接嘗試逾時。 + 等候逾時已過時。結果為已連接中斷。 @@ -2778,30 +2772,6 @@ 資料長度 '{0}' 小於 0。 - - 當 SqlResultSet 已關閉時,嘗試呼叫方法 {0} 失敗。 - - - 無法完成操作,因為 SqlResultSet 已關閉。 - - - 當記錄為唯讀時無法呼叫 '{0}'。 - - - 無法完成操作,因為記錄為唯讀。 - - - 當目前的資料列已刪除時,嘗試呼叫方法 {0} 無效 - - - 無法完成操作,因為目前的資料列已刪除。 - - - 無法完成操作,因為已經從原始的連接中解除建立 SqlResultSet 命令的關聯。SqlResultSet 已關閉。 - - - 無法建立包含欲查選項指定查詢的 SqlResultSet。 - 當 SqlDataRecord 為唯讀時,無法呼叫 '{0}'。 @@ -2871,51 +2841,21 @@ 目前載入的 System.Transactions.dll 不支援全域交易。請升級為 .NET Framework 4.6.2 或更新版本。 - - 內容連接上並不支援批次更新。 - - - Type System Version 關鍵字是唯一在要求內容連接時可能需要使用的其他連接字串關鍵字。 - - - 此內容連接不支援 Type System Version=SQL Server 2000。 - 不支援使用 Microsoft.Data.SqlClient 連線至內容連線。 - - 此內容連接已在使用中。 - - - 要求的操作需要 SqlClr 內容,這是在 Sql Server 程序中執行時唯一可用的內容。 - - - 要求的操作需要 Sql Server 執行緒。目前的執行緒是由使用者編碼或其他非 Sql Server 引擎碼所啟動的。 - 不支援巢狀 TransactionScopes。 - - 在此內容連接上無法使用要求的操作。 - - - 在此內容連接尚無法使用通知。 - 未預期的伺服器事件: {0}。 - - 在 Sql Server 程序中執行時並不允許使用者執行個體。 - '{0}' 的長度必須和 '{1}' 的長度相符。 SqlDbType '{0}' 對 {1} 是無效的。只支援 {2}。 - - 發生錯誤,已傳送優先資料列至 SqlPipe。在進行其他傳送之前,必須先呼叫 SendResultsEnd。 - 太多數值。 @@ -3663,9 +3603,6 @@ SqlCommand 物件已與其他 SqlDependency 物件關聯。 - - 在 SQL Server 程序中執行時,SqlDependency 物件就不能建立。 - 尚未啟用目前資料庫的 SQL Server Service Broker,因此不支援查詢通知。如果您想要使用通知,請啟用這個資料庫的 Service Broker。 @@ -3726,21 +3663,6 @@ SqlDataRecord 列舉之中沒有記錄。若要傳送沒有資料列的 table-valued parameter,請使用該值的 null 參考來代替。 - - 當某命令的連接不是內容連接時,SqlPipe 不支援該命令的執行。 - - - 訊息長度 {0} 超過支援的最大長度 4000。 - - - 當管線忙碌於其他操作時便無法使用。 - - - 正傳送結果集至管線中。在呼叫 {0} 前請先結束目前的結果集。 - - - 結果集尚未初始化。在呼叫 {0} 前請先呼叫 SendResultSetStart。 - HTTP 提供者 @@ -4743,4 +4665,19 @@ SqlBatchCommand 列表尚未初始化。 + + 從資料行 '{0}' 取得向量資料的嘗試無效。向量僅支援向量類型的資料行。 + + + 不支援的向量類型 '{0}'。 + + + SqlDbType 向量的輸出參數 '{0}' 不支援 'null' 值。 + + + 收到的向量標頭無效。 + + + {0} 向量的 JSON 字串無效。 + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs index 2970b1f1ce..df68f86677 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs @@ -30,7 +30,11 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable private readonly PropertyInfo _useCompatibilityProcessSniProperty; private readonly PropertyInfo _useCompatibilityAsyncBehaviourProperty; private readonly PropertyInfo _useConnectionPoolV2Property; - #if NETFRAMEWORK + private readonly PropertyInfo _truncateScaledDecimalProperty; + #if NET + private readonly PropertyInfo _globalizationInvariantModeProperty; + private readonly PropertyInfo _useManagedNetworkingProperty; + #else private readonly PropertyInfo _disableTnirByDefaultProperty; #endif @@ -51,7 +55,14 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable private readonly Tristate _useCompatibilityAsyncBehaviourOriginal; private readonly FieldInfo _useConnectionPoolV2Field; private readonly Tristate _useConnectionPoolV2Original; - #if NETFRAMEWORK + private readonly FieldInfo _truncateScaledDecimalField; + private readonly Tristate _truncateScaledDecimalOriginal; + #if NET + private readonly FieldInfo _globalizationInvariantModeField; + private readonly Tristate _globalizationInvariantModeOriginal; + private readonly FieldInfo _useManagedNetworkingField; + private readonly Tristate _useManagedNetworkingOriginal; + #else private readonly FieldInfo _disableTnirByDefaultField; private readonly Tristate _disableTnirByDefaultOriginal; #endif @@ -140,7 +151,19 @@ void InitProperty(string name, out PropertyInfo property) "UseConnectionPoolV2", out _useConnectionPoolV2Property); - #if NETFRAMEWORK + InitProperty( + "TruncateScaledDecimal", + out _truncateScaledDecimalProperty); + + #if NET + InitProperty( + "GlobalizationInvariantMode", + out _globalizationInvariantModeProperty); + + InitProperty( + "UseManagedNetworking", + out _useManagedNetworkingProperty); + #else InitProperty( "DisableTnirByDefault", out _disableTnirByDefaultProperty); @@ -201,7 +224,22 @@ void InitField(string name, out FieldInfo field, out Tristate value) out _useConnectionPoolV2Field, out _useConnectionPoolV2Original); - #if NETFRAMEWORK + InitField( + "s_truncateScaledDecimal", + out _truncateScaledDecimalField, + out _truncateScaledDecimalOriginal); + + #if NET + InitField( + "s_globalizationInvariantMode", + out _globalizationInvariantModeField, + out _globalizationInvariantModeOriginal); + + InitField( + "s_useManagedNetworking", + out _useManagedNetworkingField, + out _useManagedNetworkingOriginal); + #else InitField( "s_disableTnirByDefault", out _disableTnirByDefaultField, @@ -265,7 +303,19 @@ void RestoreField(FieldInfo field, Tristate value) _useConnectionPoolV2Field, _useConnectionPoolV2Original); - #if NETFRAMEWORK + RestoreField( + _truncateScaledDecimalField, + _truncateScaledDecimalOriginal); + + #if NET + RestoreField( + _globalizationInvariantModeField, + _globalizationInvariantModeOriginal); + + RestoreField( + _useManagedNetworkingField, + _useManagedNetworkingOriginal); + #else RestoreField( _disableTnirByDefaultField, _disableTnirByDefaultOriginal); @@ -350,7 +400,31 @@ public bool UseConnectionPoolV2 get => (bool)_useConnectionPoolV2Property.GetValue(null); } - #if NETFRAMEWORK + /// + /// Access the LocalAppContextSwitches.TruncateScaledDecimal property. + /// + public bool TruncateScaledDecimal + { + get => (bool)_truncateScaledDecimalProperty.GetValue(null); + } + + #if NET + /// + /// Access the LocalAppContextSwitches.GlobalizationInvariantMode property. + /// + public bool GlobalizationInvariantMode + { + get => (bool)_globalizationInvariantModeProperty.GetValue(null); + } + + /// + /// Access the LocalAppContextSwitches.UseManagedNetworking property. + /// + public bool UseManagedNetworking + { + get => (bool)_useManagedNetworkingProperty.GetValue(null); + } + #else /// /// Access the LocalAppContextSwitches.DisableTnirByDefault property. /// @@ -443,7 +517,34 @@ public Tristate UseConnectionPoolV2Field set => SetValue(_useConnectionPoolV2Field, value); } - #if NETFRAMEWORK + /// + /// Get or set the LocalAppContextSwitches.TruncateScaledDecimal switch value. + /// + public Tristate TruncateScaledDecimalField + { + get => GetValue(_truncateScaledDecimalField); + set => SetValue(_truncateScaledDecimalField, value); + } + + #if NET + /// + /// Get or set the LocalAppContextSwitches.GlobalizationInvariantMode switch value. + /// + public Tristate GlobalizationInvariantModeField + { + get => GetValue(_globalizationInvariantModeField); + set => SetValue(_globalizationInvariantModeField, value); + } + + /// + /// Get or set the LocalAppContextSwitches.UseManagedNetworking switch value. + /// + public Tristate UseManagedNetworkingField + { + get => GetValue(_useManagedNetworkingField); + set => SetValue(_useManagedNetworkingField, value); + } + #else /// /// Get or set the LocalAppContextSwitches.DisableTnirByDefault switch /// value. diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/MultiplexerTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/MultiplexerTests.cs index 288586fb17..efa7962d28 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/MultiplexerTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/MultiplexerTests.cs @@ -25,7 +25,7 @@ public static bool IsUsingCompatibilityProcessSni { return foundValue; } - return false; + return true; // Default to true if the switch is not set } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs index 1b17a0f4ef..b8a8654038 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs @@ -2001,7 +2001,7 @@ public void TestBeginAndEndExecuteReaderWithAsyncCallback(string connection, Com } } - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringSetupForAE))] + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsTargetReadyForAeWithKeyStore))] [ClassData(typeof(AEConnectionStringProviderWithExecutionMethod))] public void TestSqlCommandCancel(string connection, string value) { diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/ConnectionPoolHelper.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/ConnectionPoolHelper.cs index 6c828b188b..34b9070946 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/ConnectionPoolHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Common/SystemDataInternals/ConnectionPoolHelper.cs @@ -18,7 +18,6 @@ internal static class ConnectionPoolHelper private static Type s_waitHandleDbConnectionPool = s_MicrosoftDotData.GetType("Microsoft.Data.SqlClient.ConnectionPool.WaitHandleDbConnectionPool"); private static Type s_dbConnectionPoolGroup = s_MicrosoftDotData.GetType("Microsoft.Data.SqlClient.ConnectionPool.DbConnectionPoolGroup"); private static Type s_dbConnectionPoolIdentity = s_MicrosoftDotData.GetType("Microsoft.Data.SqlClient.ConnectionPool.DbConnectionPoolIdentity"); - private static Type s_dbConnectionFactory = s_MicrosoftDotData.GetType("Microsoft.Data.ProviderBase.DbConnectionFactory"); private static Type s_sqlConnectionFactory = s_MicrosoftDotData.GetType("Microsoft.Data.SqlClient.SqlConnectionFactory"); private static Type s_dbConnectionPoolKey = s_MicrosoftDotData.GetType("Microsoft.Data.SqlClient.ConnectionPool.DbConnectionPoolKey"); private static Type s_dictStringPoolGroup = typeof(Dictionary<,>).MakeGenericType(s_dbConnectionPoolKey, s_dbConnectionPoolGroup); @@ -26,9 +25,9 @@ internal static class ConnectionPoolHelper private static PropertyInfo s_dbConnectionPoolCount = s_waitHandleDbConnectionPool.GetProperty("Count", BindingFlags.Instance | BindingFlags.Public); private static PropertyInfo s_dictStringPoolGroupGetKeys = s_dictStringPoolGroup.GetProperty("Keys"); private static PropertyInfo s_dictPoolIdentityPoolValues = s_dictPoolIdentityPool.GetProperty("Values"); - private static FieldInfo s_dbConnectionFactoryPoolGroupList = s_dbConnectionFactory.GetField("_connectionPoolGroups", BindingFlags.Instance | BindingFlags.NonPublic); + private static PropertyInfo s_sqlConnectionFactorySingleton = s_sqlConnectionFactory.GetProperty("Instance", BindingFlags.Static | BindingFlags.NonPublic); + private static FieldInfo s_dbConnectionFactoryPoolGroupList = s_sqlConnectionFactory.GetField("_connectionPoolGroups", BindingFlags.Instance | BindingFlags.NonPublic); private static FieldInfo s_dbConnectionPoolGroupPoolCollection = s_dbConnectionPoolGroup.GetField("_poolCollection", BindingFlags.Instance | BindingFlags.NonPublic); - private static FieldInfo s_sqlConnectionFactorySingleton = s_sqlConnectionFactory.GetField("SingletonInstance", BindingFlags.Static | BindingFlags.Public); private static FieldInfo s_dbConnectionPoolStackOld = s_waitHandleDbConnectionPool.GetField("_stackOld", BindingFlags.Instance | BindingFlags.NonPublic); private static FieldInfo s_dbConnectionPoolStackNew = s_waitHandleDbConnectionPool.GetField("_stackNew", BindingFlags.Instance | BindingFlags.NonPublic); private static MethodInfo s_dbConnectionPoolCleanup = s_waitHandleDbConnectionPool.GetMethod("CleanupCallback", BindingFlags.Instance | BindingFlags.NonPublic); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs index 0d024170b6..b00662a90f 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs @@ -887,5 +887,196 @@ public static void CheckLegacyNullRowVersionIsEmptyArray() Assert.Equal(result, reader.GetFieldValue(0)); } } + + + private class _Row + { + public int Id; + public Guid DocumentIdentificationId; + public string Name; + public string Value; + + public _Row(int id, Guid documentIdentificationId, string name, string value) + { + Id = id; + DocumentIdentificationId = documentIdentificationId; + Name = name; + Value = value; + } + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static async Task CanReadAwkwardDataLengths() + { + // The test data is taken from a production sample which exhibited multiple problems around async string reads. + // The lengths of the field names and data values are important to reproducing the problems. The values have been + // changed to randomized values. Changes to the column names or value lengths can cause the problems to move or + // fail to reproduce entirely because of packet and data alignment. The data is not randomly generated to help with + // debugging failures in the test. + _Row[] rows = new _Row[] + { + new _Row(27967675, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "LfcK2K8CgjnXyed7Oi", "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"), + new _Row(27967690, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "Gx1zdwPUdvrgDaIHAZSVhe5P8", "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"), + new _Row(27967683, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "1O7MMFFVKh7OUY8Z17pNKIhNorz", "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"), + new _Row(27967670, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "2yF66o0lSHl", "5ig"), + new _Row(27967672, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "5tvcpUkTvlRdy", "bQl"), + new _Row(27967686, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "MigVFhYMw6jG", "2ZK883Zo1CpaZH"), + new _Row(27967687, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "BqqByWX5Wuc6DooWnEC", "PPKwf6FpnJfSTklfX"), + new _Row(27967684, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "Lwt77ImTu24Fv", "1Sb8VpRuiBoqcVXblALJW"), + new _Row(27967676, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "odZDwRGrx7hRmxR", "4SMlxm6uDqztEaWm5KgRstymBQh5yF8fLtXo"), + new _Row(27967677, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "Y2dvJ9z80n0ri9hR", "dhq5"), + new _Row(27967689, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "Dhhd7cliCo2E8Yv1FIK", "B1VC"), + new _Row(27967674, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "sy79bLYyrZ1", "t50JSsD"), + new _Row(27967673, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "37Wh8RDNCMh", "C4u4MFMySkFV"), + new _Row(27967671, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "iCQZR01", "GoODhGbRzTKk7qBWjCc2"), + new _Row(27967685, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "M8xQrKB8yEOLVGR", "LDJMXhpJvfGFKNjT3JDBatO"), + new _Row(27967666, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "FfrqRLP", null), + new _Row(27967669, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "QXSN3", "XCtCRP"), + new _Row(27967668, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "LAnC6", "Z4gmN"), + new _Row(27967667, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "fiE2vDYS7", "cB6hHKmoBiMa0dRG"), + new _Row(27967688, new Guid("58EE5163-5F41-4C83-8175-34580FA54BDE"), "YJxqBBgPfdgqH26XCUWD0gHCsv", "ZyhKMi2fBWVrNdy44Z5JIZK"), + new _Row(27942037, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "LesQjDrs6rjgDj4MaD", "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"), + new _Row(27942047, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "ho3Pv3aLmCusWltGlgEAbwwpH", "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"), + new _Row(27942040, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "uARU2FodUEIb3ixMr401zs5gGq3", "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"), + new _Row(27942032, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "UTaQip0jFLV", "6MF"), + new _Row(27942034, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "5b8zlELFQw1rW", "7Y9"), + new _Row(27942043, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "IriW7xhF2aor", "lvxx7PedzFWUBi"), + new _Row(27942044, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "u8JMj7ChBMeJnN98eOt", "ITrPShAjSMoCTMfMj"), + new _Row(27942041, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "ugXGwPnQblwxd", "tiddVfujPuz0HKaTXQLMe"), + new _Row(27942038, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "Kxl8aC5uKR0zVJl", "QPZzAKNQjt2Dm6glZCdxvGBzpVPi9BMAVv4e"), + new _Row(27942039, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "3B5ikbV50KkZ5Ffs", "aRhr"), + new _Row(27942046, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "Nw6iJxi2wWKA8twN2wO", "7RY0"), + new _Row(27942036, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "Fi4DiLijhnY", "mNhLjRF"), + new _Row(27942035, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "uuzw6oLPddS", "0ADpjuhWDHJj"), + new _Row(27942033, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "rLxgGM6", "JHMtkwRTVoTebnaPLUUY"), + new _Row(27942042, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "U4crJOexXz6N0dx", "ZKnTZYlikloyaQiVWvYlbkp"), + new _Row(27942028, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "j4Ipsks", null), + new _Row(27942031, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "PU6Yf", "UxStUM"), + new _Row(27942030, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "v1wYY", "6HQA0"), + new _Row(27942029, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "t2xP2OnIo", "xl8Kc65mfbv4PX"), + new _Row(27942045, new Guid("08E06BB1-757A-4D8B-9768-4966D16F9173"), "IhJwQDjAVByAKvnYVJd1fseUvD", "wZBP5xrHTmSd4zF8vfVXfw5"), + new _Row(27939369, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "5FEOfqCx1nSxMubDot", "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"), + new _Row(27939379, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "Eqc2KebZ3jFKtPXOCZtiNMJMq", "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"), + new _Row(27939372, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "h9zfijozkzKtEN0oIWWKtg1d37r", "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"), + new _Row(27939364, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "pl4SEzxt5aj", "y2i"), + new _Row(27939366, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "pHu8rACwoAeAR", "6P5"), + new _Row(27939375, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "I5QPIDh0pOQq", "TyN7SRqp1e9xhN"), + new _Row(27939376, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "tIs1cRUF6EQXIZLH5qL", "WdgCal1kzJsBfiLYg"), + new _Row(27939373, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "7AAFQU8b5xQjz", "48YSumTpuwo90lHwHmcuW"), + new _Row(27939370, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "bUkjxvr2Ij6Cb7I", "bQ4bNrK4yQHdJ9WTB5O8JxitdW24vEoHFhii"), + new _Row(27939371, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "1aUWwkCStvQ0aa9H", "PKjk"), + new _Row(27939378, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "JHNBoMSpcgbYBwqCTja", "0pUE"), + new _Row(27939368, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "VETrliW5qrS", "W7VBDVZ"), + new _Row(27939367, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "DZpLEj7a7x8", "fske7dTzHB0V"), + new _Row(27939365, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "BFiGUeE", "6FfoJN00SRpBwaeJMzka"), + new _Row(27939374, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "0VOL3LAUw09go4n", "joEoLSBPGNTxv4JSAV7YqRo"), + new _Row(27939360, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "YjKaV7r", null), + new _Row(27939363, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "fujpL", "5d3m8k"), + new _Row(27939362, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "0nZnP", "zCOIN"), + new _Row(27939361, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "IImbQh9yt", "8fh5FpTwXX0CT"), + new _Row(27939377, new Guid("E8F87ACF-C537-4347-9BC4-CDF125E759AC"), "nte3hSSnA5qUDrTeBps0GajHFV", "b7yn1aDZ0PopOOwVSSFwEZQ"), + new _Row(27967700, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "6PsGeEZLzUcnKGKL0O", "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"), + new _Row(27967710, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "4UemQLTq1dAnEVC19A6BwVHaG", "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"), + new _Row(27967703, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "PztxRBuJCcvIULKp6mUwHuyHQHH", "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"), + new _Row(27967695, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "ZOySIGfxJTT", "VRY"), + new _Row(27967697, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "Qx3mI5MQCPF9k", "xVy"), + new _Row(27967706, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "01TPl2U9dzNa", "bwDlLckZEHqokP"), + new _Row(27967707, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "jgp5sCGrgL08A5QGfM5", "OM1DT9iSMoVZRMIGu"), + new _Row(27967704, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "ffXeEmkW5ZiRP", "Hr9e5U4qbWHpPxNljTYUz"), + new _Row(27967701, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "D83pFinFz60LiQd", "z99W2HKNyBShCLiqWrl3oKiOYmhK6TlvlxSC"), + new _Row(27967702, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "h8kGa6byq7u6xwwH", "dFXM"), + new _Row(27967709, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "dQTHfJT3W0ZoVvphWcL", "i3sH"), + new _Row(27967699, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "iNlhTgyYc4f", "BJkaAdh"), + new _Row(27967698, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "jst3eaOfwwy", "z6wGAgKIcwOp"), + new _Row(27967696, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "GE5DZYd", "NyFnZKT73M4QV5Ih3puI"), + new _Row(27967705, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "jf2cClMdD5wiEli", "M9ovCXpQGbLw52b4jlVS9eK"), + new _Row(27967691, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "tJXdP5Q", "boSRIr1E8Eqe8BCPCdYDi"), + new _Row(27967694, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "kmuCP", "2VzY"), + new _Row(27967693, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "cLc27", "ujAix"), + new _Row(27967692, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "UUHWcCtTe", "WAqv8AbdDPoSlh"), + new _Row(27967708, new Guid("C214F6BB-53EB-4858-A944-D227F9A2846F"), "1hlglR0vEPdA3ZxqVI3E59targ", "9yl8QMmpLjCGS8F2ETvyCeo"), + }; + + using (var cn = new Microsoft.Data.SqlClient.SqlConnection(DataTestUtility.TCPConnectionString)) + { + await cn.OpenAsync(); + + string tableName = DataTestUtility.GenerateObjectName(); + + try + { + using (var cmd = cn.CreateCommand()) + { + cmd.CommandText = $""" + create table [{tableName}] + ( + [Id] [int] NOT NULL, + [DocumentIdentificationId] [uniqueidentifier] NOT NULL, + [Name] [nvarchar](40) NULL, + [Value] [nvarchar](max) NULL + ) + """; + + await cmd.ExecuteNonQueryAsync(); + + cmd.CommandText = $"INSERT INTO [{tableName}] VALUES (@id,@docId,@name,@value)"; + SqlParameter id = cmd.Parameters.AddWithValue("@id", 0); + SqlParameter docId = cmd.Parameters.AddWithValue("@docId", Guid.Empty); + SqlParameter name = cmd.Parameters.AddWithValue("@name", ""); + SqlParameter value = cmd.Parameters.AddWithValue("@value", ""); + + foreach (var row in rows) + { + id.Value = row.Id; + docId.Value = row.DocumentIdentificationId; + name.Value = row.Name; + value.Value = row.Value == null ? DBNull.Value : row.Value; + await cmd.ExecuteNonQueryAsync(); + } + } + + int counter = 0; + while (counter < 10) + { + using (var cmd = cn.CreateCommand()) + { + cmd.CommandText = $"SELECT [d].[Id], [d].[DocumentIdentificationId], [d].[Name], [d].[Value] FROM [{tableName}] AS [d]"; + using (var reader = await cmd.ExecuteReaderAsync()) + { + int row = 0; + while (await reader.ReadAsync()) + { + int id = await reader.GetFieldValueAsync(0, default); + Guid docId = await reader.GetFieldValueAsync(1, default); + string name = await reader.GetFieldValueAsync(2, default); + string value = null; + if (!await reader.IsDBNullAsync(3, default)) + { + value = await reader.GetFieldValueAsync(3, default); + } + + Assert.Equal(id, rows[row].Id); + Assert.Equal(docId, rows[row].DocumentIdentificationId); + Assert.Equal(name, rows[row].Name); + Assert.Equal(value, rows[row].Value); + row += 1; + } + } + } + counter++; + } + } + finally + { + try + { + DataTestUtility.DropTable(cn, tableName); + } + catch + { + } + } + } + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs index bf99691907..d98fe6abd4 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs @@ -11,6 +11,8 @@ using System.Threading.Tasks; using Xunit; using System.Globalization; +using Microsoft.Data.SqlClient.Tests.Common; + #if !NETFRAMEWORK using Microsoft.SqlServer.Types; @@ -570,13 +572,16 @@ public static void SqlDecimalConvertToDecimal_TestOutOfRange(string sqlDecimalVa [ClassData(typeof(ConnectionStringsProvider))] public static void TestScaledDecimalParameter_CommandInsert(string connectionString, bool truncateScaledDecimal) { + using LocalAppContextSwitchesHelper appContextSwitchesHelper = new(); + string tableName = DataTestUtility.GetUniqueNameForSqlServer("TestDecimalParameterCMD"); using SqlConnection connection = InitialDatabaseTable(connectionString, tableName); try { using (SqlCommand cmd = connection.CreateCommand()) { - AppContext.SetSwitch(TruncateDecimalSwitch, truncateScaledDecimal); + appContextSwitchesHelper.TruncateScaledDecimalField = truncateScaledDecimal ? LocalAppContextSwitchesHelper.Tristate.True : LocalAppContextSwitchesHelper.Tristate.False; + var p = new SqlParameter("@Value", null) { Precision = 18, @@ -602,6 +607,8 @@ public static void TestScaledDecimalParameter_CommandInsert(string connectionStr [ClassData(typeof(ConnectionStringsProvider))] public static void TestScaledDecimalParameter_BulkCopy(string connectionString, bool truncateScaledDecimal) { + using LocalAppContextSwitchesHelper appContextSwitchesHelper = new(); + string tableName = DataTestUtility.GetUniqueNameForSqlServer("TestDecimalParameterBC"); using SqlConnection connection = InitialDatabaseTable(connectionString, tableName); try @@ -620,7 +627,7 @@ public static void TestScaledDecimalParameter_BulkCopy(string connectionString, } bulkCopy.DestinationTableName = tableName; - AppContext.SetSwitch(TruncateDecimalSwitch, truncateScaledDecimal); + appContextSwitchesHelper.TruncateScaledDecimalField = truncateScaledDecimal ? LocalAppContextSwitchesHelper.Tristate.True : LocalAppContextSwitchesHelper.Tristate.False; bulkCopy.WriteToServer(table); } Assert.True(ValidateInsertedValues(connection, tableName, truncateScaledDecimal), $"Invalid test happened with connection string [{connection.ConnectionString}]"); @@ -636,6 +643,8 @@ public static void TestScaledDecimalParameter_BulkCopy(string connectionString, [ClassData(typeof(ConnectionStringsProvider))] public static void TestScaledDecimalTVP_CommandSP(string connectionString, bool truncateScaledDecimal) { + using LocalAppContextSwitchesHelper appContextSwitchesHelper = new(); + string tableName = DataTestUtility.GetUniqueNameForSqlServer("TestDecimalParameterBC"); string tableTypeName = DataTestUtility.GetUniqueNameForSqlServer("UDTTTestDecimalParameterBC"); string spName = DataTestUtility.GetUniqueNameForSqlServer("spTestDecimalParameterBC"); @@ -663,7 +672,7 @@ public static void TestScaledDecimalTVP_CommandSP(string connectionString, bool table.Rows.Add(newRow); } p.Value = table; - AppContext.SetSwitch(TruncateDecimalSwitch, truncateScaledDecimal); + appContextSwitchesHelper.TruncateScaledDecimalField = truncateScaledDecimal ? LocalAppContextSwitchesHelper.Tristate.True : LocalAppContextSwitchesHelper.Tristate.False; cmd.ExecuteNonQuery(); } // TVP always rounds data without attention to the configuration. diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AdjustPrecScaleForBulkCopy.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AdjustPrecScaleForBulkCopy.cs index 72bab47869..117ab67c0d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AdjustPrecScaleForBulkCopy.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AdjustPrecScaleForBulkCopy.cs @@ -5,6 +5,7 @@ using System; using System.Data; using System.Data.SqlTypes; +using Microsoft.Data.SqlClient.Tests.Common; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests @@ -14,6 +15,7 @@ public static class AdjustPrecScaleForBulkCopy [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] public static void RunTest() { + using LocalAppContextSwitchesHelper appContextSwitches = new(); SqlDecimal value = BulkCopySqlDecimalToTable(new SqlDecimal(0), 1, 0, 2, 2); Assert.Equal("0.00", value.ToString()); @@ -27,7 +29,7 @@ public static void RunTest() Assert.Equal("12.3", value.ToString()); value = BulkCopySqlDecimalToTable(new SqlDecimal(123.45), 10, 2, 4, 1); - if (AppContext.TryGetSwitch("Switch.Microsoft.Data.SqlClient.TruncateScaledDecimal", out bool switchValue) && switchValue) + if (appContextSwitches.TruncateScaledDecimal) { Assert.Equal("123.4", value.ToString()); } diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props new file mode 100644 index 0000000000..958cc8e38f --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Packages.props b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Packages.props new file mode 100644 index 0000000000..b4f3c5f805 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Packages.props @@ -0,0 +1,14 @@ + + + + + true + true + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.cs new file mode 100644 index 0000000000..1dcbde121f --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace Monitoring +{ + public interface IMonitorLoader + { + string HostMachine { get; set; } + string AssemblyPath { get; set; } + string TestName { get; set; } + bool Enabled { get; set; } + + void Action(MonitorLoaderUtils.MonitorAction monitoraction); + void AddPerfData(MonitorMetrics data); + Dictionary GetPerfData(); + } + + public class MonitorLoaderUtils + { + public enum MonitorAction + { + Initialize, + Start, + Stop, + DoNothing + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.csproj b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.csproj new file mode 100644 index 0000000000..5d04960f79 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.csproj @@ -0,0 +1,8 @@ + + + Monitoring + Monitoring + net481;net9.0 + latest + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/MonitorMetrics.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/MonitorMetrics.cs new file mode 100644 index 0000000000..ed37544e4e --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/MonitorMetrics.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Monitoring +{ + public class MonitorMetrics + { + private string _name; + private string _strValue; + private string _unit; + private bool _isPrimary; + private bool _isHigherBetter; + private double _dblValue; + private long _lngValue; + private char _valueType; // D=double, L=long, S=String + + public MonitorMetrics(string name, string value, string unit, bool HigherIsBetter, bool Primary) + { + _name = name; + _strValue = value; + _unit = unit; + _valueType = 'S'; + _isHigherBetter = HigherIsBetter; + _isPrimary = Primary; + } + + public MonitorMetrics(string name, double value, string unit, bool HigherIsBetter, bool Primary) + { + _name = name; + _dblValue = value; + _unit = unit; + _valueType = 'D'; + _isHigherBetter = HigherIsBetter; + _isPrimary = Primary; + } + + public MonitorMetrics(string name, long value, string unit, bool HigherIsBetter, bool Primary) + { + _name = name; + _lngValue = value; + _unit = unit; + _valueType = 'L'; + _isHigherBetter = HigherIsBetter; + _isPrimary = Primary; + } + + public string GetName() + { + return _name; + } + + public string GetUnit() + { + return _unit; + } + + public bool GetPrimary() + { + return _isPrimary; + } + + public bool GetHigherIsBetter() + { + return _isHigherBetter; + } + + public char GetValueType() + { + return _valueType; + } + + public string GetStringValue() + { + if (_valueType == 'S') + return _strValue; + throw new Exception("Value is not a string"); + } + + public double GetDoubleValue() + { + if (_valueType == 'D') + return _dblValue; + throw new Exception("Value is not a double"); + } + + public long GetLongValue() + { + if (_valueType == 'L') + return _lngValue; + throw new Exception("Value is not a long"); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/Readme.md b/src/Microsoft.Data.SqlClient/tests/StressTests/Readme.md new file mode 100644 index 0000000000..16ec09a874 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/Readme.md @@ -0,0 +1,183 @@ +# Microsoft.Data.SqlClient Stress Test + +This Stress testing application for `Microsoft.Data.SqlClient` is under progress. +This project intends to help finding a certain level of effectiveness under unfavorable conditions, and verifying the mode of failures. +This is a console application with targeting frameworks `.Net Framework 4.8.1`, `.NET 9.0` under driver's supported operating systems and SQL Servers. + +## Purpose of application for developers + +Define fuzz tests for all new features/APIs in the driver and to be run before every GA release. + +## Pre-Requisites + +Required in StressTest.config: + +|Field|Values|Description| +|-|-|-| +|`name`||Stress testing source configuration name.| +|`type`|`SqlServer`|Only `SqlServer` is acceptable.| +|`isDefault`|`true`, `false`|If there is a source node with `isDefault=true`, this node is returned.| +|`dataSource`||SQL Server data source name.| +|`database`||Targeting database name in the SQL Server.| +|`user`||User Id to connect the server.| +|`password`||Paired password with the user.| +|`supportsWindowsAuthentication`|`true`, `false`|Tries to use integrated security in connection string mixed with SQL Server authentication if it set to `true` by applying the randomization.| +|`isLocal`|`true`, `false`|`true` means database is local.| +|`disableMultiSubnetFailover`|`true`, `false`|Tries to add Multi-subnet Failover fake host entries when it equals `true`.| +|`disableNamedPipes`|`true`, `false`|`true` means the connections will create just using tcp protocol.| +|`encrypt`|`true`, `false`|Assigns the encrypt property of the connection strings.| + +## Adding new Tests + +- [ToDo] + +## Building the application + +To build the application we need to run the command: + +```bash +dotnet build <-f|--framework > [-c|--configuration ] +``` + +The path should be pointing to SqlClient.Stress.Runner.csproj file. + +```bash +# Builds the application for the Client Os in `Debug` Configuration for `AnyCpu` platform. +# All supported target frameworks, .NET Framework (NetFx) and .NET Core drivers are built by default (as supported by Client OS). + +> dotnet build +``` + +```bash +# Build the application for .Net framework 4.8.1 with `Debug` configuration. + +> dotnet build -f net481 +``` + +```bash +# Build the application for .Net 9.0 with `Release` configuration. + +> dotnet build -f net9.0 -c Release +``` + +```bash +# Cleans all build directories + +> dotnet clean +``` + +## Running tests + +After building the application, find the built folder with target framework and run the `stresstest.exe` file with required arguments. +Find the result in a log file inside the `logs` folder besides the command prompt. + +## Command prompt + +You must run the stress tests from the root of the Stress Tests project +directory (i.e. the same directory this readme file is in). + +```bash +# Linux +$ cd /home/paul/dev/SqlClient/src/Microsoft.Data.SqlClient/tests/StressTests + +# Via dotnet run CLI: +$ dotnet run -no-build -f net9.0 --project SqlClient.Stress.Runner/SqlClient.Stress.Runner.csproj -- -a SqlClient.Stress.Tests + +# Via dotnet CLI: +$ dotnet SqlClient.Stress.Runner/bin/Debug/net9.0/stresstest.dll -a SqlClient.Stress.Tests +``` + +```powershell +# Windows +> cd \dev\SqlClient\src\Microsoft.Data.SqlClient\tests\StressTests + +# Via dotnet run CLI: +$ dotnet run -no-build -f net9.0 --project SqlClient.Stress.Runner\SqlClient.Stress.Runner.csproj -- -a SqlClient.Stress.Tests + +# Via executable: +> .\SqlClient.Stress.Runner\bin\Debug\net481\stresstest.exe -a SqlClient.Stress.Tests +``` + +## Supported arguments + +|Argument|Values|Description| +|-|-|-| +|-all||Run all tests - best for debugging, not perf measurements.| +|-verify||Run in functional verification mode. [not implemented]| +|-duration|<n>|Duration of the test in seconds. Default value is 1 second.| +|-threads|<n>|Number of threads to use. Default value is 16.| +|-override|<name> <value>|Override the value of a test property.| +|-test|<name1;name2>|Run specific test(s).| +|-debug||Print process ID in the beginning and wait for Enter (to give your time to attach the debugger).| +|-exceptionThreshold|<n>|An optional limit on exceptions which will be caught. When reached, test will halt.| +|-monitorenabled|true, false|True or False to enable monitoring. Default is false [not implemented]| +|-randomSeed||Enables setting of the random number generator used internally. This serves both the purpose of helping to improve reproducibility and making it deterministic from Chess's perspective for a given schedule. Default is 0.| +|-filter|<filter>|Run tests whose stress test attributes match the given filter. Filter is not applied if attribute does not implement ITestAttributeFilter. Example: -filter TestType=Query,Update;IsServerTest=True| +|-printMethodName||Print tests' title in console window| +|-deadlockdetection|true, false|True or False to enable deadlock detection. Default is `false`.| + +```bash +# Run the application for a built target framework and all discovered tests without debugger attached. + +> .\stresstest.exe -a SqlClient.Stress.Tests -all +``` + +```bash +# Run the application for a built target framework and all discovered tests without debugger attached and shows the test methods' names. + +> .\stresstest.exe -a SqlClient.Stress.Tests -all -printMethodName +``` + +```bash +# Run the application for a built target framework and all discovered tests and will wait for debugger to be attached. + +> .\stresstest.exe -a SqlClient.Stress.Tests -all -debug +``` + +```bash +# Run the application for a built target framework and "TestExecuteXmlReaderAsyncCancellation" test without debugger attached. + +> .\stresstest.exe -a SqlClient.Stress.Tests -test TestExecuteXmlReaderAsyncCancellation +``` + +```bash +# Run the application for a built target framework and "TestExecuteXmlReaderAsyncCancellation" test without debugger attached. + +> .\stresstest.exe -a SqlClient.Stress.Tests -test TestExecuteXmlReaderAsyncCancellation +``` + +```bash +# Run the application for a built target framework and all discovered tests without debugger attached for 10 seconds. + +> .\stresstest.exe -a SqlClient.Stress.Tests -all -duration 10 +``` + +```bash +# Run the application for a built target framework and all discovered tests without debugger attached with 5 threads. + +> .\stresstest.exe -a SqlClient.Stress.Tests -all -threads 5 +``` + +```bash +# Run the application for a built target framework and all discovered tests without debugger attached and dead lock detection process. + +> .\stresstest.exe -a SqlClient.Stress.Tests -all -deadlockdetection true +``` + +```bash +# Run the application for a built target framework and all discovered tests without debugger attached with overriding the weight property with value 15. + +> .\stresstest.exe -a SqlClient.Stress.Tests -all -override Weight 15 +``` + +```bash +# Run the application for a built target framework and all discovered tests without debugger attached with injecting random seed of 5. + +> .\stresstest.exe -a SqlClient.Stress.Tests -all -randomSeed 5 +``` + +## Further thoughts + +- Implement the uncompleted arguments. +- Add more tests. +- Add support running tests with **System.Data.SqlClient** too. diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalExceptionHandlerAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalExceptionHandlerAttribute.cs new file mode 100644 index 0000000000..810580d9f8 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalExceptionHandlerAttribute.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace DPStressHarness +{ + [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public class GlobalExceptionHandlerAttribute : Attribute + { + public GlobalExceptionHandlerAttribute() + { + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestCleanupAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestCleanupAttribute.cs new file mode 100644 index 0000000000..2159d2630e --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestCleanupAttribute.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace DPStressHarness +{ + [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public class GlobalTestCleanupAttribute : Attribute + { + public GlobalTestCleanupAttribute() + { + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestSetupAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestSetupAttribute.cs new file mode 100644 index 0000000000..00ed3d5b05 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestSetupAttribute.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace DPStressHarness +{ + [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public class GlobalTestSetupAttribute : Attribute + { + public GlobalTestSetupAttribute() + { + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestAttribute.cs new file mode 100644 index 0000000000..3146c2d808 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestAttribute.cs @@ -0,0 +1,272 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace DPStressHarness +{ + public enum TestPriority + { + BVT = 0, + High = 1, + Medium = 2, + Low = 3 + } + + public class TestAttributeBase : Attribute + { + private string _title; + private string _description = "none provided"; + private string _applicationName = "unknown"; + private string _improvement = "ADONETV3"; + private string _owner = "unknown"; + private string _category = "unknown"; + private TestPriority _priority = TestPriority.BVT; + + public TestAttributeBase(string title) + { + _title = title; + } + + public string Title + { + get { return _title; } + set { _title = value; } + } + + public string Description + { + get { return _description; } + set { _description = value; } + } + + public string Improvement + { + get { return _improvement; } + set { _improvement = value; } + } + + public string Owner + { + get { return _owner; } + set { _owner = value; } + } + + public string ApplicationName + { + get { return _applicationName; } + set { _applicationName = value; } + } + + public TestPriority Priority + { + get { return _priority; } + set { _priority = value; } + } + + public string Category + { + get { return _category; } + set { _category = value; } + } + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class TestAttribute : TestAttributeBase + { + private int _warmupIterations = 0; + private int _testIterations = 1; + + public TestAttribute(string title) : base(title) + { + } + + public int WarmupIterations + { + get + { + string propName = "WarmupIterations"; + + if (TestMetrics.Overrides.ContainsKey(propName)) + { + return int.Parse(TestMetrics.Overrides[propName]); + } + else + { + return _warmupIterations; + } + } + set { _warmupIterations = value; } + } + + public int TestIterations + { + get + { + string propName = "TestIterations"; + + if (TestMetrics.Overrides.ContainsKey(propName)) + { + return int.Parse(TestMetrics.Overrides[propName]); + } + else + { + return _testIterations; + } + } + set { _testIterations = value; } + } + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class StressTestAttribute : TestAttributeBase + { + private int _weight = 1; + + public StressTestAttribute(string title) + : base(title) + { + } + + public int Weight + { + get { return _weight; } + set { _weight = value; } + } + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class MultiThreadedTestAttribute : TestAttributeBase + { + private int _warmupDuration = 60; + private int _testDuration = 60; + private int _threads = 16; + + public MultiThreadedTestAttribute(string title) + : base(title) + { + } + + public int WarmupDuration + { + get + { + string propName = "WarmupDuration"; + + if (TestMetrics.Overrides.ContainsKey(propName)) + { + return int.Parse(TestMetrics.Overrides[propName]); + } + else + { + return _warmupDuration; + } + } + set { _warmupDuration = value; } + } + + public int TestDuration + { + get + { + string propName = "TestDuration"; + + if (TestMetrics.Overrides.ContainsKey(propName)) + { + return int.Parse(TestMetrics.Overrides[propName]); + } + else + { + return _testDuration; + } + } + set { _testDuration = value; } + } + + public int Threads + { + get + { + string propName = "Threads"; + + if (TestMetrics.Overrides.ContainsKey(propName)) + { + return int.Parse(TestMetrics.Overrides[propName]); + } + else + { + return _threads; + } + } + set { _threads = value; } + } + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class ThreadPoolTestAttribute : TestAttributeBase + { + private int _warmupDuration = 60; + private int _testDuration = 60; + private int _threads = 64; + + public ThreadPoolTestAttribute(string title) + : base(title) + { + } + + public int WarmupDuration + { + get + { + string propName = "WarmupDuration"; + + if (TestMetrics.Overrides.ContainsKey(propName)) + { + return int.Parse(TestMetrics.Overrides[propName]); + } + else + { + return _warmupDuration; + } + } + set { _warmupDuration = value; } + } + + public int TestDuration + { + get + { + string propName = "TestDuration"; + + if (TestMetrics.Overrides.ContainsKey(propName)) + { + return int.Parse(TestMetrics.Overrides[propName]); + } + else + { + return _testDuration; + } + } + set { _testDuration = value; } + } + + public int Threads + { + get + { + string propName = "Threads"; + + if (TestMetrics.Overrides.ContainsKey(propName)) + { + return int.Parse(TestMetrics.Overrides[propName]); + } + else + { + return _threads; + } + } + set { _threads = value; } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestCleanupAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestCleanupAttribute.cs new file mode 100644 index 0000000000..32bc5ee6bc --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestCleanupAttribute.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace DPStressHarness +{ + [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public class TestCleanupAttribute : Attribute + { + public TestCleanupAttribute() + { + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestSetupAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestSetupAttribute.cs new file mode 100644 index 0000000000..5626032b69 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestSetupAttribute.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace DPStressHarness +{ + [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)] + public class TestSetupAttribute : Attribute + { + public TestSetupAttribute() + { + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestVariationAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestVariationAttribute.cs new file mode 100644 index 0000000000..e54acfa969 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestVariationAttribute.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace DPStressHarness +{ + [AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = true)] + public class TestVariationAttribute : Attribute + { + private string _variationName; + private object _variationValue; + + public TestVariationAttribute(string variationName, object variationValue) + { + _variationName = variationName; + _variationValue = variationValue; + } + + public string VariationName + { + get { return _variationName; } + set { _variationName = value; } + } + + public object VariationValue + { + get { return _variationValue; } + set { _variationValue = value; } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetection.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetection.cs new file mode 100644 index 0000000000..50fc6d3d7a --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetection.cs @@ -0,0 +1,194 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace DPStressHarness +{ + public class DeadlockDetection + { + /// + /// Information for a thread relating to deadlock detection. All of its information is stored in a reference object to make updating it easier. + /// + private class ThreadInfo + { + public ThreadInfo(long dueTime) + { + this.DueTime = dueTime; + } + + /// + /// The time (in ticks) when the thread should be completed + /// + public long DueTime; + + /// + /// True if the thread should not be aborted + /// + public bool DisableAbort; + + /// + /// The time when DisableAbort was set to true + /// + public long DisableAbortTime; + } + + /// + /// Maximum time that a test thread (i.e. a thread that is directly executing a [StressTest] method) can + /// execute before it is considered to be deadlocked. This should be longer than the + /// TaskThreadDeadlockTimeoutTicks because if the test is waiting for a task then the test will always + /// take longer to execute than the task. + /// + public const long TestThreadDeadlockTimeoutTicks = 20 * 60 * TimeSpan.TicksPerSecond; + + /// + /// Maximum time that any Task can execute before it is considered to be deadlocked + /// + public const long TaskThreadDeadlockTimeoutTicks = 10 * 60 * TimeSpan.TicksPerSecond; + + /// + /// Dictionary that maps Threads to the time (in ticks) when they should be completed. If they are not completed by that time then + /// they are considered to be deadlocked. + /// + private static ConcurrentDictionary s_threadDueTimes = null; + + /// + /// Timer that scans through _threadDueTimes to find deadlocked threads + /// + private static Timer s_deadlockWatchdog = null; + + /// + /// Interval of _deadlockWatchdog, in milliseconds + /// + private const int _watchdogIntervalMs = 60 * 1000; + + /// + /// true if deadlock detection is enabled, otherwise false. Should be set only at process startup. + /// + private static bool s_isEnabled = false; + + public static bool IsEnabled => s_isEnabled; + + /// + /// Enables deadlock detection. + /// + public static void Enable() + { + // Switch out the default TaskScheduler. We must use reflection because it is private. + FieldInfo defaultTaskScheduler = typeof(TaskScheduler).GetField("s_defaultTaskScheduler", BindingFlags.NonPublic | BindingFlags.Static); + DeadlockDetectionTaskScheduler newTaskScheduler = new DeadlockDetectionTaskScheduler(); + defaultTaskScheduler.SetValue(null, newTaskScheduler); + + s_threadDueTimes = new ConcurrentDictionary(); + s_deadlockWatchdog = new Timer(CheckForDeadlocks, null, _watchdogIntervalMs, _watchdogIntervalMs); + + s_isEnabled = true; + } + + /// + /// Adds the current Task execution thread to the tracked thread collection. + /// + public static void AddTaskThread() + { + if (s_isEnabled) + { + long dueTime = DateTime.UtcNow.Ticks + TaskThreadDeadlockTimeoutTicks; + AddThread(dueTime); + } + } + + /// + /// Adds the current Test execution thread (i.e. a thread that is directly executing a [StressTest] method) to the tracked thread collection. + /// + public static void AddTestThread() + { + if (s_isEnabled) + { + long dueTime = DateTime.UtcNow.Ticks + TestThreadDeadlockTimeoutTicks; + AddThread(dueTime); + } + } + + private static void AddThread(long dueTime) + { + s_threadDueTimes.TryAdd(Thread.CurrentThread, new ThreadInfo(dueTime)); + } + + /// + /// Removes the current thread from the tracked thread collection + /// + public static void RemoveThread() + { + if (s_isEnabled) + { + ThreadInfo unused; + s_threadDueTimes.TryRemove(Thread.CurrentThread, out unused); + } + } + + /// + /// Disables abort of current thread. Call this when the current thread is waiting on a task. + /// + public static void DisableThreadAbort() + { + if (s_isEnabled) + { + ThreadInfo threadInfo; + if (s_threadDueTimes.TryGetValue(Thread.CurrentThread, out threadInfo)) + { + threadInfo.DisableAbort = true; + threadInfo.DisableAbortTime = DateTime.UtcNow.Ticks; + } + } + } + + /// + /// Enables abort of current thread after calling DisableThreadAbort(). The elapsed time since calling DisableThreadAbort() is added to the due time. + /// + public static void EnableThreadAbort() + { + if (s_isEnabled) + { + ThreadInfo threadInfo; + if (s_threadDueTimes.TryGetValue(Thread.CurrentThread, out threadInfo)) + { + threadInfo.DueTime += DateTime.UtcNow.Ticks - threadInfo.DisableAbortTime; + threadInfo.DisableAbort = false; + } + } + } + + /// + /// Looks through the tracked thread collection and aborts any thread that is past its due time + /// + /// unused + private static void CheckForDeadlocks(object state) + { + if (s_isEnabled) + { + long now = DateTime.UtcNow.Ticks; + + // Find candidate threads + foreach (var threadDuePair in s_threadDueTimes) + { + if (!threadDuePair.Value.DisableAbort && now > threadDuePair.Value.DueTime) + { + // Abort the misbehaving thread and the return + // NOTE: We only want to abort a single thread at a time to allow the other thread in the deadlock pair to continue + Thread t = threadDuePair.Key; + Console.WriteLine("Deadlock detected on thread with managed thread id {0}", t.ManagedThreadId); + Debugger.Break(); + t.Join(); + return; + } + } + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetectionTaskScheduler.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetectionTaskScheduler.cs new file mode 100644 index 0000000000..22a540def8 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetectionTaskScheduler.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace DPStressHarness +{ + public class DeadlockDetectionTaskScheduler : TaskScheduler + { + private readonly WaitCallback _runTaskCallback; + private readonly ParameterizedThreadStart _runTaskThreadStart; +#if DEBUG + private readonly ConcurrentDictionary _queuedItems = new ConcurrentDictionary(); +#endif + + public DeadlockDetectionTaskScheduler() + { + _runTaskCallback = new WaitCallback(RunTask); + _runTaskThreadStart = new ParameterizedThreadStart(RunTask); + } + + // This is only used for debugging, so for retail we'd prefer the perf + protected override IEnumerable GetScheduledTasks() + { +#if DEBUG + return _queuedItems.Keys; +#else + return new Task[0]; +#endif + } + + protected override void QueueTask(Task task) + { + if ((task.CreationOptions & TaskCreationOptions.LongRunning) == TaskCreationOptions.LongRunning) + { + // Create a new background thread for long running tasks + Thread thread = new Thread(_runTaskThreadStart) { IsBackground = true }; + thread.Start(task); + } + else + { + // Otherwise queue the work on the threadpool +#if DEBUG + _queuedItems.TryAdd(task, null); +#endif + + ThreadPool.QueueUserWorkItem(_runTaskCallback, task); + } + } + + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + if (!taskWasPreviouslyQueued) + { + // Run the task inline + RunTask(task); + return true; + } + + // Couldn't run the task + return false; + } + + private void RunTask(object state) + { + Task inTask = state as Task; + +#if DEBUG + // Remove from the dictionary of queued items + object ignored; + _queuedItems.TryRemove(inTask, out ignored); +#endif + + // Note when the thread started work + DeadlockDetection.AddTaskThread(); + + try + { + // Run the task + base.TryExecuteTask(inTask); + } + finally + { + // Remove the thread from the list when complete + DeadlockDetection.RemoveThread(); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/SqlClient.Stress.Common.csproj b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/SqlClient.Stress.Common.csproj new file mode 100644 index 0000000000..6950cb6af8 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/SqlClient.Stress.Common.csproj @@ -0,0 +1,8 @@ + + + + net481;net9.0 + latest + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/TestMetrics.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/TestMetrics.cs new file mode 100644 index 0000000000..054a822dc1 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/TestMetrics.cs @@ -0,0 +1,368 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Collections.Generic; +using System.Reflection; + +namespace DPStressHarness +{ + public static class TestMetrics + { + private const string _defaultValue = "unknown"; + + private static bool s_valid = false; + private static bool s_reset = true; + private static Stopwatch s_stopwatch = new Stopwatch(); + private static long s_workingSet; + private static long s_peakWorkingSet; + private static long s_privateBytes; + private static Assembly s_targetAssembly; + private static string s_fileVersion = _defaultValue; + private static string s_privateBuild = _defaultValue; + private static string s_runLabel = DateTime.Now.ToString(); + private static Dictionary s_overrides; + private static List s_variations = null; + private static List s_selectedTests = null; + private static bool s_isOfficial = false; + private static string s_milestone = _defaultValue; + private static string s_branch = _defaultValue; + private static List s_categories = null; + private static bool s_profileMeasuredCode = false; + private static int s_stressThreads = 16; + private static int s_stressDuration = 1; + private static int? s_exceptionThreshold = null; + private static bool s_monitorenabled = false; + private static string s_monitormachinename = "localhost"; + private static int s_randomSeed = 0; + private static string s_filter = null; + private static bool s_printMethodName = false; + + /// Starts the sample profiler. + /// + /// Do not inline to avoid errors when the functionality is not used + /// and the profiling DLL is not available. + /// + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static void InternalStartProfiling() + { + // Microsoft.VisualStudio.Profiler.DataCollection.StartProfile( + // Microsoft.VisualStudio.Profiler.ProfileLevel.Global, + // Microsoft.VisualStudio.Profiler.DataCollection.CurrentId); + } + + /// Stops the sample profiler. + /// + /// Do not inline to avoid errors when the functionality is not used + /// and the profiling DLL is not available. + /// + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + private static void InternalStopProfiling() + { + // Microsoft.VisualStudio.Profiler.DataCollection.StopProfile( + // Microsoft.VisualStudio.Profiler.ProfileLevel.Global, + // Microsoft.VisualStudio.Profiler.DataCollection.CurrentId); + } + + public static void StartCollection() + { + s_valid = false; + + s_stopwatch.Reset(); + s_stopwatch.Start(); + s_reset = true; + } + + public static void StartProfiling() + { + if (s_profileMeasuredCode) + { + InternalStartProfiling(); + } + } + + public static void StopProfiling() + { + if (s_profileMeasuredCode) + { + InternalStopProfiling(); + } + } + + public static void StopCollection() + { + s_stopwatch.Stop(); + + Process p = Process.GetCurrentProcess(); + s_workingSet = p.WorkingSet64; + s_peakWorkingSet = p.PeakWorkingSet64; + s_privateBytes = p.PrivateMemorySize64; + + s_valid = true; + } + + public static void PauseTimer() + { + s_stopwatch.Stop(); + } + + public static void UnPauseTimer() + { + if (s_reset) + { + s_stopwatch.Reset(); + s_reset = false; + } + + s_stopwatch.Start(); + } + + private static void ThrowIfInvalid() + { + if (!s_valid) throw new InvalidOperationException("Collection must be stopped before accessing this metric."); + } + + public static void Reset() + { + s_valid = false; + s_reset = true; + s_stopwatch = new Stopwatch(); + s_workingSet = new long(); + s_peakWorkingSet = new long(); + s_privateBytes = new long(); + s_targetAssembly = null; + s_fileVersion = _defaultValue; + s_privateBuild = _defaultValue; + s_runLabel = DateTime.Now.ToString(); + s_overrides = null; + s_variations = null; + s_selectedTests = null; + s_isOfficial = false; + s_milestone = _defaultValue; + s_branch = _defaultValue; + s_categories = null; + s_profileMeasuredCode = false; + s_stressThreads = 16; + s_stressDuration = 1; + s_exceptionThreshold = null; + s_monitorenabled = false; + s_monitormachinename = "localhost"; + s_randomSeed = 0; + s_filter = null; + s_printMethodName = false; + } + + public static string FileVersion + { + get { return s_fileVersion; } + set { s_fileVersion = value; } + } + + public static string PrivateBuild + { + get { return s_privateBuild; } + set { s_privateBuild = value; } + } + + public static Assembly TargetAssembly + { + get { return s_targetAssembly; } + + set + { + s_targetAssembly = value; + s_fileVersion = VersionUtil.GetFileVersion(s_targetAssembly.ManifestModule.FullyQualifiedName); + s_privateBuild = VersionUtil.GetPrivateBuild(s_targetAssembly.ManifestModule.FullyQualifiedName); + } + } + + public static string RunLabel + { + get { return s_runLabel; } + set { s_runLabel = value; } + } + + public static string Milestone + { + get { return s_milestone; } + set { s_milestone = value; } + } + + public static string Branch + { + get { return s_branch; } + set { s_branch = value; } + } + + public static bool IsOfficial + { + get { return s_isOfficial; } + set { s_isOfficial = value; } + } + + public static bool IsDefaultValue(string val) + { + return val.Equals(_defaultValue); + } + + public static double ElapsedSeconds + { + get + { + ThrowIfInvalid(); + return s_stopwatch.ElapsedMilliseconds / 1000.0; + } + } + + public static long WorkingSet + { + get + { + ThrowIfInvalid(); + return s_workingSet; + } + } + + public static long PeakWorkingSet + { + get + { + ThrowIfInvalid(); + return s_peakWorkingSet; + } + } + + public static long PrivateBytes + { + get + { + ThrowIfInvalid(); + return s_privateBytes; + } + } + + + public static Dictionary Overrides + { + get + { + if (s_overrides == null) + { + s_overrides = new Dictionary(8); + } + return s_overrides; + } + } + + public static List Variations + { + get + { + if (s_variations == null) + { + s_variations = new List(8); + } + + return s_variations; + } + } + + public static List SelectedTests + { + get + { + if (s_selectedTests == null) + { + s_selectedTests = new List(8); + } + + return s_selectedTests; + } + } + + public static bool IncludeTest(TestAttributeBase test) + { + if (s_selectedTests == null || s_selectedTests.Count == 0) + return true; // user has no selection - run all + else + return s_selectedTests.Contains(test.Title); + } + + public static List Categories + { + get + { + if (s_categories == null) + { + s_categories = new List(8); + } + + return s_categories; + } + } + + public static bool ProfileMeasuredCode + { + get { return s_profileMeasuredCode; } + set { s_profileMeasuredCode = value; } + } + + public static int StressDuration + { + get { return s_stressDuration; } + set { s_stressDuration = value; } + } + + public static int StressThreads + { + get { return s_stressThreads; } + set { s_stressThreads = value; } + } + + public static int? ExceptionThreshold + { + get { return s_exceptionThreshold; } + set { s_exceptionThreshold = value; } + } + + public static bool MonitorEnabled + { + get { return s_monitorenabled; } + set + { + if(value) + { + throw new NotImplementedException($"The '{nameof(MonitorEnabled)}' isn't fully implemented!"); + } + s_monitorenabled = value; + } + } + + + public static string MonitorMachineName + { + get { return s_monitormachinename; } + set { s_monitormachinename = value; } + } + + public static int RandomSeed + { + get { return s_randomSeed; } + set { s_randomSeed = value; } + } + + public static string Filter + { + get { return s_filter; } + set { s_filter = value; } + } + + public static bool PrintMethodName + { + get { return s_printMethodName; } + set { s_printMethodName = value; } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/VersionUtil.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/VersionUtil.cs new file mode 100644 index 0000000000..1778903834 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/VersionUtil.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Diagnostics; + +#pragma warning disable 618 + +namespace DPStressHarness +{ + public class VersionUtil + { + public static string GetFileVersion(string moduleName) + { + FileVersionInfo info = GetFileVersionInfo(moduleName); + return info.FileVersion; + } + + public static string GetPrivateBuild(string moduleName) + { + FileVersionInfo info = GetFileVersionInfo(moduleName); + return info.PrivateBuild; + } + + private static FileVersionInfo GetFileVersionInfo(string moduleName) + { + if (File.Exists(moduleName)) + { + return FileVersionInfo.GetVersionInfo(Path.GetFullPath(moduleName)); + } + else + { + string moduleInRuntimeDir = AppContext.BaseDirectory + moduleName; + return FileVersionInfo.GetVersionInfo(moduleInRuntimeDir); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/AsyncUtils.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/AsyncUtils.cs new file mode 100644 index 0000000000..84f0fba0de --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/AsyncUtils.cs @@ -0,0 +1,185 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Data.SqlClient; +using System.Linq; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; +using System.Xml; +using DPStressHarness; + +namespace Stress.Data +{ + public enum SyncAsyncMode + { + Sync, // call sync method, e.g. connection.Open(), and return completed task + SyncOverAsync, // call async method, e.g. connection.OpenAsync().Wait(), and return completed task + Async // call async method, e.g. connection.OpenAsync(), and return running task + } + + public static class AsyncUtils + { + public static Task SyncOrAsyncMethod(Func syncFunc, Func> asyncFunc, SyncAsyncMode mode) + { + switch (mode) + { + case SyncAsyncMode.Sync: + TResult result = syncFunc(); + return Task.FromResult(result); + + case SyncAsyncMode.SyncOverAsync: + Task t = asyncFunc(); + WaitAndUnwrapException(t); + return t; + + case SyncAsyncMode.Async: + return asyncFunc(); + + default: + throw new ArgumentException(mode.ToString()); + } + } + + public static Task SyncOrAsyncMethod(Action syncFunc, Func asyncFunc, SyncAsyncMode mode) + { + switch (mode) + { + case SyncAsyncMode.Sync: + syncFunc(); + return Task.CompletedTask; + + case SyncAsyncMode.SyncOverAsync: + Task t = asyncFunc(); + WaitAndUnwrapException(t); + return t; + + case SyncAsyncMode.Async: + return asyncFunc(); + + default: + throw new ArgumentException(mode.ToString()); + } + } + + public static void WaitAll(params Task[] ts) + { + DeadlockDetection.DisableThreadAbort(); + try + { + Task.WaitAll(ts); + } + finally + { + DeadlockDetection.EnableThreadAbort(); + } + } + + public static void WaitAllNullable(params Task[] ts) + { + DeadlockDetection.DisableThreadAbort(); + try + { + Task[] tasks = ts.Where(t => t != null).ToArray(); + Task.WaitAll(tasks); + } + finally + { + DeadlockDetection.EnableThreadAbort(); + } + } + + public static void WaitAndUnwrapException(Task t) + { + DeadlockDetection.DisableThreadAbort(); + try + { + t.Wait(); + } + catch (AggregateException ae) + { + // The callers of this API may not expect AggregateException, so throw the inner exception + // If AggregateException contains more than one InnerExceptions, throw it out as it is, + // because that is unexpected + if ((ae.InnerExceptions != null) && (ae.InnerExceptions.Count == 1)) + { + if (ae.InnerException != null) + { + ExceptionDispatchInfo info = ExceptionDispatchInfo.Capture(ae.InnerException); + info.Throw(); + } + } + + throw; + } + finally + { + DeadlockDetection.EnableThreadAbort(); + } + } + + public static T GetResult(IAsyncResult result) + { + return GetResult((Task)result); + } + + public static T GetResult(Task result) + { + DeadlockDetection.DisableThreadAbort(); + try + { + return result.Result; + } + finally + { + DeadlockDetection.EnableThreadAbort(); + } + } + + public static SqlDataReader ExecuteReader(SqlCommand command) + { + DeadlockDetection.DisableThreadAbort(); + try + { + return command.ExecuteReader(); + } + finally + { + DeadlockDetection.EnableThreadAbort(); + } + } + + public static int ExecuteNonQuery(SqlCommand command) + { + DeadlockDetection.DisableThreadAbort(); + try + { + return command.ExecuteNonQuery(); + } + finally + { + DeadlockDetection.DisableThreadAbort(); + } + } + + public static XmlReader ExecuteXmlReader(SqlCommand command) + { + DeadlockDetection.DisableThreadAbort(); + try + { + return command.ExecuteXmlReader(); + } + finally + { + DeadlockDetection.EnableThreadAbort(); + } + } + + public static SyncAsyncMode ChooseSyncAsyncMode(Random rnd) + { + // Any mode is allowed + return (SyncAsyncMode)rnd.Next(3); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataSource.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataSource.cs new file mode 100644 index 0000000000..5c0fd3e362 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataSource.cs @@ -0,0 +1,160 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace Stress.Data +{ + /// + /// supported source types - values for 'type' attribute for 'source' node in App.config + /// + public enum DataSourceType + { + SqlServer + } + + /// + /// base class for database source information (SQL Server, Oracle Server, Access Database file, etc...). + /// Data sources are loaded from the app config file. + /// + public abstract class DataSource + { + /// + /// name of the source - can be used in command line: StressTest ... -override source "sourcename" + /// + public readonly string Name; + + /// + /// database type + /// + public readonly DataSourceType Type; + + /// + /// whether this source is the default one for the type specified + /// + public readonly bool IsDefault; + + /// + /// constructs new data source - called by derived class c-tors only (thus protected) + /// + protected DataSource(string name, DataSourceType type, bool isDefault) + { + this.Name = name; + this.Type = type; + this.IsDefault = isDefault; + } + + /// + /// this method is used to create the data source, based on its type + /// + public static DataSource Create(string name, DataSourceType sourceType, bool isDefault, IDictionary properties) + { + switch (sourceType) + { + case DataSourceType.SqlServer: + return new SqlServerDataSource(name, isDefault, properties); + default: + throw new ArgumentException("Wrong source type value: " + sourceType); + } + } + + /// + /// used by GetRequiredAttributeValue or derived classes to construct exception on missing required attribute + /// + /// name of the source (from XML) to include in exception message (for troubleshooting) + protected Exception MissingAttributeValueException(string sourceName, string attributeName) + { + return new ArgumentException(string.Format("Missing or empty value for {0} attribute in the config file for source: {1}", attributeName, sourceName)); + } + + /// + /// search for required attribute or fail if not found + /// + protected string GetRequiredAttributeValue(string sourceName, IDictionary properties, string valueName, bool allowEmpty) + { + string value; + if (!properties.TryGetValue(valueName, out value) || (value == null) || (!allowEmpty && value.Length == 0)) + { + throw MissingAttributeValueException(sourceName, valueName); + } + return value; + } + + /// + /// search for optional attribute or return default vale + /// + protected string GetOptionalAttributeValue(IDictionary properties, string valueName, string defaultValue) + { + string value; + if (!properties.TryGetValue(valueName, out value) || (value == null)) + { + value = defaultValue; + } + return value; + } + } + + /// + /// Represents SQL Server data source. This source is used by SqlClient as well as by ODBC and OLEDB when connecting to SQL with SNAC or MDAC/WDAC + /// + /// + /// + /// + /// + public class SqlServerDataSource : DataSource + { + public readonly string DataSource; + public readonly string Database; + public readonly bool IsLocal; + public readonly bool Encrypt; + + // if user and password are set, test can create connection strings with SQL auth settings + public readonly string User; + public readonly string Password; + + // if true, test can create connnection strings with integrated security (trusted connection) set to true (or SSPI). + public readonly bool SupportsWindowsAuthentication; + + public bool DisableMultiSubnetFailoverSetup; + + public bool DisableNamedPipes; + + internal SqlServerDataSource(string name, bool isDefault, IDictionary properties) + : base(name, DataSourceType.SqlServer, isDefault) + { + this.DataSource = GetOptionalAttributeValue(properties, "dataSource", "localhost"); + this.Database = GetOptionalAttributeValue(properties, "database", "stress"); + + this.User = GetOptionalAttributeValue(properties, "user", string.Empty); + this.Password = GetOptionalAttributeValue(properties, "password", string.Empty); + + this.IsLocal = bool.Parse(GetOptionalAttributeValue(properties, "isLocal", bool.FalseString)); + this.Encrypt = bool.Parse(GetOptionalAttributeValue(properties, "encrypt", bool.FalseString)); + + this.DisableMultiSubnetFailoverSetup = bool.Parse(GetOptionalAttributeValue(properties, "DisableMultiSubnetFailoverSetup", bool.TrueString)); + + this.DisableNamedPipes = bool.Parse(GetOptionalAttributeValue(properties, "DisableNamedPipes", bool.TrueString)); + + string temp = GetOptionalAttributeValue(properties, "supportsWindowsAuthentication", "false"); + if (!string.IsNullOrEmpty(temp)) + SupportsWindowsAuthentication = Convert.ToBoolean(temp); + else + SupportsWindowsAuthentication = false; + + if (string.IsNullOrEmpty(User) && !SupportsWindowsAuthentication) + throw new ArgumentException("SQL Server settings should include either a valid User name or SupportsWindowsAuthentication=true"); + } + } + + +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressConnection.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressConnection.cs new file mode 100644 index 0000000000..46becd4897 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressConnection.cs @@ -0,0 +1,232 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Data.Common; +using Microsoft.Data.SqlClient; +using System.Threading; +using System.Threading.Tasks; + +namespace Stress.Data +{ + public class DataStressConnection : IDisposable + { + public DbConnection DbConnection { get; private set; } + private readonly bool _clearPoolBeforeClose; + public DataStressConnection(DbConnection conn, bool clearPoolBeforeClose = false) + { + if (conn == null) + throw new ArgumentException("Cannot pass in null DbConnection to make new DataStressConnection!"); + this.DbConnection = conn; + _clearPoolBeforeClose = clearPoolBeforeClose; + } + + private short _spid = 0; + + [ThreadStatic] + private static TrackedRandom t_randomInstance; + private static TrackedRandom RandomInstance + { + get + { + if (t_randomInstance == null) + t_randomInstance = new TrackedRandom(); + return t_randomInstance; + } + } + + public void Open() + { + bool sync = RandomInstance.NextBool(); + + if (sync) + { + OpenSync(); + } + else + { + Task t = OpenAsync(); + AsyncUtils.WaitAndUnwrapException(t); + } + } + + public async Task OpenAsync() + { + int startMilliseconds = Environment.TickCount; + try + { + await DbConnection.OpenAsync(); + } + catch (ObjectDisposedException e) + { + HandleObjectDisposedException(e, true); + throw; + } + catch (InvalidOperationException e) + { + int endMilliseconds = Environment.TickCount; + + // we may be able to handle this exception + HandleInvalidOperationException(e, startMilliseconds, endMilliseconds, true); + throw; + } + + GetSpid(); + } + + private void OpenSync() + { + int startMilliseconds = Environment.TickCount; + try + { + DbConnection.Open(); + } + catch (ObjectDisposedException e) + { + HandleObjectDisposedException(e, false); + throw; + } + catch (InvalidOperationException e) + { + int endMilliseconds = Environment.TickCount; + + // we may be able to handle this exception + HandleInvalidOperationException(e, startMilliseconds, endMilliseconds, false); + throw; + } + + GetSpid(); + } + + private void HandleObjectDisposedException(ObjectDisposedException e, bool async) + { + // Race condition in DbConnectionFactory.TryGetConnection results in an ObjectDisposedException when calling OpenAsync on a non-pooled connection + string methodName = async ? "OpenAsync()" : "Open()"; + throw DataStressErrors.ProductError( + "Hit ObjectDisposedException in SqlConnection." + methodName, e); + } + + private static int s_fastTimeoutCountOpen; // number of times hit by SqlConnection.Open + private static int s_fastTimeoutCountOpenAsync; // number of times hit by SqlConnection.OpenAsync + private static readonly DateTime s_startTime = DateTime.Now; + + private const int MaxFastTimeoutCountPerDay = 200; + + /// + /// Handles InvalidOperationException generated from Open or OpenAsync calls. + /// For any other type of Exception, it simply returns + /// + private void HandleInvalidOperationException(InvalidOperationException e, int startMilliseconds, int endMilliseconds, bool async) + { + int elapsedMilliseconds = unchecked(endMilliseconds - startMilliseconds); // unchecked to handle overflow of Environment.TickCount + + // Since InvalidOperationExceptions due to timeout can be caused by issues + // (e.g. network hiccup, server unavailable, etc) we need a heuristic to guess whether or not this exception + // should have happened or not. + bool wasTimeoutFromPool = (e.GetType() == typeof(InvalidOperationException)) && + (e.Message.StartsWith("Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool")); + + bool wasTooEarly = (elapsedMilliseconds < ((DbConnection.ConnectionTimeout - 5) * 1000)); + + if (wasTimeoutFromPool && wasTooEarly) + { + if (async) + Interlocked.Increment(ref s_fastTimeoutCountOpenAsync); + else + Interlocked.Increment(ref s_fastTimeoutCountOpen); + } + } + + /// + /// Gets spid value. + /// + /// + /// If we want to kill the connection, we get its spid up front before the test case uses the connection. Otherwise if + /// we try to get the spid when KillConnection is called, then the connection could be in a bad state (e.g. enlisted in + /// aborted transaction, or has open datareader) and we will fail to get the spid. Also the randomization is put here + /// instead of in KillConnection because otherwise this method would execute a command for every single connection which + /// most of the time will not be used later. + /// + private void GetSpid() + { + if (DbConnection is SqlConnection && RandomInstance.Next(0, 20) == 0) + { + using (var cmd = DbConnection.CreateCommand()) + { + cmd.CommandText = "select @@spid"; + _spid = (short)cmd.ExecuteScalar(); + } + } + else + { + _spid = 0; + } + } + + /// + /// Kills the given connection using "kill [spid]" if the parameter is nonzero + /// + private void KillConnection() + { + DataStressErrors.Assert(_spid != 0, "Called KillConnection with spid != 0"); + + using (var killerConn = DataTestGroup.Factory.CreateConnection()) + { + killerConn.Open(); + + using (var killerCmd = killerConn.CreateCommand()) + { + killerCmd.CommandText = "begin try kill " + _spid + " end try begin catch end catch"; + killerCmd.ExecuteNonQuery(); + } + } + } + + /// + /// Kills the given connection using "kill [spid]" if the parameter is nonzero + /// + /// a Task that is asynchronously killing the connection, or null if the connection is not being killed + public Task KillConnectionAsync() + { + if (_spid == 0) + return null; + else + return Task.Factory.StartNew(() => KillConnection()); + } + + public void Close() + { + if (_spid != 0) + { + KillConnection(); + + // Wait before putting the connection back in the pool, to ensure that + // the pool checks the connection the next time it is used. + Task.Delay(10).ContinueWith((t) => DbConnection.Close()); + } + else + { + // If this is a SqlConnection, and it is a connection with a unique connection string that we will never use again, + // then call SqlConnection.ClearPool() before closing so that it is fully closed and does not waste client & server resources. + if (_clearPoolBeforeClose) + { + SqlConnection sqlConn = DbConnection as SqlConnection; + if (sqlConn != null) SqlConnection.ClearPool(sqlConn); + } + + DbConnection.Close(); + } + } + + public void Dispose() + { + Close(); + } + + public DbCommand CreateCommand() + { + return DbConnection.CreateCommand(); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressErrors.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressErrors.cs new file mode 100644 index 0000000000..46b7751d50 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressErrors.cs @@ -0,0 +1,215 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; + +namespace Stress.Data +{ + public enum ErrorHandlingAction + { + // If you add an item here, remember to add it to all of the methods below + DebugBreak, + ThrowException + } + + /// + /// Static class containing methods to report errors. + /// + /// The StressTest executor will eat exceptions that are thrown and write them out to the console. In theory these should all be + /// either harmless exceptions or product bugs, however at present there are a large number of test issues that will cause a flood + /// of exceptions. Therefore if something actually bad happens (e.g. a known product bug is hit due to regression, or a major test + /// programming error) this error would be easy to miss if it were reported just by throwing an exception. To solve this, we use + /// this class for structured & consistent handling of errors. + /// + public static class DataStressErrors + { + private static void DebugBreak(string message, Exception exception) + { + // Print out the error before breaking to make debugging easier + Console.WriteLine(message); + if (exception != null) + { + Console.WriteLine(exception); + } + + Debugger.Break(); + } + + /// + /// Reports that a product bug has been hit. The action that will be taken is configurable in the .config file. + /// This can be used to check for regressions of known product bugs. + /// + /// A description of the product bug hit (e.g. title, bug number & database, more information) + /// The exception that was thrown that indicates a product bug, or null if the product bug was detected without + /// having thrown an exception + /// An exception that the caller should throw. + public static Exception ProductError(string description, Exception exception = null) + { + switch (DataStressSettings.Instance.ActionOnProductError) + { + case ErrorHandlingAction.DebugBreak: + DebugBreak("Hit product error: " + description, exception); + return new ProductErrorException(description, exception); + + case ErrorHandlingAction.ThrowException: + return new ProductErrorException(description, exception); + + default: + throw UnhandledCaseError(DataStressSettings.Instance.ActionOnProductError); + } + } + + /// + /// Reports that a non-fatal test error has been hit. The action that will be taken is configurable in the .config file. + /// This should be used for test errors that do not prevent the test from running. + /// + /// A description of the error + /// The exception that was thrown that indicates an error, or null if the error was detected without + /// An exception that the caller should throw. + public static Exception TestError(string description, Exception exception = null) + { + switch (DataStressSettings.Instance.ActionOnTestError) + { + case ErrorHandlingAction.DebugBreak: + DebugBreak("Hit test error: " + description, exception); + return new TestErrorException(description, exception); + + case ErrorHandlingAction.ThrowException: + return new TestErrorException(description, exception); + + default: + throw UnhandledCaseError(DataStressSettings.Instance.ActionOnTestError); + } + } + + /// + /// Reports that a programming error in the test code has occurred. The action that will be taken is configurable in the .config file. + /// This must strictly be used to report programming errors. It should not be in any way possible to see one of these errors unless + /// you make an incorrect change to the code, for example having an unhandled case in a switch statement. + /// + /// A description of the error + /// The exception that was thrown that indicates an error, or null if the error was detected without + /// having thrown an exception + /// An exception that the caller should throw. + private static Exception ProgrammingError(string description, Exception exception = null) + { + switch (DataStressSettings.Instance.ActionOnProgrammingError) + { + case ErrorHandlingAction.DebugBreak: + DebugBreak("Hit programming error: " + description, exception); + return new ProgrammingErrorException(description, exception); + + case ErrorHandlingAction.ThrowException: + return new ProgrammingErrorException(description, exception); + + default: + // If we are here then it's a programming error, but calling UnhandledCaseError here would cause an inifite loop. + goto case ErrorHandlingAction.DebugBreak; + } + } + + /// + /// Reports that an unhandled case in a switch statement in the test code has occurred. The action that will be taken is configurable + /// as a programming error in the .config file. It should not be in any way possible to see one of these errors unless + /// you make an incorrect change to the test code, for example having an unhandled case in a switch statement. + /// + /// The value that was not handled in the switch statement + /// An exception that the caller should throw. + public static Exception UnhandledCaseError(T unhandledValue) + { + return ProgrammingError("Unhandled case in switch statement: " + unhandledValue); + } + + /// + /// Asserts that a condition is true. If the condition is false then throws a ProgrammingError. + /// This must strictly be used to report programming errors. It should not be in any way possible to see one of these errors unless + /// you make an incorrect change to the code, for example having an unhandled case in a switch statement. + /// + /// A condition to assert + /// A description of the error + /// if the condition is false + public static void Assert(bool condition, string description) + { + if (!condition) + { + throw ProgrammingError(description); + } + } + + /// + /// Reports that a fatal error has happened. This is an error that completely prevents the test from continuing, + /// for example a setup failure. Ordinary programming errors should not be handled by this method. + /// + /// A description of the error + /// An exception that the caller should throw. + public static Exception FatalError(string description) + { + Console.WriteLine("Fatal test error: {0}", description); + Debugger.Break(); // Give the user a chance to debug + Environment.FailFast("Fatal error. Exit."); + return new Exception(); // Caller should throw this to indicate to the compiler that any code after the call is unreachable + } + + #region Exception types + + // These exception types are provided so that they can be easily found in logs, i.e. just do a text search in the console + // output log for "ProductErrorException" + + private class ProductErrorException : Exception + { + public ProductErrorException() + : base() + { + } + + public ProductErrorException(string message) + : base(message) + { + } + + public ProductErrorException(string message, Exception innerException) + : base(message, innerException) + { + } + } + + private class ProgrammingErrorException : Exception + { + public ProgrammingErrorException() + : base() + { + } + + public ProgrammingErrorException(string message) + : base(message) + { + } + + public ProgrammingErrorException(string message, Exception innerException) + : base(message, innerException) + { + } + } + + private class TestErrorException : Exception + { + public TestErrorException() + : base() + { + } + + public TestErrorException(string message) + : base(message) + { + } + + public TestErrorException(string message, Exception innerException) + : base(message, innerException) + { + } + } + #endregion + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressFactory.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressFactory.cs new file mode 100644 index 0000000000..b35f6fa7b7 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressFactory.cs @@ -0,0 +1,952 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Data; +using System.Data.Common; +using System.Diagnostics; + +namespace Stress.Data +{ + /// + /// Base class to generate utility objects required for stress tests to run. For example: connection strings, command texts, + /// data tables and views, and other information + /// + public abstract class DataStressFactory : IDisposable + { + // This is the maximum number of rows, stress will operate on + public const int Depth = 100; + + // A string value to be used for scalar data retrieval while constructing + // a select statement that retrieves multiple result sets. + public static readonly string LargeStringParam = new string('p', 2000); + + // A temp table that when create puts the server session into a non-recoverable state until dropped. + private static readonly string s_tempTableName = string.Format("#stress_{0}", Guid.NewGuid().ToString("N")); + + // The languages used for "SET LANGUAGE [language]" statements that modify the server session state. Let's + // keep error message readable so we're only using english languages. + private static string[] s_languages = new string[] + { + "English", + "British English", + }; + + public DbProviderFactory DbFactory { get; private set; } + + protected DataStressFactory(DbProviderFactory factory) + { + DataStressErrors.Assert(factory != null, "Argument to DataStressFactory constructor is null"); + this.DbFactory = factory; + } + + + public void Dispose() + { + GC.SuppressFinalize(this); + } + + public abstract string GetParameterName(string pName); + + + public abstract bool PrimaryKeyValueIsRequired + { + get; + } + + [Flags] + public enum SelectStatementOptions + { + UseNOLOCK = 0x1, + + // keep last + Default = 0 + } + + #region PoolingStressMode + + public enum PoolingStressMode + { + RandomizeConnectionStrings, // Use many different connection strings with the same identity, which will result in many DbConnectionPoolGroups each containing one DbConnectionPool + } + + protected PoolingStressMode CurrentPoolingStressMode + { + get; + private set; + } + + #endregion + + + /// + /// Creates a new connection and initializes it with random connection string generated from the factory's source + /// Note: if rnd is null, create a connection with minimal string required to connect to the target database + /// + /// Randomizes Connection Pool enablement, the application Name to randomize connection pool + /// + /// + public DataStressConnection CreateConnection(Random rnd = null, ConnectionStringOptions options = ConnectionStringOptions.Default) + { + // Determine connection options (connection string, identity, etc) + string connectionString = CreateBaseConnectionString(rnd, options); + bool clearPoolBeforeClose = false; + + if (rnd != null) + { + // Connection string and/or identity are randomized + + // We implement this using the Application Name field in the connection string since this field + // should not affect behaviour other than connection pooling, since all connections in a pool + // must have the exact same connection string (including Application Name) + + if (rnd.NextBool(.1)) + { + // Disable pooling + connectionString += ";Pooling=false;"; + } + else if (rnd.NextBool(0.001)) + { + // Use a unique Application Name to get a new connection from a new pool. We do this in order to + // stress the code that creates/deletes pools. + connectionString = string.Format("{0}; Pooling=true; Application Name=\"{1}\";", connectionString, GetRandomApplicationName()); + + // Tell DataStressConnection to call SqlConnection.ClearPool when closing the connection. This ensures + // we do not keep a large number of connections in the pool that we will never use again. + clearPoolBeforeClose = true; + } + else + { + switch (CurrentPoolingStressMode) + { + case PoolingStressMode.RandomizeConnectionStrings: + // Use one of the pre-generated Application Names in order to get a pooled connection with a randomized connection string + connectionString = string.Format("{0}; Pooling=true; Application Name=\"{1}\";", connectionString, _applicationNames[rnd.Next(_applicationNames.Count)]); + break; + default: + throw DataStressErrors.UnhandledCaseError(CurrentPoolingStressMode); + } + } + } + + // All options have been determined, now create + DbConnection con = DbFactory.CreateConnection(); + con.ConnectionString = connectionString; + return new DataStressConnection(con, clearPoolBeforeClose); + } + + [Flags] + public enum ConnectionStringOptions + { + Default = 0, + + // by default, MARS is disabled + EnableMars = 0x2, + + // by default, MultiSubnetFailover is enabled + DisableMultiSubnetFailover = 0x8 + } + + /// + /// Creates a new connection string. + /// Note: if rnd is null, create minimal connection string required to connect to the target database (used during setup) + /// Otherwise, string is randomized to enable multiple pools. + /// + public abstract string CreateBaseConnectionString(Random rnd, ConnectionStringOptions options); + + protected virtual int GetNumDifferentApplicationNames() + { + return DataStressSettings.Instance.NumberOfConnectionPools; + } + + private string GetRandomApplicationName() + { + return Guid.NewGuid().ToString(); + } + + + /// + /// Returns index of a random table + /// This will be used to narrow down memory leaks + /// related to specific tables. + /// + public TableMetadata GetRandomTable(Random rnd) + { + return TableMetadataList[rnd.Next(TableMetadataList.Count)]; + } + + /// + /// Returns a random command object + /// + public DbCommand GetCommand(Random rnd, TableMetadata table, DataStressConnection conn, bool query, bool isXml = false) + { + if (query) + { + return GetSelectCommand(rnd, table, conn, isXml); + } + else + { + // make sure arguments are correct + DataStressErrors.Assert(!isXml, "wrong usage of GetCommand: cannot create command with FOR XML that is not query"); + + int select = rnd.Next(4); + switch (select) + { + case 0: + return GetUpdateCommand(rnd, table, conn); + case 1: + return GetInsertCommand(rnd, table, conn); + case 2: + return GetDeleteCommand(rnd, table, conn); + default: + return GetSelectCommand(rnd, table, conn); + } + } + } + + private DbCommand CreateCommand(Random rnd, DataStressConnection conn) + { + DbCommand cmd; + if (conn == null) + { + cmd = DbFactory.CreateCommand(); + } + else + { + cmd = conn.CreateCommand(); + } + + if (rnd != null) + { + cmd.CommandTimeout = rnd.NextBool() ? 30 : 600; + } + + return cmd; + } + + /// + /// Returns a random SELECT command + /// + public DbCommand GetSelectCommand(Random rnd, TableMetadata tableMetadata, DataStressConnection conn, bool isXml = false) + { + DbCommand com = CreateCommand(rnd, conn); + StringBuilder cmdText = new StringBuilder(); + cmdText.Append(GetSelectCommandForMultipleRows(rnd, com, tableMetadata, isXml)); + + // 33% of the time, we also want to add another batch to the select command to allow for + // multiple result sets. + if ((!isXml) && (rnd.Next(0, 3) == 0)) + { + cmdText.Append(";").Append(GetSelectCommandForScalarValue(com)); + } + + if ((!isXml) && ShouldModifySession(rnd)) + { + cmdText.Append(";").Append(GetRandomSessionModificationStatement(rnd)); + } + + com.CommandText = cmdText.ToString(); + return com; + } + + /// + /// Returns a SELECT command that retrieves data from a table + /// + private string GetSelectCommandForMultipleRows(Random rnd, DbCommand com, TableMetadata inputTable, bool isXml) + { + int rowcount = rnd.Next(Depth); + + StringBuilder cmdText = new StringBuilder(); + cmdText.Append("SELECT TOP "); + cmdText.Append(rowcount); //Jonfo added this to prevent table scan of 75k row tables + cmdText.Append(" PrimaryKey"); + + List columns = inputTable.Columns; + int colindex = rnd.Next(0, columns.Count); + + for (int i = 0; i <= colindex; i++) + { + if (columns[i].ColumnName == "PrimaryKey") continue; + cmdText.Append(", "); + cmdText.Append(columns[i].ColumnName); + } + + cmdText.Append(" FROM \""); + cmdText.Append(inputTable.TableName); + cmdText.Append("\" WITH(NOLOCK) WHERE PrimaryKey "); + + // We randomly pick an operator from '>' or '=' to allow for randomization + // of possible rows returned by this query. This approach *may* help + // in reducing the likelihood of multiple threads accessing same rows. + // If multiple threads access same rows, there may be locking issues + // which may be avoided because of this randomization. + string op = rnd.NextBool() ? ">" : "="; + cmdText.Append(op).Append(" "); + + string pName = GetParameterName("P0"); + cmdText.Append(pName); + + DbParameter param = DbFactory.CreateParameter(); + param.ParameterName = pName; + param.Value = GetRandomPK(rnd, inputTable); + param.DbType = DbType.Int32; + com.Parameters.Add(param); + + return cmdText.ToString(); + } + + /// + /// Returns a SELECT command that returns a single string parameter value. + /// + private string GetSelectCommandForScalarValue(DbCommand com) + { + string pName = GetParameterName("P1"); + StringBuilder cmdText = new StringBuilder(); + + cmdText.Append("SELECT ").Append(pName); + + DbParameter param = DbFactory.CreateParameter(); + param.ParameterName = pName; + param.Value = LargeStringParam; + param.Size = LargeStringParam.Length; + param.DbType = DbType.String; + com.Parameters.Add(param); + + return cmdText.ToString(); + } + + /// + /// Returns a random existing Primary Key value + /// + private int GetRandomPK(Random rnd, TableMetadata table) + { + using (DataStressConnection conn = CreateConnection()) + { + conn.Open(); + + // This technique to get a random row comes from http://www.4guysfromrolla.com/webtech/042606-1.shtml + // When you set rowcount and then select into a scalar value, then the query is optimised so that + // just the last value is selected. So if n = ROWCOUNT then the query returns the n'th row. + + int rowNumber = rnd.Next(Depth); + + DbCommand com = conn.CreateCommand(); + string cmdText = string.Format( + @"SET ROWCOUNT {0}; + DECLARE @PK INT; + SELECT @PK = PrimaryKey FROM {1} WITH(NOLOCK) + SELECT @PK", rowNumber, table.TableName); + + com.CommandText = cmdText; + + object result = com.ExecuteScalarSyncOrAsync(CancellationToken.None, rnd).Result; + if (result == DBNull.Value) + { + throw DataStressErrors.TestError(string.Format("Table {0} returned DBNull for primary key", table.TableName)); + } + else + { + int primaryKey = (int)result; + return primaryKey; + } + } + } + + private DbParameter CreateRandomParameter(Random rnd, string prefix, TableColumn column) + { + DbParameter param = DbFactory.CreateParameter(); + + param.ParameterName = GetParameterName(prefix); + + param.Value = GetRandomData(rnd, column); + + return param; + } + + /// + /// Returns a random UPDATE command + /// + public DbCommand GetUpdateCommand(Random rnd, TableMetadata table, DataStressConnection conn) + { + DbCommand com = CreateCommand(rnd, conn); + + StringBuilder cmdText = new StringBuilder(); + cmdText.Append("UPDATE \""); + cmdText.Append(table.TableName); + cmdText.Append("\" SET "); + + List columns = table.Columns; + int numColumns = rnd.Next(2, columns.Count); + bool mostlyNull = rnd.NextBool(0.1); // 10% of rows have 90% chance of each column being null, in order to test nbcrow + + for (int i = 0; i < numColumns; i++) + { + if (columns[i].ColumnName == "PrimaryKey") continue; + if (columns[i].ColumnName.ToUpper() == "TIMESTAMP_FLD") continue; + + if (i > 1) cmdText.Append(", "); + cmdText.Append(columns[i].ColumnName); + cmdText.Append(" = "); + + if (mostlyNull && rnd.NextBool(0.9)) + { + cmdText.Append("NULL"); + } + else + { + DbParameter param = CreateRandomParameter(rnd, string.Format("P{0}", (i + 1)), columns[i]); + cmdText.Append(param.ParameterName); + com.Parameters.Add(param); + } + } + + cmdText.Append(" WHERE PrimaryKey = "); + string pName = GetParameterName("P0"); + cmdText.Append(pName); + DbParameter keyParam = DbFactory.CreateParameter(); + keyParam.ParameterName = pName; + keyParam.Value = GetRandomPK(rnd, table); + com.Parameters.Add(keyParam); + + if (ShouldModifySession(rnd)) + { + cmdText.Append(";").Append(GetRandomSessionModificationStatement(rnd)); + } + + com.CommandText = cmdText.ToString(); ; + return com; + } + + /// + /// Returns a random INSERT command + /// + public DbCommand GetInsertCommand(Random rnd, TableMetadata table, DataStressConnection conn) + { + DbCommand com = CreateCommand(rnd, conn); + + StringBuilder cmdText = new StringBuilder(); + cmdText.Append("INSERT INTO \""); + cmdText.Append(table.TableName); + cmdText.Append("\" ("); + + StringBuilder valuesText = new StringBuilder(); + valuesText.Append(") VALUES ("); + + List columns = table.Columns; + int numColumns = rnd.Next(2, columns.Count); + bool mostlyNull = rnd.NextBool(0.1); // 10% of rows have 90% chance of each column being null, in order to test nbcrow + + for (int i = 0; i < numColumns; i++) + { + if (columns[i].ColumnName.ToUpper() == "PRIMARYKEY") continue; + + if (i > 1) + { + cmdText.Append(", "); + valuesText.Append(", "); + } + + cmdText.Append(columns[i].ColumnName); + + if (columns[i].ColumnName.ToUpper() == "TIMESTAMP_FLD") + { + valuesText.Append("DEFAULT"); // Cannot insert an explicit value in a timestamp field + } + else if (mostlyNull && rnd.NextBool(0.9)) + { + valuesText.Append("NULL"); + } + else + { + DbParameter param = CreateRandomParameter(rnd, string.Format("P{0}", i + 1), columns[i]); + + valuesText.Append(param.ParameterName); + com.Parameters.Add(param); + } + } + + // To deal databases that do not support auto-incremented columns (Oracle?) + // if (!columns["PrimaryKey"].AutoIncrement) + if (PrimaryKeyValueIsRequired) + { + DbParameter param = CreateRandomParameter(rnd, "P0", table.GetColumn("PrimaryKey")); + cmdText.Append(", PrimaryKey"); + valuesText.Append(", "); + valuesText.Append(param.ParameterName); + com.Parameters.Add(param); + } + + valuesText.Append(")"); + cmdText.Append(valuesText); + + if (ShouldModifySession(rnd)) + { + cmdText.Append(";").Append(GetRandomSessionModificationStatement(rnd)); + } + + com.CommandText = cmdText.ToString(); + return com; + } + + /// + /// Returns a random DELETE command + /// + public DbCommand GetDeleteCommand(Random rnd, TableMetadata table, DataStressConnection conn) + { + DbCommand com = CreateCommand(rnd, conn); + + StringBuilder cmdText = new StringBuilder(); + cmdText.Append("DELETE FROM \""); + + List columns = table.Columns; + string pName = GetParameterName("P0"); + cmdText.Append(table.TableName); + cmdText.Append("\" WHERE PrimaryKey = "); + cmdText.Append(pName); + + DbParameter param = DbFactory.CreateParameter(); + param.ParameterName = pName; + param.Value = GetRandomPK(rnd, table); + com.Parameters.Add(param); + + if (ShouldModifySession(rnd)) + { + cmdText.Append(";").Append(GetRandomSessionModificationStatement(rnd)); + } + + com.CommandText = cmdText.ToString(); + return com; + } + + public bool ShouldModifySession(Random rnd) + { + // 33% of the time, we want to modify the user session on the server + return rnd.NextBool(.33); + } + + /// + /// Returns a random statement that will modify the session on the server. + /// + public string GetRandomSessionModificationStatement(Random rnd) + { + string sessionStmt = null; + int select = rnd.Next(3); + switch (select) + { + case 0: + // Create a SET CONTEXT_INFO statement using a hex string of random data + StringBuilder sb = new StringBuilder("0x"); + int count = rnd.Next(1, 129); + for (int i = 0; i < count; i++) + { + sb.AppendFormat("{0:x2}", (byte)rnd.Next(0, (int)(byte.MaxValue + 1))); + } + string contextInfoData = sb.ToString(); + sessionStmt = string.Format("SET CONTEXT_INFO {0}", contextInfoData); + break; + + case 1: + // Create or drop the temp table + sessionStmt = string.Format("IF OBJECT_ID('tempdb..{0}') IS NULL CREATE TABLE {0}(id INT) ELSE DROP TABLE {0}", s_tempTableName); + break; + + default: + // Create a SET LANGUAGE statement + sessionStmt = string.Format("SET LANGUAGE N'{0}'", s_languages[rnd.Next(s_languages.Length)]); + break; + } + return sessionStmt; + } + + /// + /// Returns random data + /// + public object GetRandomData(Random rnd, TableColumn column) + { + int length = column.MaxLength; + int maxTargetLength = (length > 255 || length == -1) ? 255 : length; + + DbType dbType = GetDbType(column); + return GetRandomData(rnd, dbType, maxTargetLength); + } + + private DbType GetDbType(TableColumn column) + { + switch (column.ColumnName) + { + case "bit_FLD": return DbType.Boolean; + case "tinyint_FLD": return DbType.Byte; + case "smallint_FLD": return DbType.Int16; + case "int_FLD": return DbType.Int32; + case "PrimaryKey": return DbType.Int32; + case "bigint_FLD": return DbType.Int64; + case "real_FLD": return DbType.Single; + case "float_FLD": return DbType.Double; + case "smallmoney_FLD": return DbType.Decimal; + case "money_FLD": return DbType.Decimal; + case "decimal_FLD": return DbType.Decimal; + case "numeric_FLD": return DbType.Decimal; + case "datetime_FLD": return DbType.DateTime; + case "smalldatetime_FLD": return DbType.DateTime; + case "datetime2_FLD": return DbType.DateTime2; + case "timestamp_FLD": return DbType.Binary; + case "date_FLD": return DbType.Date; + case "time_FLD": return DbType.Time; + case "datetimeoffset_FLD": return DbType.DateTimeOffset; + case "uniqueidentifier_FLD": return DbType.Guid; + case "sql_variant_FLD": return DbType.Object; + case "image_FLD": return DbType.Binary; + case "varbinary_FLD": return DbType.Binary; + case "binary_FLD": return DbType.Binary; + case "char_FLD": return DbType.String; + case "varchar_FLD": return DbType.String; + case "text_FLD": return DbType.String; + case "ntext_FLD": return DbType.String; + case "nvarchar_FLD": return DbType.String; + case "nchar_FLD": return DbType.String; + case "nvarcharmax_FLD": return DbType.String; + case "varbinarymax_FLD": return DbType.Binary; + case "varcharmax_FLD": return DbType.String; + case "xml_FLD": return DbType.Xml; + default: throw DataStressErrors.UnhandledCaseError(column.ColumnName); + } + } + + protected virtual object GetRandomData(Random rnd, DbType dbType, int maxLength) + { + byte[] buffer; + switch (dbType) + { + case DbType.Boolean: + return (rnd.Next(2) == 0 ? false : true); + case DbType.Byte: + return rnd.Next(byte.MinValue, byte.MaxValue + 1); + case DbType.Int16: + return rnd.Next(short.MinValue, short.MaxValue + 1); + case DbType.Int32: + return (rnd.Next(2) == 0 ? int.MaxValue / rnd.Next(1, 3) : int.MinValue / rnd.Next(1, 3)); + case DbType.Int64: + return (rnd.Next(2) == 0 ? long.MaxValue / rnd.Next(1, 3) : long.MinValue / rnd.Next(1, 3)); + case DbType.Single: + return rnd.NextDouble() * (rnd.Next(2) == 0 ? float.MaxValue : float.MinValue); + case DbType.Double: + return rnd.NextDouble() * (rnd.Next(2) == 0 ? double.MaxValue : double.MinValue); + case DbType.Decimal: + return rnd.Next(short.MinValue, short.MaxValue + 1); + case DbType.DateTime: + case DbType.DateTime2: + return DateTime.Now; + case DbType.Date: + return DateTime.Now.Date; + case DbType.Time: + return DateTime.Now.TimeOfDay.ToString("c"); + case DbType.DateTimeOffset: + return DateTimeOffset.Now; + case DbType.Guid: + buffer = new byte[16]; + rnd.NextBytes(buffer); + return (new Guid(buffer)); + case DbType.Object: + case DbType.Binary: + rnd.NextBytes(buffer = new byte[rnd.Next(1, maxLength)]); + return buffer; + case DbType.String: + case DbType.Xml: + string openTag = ""; + string closeTag = ""; + int tagLength = openTag.Length + closeTag.Length; + + if (tagLength > maxLength) + { + // Case (1): tagLength > maxTargetLength + return ""; + } + else + { + StringBuilder builder = new StringBuilder(maxLength); + + builder.Append(openTag); + + // The data is just a repeat of one character because to the managed provider + // it is only really the length that matters, not the content of the data + char characterToUse = (char)rnd.Next((int)'@', (int)'~'); // Choosing random characters in this range to avoid special + // xml chars like '<' or '&' + int numRepeats = rnd.Next(0, maxLength - tagLength); // Case (2): tagLength == maxTargetLength + // Case (3): tagLength < maxTargetLength <-- most common + builder.Append(characterToUse, numRepeats); + + builder.Append(closeTag); + + DataStressErrors.Assert(builder.Length <= maxLength, "Incorrect length of randomly generated string"); + + return builder.ToString(); + } + default: + throw DataStressErrors.UnhandledCaseError(dbType); + } + } + + #region Table information to be used by stress + + // method used to create stress tables in the database + protected void BuildUserTables(List TableMetadataList) + { + string CreateTable1 = + "CREATE TABLE stress_test_table_1 (PrimaryKey int identity(1,1) primary key, int_FLD int, smallint_FLD smallint, real_FLD real, float_FLD float, decimal_FLD decimal(28,4), " + + "smallmoney_FLD smallmoney, bit_FLD bit, tinyint_FLD tinyint, uniqueidentifier_FLD uniqueidentifier, varbinary_FLD varbinary(756), binary_FLD binary(756), " + + "image_FLD image, varbinarymax_FLD varbinary(max), timestamp_FLD timestamp, char_FLD char(756), text_FLD text, varcharmax_FLD varchar(max), " + + "varchar_FLD varchar(756), nchar_FLD nchar(756), ntext_FLD ntext, nvarcharmax_FLD nvarchar(max), nvarchar_FLD nvarchar(756), datetime_FLD datetime, " + + "smalldatetime_FLD smalldatetime);" + + "CREATE UNIQUE INDEX stress_test_table_1 on stress_test_table_1 ( PrimaryKey );" + + "insert into stress_test_table_1(int_FLD, smallint_FLD, real_FLD, float_FLD, decimal_FLD, " + + "smallmoney_FLD, bit_FLD, tinyint_FLD, uniqueidentifier_FLD, varbinary_FLD, binary_FLD, " + + "image_FLD, varbinarymax_FLD, char_FLD, text_FLD, varcharmax_FLD, " + + "varchar_FLD, nchar_FLD, ntext_FLD, nvarcharmax_FLD, nvarchar_FLD, datetime_FLD, " + + "smalldatetime_FLD) values ( 0, 0, 0, 0, 0, $0, 0, 0, '00000000-0000-0000-0000-000000000000', " + + "0x00, 0x00, 0x00, 0x00, '0', '0', '0', '0', N'0', N'0', N'0', N'0', '01/11/2000 12:54:01', '01/11/2000 12:54:00' );" + ; + + string CreateTable2 = + "CREATE TABLE stress_test_table_2 (PrimaryKey int identity(1,1) primary key, bigint_FLD bigint, money_FLD money, numeric_FLD numeric, " + + "time_FLD time, date_FLD date, datetimeoffset_FLD datetimeoffset, sql_variant_FLD sql_variant, " + + "datetime2_FLD datetime2, xml_FLD xml);" + + "CREATE UNIQUE INDEX stress_test_table_2 on stress_test_table_2 ( PrimaryKey );" + + "insert into stress_test_table_2(bigint_FLD, money_FLD, numeric_FLD, " + + "time_FLD, date_FLD, datetimeoffset_FLD, sql_variant_FLD, " + + "datetime2_FLD, xml_FLD) values ( 0, $0, 0, '01/11/2015 12:54:01', '01/11/2015 12:54:01', '01/11/2000 12:54:01 -08:00', 0, '01/11/2000 12:54:01', '0' );" + ; + + if (TableMetadataList == null) + { + TableMetadataList = new List(); + } + + List tableColumns1 = new List(); + tableColumns1.Add(new TableColumn("PrimaryKey", -1)); + tableColumns1.Add(new TableColumn("int_FLD", -1)); + tableColumns1.Add(new TableColumn("smallint_FLD", -1)); + tableColumns1.Add(new TableColumn("real_FLD", -1)); + tableColumns1.Add(new TableColumn("float_FLD", -1)); + tableColumns1.Add(new TableColumn("decimal_FLD", -1)); + tableColumns1.Add(new TableColumn("smallmoney_FLD", -1)); + tableColumns1.Add(new TableColumn("bit_FLD", -1)); + tableColumns1.Add(new TableColumn("tinyint_FLD", -1)); + tableColumns1.Add(new TableColumn("uniqueidentifier_FLD", -1)); + tableColumns1.Add(new TableColumn("varbinary_FLD", 756)); + tableColumns1.Add(new TableColumn("binary_FLD", 756)); + tableColumns1.Add(new TableColumn("image_FLD", -1)); + tableColumns1.Add(new TableColumn("varbinarymax_FLD", -1)); + tableColumns1.Add(new TableColumn("timestamp_FLD", -1)); + tableColumns1.Add(new TableColumn("char_FLD", -1)); + tableColumns1.Add(new TableColumn("text_FLD", -1)); + tableColumns1.Add(new TableColumn("varcharmax_FLD", -1)); + tableColumns1.Add(new TableColumn("varchar_FLD", 756)); + tableColumns1.Add(new TableColumn("nchar_FLD", 756)); + tableColumns1.Add(new TableColumn("ntext_FLD", -1)); + tableColumns1.Add(new TableColumn("nvarcharmax_FLD", -1)); + tableColumns1.Add(new TableColumn("nvarchar_FLD", 756)); + tableColumns1.Add(new TableColumn("datetime_FLD", -1)); + tableColumns1.Add(new TableColumn("smalldatetime_FLD", -1)); + TableMetadata tableMeta1 = new TableMetadata("stress_test_table_1", tableColumns1); + TableMetadataList.Add(tableMeta1); + + List tableColumns2 = new List(); + tableColumns2.Add(new TableColumn("PrimaryKey", -1)); + tableColumns2.Add(new TableColumn("bigint_FLD", -1)); + tableColumns2.Add(new TableColumn("money_FLD", -1)); + tableColumns2.Add(new TableColumn("numeric_FLD", -1)); + tableColumns2.Add(new TableColumn("time_FLD", -1)); + tableColumns2.Add(new TableColumn("date_FLD", -1)); + tableColumns2.Add(new TableColumn("datetimeoffset_FLD", -1)); + tableColumns2.Add(new TableColumn("sql_variant_FLD", -1)); + tableColumns2.Add(new TableColumn("datetime2_FLD", -1)); + tableColumns2.Add(new TableColumn("xml_FLD", -1)); + TableMetadata tableMeta2 = new TableMetadata("stress_test_table_2", tableColumns2); + TableMetadataList.Add(tableMeta2); + + using (DataStressConnection conn = CreateConnection(null)) + { + conn.Open(); + using (DbCommand com = conn.CreateCommand()) + { + try + { + com.CommandText = CreateTable1; + com.ExecuteNonQuery(); + } + catch (DbException de) + { + // This can be improved by doing a Drop Table if exists. + if (de.Message.Contains("There is already an object named \'" + tableMeta1.TableName + "\' in the database.")) + { + CleanupUserTables(tableMeta1); + com.ExecuteNonQuery(); + } + else + { + throw; + } + } + + try + { + com.CommandText = CreateTable2; + com.ExecuteNonQuery(); + } + catch (DbException de) + { + // This can be improved by doing a Drop Table if exists in the query itself. + if (de.Message.Contains("There is already an object named \'" + tableMeta2.TableName + "\' in the database.")) + { + CleanupUserTables(tableMeta2); + com.ExecuteNonQuery(); + } + else + { + throw; + } + } + + for (int i = 0; i < Depth; i++) + { + TrackedRandom randomInstance = new TrackedRandom(); + randomInstance.Mark(); + + DbCommand comInsert1 = GetInsertCommand(randomInstance, tableMeta1, conn); + comInsert1.ExecuteNonQuery(); + + DbCommand comInsert2 = GetInsertCommand(randomInstance, tableMeta2, conn); + comInsert2.ExecuteNonQuery(); + } + } + } + } + + // method used to delete stress tables in the database + protected void CleanupUserTables(TableMetadata tableMetadata) + { + string DropTable = "drop TABLE " + tableMetadata.TableName + ";"; + + using (DataStressConnection conn = CreateConnection(null)) + { + conn.Open(); + using (DbCommand com = conn.CreateCommand()) + { + try + { + com.CommandText = DropTable; + com.ExecuteNonQuery(); + } + catch (Exception) { } + } + } + } + + public List TableMetadataList + { + get; + private set; + } + + public class TableMetadata + { + private string _tableName; + private List _columns = new List(); + + public TableMetadata(string tbleName, List cols) + { + _tableName = tbleName; + _columns = cols; + } + + public string TableName + { + get { return _tableName; } + } + + public List Columns + { + get { return _columns; } + } + + public TableColumn GetColumn(string colName) + { + foreach (TableColumn column in _columns) + { + if (column.ColumnName.Equals(colName)) + { + return column; + } + } + return null; + } + } + + public class TableColumn + { + private string _columnName; + private int _maxLength; + + public TableColumn(string colName, int maxLen) + { + _columnName = colName; + _maxLength = maxLen; + } + + public string ColumnName + { + get { return _columnName; } + } + + public int MaxLength + { + get { return _maxLength; } + } + } + + private List _applicationNames; + + /// + /// Gets schema of all tables from the back-end database and fills + /// the m_Tables DataSet with this schema. This DataSet is used to + /// generate random command text for tests. + /// + public void InitializeSharedData(DataSource source) + { + Trace.WriteLine("Creating shared objects", this.ToString()); + + // Initialize m_sharedDataSet + TableMetadataList = new List(); + BuildUserTables(TableMetadataList); + + // Initialize m_applicationNames + _applicationNames = new List(); + for (int i = 0; i < GetNumDifferentApplicationNames(); i++) + { + _applicationNames.Add(GetRandomApplicationName()); + } + + // Initialize CurrentPoolingStressMode + CurrentPoolingStressMode = PoolingStressMode.RandomizeConnectionStrings; + + + Trace.WriteLine("Finished creating shared objects", this.ToString()); + } + + public void CleanupSharedData() + { + foreach (TableMetadata meta in TableMetadataList) + { + CleanupUserTables(meta); + } + TableMetadataList = null; + } + + #endregion + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressReader.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressReader.cs new file mode 100644 index 0000000000..cad1bfa579 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressReader.cs @@ -0,0 +1,350 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Xml; +using System.Data; +using System.Data.Common; +using Microsoft.Data.SqlClient; +using System.Data.SqlTypes; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Stress.Data +{ + public class DataStressReader : IDisposable + { + #region Type method mapping + + private static Dictionary>> s_sqlTypes; + private static Dictionary>> s_clrTypes; + + static DataStressReader() + { + InitSqlTypes(); + InitClrTypes(); + } + + private static void InitSqlTypes() + { + s_sqlTypes = new Dictionary>>(); + + s_sqlTypes.Add(typeof(SqlBinary), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlBoolean), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlByte), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlBytes), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlChars), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlDateTime), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlDecimal), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlDouble), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlGuid), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlInt16), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlInt32), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlInt64), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlMoney), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlSingle), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlString), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_sqlTypes.Add(typeof(SqlXml), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + } + + private static void InitClrTypes() + { + s_clrTypes = new Dictionary>>(); + + s_clrTypes.Add(typeof(bool), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(byte), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(short), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(int), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(long), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(float), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(double), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(string), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(char), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(decimal), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(Guid), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(DateTime), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(TimeSpan), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + s_clrTypes.Add(typeof(DateTimeOffset), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd)); + } + + #endregion + + private readonly DbDataReader _reader; + private SemaphoreSlim _closeAsyncSemaphore; + + public DataStressReader(DbDataReader internalReader) + { + _reader = internalReader; + } + + public void Close() + { + _reader.Dispose(); + } + + public void Dispose() + { + _reader.Dispose(); + if (_closeAsyncSemaphore != null) _closeAsyncSemaphore.Dispose(); + } + + public Task CloseAsync() + { + _closeAsyncSemaphore = new SemaphoreSlim(1); + return Task.Run(() => ExecuteWithCloseAsyncSemaphore(Close)); + } + + /// + /// Executes the action while holding the CloseAsync Semaphore. + /// This MUST be used for reader.Close() and all methods that are not safe to call at the same time as reader.Close(), i.e. all sync methods. + /// Otherwise we will see AV's. + /// + public void ExecuteWithCloseAsyncSemaphore(Action a) + { + try + { + if (_closeAsyncSemaphore != null) _closeAsyncSemaphore.Wait(); + a(); + } + finally + { + if (_closeAsyncSemaphore != null) _closeAsyncSemaphore.Release(); + } + } + + /// + /// Executes the action while holding the CloseAsync Semaphore. + /// This MUST be used for reader.Close() and all methods that are not safe to call at the same time as reader.Close(), i.e. all sync methods. + /// Otherwise we will see AV's. + /// + public T ExecuteWithCloseAsyncSemaphore(Func f) + { + try + { + if (_closeAsyncSemaphore != null) _closeAsyncSemaphore.Wait(); + return f(); + } + finally + { + if (_closeAsyncSemaphore != null) _closeAsyncSemaphore.Release(); + } + } + + #region SyncOrAsync methods + + public Task ReadSyncOrAsync(CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + () => ExecuteWithCloseAsyncSemaphore(() => _reader.Read()), + () => ExecuteWithCloseAsyncSemaphore(() => _reader.ReadAsync(token)), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public Task NextResultSyncOrAsync(CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + () => ExecuteWithCloseAsyncSemaphore(() => _reader.NextResult()), + () => ExecuteWithCloseAsyncSemaphore(() => _reader.NextResultAsync(token)), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public Task IsDBNullSyncOrAsync(int ordinal, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + () => ExecuteWithCloseAsyncSemaphore(() => _reader.IsDBNull(ordinal)), + () => ExecuteWithCloseAsyncSemaphore(() => _reader.IsDBNullAsync(ordinal, token)), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public Task GetValueSyncOrAsync(int ordinal, CancellationToken token, Random rnd) + { + if (rnd.NextBool(0.3)) + { + // Use sync-only GetValue + return Task.FromResult(GetValue(ordinal)); + } + else + { + // Use GetFieldValue or GetFieldValueAsync + Func> getFieldValueFunc = null; + + if (rnd.NextBool()) + { + // Choose provider-specific getter + Type sqlType = GetProviderSpecificFieldType(ordinal); + s_sqlTypes.TryGetValue(sqlType, out getFieldValueFunc); + } + else + { + // Choose clr type getter + Type clrType = GetFieldType(ordinal); + s_clrTypes.TryGetValue(clrType, out getFieldValueFunc); + } + + if (getFieldValueFunc != null) + { + // Execute the type-specific func, e.g. GetFieldValue or GetFieldValueAsync + return getFieldValueFunc(this, ordinal, token, rnd); + } + else + { + // Execute GetFieldValue or GetFieldValueAsync as a fallback + return GetFieldValueSyncOrAsync(ordinal, token, rnd); + } + } + } + + private Task GetFieldValueSyncOrAsync(int ordinal, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + () => ExecuteWithCloseAsyncSemaphore(() => _reader.GetFieldValue(ordinal)), + async () => ((object)(await ExecuteWithCloseAsyncSemaphore(() => _reader.GetFieldValueAsync(ordinal, token)))), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + #endregion + + #region Sync-only methods + + public long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length) + { + return ExecuteWithCloseAsyncSemaphore(() => _reader.GetBytes(ordinal, dataOffset, buffer, bufferOffset, length)); + } + + public long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length) + { + return ExecuteWithCloseAsyncSemaphore(() => _reader.GetChars(ordinal, dataOffset, buffer, bufferOffset, length)); + } + + public Type GetFieldType(int ordinal) + { + return ExecuteWithCloseAsyncSemaphore(() => _reader.GetFieldType(ordinal)); + } + + public string GetName(int ordinal) + { + return ExecuteWithCloseAsyncSemaphore(() => _reader.GetName(ordinal)); + } + + public Type GetProviderSpecificFieldType(int ordinal) + { + return ExecuteWithCloseAsyncSemaphore(() => _reader.GetProviderSpecificFieldType(ordinal)); + } + + + public DataStressStream GetStream(int ordinal) + { + Stream s = ExecuteWithCloseAsyncSemaphore(() => _reader.GetStream(ordinal)); + return new DataStressStream(s, this); + } + + public DataStressTextReader GetTextReader(int ordinal) + { + TextReader t = ExecuteWithCloseAsyncSemaphore(() => _reader.GetTextReader(ordinal)); + return new DataStressTextReader(t, this); + } + + public DataStressXmlReader GetXmlReader(int ordinal) + { + XmlReader x = ExecuteWithCloseAsyncSemaphore(() => ((SqlDataReader)_reader).GetXmlReader(ordinal)); + return new DataStressXmlReader(x, this); + } + + public object GetValue(int ordinal) + { + return ExecuteWithCloseAsyncSemaphore(() => _reader.GetValue(ordinal)); + } + + public int FieldCount + { + get { return ExecuteWithCloseAsyncSemaphore(() => _reader.FieldCount); } + } + + #endregion + } + + public class DataStressStream : IDisposable + { + private Stream _stream; + private DataStressReader _reader; + + public DataStressStream(Stream stream, DataStressReader reader) + { + _stream = stream; + _reader = reader; + } + + public void Dispose() + { + _stream.Dispose(); + } + + public Task ReadSyncOrAsync(byte[] buffer, int offset, int count, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + () => _reader.ExecuteWithCloseAsyncSemaphore(() => _stream.Read(buffer, offset, count)), + () => _reader.ExecuteWithCloseAsyncSemaphore(() => _stream.ReadAsync(buffer, offset, count)), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + } + + public class DataStressTextReader : IDisposable + { + private TextReader _textReader; + private DataStressReader _reader; + + public DataStressTextReader(TextReader textReader, DataStressReader reader) + { + _textReader = textReader; + _reader = reader; + } + + public void Dispose() + { + _textReader.Dispose(); + } + + public int Peek() + { + return _reader.ExecuteWithCloseAsyncSemaphore(() => _textReader.Peek()); + } + + public Task ReadSyncOrAsync(char[] buffer, int index, int count, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + () => _reader.ExecuteWithCloseAsyncSemaphore(() => _textReader.Read(buffer, index, count)), + () => _reader.ExecuteWithCloseAsyncSemaphore(() => _textReader.ReadAsync(buffer, index, count)), + AsyncUtils.ChooseSyncAsyncMode(rnd)); + } + } + + public class DataStressXmlReader : IDisposable + { + private XmlReader _xmlReader; + private DataStressReader _reader; + + public DataStressXmlReader(XmlReader xmlReader, DataStressReader reader) + { + _xmlReader = xmlReader; + _reader = reader; + } + + public void Dispose() + { + _xmlReader.Dispose(); + } + + public void Read() + { + _reader.ExecuteWithCloseAsyncSemaphore(() => _xmlReader.Read()); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressSettings.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressSettings.cs new file mode 100644 index 0000000000..2208284ba1 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressSettings.cs @@ -0,0 +1,303 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace Stress.Data +{ + /// + /// Loads dataStressSettings section from Stress.Data.Framework.dll.config (App.config in source tree) + /// + public class DataStressSettings + { + + internal static readonly string s_configFileName = "StressTest.config"; + + // use Instance to access the settings + private DataStressSettings() + { + } + + private bool Initialized { get; set; } + + private DataStressConfigurationSection _dataStressSettings = new DataStressConfigurationSection(); + + // list of sources read from the config file + private Dictionary _sources = new Dictionary(StringComparer.CurrentCultureIgnoreCase); + public ErrorHandlingAction ActionOnProductError + { + get; + private set; + } + public ErrorHandlingAction ActionOnTestError + { + get; + private set; + } + public ErrorHandlingAction ActionOnProgrammingError + { + get; + private set; + } + + public int NumberOfConnectionPools + { + get; + private set; + } + + // singleton instance, lazy evaluation + private static DataStressSettings s_instance = new DataStressSettings(); + public static DataStressSettings Instance + { + get + { + if (!s_instance.Initialized) + { + lock (s_instance) + { + if (!s_instance.Initialized) + { + s_instance.Load(); + } + } + } + return s_instance; + } + } + + #region Configuration file handlers + + private class DataStressConfigurationSection + { + private List _sources = new List(); + private ErrorHandlingPolicyElement _errorHandlingPolicy = new ErrorHandlingPolicyElement(); + private ConnectionPoolPolicyElement _connectionPoolPolicy = new ConnectionPoolPolicyElement(); + + StressConfigReader reader = new StressConfigReader(s_configFileName); + + public List Sources + { + get + { + if(_sources.Count == 0) + { + reader.Load(); + _sources = reader.Sources; + } + return _sources; + } + } + + public ErrorHandlingPolicyElement ErroHandlingPolicy + { + get + { + return _errorHandlingPolicy; + } + } + + public ConnectionPoolPolicyElement ConnectionPoolPolicy + { + get + { + return _connectionPoolPolicy; + } + } + } + + + internal class DataSourceElement + { + private string _name; + private string _type; + private bool _isDefault = false; + + public readonly Dictionary SourceProperties = new Dictionary(); + + + public DataSourceElement(string ds_name, + string ds_type, + string ds_server, + string ds_datasource, + string ds_database, + string ds_user, + string ds_password, + bool ds_isDefault = false, + bool ds_winAuth = false, + bool ds_isLocal = false, + string ds_dbFile = null, + bool disableMultiSubnetFailoverSetup = true, + bool disableNamedPipes = true, + bool encrypt = false) + { + _name = ds_name; + _type = ds_type; + _isDefault = ds_isDefault; + + if (ds_server != null) + { + SourceProperties.Add("server", ds_server); + } + if (ds_datasource != null) + { + SourceProperties.Add("dataSource", ds_datasource); + } + if (ds_database != null) + { + SourceProperties.Add("database", ds_database); + } + if (ds_user != null) + { + SourceProperties.Add("user", ds_user); + } + if (ds_password != null) + { + SourceProperties.Add("password", ds_password); + } + + SourceProperties.Add("supportsWindowsAuthentication", ds_winAuth.ToString()); + SourceProperties.Add("isLocal", ds_isLocal.ToString()); + + SourceProperties.Add("DisableMultiSubnetFailoverSetup", disableMultiSubnetFailoverSetup.ToString()); + + SourceProperties.Add("DisableNamedPipes", disableNamedPipes.ToString()); + + SourceProperties.Add("Encrypt", encrypt.ToString()); + + if (ds_dbFile != null) + { + SourceProperties.Add("databaseFile", ds_dbFile); + } + } + + public string Name + { + get { return _name; } + } + + public string Type + { + get { return _type; } + } + + public bool IsDefault + { + get { return _isDefault; } + } + } + + private class ErrorHandlingPolicyElement + { + private string _onProductError = "debugBreak"; + private string _onTestError = "throwException"; + private string _onProgrammingError = "debugBreak"; + + public string OnProductError + { + get + { + return _onProductError; + } + } + + public string OnTestError + { + get + { + return _onTestError; + } + } + + public string OnProgrammingError + { + get + { + return _onProgrammingError; + } + } + } + + private class ConnectionPoolPolicyElement + { + private int _numberOfPools = 10; + + public int NumberOfPools + { + get + { + return _numberOfPools; + } + } + } + + #endregion + + /// + /// loads the configuration data from the app config file (Stress.Data.Framework.dll.config) and initializes the Sources collection + /// + private void Load() + { + // Parse + foreach (DataSourceElement sourceElement in _dataStressSettings.Sources) + { + // if Parse raises exception, check that the type attribute is set to the relevant the SourceType enumeration value name + DataSourceType sourceType = (DataSourceType)Enum.Parse(typeof(DataSourceType), sourceElement.Type, true); + + DataSource newSource = DataSource.Create(sourceElement.Name, sourceType, sourceElement.IsDefault, sourceElement.SourceProperties); + _sources.Add(newSource.Name, newSource); + } + + // Parse + // if Parse raises exception, check that the action attribute is set to a valid ActionOnProductBugFound enumeration value name + this.ActionOnProductError = (ErrorHandlingAction)Enum.Parse(typeof(ErrorHandlingAction), _dataStressSettings.ErroHandlingPolicy.OnProductError, true); + this.ActionOnTestError = (ErrorHandlingAction)Enum.Parse(typeof(ErrorHandlingAction), _dataStressSettings.ErroHandlingPolicy.OnTestError, true); + this.ActionOnProgrammingError = (ErrorHandlingAction)Enum.Parse(typeof(ErrorHandlingAction), _dataStressSettings.ErroHandlingPolicy.OnProgrammingError, true); + + // Parse + this.NumberOfConnectionPools = _dataStressSettings.ConnectionPoolPolicy.NumberOfPools; + + this.Initialized = true; + } + + + /// + /// use this method to retrieve the source data by its name (represented with 'name' attribute in config file) + /// + /// case-sensitive name + public DataSource GetSourceByName(string name) + { + return _sources[name]; + } + + /// + /// Use this method to retrieve the default source associated with the type specified. + /// The type of the node is specified with 'type' attribute on the sources file - see DataSourceType enum for list of supported types. + /// If there is a source node with isDefault=true, this node is returned (first one found in config file). + /// Otherwise, first source node from type specified is returned. + /// + public DataSource GetDefaultSourceByType(DataSourceType type) + { + DataSource defaultSource = null; + foreach (DataSource source in _sources.Values) + { + if (source.Type == type) + { + if (defaultSource == null) + { + // use the first found source, if default is not set + defaultSource = source; + } + else if (source.IsDefault) + { + defaultSource = source; + break; + } + } + } + return defaultSource; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataTestGroup.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataTestGroup.cs new file mode 100644 index 0000000000..96f2d16d6c --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataTestGroup.cs @@ -0,0 +1,696 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using Microsoft.Data.SqlClient; +using System.Data.SqlTypes; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +using DPStressHarness; + +namespace Stress.Data +{ + /// + /// basic set of tests to run on each managed provider + /// + public abstract class DataTestGroup + { + // random is not thread-safe, create one per thread - use RandomInstance to access it. + // note that each thread and each test method has a different instance of this object, so it + // doesn't need to be synchronised or have [ThreadStatic], etc + private TrackedRandom _randomInstance = new TrackedRandom(); + protected Random RandomInstance + { + get + { + _randomInstance.Mark(); + return _randomInstance; + } + } + + /// + /// Test factory to use for generation of connection strings and other test objects. Factory is initialized during setup. + /// null is not returned - if setup was not called yet, exception is raised + /// This is static so that is shared across all threads (since stresstest will create a new DataTestGroup object for each thread) + /// + private static DataStressFactory s_factory; + public static DataStressFactory Factory + { + get + { + DataStressErrors.Assert(s_factory != null, "Tried to access DataTestGroup.Factory before Setup has been called"); + return s_factory; + } + } + + /// + /// This method is called to create the stress factory used to create connections, commands, etc... + /// Implementation should set the source and the scenario to valid values if inputs are null/empty. + /// + /// Scenario string specified by the user or empty to set default + /// DataSource string specified by the user or empty to use connection string as is, useful when developing new tests + protected abstract DataStressFactory CreateFactory(ref string scenario, ref DataSource source); + + /// + /// scenario to run, initialized in setup + /// null is not returned - if setup was not called yet, exception is raised + /// This is static so that is shared across all threads (since stresstest will create a new DataTestGroup object for each thread) + /// + private static string s_scenario; + protected static string Scenario + { + get + { + DataStressErrors.Assert(s_scenario != null, "Tried to access DataTestGroup.Scenario before Setup has been called"); + return s_scenario; + } + } + + /// + /// data source information used by stress, initialized in Setup + /// null is not returned - if setup was not called yet, exception is raised + /// This is static so that is shared across all threads (since stresstest will create a new DataTestGroup object for each thread) + /// + private static DataSource s_source; + protected static DataSource Source + { + get + { + DataStressErrors.Assert(s_source != null, "Tried to access DataTestGroup.Source before Setup has been called"); + return s_source; + } + } + + + /// + /// Does test setup that is shared across all threads. This method will be called only once, before + /// any [TestSetup] methods are called. + /// If you override this method you must call base.GlobalTestSetup() at the beginning. + /// + [GlobalTestSetup] + public virtual void GlobalTestSetup() + { + // Preconditions - ensure this setup is only called once + DataStressErrors.Assert(string.IsNullOrEmpty(s_scenario), "Scenario was already set"); + DataStressErrors.Assert(s_source == null, "Source was already set"); + DataStressErrors.Assert(s_factory == null, "Factory was already set"); + + // Set m_scenario + string userProvidedScenario; + TestMetrics.Overrides.TryGetValue("scenario", out userProvidedScenario); + // Empty means default scenario for the test group + s_scenario = (userProvidedScenario ?? string.Empty); + s_scenario = s_scenario.ToUpperInvariant(); + + // Set m_source + // Empty means that test group will peek the default data source from the config file based on the scenario + string userProvidedSourceName; + if (TestMetrics.Overrides.TryGetValue("source", out userProvidedSourceName)) + { + s_source = DataStressSettings.Instance.GetSourceByName(userProvidedSourceName); + } + + // Set m_factory + s_factory = CreateFactory(ref s_scenario, ref s_source); + s_factory.InitializeSharedData(s_source); + + // Postconditions + DataStressErrors.Assert(!string.IsNullOrEmpty(s_scenario), "Scenario was not set"); + DataStressErrors.Assert(s_source != null, "Source was not set"); + DataStressErrors.Assert(s_factory != null, "Factory was not set"); + } + + /// + /// Does test cleanup that is shared across all threads. This method will not be called until all + /// threads have finished executing [StressTest] methods. This method will be called only once. + /// If you override this method you must call base.GlobalTestSetup() at the beginning. + /// + [GlobalTestCleanup] + public virtual void GlobalTestCleanup() + { + s_factory.CleanupSharedData(); + s_source = null; + s_scenario = null; + s_factory.Dispose(); + s_factory = null; + } + + + protected bool OpenConnection(DataStressConnection conn) + { + try + { + conn.Open(); + return true; + } + catch (Exception e) + { + if (IsServerNotAccessibleException(e, conn.DbConnection.ConnectionString, conn.DbConnection.DataSource)) + { + // Ignore this exception. + // This exception will fire when using named pipes with MultiSubnetFailover option set to true. + // MultiSubnetFailover=true only works with TCP/IP protocol and will result in exception when using with named pipes. + return false; + } + + throw; + } + } + + + [GlobalExceptionHandler] + public virtual void GlobalExceptionHandler(Exception e) + { + if(e is System.Reflection.TargetInvocationException && Debugger.IsAttached) + { + StackTrace trace = new StackTrace(e); + Console.WriteLine(trace); + } + } + + /// + /// Returns whether or not the datareader should be closed + /// + protected virtual bool ShouldCloseDataReader() + { + // Ignore commandCancelled, instead randomly close it 9/10 of the time + return RandomInstance.Next(10) != 0; + } + + + #region CommandExecute and Consume methods + + /// + /// Utility function used by command tests + /// + protected virtual void CommandExecute(Random rnd, DbCommand com, bool query) + { + AsyncUtils.WaitAndUnwrapException(CommandExecuteAsync(rnd, com, query)); + } + + protected async virtual Task CommandExecuteAsync(Random rnd, DbCommand com, bool query) + { + CancellationTokenSource cts = null; + + // Cancel 1/10 commands + Task cancelTask = null; + bool cancelCommand = rnd.NextBool(0.1); + if (cancelCommand) + { + if (rnd.NextBool()) + { + // Use DbCommand.Cancel + cancelTask = Task.Run(() => CommandCancel(com)); + } + else + { + // Use CancellationTokenSource + if (cts == null) cts = new CancellationTokenSource(); + cancelTask = Task.Run(() => cts.Cancel()); + } + } + + // Get the CancellationToken + CancellationToken token = (cts != null) ? cts.Token : CancellationToken.None; + + DataStressReader reader = null; + try + { + if (query) + { + CommandBehavior commandBehavior = CommandBehavior.Default; + if (rnd.NextBool(0.5)) commandBehavior |= CommandBehavior.SequentialAccess; + if (rnd.NextBool(0.25)) commandBehavior |= CommandBehavior.KeyInfo; + if (rnd.NextBool(0.1)) commandBehavior |= CommandBehavior.SchemaOnly; + + // Get the reader + reader = new DataStressReader(await com.ExecuteReaderSyncOrAsync(commandBehavior, token, rnd)); + + // Consume the reader's data + await ConsumeReaderAsync(reader, commandBehavior.HasFlag(CommandBehavior.SequentialAccess), token, rnd); + } + else + { + await com.ExecuteNonQuerySyncOrAsync(token, rnd); + } + } + catch (Exception e) + { + if (cancelCommand && IsCommandCancelledException(e)) + { + // Catch command canceled exception + } + else + { + throw; + } + } + finally + { + if (cancelTask != null) AsyncUtils.WaitAndUnwrapException(cancelTask); + if (reader != null && ShouldCloseDataReader()) reader.Close(); + } + } + + /// + /// Utility function to consume a reader in a random fashion + /// + protected virtual async Task ConsumeReaderAsync(DataStressReader reader, bool sequentialAccess, CancellationToken token, Random rnd) + { + // Close 1/10 of readers while they are reading + Task closeTask = null; + if (AllowReaderCloseDuringReadAsync() && rnd.NextBool(0.1)) + { + // Begin closing now on another thread + closeTask = reader.CloseAsync(); + } + + try + { + do + { + while (await reader.ReadSyncOrAsync(token, rnd)) + { + // Optionally stop reading the current result set + if (rnd.NextBool(0.1)) break; + + // Read the current row + await ConsumeRowAsync(reader, sequentialAccess, token, rnd); + } + + // Executing NextResult only 50% of the time + if (rnd.NextBool()) + break; + } while (await reader.NextResultSyncOrAsync(token, rnd)); + } + catch (Exception e) + { + if (closeTask != null && IsReaderClosedException(e)) + { + // Catch reader closed exception + } + else + { + throw; + } + } + finally + { + if (closeTask != null) AsyncUtils.WaitAndUnwrapException(closeTask); + } + } + + /// + /// Utility function to consume a single row of a reader in a random fashion after Read/ReadAsync has been invoked. + /// + protected virtual async Task ConsumeRowAsync(DataStressReader reader, bool sequentialAccess, CancellationToken token, Random rnd) + { + for (int i = 0; i < reader.FieldCount; i++) + { + if (rnd.Next(10) == 0) break; // stop reading from this row + if (rnd.Next(2) == 0) continue; // skip this field + bool hasBeenRead = false; + + // If the field is not null, we can optionally use streaming API + if ((!await reader.IsDBNullSyncOrAsync(i, token, rnd)) && (rnd.NextBool())) + { + Type t = reader.GetFieldType(i); + if (t == typeof(byte[])) + { + await ConsumeBytesAsync(reader, i, token, rnd); + hasBeenRead = true; + } + else if (t == typeof(string)) + { + await ConsumeCharsAsync(reader, i, token, rnd); + hasBeenRead = true; + } + } + + // If the field has not yet been read, or if it is non-sequential then we can re-read it + if ((!hasBeenRead) || (!sequentialAccess)) + { + if (!await reader.IsDBNullSyncOrAsync(i, token, rnd)) + { + // Field value is not null, we can use new GetFieldValue methods + await reader.GetValueSyncOrAsync(i, token, rnd); + } + else + { + // Field value is null, we have to use old GetValue method + reader.GetValue(i); + } + } + + // Do IsDBNull check again with 50% probability + if (rnd.NextBool()) await reader.IsDBNullSyncOrAsync(i, token, rnd); + } + } + + protected virtual async Task ConsumeBytesAsync(DataStressReader reader, int i, CancellationToken token, Random rnd) + { + byte[] buffer = new byte[255]; + + if (rnd.NextBool()) + { + // We can optionally use GetBytes + reader.GetBytes(i, rnd.Next(20), buffer, rnd.Next(20), rnd.Next(200)); + } + else if (reader.GetName(i) != "timestamp_FLD") + { + // Timestamp appears to be binary, but cannot be read by Stream + DataStressStream stream = reader.GetStream(i); + await stream.ReadSyncOrAsync(buffer, rnd.Next(20), rnd.Next(200), token, rnd); + } + else + { + // It is timestamp column, so read it later with GetValueSyncOrAsync + await reader.GetValueSyncOrAsync(i, token, rnd); + } + } + + protected virtual async Task ConsumeCharsAsync(DataStressReader reader, int i, CancellationToken token, Random rnd) + { + char[] buffer = new char[255]; + + if (rnd.NextBool()) + { + // Read with GetChars + reader.GetChars(i, rnd.Next(20), buffer, rnd.Next(20), rnd.Next(200)); + } + else if (reader.GetProviderSpecificFieldType(i) == typeof(SqlXml)) + { + // SqlClient only: Xml is read by XmlReader + DataStressXmlReader xmlReader = reader.GetXmlReader(i); + xmlReader.Read(); + } + else + { + // Read with TextReader + DataStressTextReader textReader = reader.GetTextReader(i); + if (rnd.NextBool()) + { + textReader.Peek(); + } + await textReader.ReadSyncOrAsync(buffer, rnd.Next(20), rnd.Next(200), rnd); + if (rnd.NextBool()) + { + textReader.Peek(); + } + } + } + + /// + /// Returns true if the given exception is expected for the current provider when a command is cancelled by another thread. + /// + /// + protected virtual bool IsCommandCancelledException(Exception e) + { + return e is TaskCanceledException; + } + + /// + /// Returns true if the given exception is expected for the current provider when trying to read from a reader that has been closed + /// + /// + protected virtual bool IsReaderClosedException(Exception e) + { + return false; + } + + /// + /// Returns true if the given exception is expected for the current provider when trying to connect to unavailable/non-existent server + /// + /// + protected bool IsServerNotAccessibleException(Exception e, string connString, string dataSource) + { + return + e is ArgumentException && + connString.Contains("MultiSubnetFailover=True") && + dataSource.Contains("np:") && + e.Message.Contains("Connecting to a SQL Server instance using the MultiSubnetFailover connection option is only supported when using the TCP protocol."); + } + + /// + /// Returns true if the backend provider supports closing a datareader while asynchronously reading from it + /// + /// + protected virtual bool AllowReaderCloseDuringReadAsync() + { + return false; + } + + /// + /// Thread Callback function which cancels queries using DbCommand.Cancel() + /// + /// + protected void CommandCancel(object o) + { + try + { + DbCommand cmd = (DbCommand)o; + cmd.Cancel(); + } + catch (Exception ex) + { + Trace.WriteLine(ex.ToString(), this.ToString()); + } + } + + #endregion + + #region Command and Parameter Tests + + /// + /// Command Reader Test: Executes a simple SELECT statement without parameters + /// + [StressTest("TestCommandReader", Weight = 10)] + public void TestCommandReader() + { + Random rnd = RandomInstance; + + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + DbCommand com = Factory.GetCommand(rnd, table, conn, true); + CommandExecute(rnd, com, true); + } + } + + /// + /// Command Select Test: Executes a single SELECT statement with parameters + /// + [StressTest("TestCommandSelect", Weight = 10)] + public void TestCommandSelect() + { + Random rnd = RandomInstance; + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + DbCommand com = Factory.GetSelectCommand(rnd, table, conn); + CommandExecute(rnd, com, true); + } + } + + /// + /// Command Insert Test: Executes a single INSERT statement with parameters + /// + [StressTest("TestCommandInsert", Weight = 10)] + public void TestCommandInsert() + { + Random rnd = RandomInstance; + + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + DbCommand com = Factory.GetInsertCommand(rnd, table, conn); + CommandExecute(rnd, com, false); + } + } + + /// + /// Command Update Test: Executes a single UPDATE statement with parameters + /// + [StressTest("TestCommandUpdate", Weight = 10)] + public void TestCommandUpdate() + { + Random rnd = RandomInstance; + + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + DbCommand com = Factory.GetUpdateCommand(rnd, table, conn); + CommandExecute(rnd, com, false); + } + } + + /// + /// Command Update Test: Executes a single DELETE statement with parameters + /// + [StressTest("TestCommandDelete", Weight = 10)] + public void TestCommandDelete() + { + Random rnd = RandomInstance; + + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + DbCommand com = Factory.GetDeleteCommand(rnd, table, conn); + CommandExecute(rnd, com, false); + } + } + + [StressTest("TestCommandTimeout", Weight = 10)] + public void TestCommandTimeout() + { + Random rnd = RandomInstance; + DataStressConnection conn = null; + try + { + // Use a transaction 50% of the time + if (rnd.NextBool()) + { + } + + // Create a select command + conn = Factory.CreateConnection(rnd); + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + DbCommand com = Factory.GetSelectCommand(rnd, table, conn); + + // Setup timeout. We want to see various possibilities of timeout happening before, after, or at the same time as when the result comes in. + int delay = rnd.Next(0, 10); // delay is from 0 to 9 seconds inclusive + int timeout = rnd.Next(1, 10); // timeout is from 1 to 9 seconds inclusive + com.CommandText += string.Format("; WAITFOR DELAY '00:00:0{0}'", delay); + com.CommandTimeout = timeout; + + // Execute command and catch timeout exception + try + { + CommandExecute(rnd, com, true); + } + catch (DbException e) + { + if (e is SqlException && ((SqlException)e).Number == 3989) + { + throw DataStressErrors.ProductError("Timing issue between OnTimeout and ReadAsyncCallback results in SqlClient's packet parsing going out of sync", e); + } + else if (!e.Message.ToLower().Contains("timeout")) + { + throw; + } + } + } + finally + { + if (conn != null) conn.Dispose(); + } + } + + [StressTest("TestCommandAndReaderAsync", Weight = 10)] + public void TestCommandAndReaderAsync() + { + // Since we're calling an "async" method, we need to do a Wait() here. + AsyncUtils.WaitAndUnwrapException(TestCommandAndReaderAsyncInternal()); + } + + /// + /// Utility method to test Async scenario using await keyword + /// + /// + protected virtual async Task TestCommandAndReaderAsyncInternal() + { + Random rnd = RandomInstance; + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + DbCommand com; + + com = Factory.GetInsertCommand(rnd, table, conn); + await CommandExecuteAsync(rnd, com, false); + + com = Factory.GetDeleteCommand(rnd, table, conn); + await CommandExecuteAsync(rnd, com, false); + + com = Factory.GetSelectCommand(rnd, table, conn); + await com.ExecuteScalarAsync(); + + com = Factory.GetSelectCommand(rnd, table, conn); + await CommandExecuteAsync(rnd, com, true); + } + } + + /// + /// Utility function used by MARS tests + /// + private void TestCommandMARS(Random rnd, bool query) + { + if (Source.Type != DataSourceType.SqlServer) + return; // skip for non-SQL Server databases + + using (DataStressConnection conn = Factory.CreateConnection(rnd, DataStressFactory.ConnectionStringOptions.EnableMars)) + { + if (!OpenConnection(conn)) return; + DbCommand[] commands = new DbCommand[rnd.Next(5, 10)]; + List tasks = new List(); + // Create commands + for (int i = 0; i < commands.Length; i++) + { + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + commands[i] = Factory.GetCommand(rnd, table, conn, query); + } + + try + { + // Execute commands + for (int i = 0; i < commands.Length; i++) + { + if (rnd.NextBool(0.7)) + tasks.Add(CommandExecuteAsync(rnd, commands[i], query)); + else + CommandExecute(rnd, commands[i], query); + } + } + finally + { + // All commands must be complete before closing the connection + AsyncUtils.WaitAll(tasks.ToArray()); + } + } + } + + /// + /// Command MARS Test: Tests MARS by executing multiple readers on same connection + /// + [StressTest("TestCommandMARSRead", Weight = 10)] + public void TestCommandMARSRead() + { + Random rnd = RandomInstance; + TestCommandMARS(rnd, true); + } + + /// + /// Command MARS Test: Tests MARS by getting multiple connection objects from same connection + /// + [StressTest("TestCommandMARSWrite", Weight = 10)] + public void TestCommandMARSWrite() + { + Random rnd = RandomInstance; + TestCommandMARS(rnd, false); + } + + #endregion + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/Extensions.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/Extensions.cs new file mode 100644 index 0000000000..2629bc20bb --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/Extensions.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Data; +using System.Data.Common; +using Microsoft.Data.SqlClient; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; + +namespace Stress.Data +{ + public static class Extensions + { + /// the probability that true will be returned + public static bool NextBool(this Random rnd, double probability) + { + return rnd.NextDouble() < probability; + } + + /// + /// Generate a true or false with equal probability. + /// + public static bool NextBool(this Random rnd) + { + return rnd.NextBool(0.5); + } + + public static Task ExecuteNonQuerySyncOrAsync(this DbCommand command, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + command.ExecuteNonQuery, + () => command.ExecuteNonQueryAsync(token), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public static Task ExecuteScalarSyncOrAsync(this DbCommand command, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + command.ExecuteScalar, + () => command.ExecuteScalarAsync(token), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public static Task ExecuteReaderSyncOrAsync(this DbCommand command, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + command.ExecuteReader, + () => command.ExecuteReaderAsync(token), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public static Task ExecuteReaderSyncOrAsync(this DbCommand command, CommandBehavior cb, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + () => command.ExecuteReader(cb), + () => command.ExecuteReaderAsync(cb, token), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public static Task ExecuteReaderSyncOrAsync(this SqlCommand command, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + command.ExecuteReader, + () => command.ExecuteReaderAsync(token), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public static Task ExecuteReaderSyncOrAsync(this SqlCommand command, CommandBehavior cb, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + () => command.ExecuteReader(cb), + () => command.ExecuteReaderAsync(cb, token), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + + public static Task ExecuteXmlReaderSyncOrAsync(this SqlCommand command, CancellationToken token, Random rnd) + { + return AsyncUtils.SyncOrAsyncMethod( + command.ExecuteXmlReader, + () => command.ExecuteXmlReaderAsync(token), + AsyncUtils.ChooseSyncAsyncMode(rnd) + ); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/SqlClient.Stress.Framework.csproj b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/SqlClient.Stress.Framework.csproj new file mode 100644 index 0000000000..e76a05f52a --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/SqlClient.Stress.Framework.csproj @@ -0,0 +1,23 @@ + + + + Stress.Data + net481;net9.0 + latest + + + + + + + + + + + + + Always + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressConfigReader.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressConfigReader.cs new file mode 100644 index 0000000000..24344a04f9 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressConfigReader.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.IO; +using System.Xml; +using System.Xml.XPath; +using static Stress.Data.DataStressSettings; + +namespace Stress.Data +{ + /// + /// Reads the configuration from a configuration file and provides the configuration + /// + internal class StressConfigReader + { + private string _configFilePath; + private const string dataStressSettings = "dataStressSettings"; + private const string sourcePath = "//dataStressSettings/sources/source"; + internal List Sources + { + get; private set; + } + + public StressConfigReader(string configFilePath) + { + this._configFilePath = configFilePath; + } + + internal void Load() + { + XmlReader reader = null; + try + { + Sources = new List(); + reader = CreateReader(); + + XPathDocument xpathDocument = new XPathDocument(reader); + + XPathNavigator navigator = xpathDocument.CreateNavigator(); + + XPathNodeIterator sourceIterator = navigator.Select(sourcePath); + + foreach (XPathNavigator sourceNavigator in sourceIterator) + { + string nsUri = sourceNavigator.NamespaceURI; + string sourceName = sourceNavigator.GetAttribute("name", nsUri); + string sourceType = sourceNavigator.GetAttribute("type", nsUri); + bool isDefault; + isDefault = bool.TryParse(sourceNavigator.GetAttribute("isDefault", nsUri), out isDefault) ? isDefault : false; + string dataSource = sourceNavigator.GetAttribute("dataSource", nsUri); + string user = sourceNavigator.GetAttribute("user", nsUri); + string password = sourceNavigator.GetAttribute("password", nsUri); + string database = sourceNavigator.GetAttribute("database", nsUri); + bool supportsWindowsAuthentication; + supportsWindowsAuthentication = bool.TryParse(sourceNavigator.GetAttribute("supportsWindowsAuthentication", nsUri), out supportsWindowsAuthentication) ? supportsWindowsAuthentication : false; + bool isLocal; + isLocal = bool.TryParse(sourceNavigator.GetAttribute("isLocal", nsUri), out isLocal) ? isLocal : false; + bool disableMultiSubnetFailover; + disableMultiSubnetFailover = bool.TryParse(sourceNavigator.GetAttribute("disableMultiSubnetFailover", nsUri), out disableMultiSubnetFailover) ? disableMultiSubnetFailover : false; + bool disableNamedPipes; + disableMultiSubnetFailover = bool.TryParse(sourceNavigator.GetAttribute("disableNamedPipes", nsUri), out disableNamedPipes) ? disableNamedPipes : false; + bool encrypt; + encrypt = bool.TryParse(sourceNavigator.GetAttribute("encrypt", nsUri), out encrypt) ? encrypt : false; + + DataSourceElement element = new DataSourceElement(sourceName, sourceType, null, dataSource, database, user, password, ds_isDefault: isDefault, ds_isLocal: isLocal, disableMultiSubnetFailoverSetup: disableMultiSubnetFailover, disableNamedPipes: disableNamedPipes, encrypt: encrypt); + Sources.Add(element); + } + } + catch (XmlException e) + { + throw new InvalidDataException($"Error reading configuration file '{_configFilePath}': {e.Message}", e); + } + catch (IOException e) + { + throw new InvalidDataException($"Error reading configuration file '{_configFilePath}': {e.Message}", e); + } + catch (System.Exception e) + { + throw new InvalidDataException($"Error reading configuration file '{_configFilePath}': {e.Message}", e); + } + finally + { + reader?.Dispose(); + } + } + + private XmlReader CreateReader() + { + FileStream configurationStream = new FileStream("SqlClient.Stress.Framework/" + _configFilePath, FileMode.Open); + XmlReaderSettings settings = new XmlReaderSettings(); + settings.DtdProcessing = DtdProcessing.Prohibit; + XmlReader reader = XmlReader.Create(configurationStream, settings); + return reader; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressTest.config b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressTest.config new file mode 100644 index 0000000000..7a6a422924 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressTest.config @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/TrackedRandom.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/TrackedRandom.cs new file mode 100644 index 0000000000..b42128fff5 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/TrackedRandom.cs @@ -0,0 +1,184 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace Stress.Data +{ + /// + /// Random number generator that tracks information necessary to reproduce a sequence of random numbers. + /// + /// + /// There are three items maintained by instances of this class + /// that are used to assist in the reproduction of a sequence of generated numbers: + /// + /// 1. The seed used for initialization. + /// 2. The count of numbers generated. + /// 3. Markers to indicate relevant points in the sequence. + /// + /// For tests that use random numbers to control execution, + /// these tracked items can be used to help determine the specific code path that was executed. + /// Here's an example: + /// + /// A test starts to execute, and retrieves an instance of this class. + /// If an instance of this class has not been created beforehand, it is constructed and the *seed* is stored. + /// The test inserts a *marker* to track the *count* of numbers generated before the test starts its work. + /// As the test executes, it asks for a sequence of random numbers. At some point, the test causes a crash. + /// Using the resulting dump (or live debugging session if available), it is possible to examine an instance + /// of this class to recreate the sequence of numbers used by the test. + /// You can create an instance of a Random offline using the tracked *seed*, + /// and generate numbers up to the *marked* count to determine the starting point for the sequence of numbers used by the test. + /// The length of the sequence is indicated by the last *count* of number generated. + /// So for a failed test, you can use the numbers from Mark+1 to Count to retrace the code path taken by the test. + /// + /// Instances of this class keep track of a finite number of multiple marks, + /// so it is possible to track the beginning and end of a series of tests, + /// assuming they all mark at least the start of their execution. + /// + public class TrackedRandom : Random + { + private readonly int _seed; + + /// + /// Number of random numbers generated. + /// + private long _count; + + /// + /// Circular buffer to track the most recent marks that indicate the count at the time a given mark was created. + /// + private readonly long[] _marks = new long[16]; + + /// + /// Index of where to place next mark in buffer. + /// This index is incremented after each mark, and wraps around as necessary. + /// + private int _nextMark; + + private const int EmptyMark = -1; + + public TrackedRandom() + : this(Environment.TickCount) + { + } + + public TrackedRandom(int seed) + : base(seed) + { + _seed = seed; + + for (int i = 0; i < _marks.Length; i++) + { + _marks[i] = EmptyMark; + } + } + + public int Seed + { + get + { + return _seed; + } + } + + public long Count + { + get + { + return _count; + } + } + + public void Mark() + { + long mark = _count; + + // marking forward + _marks[_nextMark++] = mark; + + // wrap when necessary + if (_nextMark == _marks.Length) + { + _nextMark = 0; + } + } + + /// + /// Return an enumerable that can be used to iterate over the most recent marks, + /// starting from the most recent, and ending with the earliest mark still being tracked. + /// + public IEnumerable Marks + { + get + { + // Iterate backwards through the mark array, + // starting just before the index of the next mark, + // and ending at the next mark. + // Iteration stops earlier if an empty mark is found. + int index; + long mark; + + for (int i = 1; i <= _marks.Length; i++) + { + // Index of current element determined by: + // ((L+n) - i) % L + // where + // L is the length of the array, + // n is the index of where to insert the next mark, 0 <= n < L, + // i is the current iteration variable value, 0 < i <= L. + index = (_marks.Length + _nextMark - i) % _marks.Length; + mark = _marks[index]; + + if (mark == EmptyMark) + { + break; + } + + yield return mark; + } + } + } + + private void IncrementCount() + { + if (_count == long.MaxValue) + { + _count = -1; + } + + ++_count; + } + + public override int Next() + { + IncrementCount(); + return base.Next(); + } + + public override int Next(int minValue, int maxValue) + { + IncrementCount(); + return base.Next(minValue, maxValue); + } + + public override int Next(int maxValue) + { + IncrementCount(); + return base.Next(maxValue); + } + + public override void NextBytes(byte[] buffer) + { + IncrementCount(); + base.NextBytes(buffer); + } + + public override double NextDouble() + { + IncrementCount(); + return base.NextDouble(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Constants.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Constants.cs new file mode 100644 index 0000000000..10f0ecb41b --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Constants.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace DPStressHarness +{ + public static class Constants + { + public const string XML_ELEM_RESULTS = "PerfResults"; + public const string XML_ELEM_RUN = "Run"; + public const string XML_ELEM_RUN_METRIC = "RunMetric"; + public const string XML_ELEM_TEST = "Test"; + public const string XML_ELEM_TEST_METRIC = "TestMetric"; + public const string XML_ELEM_EXCEPTION = "Exception"; + + public const string XML_ATTR_RUN_LABEL = "label"; + public const string XML_ATTR_RUN_START_TIME = "startTime"; + public const string XML_ATTR_RUN_OFFICIAL = "official"; + public const string XML_ATTR_RUN_MILESTONE = "milestone"; + public const string XML_ATTR_RUN_BRANCH = "branch"; + public const string XML_ATTR_RUN_UPLOADED = "uploaded"; + public const string XML_ATTR_RUN_METRIC_NAME = "name"; + public const string XML_ATTR_TEST_NAME = "name"; + public const string XML_ATTR_TEST_METRIC_NAME = "name"; + public const string XML_ATTR_TEST_METRIC_UNITS = "units"; + public const string XML_ATTR_TEST_METRIC_ISHIGHERBETTER = "isHigherBetter"; + + public const string XML_ATTR_VALUE_TRUE = "true"; + public const string XML_ATTR_VALUE_FALSE = "false"; + + public const string RUN_METRIC_PROCESSOR_COUNT = "Processor Count"; + public const string RUN_DNS_HOST_NAME = "DNS Host Name"; + public const string RUN_IDENTITY_NAME = "Identity Name"; + public const string RUN_PROCESS_MACHINE_NAME = "Process Machine Name"; + + public const string TEST_METRIC_TEST_ASSEMBLY = "Test Assembly"; + public const string TEST_METRIC_TEST_IMPROVEMENT = "Improvement"; + public const string TEST_METRIC_TEST_OWNER = "Owner"; + public const string TEST_METRIC_TEST_CATEGORY = "Category"; + public const string TEST_METRIC_TEST_PRIORITY = "Priority"; + public const string TEST_METRIC_APPLICATION_NAME = "Application Name"; + public const string TEST_METRIC_TARGET_ASSEMBLY_NAME = "Target Assembly Name"; + public const string TEST_METRIC_ELAPSED_SECONDS = "Elapsed Seconds"; + public const string TEST_METRIC_RPS = "Requests Per Second"; + public const string TEST_METRIC_PEAK_WORKING_SET = "Peak Working Set"; + public const string TEST_METRIC_WORKING_SET = "Working Set"; + public const string TEST_METRIC_PRIVATE_BYTES = "Private Bytes"; + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Ex API/MemApi.Windows.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Ex API/MemApi.Windows.cs new file mode 100644 index 0000000000..053aea09a1 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Ex API/MemApi.Windows.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; + +namespace DPStressHarness +{ + static class MemApi + { + [DllImport("KERNEL32")] + public static extern IntPtr GetCurrentProcess(); + + [DllImport("KERNEL32")] + public static extern bool SetProcessWorkingSetSize(IntPtr hProcess, int dwMinimumWorkingSetSize, int dwMaximumWorkingSetSize); + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/ITestAttributeFilter.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/ITestAttributeFilter.cs new file mode 100644 index 0000000000..c3afa9251d --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/ITestAttributeFilter.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace DPStressHarness +{ + public interface ITestAttributeFilter + { + bool MatchFilter(string filterString); + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/LogManager.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/LogManager.cs new file mode 100644 index 0000000000..9b534a24b5 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/LogManager.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Text; + +namespace DPStressHarness +{ + public class LogManager: IDisposable + { + private static readonly LogManager s_instance = new LogManager(); + private readonly ConcurrentDictionary _logs = new ConcurrentDictionary(); + private DirectoryInfo _directoryInfo; + + private LogManager() + { + try + { + _directoryInfo = Directory.CreateDirectory("../../../logs"); + } + catch (Exception e) + { + Console.WriteLine($"The process failed: {e}"); + } + } + + public static LogManager Instance => s_instance; + + public void Dispose() + { + _logs.ToList().ForEach(l => l.Value.Close()); + } + + public TextWriter GetLog(string name) + { + if (!_logs.TryGetValue(name, out TextWriter log)) + { + Console.WriteLine($"{_directoryInfo.FullName}/{name}.log log file created!"); + log = new StreamWriter($"{_directoryInfo.FullName}/{name}.log", false, Encoding.UTF8) { AutoFlush = true } ; + _logs.TryAdd(name, log); + } + return log; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/FakeConsole.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/FakeConsole.cs new file mode 100644 index 0000000000..f13949ae8e --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/FakeConsole.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace DPStressHarness +{ + public static class FakeConsole + { + public static void Write(string value) + { +#if DEBUG + Console.Write(value); +#endif + } + + public static void WriteLine(string value) + { +#if DEBUG + Console.WriteLine(value); +#endif + } + + public static void WriteLine(string format, params object[] arg) + { +#if DEBUG + Console.WriteLine(format, arg); +#endif + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/Logger.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/Logger.cs new file mode 100644 index 0000000000..9226c4b930 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/Logger.cs @@ -0,0 +1,226 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Xml; +using System.Diagnostics; + +namespace DPStressHarness +{ + public class Logger + { + private const string _resultDocumentName = "perfout.xml"; + + private XmlDocument _doc; + private XmlElement _runElem; + private XmlElement _testElem; + + public Logger(string runLabel, bool isOfficial, string milestone, string branch) + { + _doc = GetTestResultDocument(); + + _runElem = GetRunElement(_doc, runLabel, DateTime.Now.ToString(), isOfficial, milestone, branch); + + Process currentProcess = Process.GetCurrentProcess(); + AddRunMetric(Constants.RUN_PROCESS_MACHINE_NAME, currentProcess.MachineName); + AddRunMetric(Constants.RUN_DNS_HOST_NAME, System.Net.Dns.GetHostName()); + AddRunMetric(Constants.RUN_IDENTITY_NAME, Environment.UserName); + AddRunMetric(Constants.RUN_METRIC_PROCESSOR_COUNT, Environment.ProcessorCount.ToString()); + } + + public void AddRunMetric(string metricName, string metricValue) + { + Debug.Assert(_runElem != null); + + if (metricValue.Equals(string.Empty)) + return; + + AddRunMetricElement(_runElem, metricName, metricValue); + } + + public void AddTest(string testName) + { + Debug.Assert(_runElem != null); + + _testElem = AddTestElement(_runElem, testName); + } + + public void AddTestMetric(string metricName, string metricValue, string metricUnits) + { + AddTestMetric(metricName, metricValue, metricUnits, null); + } + + public void AddTestMetric(string metricName, string metricValue, string metricUnits, bool? isHigherBetter) + { + Debug.Assert(_runElem != null); + Debug.Assert(_testElem != null); + + if (metricValue.Equals(string.Empty)) + return; + + AddTestMetricElement(_testElem, metricName, metricValue, metricUnits, isHigherBetter); + } + + public void AddTestException(string exceptionData) + { + Debug.Assert(_runElem != null); + Debug.Assert(_testElem != null); + + AddTestExceptionElement(_testElem, exceptionData); + } + + public void Save() + { + FileStream resultDocumentStream = new FileStream(_resultDocumentName, FileMode.Create); + _doc.Save(resultDocumentStream); + resultDocumentStream.Dispose(); + } + + private static XmlDocument GetTestResultDocument() + { + if (File.Exists(_resultDocumentName)) + { + XmlDocument doc = new XmlDocument(); + FileStream resultDocumentStream = new FileStream(_resultDocumentName, FileMode.Open, FileAccess.Read); + doc.Load(resultDocumentStream); + resultDocumentStream.Dispose(); + return doc; + } + else + { + XmlDocument doc = new XmlDocument(); + doc.LoadXml(""); + FileStream resultDocumentStream = new FileStream(_resultDocumentName, FileMode.CreateNew); + doc.Save(resultDocumentStream); + resultDocumentStream.Dispose(); + return doc; + } + } + + + private static XmlElement GetRunElement(XmlDocument doc, string label, string startTime, bool isOfficial, string milestone, string branch) + { + foreach (XmlNode node in doc.DocumentElement.ChildNodes) + { + if (node.NodeType == XmlNodeType.Element && + node.Name.Equals(Constants.XML_ELEM_RUN) && + ((XmlElement)node).GetAttribute(Constants.XML_ATTR_RUN_LABEL).Equals(label)) + { + return (XmlElement)node; + } + } + + XmlElement runElement = doc.CreateElement(Constants.XML_ELEM_RUN); + + XmlAttribute attrLabel = doc.CreateAttribute(Constants.XML_ATTR_RUN_LABEL); + attrLabel.Value = label; + runElement.Attributes.Append(attrLabel); + + XmlAttribute attrStartTime = doc.CreateAttribute(Constants.XML_ATTR_RUN_START_TIME); + attrStartTime.Value = startTime; + runElement.Attributes.Append(attrStartTime); + + XmlAttribute attrOfficial = doc.CreateAttribute(Constants.XML_ATTR_RUN_OFFICIAL); + attrOfficial.Value = isOfficial.ToString(); + runElement.Attributes.Append(attrOfficial); + + if (milestone != null) + { + XmlAttribute attrMilestone = doc.CreateAttribute(Constants.XML_ATTR_RUN_MILESTONE); + attrMilestone.Value = milestone; + runElement.Attributes.Append(attrMilestone); + } + + if (branch != null) + { + XmlAttribute attrBranch = doc.CreateAttribute(Constants.XML_ATTR_RUN_BRANCH); + attrBranch.Value = branch; + runElement.Attributes.Append(attrBranch); + } + + doc.DocumentElement.AppendChild(runElement); + + return runElement; + } + + + private static void AddRunMetricElement(XmlElement runElement, string name, string value) + { + // First check and make sure the metric hasn't already been added. + // If it has, it's from a previous test in the same run, so just return. + foreach (XmlNode node in runElement.ChildNodes) + { + if (node.NodeType == XmlNodeType.Element && node.Name.Equals(Constants.XML_ELEM_RUN_METRIC)) + { + if (node.Attributes[Constants.XML_ATTR_RUN_METRIC_NAME].Value.Equals(name)) + return; + } + } + + XmlElement runMetricElement = runElement.OwnerDocument.CreateElement(Constants.XML_ELEM_RUN_METRIC); + + XmlAttribute attrName = runElement.OwnerDocument.CreateAttribute(Constants.XML_ATTR_RUN_METRIC_NAME); + attrName.Value = name; + runMetricElement.Attributes.Append(attrName); + + XmlText nodeValue = runElement.OwnerDocument.CreateTextNode(value); + runMetricElement.AppendChild(nodeValue); + + runElement.AppendChild(runMetricElement); + } + + + private static XmlElement AddTestElement(XmlElement runElement, string name) + { + XmlElement testElement = runElement.OwnerDocument.CreateElement(Constants.XML_ELEM_TEST); + + XmlAttribute attrName = runElement.OwnerDocument.CreateAttribute(Constants.XML_ATTR_TEST_NAME); + attrName.Value = name; + testElement.Attributes.Append(attrName); + + runElement.AppendChild(testElement); + + return testElement; + } + + + private static void AddTestMetricElement(XmlElement testElement, string name, string value, string units, bool? isHigherBetter) + { + XmlElement testMetricElement = testElement.OwnerDocument.CreateElement(Constants.XML_ELEM_TEST_METRIC); + + XmlAttribute attrName = testElement.OwnerDocument.CreateAttribute(Constants.XML_ATTR_TEST_METRIC_NAME); + attrName.Value = name; + testMetricElement.Attributes.Append(attrName); + + if (units != null) + { + XmlAttribute attrUnits = testElement.OwnerDocument.CreateAttribute(Constants.XML_ATTR_TEST_METRIC_UNITS); + attrUnits.Value = units; + testMetricElement.Attributes.Append(attrUnits); + } + + if (isHigherBetter.HasValue) + { + XmlAttribute attrIsHigherBetter = testElement.OwnerDocument.CreateAttribute(Constants.XML_ATTR_TEST_METRIC_ISHIGHERBETTER); + attrIsHigherBetter.Value = isHigherBetter.ToString(); + testMetricElement.Attributes.Append(attrIsHigherBetter); + } + + XmlText nodeValue = testElement.OwnerDocument.CreateTextNode(value); + testMetricElement.AppendChild(nodeValue); + + testElement.AppendChild(testMetricElement); + } + + private static void AddTestExceptionElement(XmlElement testElement, string exceptionData) + { + XmlElement testFailureElement = testElement.OwnerDocument.CreateElement(Constants.XML_ELEM_EXCEPTION); + XmlText txtNode = testFailureElement.OwnerDocument.CreateTextNode(exceptionData); + testFailureElement.AppendChild(txtNode); + + testElement.AppendChild(testFailureElement); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/MonitorLoadUtils.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/MonitorLoadUtils.cs new file mode 100644 index 0000000000..80661566fa --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/MonitorLoadUtils.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Monitoring; +using System.Reflection; + +namespace DPStressHarness +{ + public static class MonitorLoader + { + public static IMonitorLoader LoadMonitorLoaderAssembly() + { + IMonitorLoader monitorloader = null; + const string classname = "Monitoring.MonitorLoader"; + const string interfacename = "IMonitorLoader"; + Assembly mainAssembly = typeof(Monitoring.IMonitorLoader).GetTypeInfo().Assembly; + + Type t = mainAssembly.GetType(classname); + //make sure the type is derived from IMonitorLoader + Type[] interfaces = t.GetInterfaces(); + bool derivedFromIMonitorLoader = false; + if (interfaces != null) + { + foreach (Type intrface in interfaces) + { + if (intrface.Name == interfacename) + { + derivedFromIMonitorLoader = true; + } + } + } + if (derivedFromIMonitorLoader) + + { + monitorloader = (IMonitorLoader)Activator.CreateInstance(t); + + monitorloader.AssemblyPath = mainAssembly.FullName; + } + else + { + throw new Exception("The specified assembly does not implement " + interfacename + "!!"); + } + return monitorloader; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/RecordedExceptions.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/RecordedExceptions.cs new file mode 100644 index 0000000000..72a10f4d30 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/RecordedExceptions.cs @@ -0,0 +1,110 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; + +namespace DPStressHarness +{ + public class RecordedExceptions + { + // Reference wrapper around an integer which is used in order to make updating a little easier & more efficient + public class ExceptionCount + { + public int Count = 0; + } + + private ConcurrentDictionary> _exceptions = new ConcurrentDictionary>(); + + /// + /// Records an exception and returns true if the threshold is exceeded for that exception + /// + public bool Record(string testName, Exception ex) + { + // Converting from exception to string can be expensive so only do it once and cache the string + string exceptionString = ex.ToString(); + TraceException(testName, exceptionString); + + // Get the exceptions for the current test case + ConcurrentDictionary exceptionsForTest = _exceptions.GetOrAdd(testName, _ => new ConcurrentDictionary()); + + // Get the count for the current exception + ExceptionCount exCount = exceptionsForTest.GetOrAdd(exceptionString, _ => new ExceptionCount()); + + // Increment the count + Interlocked.Increment(ref exCount.Count); + + // If the count is over the threshold, return true + return TestMetrics.ExceptionThreshold.HasValue && (exCount.Count > TestMetrics.ExceptionThreshold); + } + + private void TraceException(string testName, string exceptionString) + { + StringBuilder status = new StringBuilder(); + status.AppendLine("========================================================================"); + status.AppendLine("Exception Report"); + status.AppendLine("========================================================================"); + + status.AppendLine(string.Format("Test: {0}", testName)); + status.AppendLine(exceptionString); + + status.AppendLine("========================================================================"); + status.AppendLine("End of Exception Report"); + status.AppendLine("========================================================================"); + Trace.WriteLine(status.ToString()); + } + + public void TraceAllExceptions() + { + StringBuilder status = new StringBuilder(); + status.AppendLine("========================================================================"); + status.AppendLine("All Exceptions Report"); + status.AppendLine(string.Format("Total test(s) with exception: {0}", _exceptions.Count)); + status.AppendLine("========================================================================"); + + foreach (string testName in _exceptions.Keys) + { + ConcurrentDictionary exceptionsForTest = _exceptions[testName]; + + int count = 1; + status.AppendLine(string.Format("Test: {0}", testName)); + foreach (var exceptionString in exceptionsForTest.Keys) + { + status.AppendLine(string.Format(" No: {0} of {1} [{2}]", count ++, exceptionsForTest.Count, testName)); + status.AppendLine(string.Format(" Count: {0}", exceptionsForTest[exceptionString].Count)); + status.AppendLine(string.Format(" Exception: {0}", exceptionString)); + status.AppendLine(); + } + + status.AppendLine(); + status.AppendLine(); + } + + status.AppendLine("========================================================================"); + status.AppendLine("End of All Exceptions Report"); + status.AppendLine("========================================================================"); + Trace.WriteLine(status.ToString()); + } + + public int GetExceptionsCount() + { + int count = 0; + + foreach (string testName in _exceptions.Keys) + { + ConcurrentDictionary exceptionsForTest = _exceptions[testName]; + + foreach (var exceptionString in exceptionsForTest.Keys) + { + count += exceptionsForTest[exceptionString].Count; + } + } + + return count; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/PerfCounters.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/PerfCounters.cs new file mode 100644 index 0000000000..84a3ccfba0 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/PerfCounters.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace DPStressHarness +{ + public class PerfCounters + { + private long _requestsCounter; + //private long rpsCounter; + + private long _exceptionsCounter; + //private long epsCounter; + + public PerfCounters() + { + } + + public void IncrementRequestsCounter() + { + _requestsCounter++; + } + + public void IncrementExceptionsCounter() + { + _exceptionsCounter++; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Program.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Program.cs new file mode 100644 index 0000000000..bc13ea7edc --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Program.cs @@ -0,0 +1,253 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace DPStressHarness//Microsoft.Data.SqlClient.Stress +{ + class Program + { + private static bool s_debugMode = false; + static void Main(string[] args) + { + Init(args); + Run(); + } + + public enum RunMode + { + RunAll, + RunVerify, + Help, + ExitWithError + }; + + private static RunMode s_mode = RunMode.RunAll; + private static IEnumerable s_tests; + private static StressEngine s_eng; + private static string s_error; + + public static void Init(string[] args) + { + for (int i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "-a": + string assemblyName = args[++i]; + TestFinder.AssemblyName = new AssemblyName(assemblyName); + break; + + case "-all": + s_mode = RunMode.RunAll; + break; + + case "-override": + TestMetrics.Overrides.Add(args[++i], args[++i]); + break; + + case "-variation": + TestMetrics.Variations.Add(args[++i]); + break; + + case "-test": + TestMetrics.SelectedTests.AddRange(args[++i].Split(';')); + break; + + case "-duration": + TestMetrics.StressDuration = int.Parse(args[++i]); + break; + + case "-threads": + TestMetrics.StressThreads = int.Parse(args[++i]); + break; + + case "-verify": + s_mode = RunMode.RunVerify; + break; + + case "-debug": + s_debugMode = true; + if (System.Diagnostics.Debugger.IsAttached) + { + System.Diagnostics.Debugger.Break(); + } + else + { + Console.WriteLine("Current PID: {0}, attach the debugger and press Enter to continue the execution...", System.Diagnostics.Process.GetCurrentProcess().Id); + Console.ReadLine(); + } + break; + + case "-exceptionThreshold": + TestMetrics.ExceptionThreshold = int.Parse(args[++i]); + break; + + case "-monitorenabled": + TestMetrics.MonitorEnabled = bool.Parse(args[++i]); + break; + + case "-randomSeed": + TestMetrics.RandomSeed = int.Parse(args[++i]); + break; + + case "-filter": + TestMetrics.Filter = args[++i]; + break; + + case "-printMethodName": + TestMetrics.PrintMethodName = true; + break; + + case "-deadlockdetection": + if (bool.Parse(args[++i])) + { + DeadlockDetection.Enable(); + } + break; + + default: + s_mode = RunMode.Help; + break; + } + } + + PrintConfigSummary(); + + if (TestFinder.AssemblyName != null) + { + Console.WriteLine("Assembly Found for the Assembly Name " + TestFinder.AssemblyName); + + // get and load all the tests + s_tests = TestFinder.GetTests(Assembly.Load(TestFinder.AssemblyName)); + + // instantiate the stress engine + s_eng = new StressEngine(TestMetrics.StressThreads, TestMetrics.StressDuration, s_tests, TestMetrics.RandomSeed); + } + else + { + Program.s_error = string.Format("Assembly {0} cannot be found.", TestFinder.AssemblyName); + s_mode = RunMode.ExitWithError; + } + } + + public static void Run() + { + if (TestFinder.AssemblyName == null) + { + s_mode = RunMode.Help; + } + switch (s_mode) + { + case RunMode.RunAll: + RunStress(); + break; + + case RunMode.RunVerify: + RunVerify(); + break; + + case RunMode.ExitWithError: + ExitWithError(); + break; + + case RunMode.Help: + PrintHelp(); + break; + } + } + + private static void PrintHelp() + { + Console.WriteLine("stresstest.exe [-a ] "); + Console.WriteLine(); + Console.WriteLine(" -a should specify path to the assembly containing the tests."); + Console.WriteLine(); + Console.WriteLine("Supported options are:"); + Console.WriteLine(); + Console.WriteLine(" -all Run all tests - best for debugging, not perf measurements."); + Console.WriteLine(); + Console.WriteLine(" -verify Run in functional verification mode."); + Console.WriteLine(); + Console.WriteLine(" -duration Duration of the test in seconds."); + Console.WriteLine(); + Console.WriteLine(" -threads Number of threads to use."); + Console.WriteLine(); + Console.WriteLine(" -override Override the value of a test property."); + Console.WriteLine(); + Console.WriteLine(" -test Run specific test(s)."); + Console.WriteLine(); + Console.WriteLine(" -debug Print process ID in the beginning and wait for Enter (to give your time to attach the debugger)."); + Console.WriteLine(); + Console.WriteLine(" -exceptionThreshold An optional limit on exceptions which will be caught. When reached, test will halt."); + Console.WriteLine(); + Console.WriteLine(" -monitorenabled True or False to enable monitoring. Default is false"); + Console.WriteLine(); + Console.WriteLine(" -randomSeed Enables setting of the random number generator used internally. This serves both the purpose"); + Console.WriteLine(" of helping to improve reproducibility and making it deterministic from Chess's perspective"); + Console.WriteLine(" for a given schedule. Default is " + TestMetrics.RandomSeed + "."); + Console.WriteLine(); + Console.WriteLine(" -filter Run tests whose stress test attributes match the given filter. Filter is not applied if attribute"); + Console.WriteLine(" does not implement ITestAttributeFilter. Example: -filter TestType=Query,Update;IsServerTest=True "); + Console.WriteLine(); + Console.WriteLine(" -printMethodName Print tests' title in console window"); + Console.WriteLine(); + Console.WriteLine(" -deadlockdetection True or False to enable deadlock detection. Default is false"); + Console.WriteLine(); + } + + private static void PrintConfigSummary() + { + const int border = 140; + Console.WriteLine(new string('#', border)); + Console.WriteLine($"\t AssemblyName:\t{TestFinder.AssemblyName}"); + Console.WriteLine($"\t Run mode:\t{Enum.GetName(typeof(RunMode), s_mode)}"); + foreach (KeyValuePair item in TestMetrics.Overrides) Console.WriteLine($"\t Override:\t{item.Key} = {item.Value}"); + foreach (string item in TestMetrics.SelectedTests) Console.WriteLine($"\t Test:\t{item}"); + Console.WriteLine($"\t Duration:\t{TestMetrics.StressDuration} second(s)"); + Console.WriteLine($"\t Threads No.:\t{TestMetrics.StressThreads}"); + Console.WriteLine($"\t Debug mode:\t{s_debugMode}"); + Console.WriteLine($"\t Exception threshold:\t{TestMetrics.ExceptionThreshold}"); + Console.WriteLine($"\t Random seed:\t{TestMetrics.RandomSeed}"); + Console.WriteLine($"\t Filter:\t{TestMetrics.Filter}"); + Console.WriteLine($"\t Deadlock detection enabled:\t{DeadlockDetection.IsEnabled}"); + Console.WriteLine(new string('#', border)); + } + + private static int ExitWithError() + { + Environment.FailFast("Exit with error(s)."); + return 1; + } + + private static int RunVerify() + { + throw new NotImplementedException(); + } + + private static int RunStress() + { + if (!s_debugMode) + { + try + { + TextWriter logOut = LogManager.Instance.GetLog("MDSStressTest-" + Environment.Version + + "-[" + Environment.OSVersion + "]-" + + DateTime.Now.ToString("MMMM dd yyyy @HHmmssFFF")); + Console.SetOut(logOut); + PrintConfigSummary(); + } + catch (Exception e) + { + Console.WriteLine($"Cannot open log file for writing!"); + Console.WriteLine(e); + } + } + return s_eng.Run(); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/SqlClient.Stress.Runner.csproj b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/SqlClient.Stress.Runner.csproj new file mode 100644 index 0000000000..23aebda620 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/SqlClient.Stress.Runner.csproj @@ -0,0 +1,16 @@ + + + + Exe + stresstest + net481;net9.0 + latest + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/StressEngine.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/StressEngine.cs new file mode 100644 index 0000000000..349b5c0539 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/StressEngine.cs @@ -0,0 +1,208 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Diagnostics; +using Monitoring; + +namespace DPStressHarness +{ + public class StressEngine + { + private Random _rnd; + private int _threads; + private int _duration; + private int _threadsRunning; + private bool _continue; + private List _allTests; + private RecordedExceptions _exceptions = new RecordedExceptions(); + private PerfCounters _perfcounters = null; + private static long s_globalRequestsCounter = 0; + + public RecordedExceptions Exceptions => _exceptions; + + public StressEngine(int threads, int duration, IEnumerable allTests, int seed) + { + if (seed != 0) + { + _rnd = new Random(seed); + } + else + { + Random rndBootstrap = new Random(); + + seed = rndBootstrap.Next(); + + _rnd = new Random(seed); + } + + Console.WriteLine("Seeding stress engine random number generator with {0}\n", seed); + + + _threads = threads; + _duration = duration; + _allTests = new List(); + + List tmpWeightedLookup = new List(); + + foreach (TestBase t in allTests) + { + if (t is StressTest) + { + _allTests.Add(t as StressTest); + } + } + + try + { + _perfcounters = new PerfCounters(); + } + catch (Exception e) + { + Console.WriteLine("Warning: An error occurred initializing performance counters. Performance counters can only be initialized when running with Administrator privileges. Error Message: " + e.Message); + } + } + + public int Run() + { + TraceListener listener = new TextWriterTraceListener(Console.Out); + Trace.Listeners.Add(listener); + Trace.UseGlobalLock = true; + + _threadsRunning = 0; + _continue = true; + + if (_allTests.Count == 0) + { + throw new ArgumentException("The specified assembly doesn't contain any tests to run. Test methods must be decorated with a Test, StressTest, MultiThreadedTest, or ThreadPoolTest attribute."); + } + + // Run any global setup + StressTest firstStressTest = _allTests.Find(t => t is StressTest); + if (null != firstStressTest) + { + firstStressTest.RunGlobalSetup(); + } + + //Monitoring Start + IMonitorLoader _monitorloader = null; + if (TestMetrics.MonitorEnabled) + { + _monitorloader = MonitorLoader.LoadMonitorLoaderAssembly(); + if (_monitorloader != null) + { + _monitorloader.Enabled = TestMetrics.MonitorEnabled; + _monitorloader.HostMachine = TestMetrics.MonitorMachineName; + _monitorloader.TestName = firstStressTest.Title; + _monitorloader.Action(MonitorLoaderUtils.MonitorAction.Start); + } + } + + for (int i = 0; i < _threads; i++) + { + Interlocked.Increment(ref _threadsRunning); + Thread t = new Thread(new ThreadStart(this.RunStressThread)); + t.Start(); + } + + while (_threadsRunning > 0) + { + Thread.Sleep(1000); + } + + //Monitoring Stop + if (TestMetrics.MonitorEnabled) + { + if (_monitorloader != null) + _monitorloader.Action(MonitorLoaderUtils.MonitorAction.Stop); + } + + // Run any global cleanup + if (null != firstStressTest) + { + firstStressTest.RunGlobalCleanup(); + } + + // Write out all exceptions + _exceptions.TraceAllExceptions(); + return _exceptions.GetExceptionsCount(); + } + + public void RunStressThread() + { + try + { + StressTest[] tests = new StressTest[_allTests.Count]; + List tmpWeightedLookup = new List(); + + for (int i = 0; i < tests.Length; i++) + { + tests[i] = _allTests[i].Clone(); + tests[i].RunSetup(); + + for (int j = 0; j < tests[i].Weight; j++) + { + tmpWeightedLookup.Add(i); + } + } + + int[] weightedLookup = tmpWeightedLookup.ToArray(); + + Stopwatch timer = new Stopwatch(); + long testDuration = _duration * Stopwatch.Frequency; + + timer.Reset(); + timer.Start(); + + while (_continue && timer.ElapsedTicks < testDuration) + { + int n = _rnd.Next(0, weightedLookup.Length); + StressTest test = tests[weightedLookup[n]]; + + if (TestMetrics.PrintMethodName) + { + FakeConsole.WriteLine("{0}: {1}", ++s_globalRequestsCounter, test.Title); + } + + try + { + DeadlockDetection.AddTestThread(); + test.Run(); + _perfcounters?.IncrementRequestsCounter(); + } + catch (Exception e) + { + _perfcounters?.IncrementExceptionsCounter(); + + test.HandleException(e); + + bool thresholdExceeded = _exceptions.Record(test.Title, e); + if (thresholdExceeded) + { + FakeConsole.WriteLine("Exception Threshold of {0} has been exceeded on {1} - Halting!\n", + TestMetrics.ExceptionThreshold, test.Title); + break; + } + } + finally + { + DeadlockDetection.RemoveThread(); + } + } + + foreach (StressTest t in tests) + { + t.RunCleanup(); + } + } + finally + { + _continue = false; + Interlocked.Decrement(ref _threadsRunning); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/TestFinder.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/TestFinder.cs new file mode 100644 index 0000000000..3f18c6df43 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/TestFinder.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace DPStressHarness +{ + internal class TestFinder + { + private static AssemblyName s_assemblyName; + + public static AssemblyName AssemblyName + { + get { return s_assemblyName; } + set { s_assemblyName = value; } + } + + public static IEnumerable GetTests(Assembly assembly) + { + List tests = new List(); + + + Type[] typesInModule = null; + try + { + typesInModule = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + Console.WriteLine("ReflectionTypeLoadException Errors"); + foreach (Exception loadEx in ex.LoaderExceptions) + { + Console.WriteLine("\t" + loadEx.Message); + } + } + catch (Exception ex) + { + Console.WriteLine("Error." + ex.Message); + } + + foreach (Type t in typesInModule) + { + MethodInfo[] methods = t.GetMethods(BindingFlags.Instance | BindingFlags.Public); + List setupMethods = new List(); + List cleanupMethods = new List(); + + MethodInfo globalSetupMethod = null; + MethodInfo globalCleanupMethod = null; + MethodInfo globalExceptionHandlerMethod = null; + + foreach (MethodInfo m in methods) + { + GlobalTestSetupAttribute[] globalSetupAttributes = (GlobalTestSetupAttribute[])m.GetCustomAttributes(typeof(GlobalTestSetupAttribute), true); + if (globalSetupAttributes.Length > 0) + { + if (null == globalSetupMethod) + { + globalSetupMethod = m; + } + else + { + throw new NotSupportedException("Only one GlobalTestSetup method may be specified per type."); + } + } + + GlobalTestCleanupAttribute[] globalCleanupAttributes = (GlobalTestCleanupAttribute[])m.GetCustomAttributes(typeof(GlobalTestCleanupAttribute), true); + if (globalCleanupAttributes.Length > 0) + { + if (null == globalCleanupMethod) + { + globalCleanupMethod = m; + } + else + { + throw new NotSupportedException("Only one GlobalTestCleanup method may be specified per type."); + } + } + + GlobalExceptionHandlerAttribute[] globalExceptionHandlerAttributes = (GlobalExceptionHandlerAttribute[])m.GetCustomAttributes(typeof(GlobalExceptionHandlerAttribute), true); + if (globalExceptionHandlerAttributes.Length > 0) + { + if (null == globalExceptionHandlerMethod) + { + globalExceptionHandlerMethod = m; + } + else + { + throw new NotSupportedException("Only one GlobalExceptionHandler method may be specified."); + } + } + + TestSetupAttribute[] testSetupAttrs = (TestSetupAttribute[])m.GetCustomAttributes(typeof(TestSetupAttribute), true); + if (testSetupAttrs.Length > 0) + { + setupMethods.Add(m); ; + } + + TestCleanupAttribute[] testCleanupAttrs = (TestCleanupAttribute[])m.GetCustomAttributes(typeof(TestCleanupAttribute), true); + if (testCleanupAttrs.Length > 0) + { + cleanupMethods.Add(m); ; + } + } + + foreach (MethodInfo m in methods) + { + // add single-threaded tests to the list + TestAttribute[] testAttrs = (TestAttribute[])m.GetCustomAttributes(typeof(TestAttribute), true); + foreach (TestAttribute attr in testAttrs) + { + tests.Add(new Test(attr, m, t, setupMethods, cleanupMethods)); + } + + // add any declared stress tests. + StressTestAttribute[] stressTestAttrs = (StressTestAttribute[])m.GetCustomAttributes(typeof(StressTestAttribute), true); + foreach (StressTestAttribute attr in stressTestAttrs) + { + if (TestMetrics.IncludeTest(attr) && MatchFilter(attr)) + tests.Add(new StressTest(attr, m, globalSetupMethod, globalCleanupMethod, t, setupMethods, cleanupMethods, globalExceptionHandlerMethod)); + } + + // add multi-threaded (non thread pool) tests to the list + MultiThreadedTestAttribute[] multiThreadedTestAttrs = (MultiThreadedTestAttribute[])m.GetCustomAttributes(typeof(MultiThreadedTestAttribute), true); + foreach (MultiThreadedTestAttribute attr in multiThreadedTestAttrs) + { + if (TestMetrics.IncludeTest(attr)) + tests.Add(new MultiThreadedTest(attr, m, t, setupMethods, cleanupMethods)); + } + + // add multi-threaded (with thread pool) tests to the list + ThreadPoolTestAttribute[] threadPoolTestAttrs = (ThreadPoolTestAttribute[])m.GetCustomAttributes(typeof(ThreadPoolTestAttribute), true); + foreach (ThreadPoolTestAttribute attr in threadPoolTestAttrs) + { + if (TestMetrics.IncludeTest(attr)) + tests.Add(new ThreadPoolTest(attr, m, t, setupMethods, cleanupMethods)); + } + } + } + + return tests; + } + + private static bool MatchFilter(StressTestAttribute attr) + { + // This change should not have impacts on any existing tests. + // 1. If filter is not provided in command line, we do not apply filter and select all the tests. + // 2. If current test attribute (such as StressTestAttribute) does not implement ITestAttriuteFilter, it is not affected and still selected. + + if (string.IsNullOrEmpty(TestMetrics.Filter)) + { + return true; + } + + var filter = attr as ITestAttributeFilter; + if (filter == null) + { + return true; + } + + return filter.MatchFilter(TestMetrics.Filter); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/MultithreadedTest.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/MultithreadedTest.cs new file mode 100644 index 0000000000..01ea961426 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/MultithreadedTest.cs @@ -0,0 +1,170 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Diagnostics; +using System.Threading; + +namespace DPStressHarness +{ + internal class MultiThreadedTest : TestBase + { + private MultiThreadedTestAttribute _attr; + public static bool _continue; + public static int _threadsRunning; + public static int _rps; + public static Exception _firstException = null; + + private struct TestInfo + { + public object _instance; + public TestMethodDelegate _delegateTest; + } + + public MultiThreadedTest(MultiThreadedTestAttribute attr, + MethodInfo testMethodInfo, + Type type, + List setupMethods, + List cleanupMethods) + : base(attr, testMethodInfo, type, setupMethods, cleanupMethods) + + { + _attr = attr; + } + + public override void Run() + { + try + { + Stopwatch timer = new Stopwatch(); + long warmupDuration = (long)_attr.WarmupDuration * Stopwatch.Frequency; + long testDuration = (long)_attr.TestDuration * Stopwatch.Frequency; + int threads = _attr.Threads; + + TestInfo[] info = new TestInfo[threads]; + ConstructorInfo targetConstructor = _type.GetConstructor(Type.EmptyTypes); + + for (int i = 0; i < threads; i++) + { + info[i] = new TestInfo(); + info[i]._instance = targetConstructor.Invoke(null); + info[i]._delegateTest = CreateTestMethodDelegate(); + + SetVariations(info[i]._instance); + ExecuteSetupPhase(info[i]._instance); + } + + _firstException = null; + _continue = true; + _rps = 0; + + for (int i = 0; i < threads; i++) + { + Interlocked.Increment(ref _threadsRunning); + Thread t = new Thread(new ParameterizedThreadStart(MultiThreadedTest.RunThread)); + t.Start(info[i]); + } + + timer.Reset(); + timer.Start(); + + while (timer.ElapsedTicks < warmupDuration) + { + Thread.Sleep(1000); + } + + int warmupRequests = Interlocked.Exchange(ref _rps, 0); + timer.Reset(); + timer.Start(); + TestMetrics.StartCollection(); + + while (timer.ElapsedTicks < testDuration) + { + Thread.Sleep(1000); + } + + int requests = Interlocked.Exchange(ref _rps, 0); + double elapsedSeconds = timer.ElapsedTicks / Stopwatch.Frequency; + TestMetrics.StopCollection(); + _continue = false; + + while (_threadsRunning > 0) + { + Thread.Sleep(1000); + } + + for (int i = 0; i < threads; i++) + { + ExecuteCleanupPhase(info[i]._instance); + } + + double rps = (double)requests / elapsedSeconds; + + if (_firstException == null) + { + LogTest(rps); + } + else + { + LogTestFailure(_firstException.ToString()); + } + } + catch (TargetInvocationException e) + { + LogTestFailure(e.InnerException.ToString()); + } + catch (Exception e) + { + LogTestFailure(e.ToString()); + } + } + + + public static void RunThread(object state) + { + try + { + while (_continue) + { + TestInfo info = (TestInfo)state; + info._delegateTest(info._instance); + Interlocked.Increment(ref _rps); + } + } + catch (Exception e) + { + if (_firstException == null) + { + _firstException = e; + } + _continue = false; + } + finally + { + Interlocked.Decrement(ref _threadsRunning); + } + } + + protected void LogTest(double rps) + { + Logger logger = new Logger(TestMetrics.RunLabel, TestMetrics.IsOfficial, TestMetrics.Milestone, TestMetrics.Branch); + logger.AddTest(this.Title); + + LogStandardMetrics(logger); + + logger.AddTestMetric(Constants.TEST_METRIC_RPS, string.Format("{0:F2}", rps), "rps", true); + + logger.Save(); + + Console.WriteLine("{0}: Requests per Second={1:F2}, Working Set={2}, Peak Working Set={3}, Private Bytes={4}", + this.Title, + rps, + TestMetrics.WorkingSet, + TestMetrics.PeakWorkingSet, + TestMetrics.PrivateBytes); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/StressTest.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/StressTest.cs new file mode 100644 index 0000000000..c2637d5e5d --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/StressTest.cs @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace DPStressHarness +{ + internal class StressTest : TestBase + { + private StressTestAttribute _attr; + private object _targetInstance; + private TestMethodDelegate _tmd; + + // TODO: MethodInfo objects below can have associated delegates to improve + // runtime performance. + protected MethodInfo _globalSetupMethod; + protected MethodInfo _globalCleanupMethod; + + public delegate void ExceptionHandler(Exception e); + + /// + /// Cache the global exception handler method reference. It is + /// recommended not to actually use this reference to call the + /// method. Use the delegate instead. + /// + protected MethodInfo _globalExceptionHandlerMethod; + + /// + /// Create a delegate to call global exception handler method. + /// Use this delegate to call test assembly's exception handler. + /// + protected ExceptionHandler _globalExceptionHandlerDelegate; + + public StressTest(StressTestAttribute attr, + MethodInfo testMethodInfo, + MethodInfo globalSetupMethod, + MethodInfo globalCleanupMethod, + Type type, + List setupMethods, + List cleanupMethods, + MethodInfo globalExceptionHandlerMethod) + : base(attr, testMethodInfo, type, setupMethods, cleanupMethods) + { + _attr = attr; + _globalSetupMethod = globalSetupMethod; + _globalCleanupMethod = globalCleanupMethod; + _globalExceptionHandlerMethod = globalExceptionHandlerMethod; + } + + public StressTest Clone() + { + StressTest t = new StressTest(_attr, this._testMethod, this._globalSetupMethod, this._globalCleanupMethod, this._type, this._setupMethods, this._cleanupMethods, this._globalExceptionHandlerMethod); + return t; + } + + private void InitTargetInstance() + { + _targetInstance = _type.GetConstructor(Type.EmptyTypes).Invoke(null); + + // Create a delegate for exception handling on _targetInstance + if (_globalExceptionHandlerMethod != null) + { + _globalExceptionHandlerDelegate = (ExceptionHandler)_globalExceptionHandlerMethod.CreateDelegate( + typeof(ExceptionHandler), + _targetInstance + ); + } + } + + /// + /// Perform any global initialization for the test assembly. For example, make the connection to the database, load a workspace, etc. + /// + public void RunGlobalSetup() + { + if (null == _targetInstance) + { + InitTargetInstance(); + } + + if (null != _globalSetupMethod) + { + _globalSetupMethod.Invoke(_targetInstance, null); + } + } + + /// + /// Run any per-thread setup needed + /// + public void RunSetup() + { + // create an instance of the class that defines the test method. + if (null == _targetInstance) + { + InitTargetInstance(); + } + _tmd = CreateTestMethodDelegate(); + + // Set variation fields on the target instance + SetVariations(_targetInstance); + + // Execute the setup phase for this thread. + ExecuteSetupPhase(_targetInstance); + } + + /// + /// Execute the test method(s) + /// + public override void Run() + { + _tmd(_targetInstance); + } + + /// + /// Provide an opportunity to handle the exception + /// + /// + public void HandleException(Exception e) + { + if (null != _globalExceptionHandlerDelegate) + { + _globalExceptionHandlerDelegate(e); + } + } + + /// + /// Run any per-thread cleanup for the test + /// + public void RunCleanup() + { + ExecuteCleanupPhase(_targetInstance); + } + + /// + /// Run final global cleanup for the test assembly. Could be used to release resources or for reporting, etc. + /// + public void RunGlobalCleanup() + { + if (null != _globalCleanupMethod) + { + _globalCleanupMethod.Invoke(_targetInstance, null); + } + } + + public int Weight + { + get { return _attr.Weight; } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/Test.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/Test.cs new file mode 100644 index 0000000000..a73448a30b --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/Test.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Reflection; + + +namespace DPStressHarness +{ + internal class Test : TestBase + { + private TestAttribute _attr; + private int _overrideIterations = -1; + private int _overrideWarmup = -1; + + public Test(TestAttribute attr, + MethodInfo testMethodInfo, + Type type, + List setupMethods, + List cleanupMethods) + : base(attr, testMethodInfo, type, setupMethods, cleanupMethods) + { + _attr = attr; + } + + + public override void Run() + { + try + { + // create an instance of the class that defines the test method. + object targetInstance = _type.GetConstructor(Type.EmptyTypes).Invoke(null); + + SetVariations(targetInstance); + + ExecuteSetupPhase(targetInstance); + + TestMethodDelegate tmd = CreateTestMethodDelegate(); + + ExecuteTest(targetInstance, tmd); + + ExecuteCleanupPhase(targetInstance); + + LogTest(); + } + catch (TargetInvocationException e) + { + LogTestFailure(e.InnerException.ToString()); + } + catch (Exception e) + { + LogTestFailure(e.ToString()); + } + } + + protected void LogTest() + { + Logger logger = new Logger(TestMetrics.RunLabel, TestMetrics.IsOfficial, TestMetrics.Milestone, TestMetrics.Branch); + logger.AddTest(this.Title); + + LogStandardMetrics(logger); + + logger.AddTestMetric(Constants.TEST_METRIC_ELAPSED_SECONDS, string.Format("{0:F2}", TestMetrics.ElapsedSeconds), "sec", false); + + logger.Save(); + + Console.WriteLine("{0}: Elapsed Seconds={1:F2}, Working Set={2}, Peak Working Set={3}, Private Bytes={4}", + this.Title, + TestMetrics.ElapsedSeconds, + TestMetrics.WorkingSet, + TestMetrics.PeakWorkingSet, + TestMetrics.PrivateBytes); + } + + + private void ExecuteTest(object targetInstance, TestMethodDelegate tmd) + { + int warmupIterations = _attr.WarmupIterations; + int testIterations = _attr.TestIterations; + + if (_overrideIterations >= 0) + { + testIterations = _overrideIterations; + } + if (_overrideWarmup >= 0) + { + warmupIterations = _overrideWarmup; + } + + /** do some cleanup to make memory tests more accurate **/ + System.GC.Collect(); + System.GC.WaitForPendingFinalizers(); + System.GC.Collect(); + + IntPtr h = MemApi.GetCurrentProcess(); + bool fRes = MemApi.SetProcessWorkingSetSize(h, -1, -1); + /****/ + + System.Threading.Thread.Sleep(10000); + + for (int i = 0; i < warmupIterations; i++) + { + tmd(targetInstance); + } + + TestMetrics.StartCollection(); + for (int i = 0; i < testIterations; i++) + { + tmd(targetInstance); + } + TestMetrics.StopCollection(); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/TestBase.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/TestBase.cs new file mode 100644 index 0000000000..95546547da --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/TestBase.cs @@ -0,0 +1,163 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace DPStressHarness +{ + public abstract class TestBase + { + private TestAttributeBase _attr; + private string _variationSuffix = ""; + + protected MethodInfo _testMethod; + + protected Type _type; + + protected List _setupMethods; + + protected List _cleanupMethods; + + protected delegate void TestMethodDelegate(object t); + + public TestBase(TestAttributeBase attr, + MethodInfo testMethodInfo, + Type type, + List setupMethods, + List cleanupMethods) + { + _attr = attr; + _testMethod = testMethodInfo; + _type = type; + _setupMethods = setupMethods; + _cleanupMethods = cleanupMethods; + } + + public string Title + { + get { return _attr.Title + _variationSuffix; } + } + + public string Description + { + get { return _attr.Description; } + } + + public string Category + { + get { return _attr.Category; } + } + + public TestPriority Priority + { + get { return _attr.Priority; } + } + + public List GetVariations() + { + FieldInfo[] fields = _type.GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); + + List variations = new List(10); + foreach (FieldInfo fi in fields) + { + TestVariationAttribute[] attrs = (TestVariationAttribute[])fi.GetCustomAttributes(typeof(TestVariationAttribute), false); + + foreach (TestVariationAttribute testVarAttr in attrs) + { + if (!variations.Contains(testVarAttr.VariationName)) + { + variations.Add(testVarAttr.VariationName); + } + } + } + + return variations; + } + + public abstract void Run(); + + protected void ExecuteSetupPhase(object targetInstance) + { + if (_setupMethods != null) + { + foreach (MethodInfo setupMthd in _setupMethods) + { + setupMthd.Invoke(targetInstance, null); + } + } + } + + protected void ExecuteCleanupPhase(object targetInstance) + { + if (_cleanupMethods != null) + { + foreach (MethodInfo cleanupMethod in _cleanupMethods) + { + cleanupMethod.Invoke(targetInstance, null); + } + } + } + + protected void SetVariations(object targetInstance) + { + FieldInfo[] fields = targetInstance.GetType().GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); + + foreach (FieldInfo fi in fields) + { + TestVariationAttribute[] attrs = (TestVariationAttribute[])fi.GetCustomAttributes(typeof(TestVariationAttribute), false); + + foreach (TestVariationAttribute testVarAttr in attrs) + { + foreach (string specifiedVariation in TestMetrics.Variations) + { + if (specifiedVariation.Equals(testVarAttr.VariationName)) + { + fi.SetValue(targetInstance, testVarAttr.VariationValue); + _variationSuffix += "_" + testVarAttr.VariationName; + break; + } + } + } + } + } + + protected TestMethodDelegate CreateTestMethodDelegate() + { + return new TestMethodDelegate((instance) => _testMethod.Invoke(instance, null)); + } + + protected void LogTestFailure(string exceptionData) + { + Console.WriteLine("{0}: Failed", this.Title); + Console.WriteLine(exceptionData); + + Logger logger = new Logger(TestMetrics.RunLabel, false, TestMetrics.Milestone, TestMetrics.Branch); + logger.AddTest(this.Title); + logger.AddTestMetric("Test Assembly", _testMethod.Module.FullyQualifiedName, null); + logger.AddTestException(exceptionData); + logger.Save(); + } + + protected void LogStandardMetrics(Logger logger) + { + logger.AddTestMetric(Constants.TEST_METRIC_TEST_ASSEMBLY, _testMethod.Module.FullyQualifiedName, null); + logger.AddTestMetric(Constants.TEST_METRIC_TEST_IMPROVEMENT, _attr.Improvement, null); + logger.AddTestMetric(Constants.TEST_METRIC_TEST_OWNER, _attr.Owner, null); + logger.AddTestMetric(Constants.TEST_METRIC_TEST_CATEGORY, _attr.Category, null); + logger.AddTestMetric(Constants.TEST_METRIC_TEST_PRIORITY, _attr.Priority.ToString(), null); + logger.AddTestMetric(Constants.TEST_METRIC_APPLICATION_NAME, _attr.Improvement, null); + + if (TestMetrics.TargetAssembly != null) + { + logger.AddTestMetric(Constants.TEST_METRIC_TARGET_ASSEMBLY_NAME, (new AssemblyName(TestMetrics.TargetAssembly.FullName)).Name, null); + } + + logger.AddTestMetric(Constants.TEST_METRIC_PEAK_WORKING_SET, string.Format("{0}", TestMetrics.PeakWorkingSet), "bytes"); + logger.AddTestMetric(Constants.TEST_METRIC_WORKING_SET, string.Format("{0}", TestMetrics.WorkingSet), "bytes"); + logger.AddTestMetric(Constants.TEST_METRIC_PRIVATE_BYTES, string.Format("{0}", TestMetrics.PrivateBytes), "bytes"); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/ThreadPoolTest.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/ThreadPoolTest.cs new file mode 100644 index 0000000000..f065c15312 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/ThreadPoolTest.cs @@ -0,0 +1,174 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; + +namespace DPStressHarness +{ + internal class ThreadPoolTest : TestBase + { + private ThreadPoolTestAttribute _attr; + public static bool _continue; + public static int _threadsRunning; + public static int _rps; + public static WaitCallback _waitCallback = new WaitCallback(RunThreadPool); + public static Exception _firstException = null; + + private struct TestInfo + { + public object _instance; + public TestMethodDelegate _delegateTest; + } + + public ThreadPoolTest(ThreadPoolTestAttribute attr, + MethodInfo testMethodInfo, + Type type, + List setupMethods, + List cleanupMethods) + : base(attr, testMethodInfo, type, setupMethods, cleanupMethods) + { + _attr = attr; + } + + public override void Run() + { + try + { + Stopwatch timer = new Stopwatch(); + long warmupDuration = (long)_attr.WarmupDuration * Stopwatch.Frequency; + long testDuration = (long)_attr.TestDuration * Stopwatch.Frequency; + int threads = _attr.Threads; + + TestInfo[] info = new TestInfo[threads]; + ConstructorInfo targetConstructor = _type.GetConstructor(Type.EmptyTypes); + + for (int i = 0; i < threads; i++) + { + info[i] = new TestInfo(); + info[i]._instance = targetConstructor.Invoke(null); + info[i]._delegateTest = CreateTestMethodDelegate(); + + ExecuteSetupPhase(info[i]._instance); + } + + _firstException = null; + _continue = true; + _rps = 0; + + for (int i = 0; i < threads; i++) + { + Interlocked.Increment(ref _threadsRunning); + ThreadPool.QueueUserWorkItem(_waitCallback, info[i]); + } + + timer.Reset(); + timer.Start(); + + while (timer.ElapsedTicks < warmupDuration) + { + Thread.Sleep(1000); + } + + int warmupRequests = Interlocked.Exchange(ref _rps, 0); + timer.Reset(); + timer.Start(); + TestMetrics.StartCollection(); + + while (timer.ElapsedTicks < testDuration) + { + Thread.Sleep(1000); + } + + int requests = Interlocked.Exchange(ref _rps, 0); + double elapsedSeconds = timer.ElapsedTicks / Stopwatch.Frequency; + TestMetrics.StopCollection(); + _continue = false; + + while (_threadsRunning > 0) + { + Thread.Sleep(1000); + } + + for (int i = 0; i < threads; i++) + { + ExecuteCleanupPhase(info[i]._instance); + } + + double rps = (double)requests / elapsedSeconds; + + if (_firstException == null) + { + LogTest(rps); + } + else + { + LogTestFailure(_firstException.ToString()); + } + } + catch (TargetInvocationException e) + { + LogTestFailure(e.InnerException.ToString()); + } + catch (Exception e) + { + LogTestFailure(e.ToString()); + } + } + + + public static void RunThreadPool(object state) + { + try + { + TestInfo info = (TestInfo)state; + info._delegateTest(info._instance); + Interlocked.Increment(ref _rps); + } + catch (Exception e) + { + if (_firstException == null) + { + _firstException = e; + } + _continue = false; + } + finally + { + if (_continue) + { + ThreadPool.QueueUserWorkItem(_waitCallback, state); + } + else + { + Interlocked.Decrement(ref _threadsRunning); + } + } + } + + protected void LogTest(double rps) + { + Logger logger = new Logger(TestMetrics.RunLabel, TestMetrics.IsOfficial, TestMetrics.Milestone, TestMetrics.Branch); + logger.AddTest(this.Title); + + LogStandardMetrics(logger); + + logger.AddTestMetric(Constants.TEST_METRIC_RPS, string.Format("{0:F2}", rps), "rps", true); + + logger.Save(); + + Console.WriteLine("{0}: Requests per Second={1:F2}, Working Set={2}, Peak Working Set={3}, Private Bytes={4}", + this.Title, + rps, + TestMetrics.WorkingSet, + TestMetrics.PeakWorkingSet, + TestMetrics.PrivateBytes); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/FilteredDefaultTraceListener.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/FilteredDefaultTraceListener.cs new file mode 100644 index 0000000000..ce29f8ee29 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/FilteredDefaultTraceListener.cs @@ -0,0 +1,210 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace Stress.Data.SqlClient +{ + /// + /// A DefaultTraceListener that can filter out given asserts + /// + internal class FilteredDefaultTraceListener : DefaultTraceListener + { + private static readonly Assembly s_systemDataAssembly = typeof(Microsoft.Data.SqlClient.SqlConnection).GetTypeInfo().Assembly; + private const RegexOptions AssertMessageRegexOptions = RegexOptions.Singleline | RegexOptions.CultureInvariant; + + private enum MatchType : byte + { + Exact, + Regex, + } + + private enum HandlingOption : byte + { + CovertToException, + WriteToConsole, + } + + /// + /// Represents a single assert to filter out + /// + private struct FilteredAssert + { + public FilteredAssert(string messageOrRegex, int bugNumber, MatchType matchType, HandlingOption assertHandlingOption, params string[] stackFrames) + { + if (matchType == MatchType.Exact) + { + Message = messageOrRegex; + MessageRegex = null; + } + else + { + Message = null; + MessageRegex = new Regex(messageOrRegex, AssertMessageRegexOptions); + } + + + StackFrames = stackFrames; + BugNumber = bugNumber; + Handler = assertHandlingOption; + } + + /// + /// The assert's message (NOTE: MessageRegex must be null if this is specified) + /// + public string Message; + /// + /// A regex that matches the assert's message (NOTE: Message must be null if this is specified) + /// + public Regex MessageRegex; + /// + /// The most recent frames on the stack when the assert was hit (i.e. 0 is most recent, 1 is next, etc.). Null if stack should not be checked. + /// + public string[] StackFrames; + /// + /// Product bug to fix the assert + /// + public int BugNumber; + /// + /// How the assert will be handled once it is matched + /// + /// + /// In most cases this can be set to WriteToConsole - typically the assert is either invalid or there will be an exception thrown by the product code anyway. + /// However, in the case where this is state corruption AND the product code has no exception in place, this will need to be set to CovertToException to prevent further corruption\asserts + /// + public HandlingOption Handler; + } + + private static readonly FilteredAssert[] s_assertsToFilter = new FilteredAssert[] { + new FilteredAssert("TdsParser::ThrowExceptionAndWarning called with no exceptions or warnings!", 433324, MatchType.Exact, HandlingOption.WriteToConsole, + "Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning", + "Microsoft.Data.SqlClient.TdsParserStateObject.ThrowExceptionAndWarning", + "Microsoft.Data.SqlClient.TdsParserStateObject.ReadAsyncCallbackCaptureException"), + }; + + public FilteredDefaultTraceListener(DefaultTraceListener listenerToClone) : base() + { + base.Filter = listenerToClone.Filter; + base.IndentLevel = listenerToClone.IndentLevel; + base.IndentSize = listenerToClone.IndentSize; + base.TraceOutputOptions = listenerToClone.TraceOutputOptions; + } + + public override void Fail(string message) + { + Fail(message, null); + } + + public override void Fail(string message, string detailMessage) + { + FilteredAssert? foundAssert = FindAssertInList(message); + if (!foundAssert.HasValue) + { + // Don't filter this assert - pass it down to the underlying DefaultTraceListener which will show the UI, break into the debugger, etc. + base.Fail(message, detailMessage); + } + else + { + // Assert is to be filtered, either convert to an exception or a message + var assert = foundAssert.Value; + if (assert.Handler == HandlingOption.CovertToException) + { + throw new FailedAssertException(message, assert.BugNumber); + } + else if (assert.Handler == HandlingOption.WriteToConsole) + { + Console.WriteLine("Hit known assert, Bug {0}: {1}", assert.BugNumber, message); + } + } + } + + private FilteredAssert? FindAssertInList(string message) + { + StackTrace actualCallstack = null; + foreach (var assert in s_assertsToFilter) + { + if (((assert.Message != null) && (assert.Message == message)) || ((assert.MessageRegex != null) && (assert.MessageRegex.IsMatch(message)))) + { + if (assert.StackFrames != null) + { + // Skipping four frames: + // Stress.Data.SqlClient.FilteredDefaultTraceListener.FindAssertInList + // Stress.Data.SqlClient.FilteredDefaultTraceListener.Fail (This may be in the stack twice due to the overloads calling each other) + // System.Diagnostics.TraceInternal.Fail + // System.Diagnostics.Debug.Assert + if (actualCallstack == null) + { + actualCallstack = new StackTrace(e: new InvalidOperationException(), fNeedFileInfo: false); + } + + StackFrame[] frames = actualCallstack.GetFrames(); + if (frames.Length >= assert.StackFrames.Length) + { + int actualStackFrameCounter = 0; + bool foundMatch = true; + foreach (var expectedStack in assert.StackFrames) + { + // Get the method information for the next stack which came from System.Data.dll + MethodBase actualStackMethod; + do + { + actualStackMethod = frames[actualStackFrameCounter].GetMethod(); + actualStackFrameCounter++; + } while (((actualStackMethod.DeclaringType == null) || (actualStackMethod.DeclaringType.GetTypeInfo().Assembly != s_systemDataAssembly)) && (actualStackFrameCounter < frames.Length)); + + if ((actualStackFrameCounter > frames.Length) || (string.Format("{0}.{1}", actualStackMethod.DeclaringType.FullName, actualStackMethod.Name) != expectedStack)) + { + // Ran out of actual frames while there were still expected frames or the current frames didn't match + foundMatch = false; + break; + } + } + + // Message and all frames matched + if (foundMatch) + { + return assert; + } + } + } + else + { + // Messages match, and there are no frames to verify + return assert; + } + } + } + + // Fall through - didn't find the assert + return null; + } + } + + internal class FailedAssertException : Exception + { + /// + /// Number of the bug that caused the assert to fire + /// + public int BugNumber { get; private set; } + + /// + /// Creates an exception to represent hitting a known assert + /// + /// Message of the assert + /// Number of the bug that caused the assert + public FailedAssertException(string message, int bugNumber) + : base(message) + { + BugNumber = bugNumber; + } + + public override string ToString() + { + return string.Format("{1}\r\nAssert caused by Bug {0}", BugNumber, base.ToString()); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/HostsFileManager.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/HostsFileManager.cs new file mode 100644 index 0000000000..6d44909ddb --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/HostsFileManager.cs @@ -0,0 +1,481 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.IO; +using System.Net; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Microsoft.Test.Data.SqlClient +{ + /// + /// allows user to manipulate %windir%\system32\drivers\etc\hosts + /// the hosts file must be reverted if changed even if test application crashes, thus inherit from CriticalFinalizerObject. Make sure the instance is disposed after its use. + /// The last dispose call on the active instance reverts the hosts file. + /// + /// Usage: + /// using (var hostsFile = new HostsFileManager()) + /// { + /// // use the hostsFile methods to add/remove entries + /// // simultaneous usage of HostsFileManager in two app domains or processes on the same machine is not allowed + /// } + /// + public sealed class HostsFileManager : IDisposable + { + // define global (machine-wide) lock instance + private static EventWaitHandle s_globalLock = new EventWaitHandle(true /* create as signalled */, EventResetMode.AutoReset, @"Global\HostsFileManagerLock"); + private static bool s_globalLockTaken; // set when global (machine-wide) lock is in use + + private static int s_localUsageRefCount; + private static object s_localLock = new object(); + + private static string s_hostsFilePath; + private static string s_backupPath; + private static bool s_hasBackup; + private static TextReader s_activeReader; + private static TextWriter s_activeWriter; + private static List s_entriesCache; + + private const string HostsFilePathUnderSystem32 = @"C:\Windows\System32\drivers\etc\hosts"; + private const string HostsFilePathUnderLinux = "/etc/hosts"; + private const string HostsFilePathUnderMacOS = "/private/etc/hosts"; + + + private static void InitializeGlobal(ref bool mustRelease) + { + if (mustRelease) + { + // already initialized + return; + } + + lock (s_localLock) + { + if (mustRelease) + { + // check again under lock + return; + } + + if (s_localUsageRefCount > 0) + { + // initialized by another thread + ++s_localUsageRefCount; + return; + } + + // first call to initialize in this app domain + // note: simultanious use of HostsFileManager is currently supported only within single AppDomain scope + + // non-critical initialization goes first + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + s_hostsFilePath = HostsFilePathUnderSystem32; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + s_hostsFilePath = HostsFilePathUnderLinux; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + s_hostsFilePath = HostsFilePathUnderMacOS; + } + + s_backupPath = Path.Combine(Path.GetTempPath(), string.Format("Hosts_{0}.bak", Guid.NewGuid().ToString("N"))); + + // try to get global lock + // note that once global lock is aquired, it must be released + try { } + finally + { + if (s_globalLock.WaitOne(0)) + { + s_globalLockTaken = true; + mustRelease = true; + ++s_localUsageRefCount; // increment ref count for the first thread using the manager + } + } + + if (!s_globalLockTaken) + { + throw new InvalidOperationException("HostsFileManager cannot initialize because hosts file is in use by another instance of the manager in the same or a different process (concurrent access is not allowed)"); + } + + // locked now, take snapshot of hosts file and save it as a backup + File.Copy(s_hostsFilePath, s_backupPath); + s_hasBackup = true; + + // load the current entries + InternalRefresh(); + } + } + + private static void TerminateGlobal(ref bool originalMustRelease) + { + if (!originalMustRelease) + { + // already disposed + return; + } + + lock (s_localLock) + { + if (!originalMustRelease) + { + // check again under lock + return; + } + + // not yet disposed, do it now + if (s_localUsageRefCount > 1) + { + // still in use by another thread(s) + --s_localUsageRefCount; + return; + } + + if (s_activeReader != null) + { + s_activeReader.Dispose(); + s_activeReader = null; + } + if (s_activeWriter != null) + { + s_activeWriter.Dispose(); + s_activeWriter = null; + } + bool deleteBackup = false; + if (s_hasBackup) + { + // revert the hosts file + File.Copy(s_backupPath, s_hostsFilePath, overwrite: true); + s_hasBackup = false; + deleteBackup = true; + } + + // Note: if critical finalizer fails to revert the hosts file, the global lock might remain reset until the machine is rebooted. + // if this happens, Hosts file in unpredictable state so there is no point in running tests anyway + if (s_globalLockTaken) + { + try { } + finally + { + s_globalLock.Set(); + s_globalLockTaken = false; + --s_localUsageRefCount; // decrement local ref count + originalMustRelease = false; + } + } + + // now we can destroy the backup + if (deleteBackup) + { + File.Delete(s_backupPath); + } + } + } + + private bool _mustRelease; + private bool _disposed; + + public HostsFileManager() + { + // lazy initialization + _mustRelease = false; + _disposed = false; + } + + ~HostsFileManager() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!_disposed) + { + _disposed = true; + TerminateGlobal(ref _mustRelease); + } + } + + public class HostEntry + { + public HostEntry(string name, IPAddress address) + { + ValidateName(name); + ValidateAddress(address); + + this.Name = name; + this.Address = address; + } + + public readonly string Name; + public readonly IPAddress Address; + } + + // helper methods + + // must be called under lock(_localLock) from each public API that uses static fields + private void InitializeLocal() + { + if (_disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + + InitializeGlobal(ref _mustRelease); + } + + private static readonly char[] s_whiteSpaceChars = new char[] { ' ', '\t' }; + + private static void ValidateName(string name) + { + if (string.IsNullOrEmpty(name) || name.IndexOfAny(s_whiteSpaceChars) >= 0) + { + throw new ArgumentException("name cannot be null or empty or have whitespace characters in it"); + } + } + + private static void ValidateAddress(IPAddress address) + { + ValidateNonNull(address, "address"); + + if (address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork && + address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetworkV6) + { + throw new ArgumentException("only IPv4 or IPv6 addresses are allowed"); + } + } + + private static void ValidateNonNull(T value, string argName) where T : class + { + if (value == null) + { + throw new ArgumentNullException(argName); + } + } + + private static HostEntry TryParseLine(string line) + { + line = line.Trim(); + if (line.StartsWith("#")) + { + // comment, ignore + return null; + } + + string[] items = line.Split(s_whiteSpaceChars, StringSplitOptions.RemoveEmptyEntries); + if (items.Length == 0) + { + // empty or white-space only line - ignore + return null; + } + + if (items.Length != 2) + { + Trace.WriteLine("Wrong entry in the hosts file (exactly two columns expected): \"" + line + "\""); + return null; + } + + string name = items[1]; + IPAddress address; + if (!IPAddress.TryParse(items[0], out address)) + { + Trace.WriteLine("Wrong entry in the hosts file (cannot parse the IP address): \"" + line + "\""); + return null; + } + + try + { + return new HostEntry(name, address); + } + catch (ArgumentException e) + { + Console.WriteLine("Wrong entry in the hosts file, cannot create host entry: " + e.Message); + return null; + } + } + + private bool NameMatch(HostEntry entry, string name) + { + ValidateNonNull(entry, "entry"); + ValidateName(name); + + return string.Equals(entry.Name, name, StringComparison.OrdinalIgnoreCase); + } + + // hosts file manipulation methods + + // reloads the hosts file, must be called under lock(_localLock) + private static void InternalRefresh() + { + List entries = new List(); + + try + { + s_activeReader = new StreamReader(new FileStream(s_hostsFilePath, FileMode.Open)); + + string line; + while ((line = s_activeReader.ReadLine()) != null) + { + HostEntry nextEntry = TryParseLine(line); + if (nextEntry != null) + { + entries.Add(nextEntry); + } + } + } + finally + { + if (s_activeReader != null) + { + s_activeReader.Dispose(); + s_activeReader = null; + } + } + + s_entriesCache = entries; + } + + // reloads the hosts file, must be called while still under lock(_localLock) + private void InternalSave() + { + try + { + s_activeWriter = new StreamWriter(new FileStream(s_hostsFilePath, FileMode.Create)); + + foreach (HostEntry entry in s_entriesCache) + { + s_activeWriter.WriteLine(" {0} {1}", entry.Address, entry.Name); + } + + s_activeWriter.Flush(); + } + finally + { + if (s_activeWriter != null) + { + s_activeWriter.Dispose(); + s_activeWriter = null; + } + } + } + + public int RemoveAll(string name) + { + lock (s_localLock) + { + InitializeLocal(); + ValidateName(name); + + int removed = s_entriesCache.RemoveAll(entry => NameMatch(entry, name)); + + if (removed > 0) + { + InternalSave(); + } + + return removed; + } + } + + public IEnumerable EnumerateAddresses(string name) + { + lock (s_localLock) + { + InitializeLocal(); + ValidateName(name); + + return from entry in s_entriesCache where NameMatch(entry, name) select entry.Address; + } + } + + public void Add(string name, IPAddress address) + { + lock (s_localLock) + { + InitializeLocal(); + + HostEntry entry = new HostEntry(name, address); // c-tor validates the arguments + s_entriesCache.Add(entry); + + InternalSave(); + } + } + + public void Add(HostEntry entry) + { + lock (s_localLock) + { + InitializeLocal(); + ValidateNonNull(entry, "entry"); + + s_entriesCache.Add(entry); + + InternalSave(); + } + } + + public void AddRange(string name, IEnumerable addresses) + { + lock (s_localLock) + { + InitializeLocal(); + ValidateName(name); + ValidateNonNull(addresses, "addresses"); + + foreach (IPAddress address in addresses) + { + HostEntry entry = new HostEntry(name, address); + + s_entriesCache.Add(entry); + } + + InternalSave(); + } + } + + public void AddRange(IEnumerable entries) + { + lock (s_localLock) + { + InitializeLocal(); + ValidateNonNull(entries, "entries"); + + foreach (HostEntry entry in entries) + { + ValidateNonNull(entry, "entries element"); + + s_entriesCache.Add(entry); + } + + InternalSave(); + } + } + + public void Clear() + { + lock (s_localLock) + { + InitializeLocal(); + + s_entriesCache.Clear(); + + InternalSave(); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/MultiSubnetFailoverSetup.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/MultiSubnetFailoverSetup.cs new file mode 100644 index 0000000000..2a0719d201 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/MultiSubnetFailoverSetup.cs @@ -0,0 +1,135 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Test.Data.SqlClient; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +namespace Stress.Data.SqlClient +{ + internal class MultiSubnetFailoverSetup + { + private HostsFileManager _hostsFile; + + internal MultiSubnetFailoverSetup(SqlServerDataSource source) + { + this.Source = source; + } + + internal string MultiSubnetFailoverHostNameForIntegratedSecurity { get; private set; } + + private List _multiSubnetFailoverHostNames; + + internal string GetMultiSubnetFailoverHostName(Random rnd) + { + return _multiSubnetFailoverHostNames[rnd.Next(_multiSubnetFailoverHostNames.Count)]; + } + + public SqlServerDataSource Source { get; private set; } + + internal void InitializeFakeHostsForMultiSubnetFailover() + { + // initialize fake hosts for MultiSubnetFailover + string originalHost, protocol, instance; + int? port; + NetUtils.ParseDataSource(this.Source.DataSource, out protocol, out originalHost, out instance, out port); + + // get the IPv4 addresses + IPAddress[] ipV4 = NetUtils.EnumerateIPv4Addresses(originalHost).ToArray(); + if (ipV4 == null || ipV4.Length == 0) + { + // consider supporting IPv6 when it becomes relevant (not a goal right now) + throw new ArgumentException("The target server " + originalHost + " has no IPv4 addresses associated with it in DNS"); + } + + // construct different host names for MSF with valid server IP located in a different place each time + List allEntries = new List(); + + int nextValidIp = 0; + int nextInvalidIp = 0; + _multiSubnetFailoverHostNames = new List(); + + // construct some interesting cases for MultiSubnetFailover stress + + // for integrated security to work properly, the server name in connection string must match the target server host name. + // thus, create one entry in the hosts with the true server name: either FQDN or the short name + Task task = Dns.GetHostEntryAsync(ipV4[0]); + string nameToUse = task.Result.HostName; + if (originalHost.Contains('.')) + { + // if the original hosts is FQDN, put short name in the hosts instead + // otherwise, put FQDN in hosts + int shortNameEnd = nameToUse.IndexOf('.'); + if (shortNameEnd > 0) + nameToUse = nameToUse.Substring(0, shortNameEnd); + } + // since true server name is being re-mapped, keep the valid IP first in the list + AddEntryHelper(allEntries, _multiSubnetFailoverHostNames, nameToUse, + ipV4[(nextValidIp++) % ipV4.Length], + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount)); + this.MultiSubnetFailoverHostNameForIntegratedSecurity = nameToUse; + + // single valid IP + AddEntryHelper(allEntries, _multiSubnetFailoverHostNames, "MSF_MP_Stress_V", + ipV4[(nextValidIp++) % ipV4.Length]); + + // valid + invalid + AddEntryHelper( + allEntries, _multiSubnetFailoverHostNames, "MSF_MP_Stress_VI", + ipV4[(nextValidIp++) % ipV4.Length], + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount)); + + // invalid + valid + invalid + AddEntryHelper( + allEntries, _multiSubnetFailoverHostNames, "MSF_MP_Stress_IVI", + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount), + ipV4[(nextValidIp++) % ipV4.Length], + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount)); + + // Using more than one active IP associated with the virtual name (VNN) is not a supported scenario with MultiSubnetFailover. + // But, this can definitly happen in reality - add special cases here to cover two valid IPs. + AddEntryHelper( + allEntries, _multiSubnetFailoverHostNames, "MSF_MP_Stress_IVI", + ipV4[(nextValidIp++) % ipV4.Length], + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount), + ipV4[(nextValidIp++) % ipV4.Length]); + + // big boom with 7 IPs - for stress purposes only + AddEntryHelper( + allEntries, _multiSubnetFailoverHostNames, "MSF_MP_Stress_BIGBOOM", + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount), + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount), + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount), + ipV4[(nextValidIp++) % ipV4.Length], + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount), + ipV4[(nextValidIp++) % ipV4.Length], + NetUtils.GetNonExistingIPv4((nextInvalidIp++) % NetUtils.NonExistingIPv4AddressCount) + ); + + // list of fake hosts is ready, initialize hosts file manager and update the file + _hostsFile = new HostsFileManager(); + _hostsFile.AddRange(allEntries); + } + + + private static void AddEntryHelper(List entries, List names, string msfHostName, params IPAddress[] addresses) + { + for (int i = 0; i < addresses.Length; i++) + entries.Add(new HostsFileManager.HostEntry(msfHostName, addresses[i])); + names.Add(msfHostName); + } + + internal void Terminate() + { + // revert hosts file + if (_hostsFile != null) + { + _hostsFile.Dispose(); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/NetUtils.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/NetUtils.cs new file mode 100644 index 0000000000..0e756d1bf9 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/NetUtils.cs @@ -0,0 +1,206 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Net; +using System.Diagnostics; +using Microsoft.Data.SqlClient; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.Test.Data.SqlClient +{ + public static class NetUtils + { + // according to RFC 5737 (http://tools.ietf.org/html/rfc5737): The blocks 192.0.2.0/24 (TEST-NET-1), 198.51.100.0/24 (TEST-NET-2), + // and 203.0.113.0/24 (TEST-NET-3) are provided for use in documentation and should not be in use by any public network + private static readonly IPAddress[] s_testNets = new IPAddress[] + { + IPAddress.Parse("192.0.2.0"), + IPAddress.Parse("198.51.100.0"), + IPAddress.Parse("203.0.113.0") + }; + + private const int TestNetAddressRangeLength = 256; + + public static readonly int NonExistingIPv4AddressCount = TestNetAddressRangeLength * s_testNets.Length; + + public static IPAddress GetNonExistingIPv4(int index) + { + if (index < 0 || index > NonExistingIPv4AddressCount) + { + throw new ArgumentOutOfRangeException("index"); + } + + byte[] address = s_testNets[index / TestNetAddressRangeLength].GetAddressBytes(); + + Debug.Assert(address[3] == 0, "address ranges above must end with .0"); + address[3] = checked((byte)(index % TestNetAddressRangeLength)); + + return new IPAddress(address); + } + + public static IEnumerable EnumerateIPv4Addresses(string hostName) + { + hostName = hostName.Trim(); + + if ((hostName == ".") || + (string.Compare("(local)", hostName, StringComparison.OrdinalIgnoreCase) == 0)) + { + hostName = Dns.GetHostName(); + } + + Task task = Dns.GetHostAddressesAsync(hostName); + IPAddress[] allAddresses = task.Result; + + foreach (var addr in allAddresses) + { + if (addr.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + yield return addr; + } + } + } + + /// + /// Splits data source into protocol, host name, instance name and port. + /// + /// Note that this algorithm does not cover all valid combinations of data source; only those we actually use in tests are supported now. + /// Please update as needed. + /// + public static void ParseDataSource(string dataSource, out string protocol, out string hostName, out string instanceName, out int? port) + { + // check for protocol prefix + int i = dataSource.IndexOf(':'); + if (i >= 0) + { + protocol = dataSource.Substring(0, i); + + // remove the protocol + dataSource = dataSource.Substring(i + 1); + } + else + { + protocol = null; + } + + // check for server port + i = dataSource.IndexOf(','); + if (i >= 0) + { + // there is a port value in connection string + port = int.Parse(dataSource.Substring(i + 1)); + dataSource = dataSource.Substring(0, i); + } + else + { + port = null; + } + + // check for the instance name + i = dataSource.IndexOf('\\'); + if (i >= 0) + { + instanceName = dataSource.Substring(i + 1); + dataSource = dataSource.Substring(0, i); + } + else + { + instanceName = null; + } + + // trim redundant whitespace + dataSource = dataSource.Trim(); + hostName = dataSource; + } + + private static Dictionary s_dataSourceToPortCache = new Dictionary(); + + /// + /// the method converts the regular connection string to one supported by MultiSubnetFailover (connect to the port, bypassing the browser) + /// it does the following: + /// * removes Failover Partner, if presents + /// * removes the network library and protocol prefix (only TCP is supported) + /// * if instance name is specified without port value, data source is replaced with "server, port" format instead of "server\name" + /// + /// Note that this method can create a connection to the server in case TCP port is needed. The port value is cached per data source, to avoid round trip to the server on next use. + /// + /// original connection string, must be valid + /// optionally, replace the (network) server name with a different one + /// holds the original server name on return + /// MultiSubnetFailover-enabled connection string builder + public static SqlConnectionStringBuilder GetMultiSubnetFailoverConnectionString(string connectionString, string replaceServerName, out string originalServerName) + { + SqlConnectionStringBuilder sb = new SqlConnectionStringBuilder(connectionString); + + sb["Network Library"] = null; // MSF supports TCP only, no need to specify the protocol explicitly + sb["Failover Partner"] = null; // not supported, remove it if present + + string protocol, instance; + int? serverPort; + + ParseDataSource(sb.DataSource, out protocol, out originalServerName, out instance, out serverPort); + + // Note: protocol value is ignored, connection to the server will fail if TCP is not enabled on the server + + if (!serverPort.HasValue) + { + // to get server listener's TCP port, connect to it using the original string, with TCP protocol enforced + // to improve stress performance, cache the port value to avoid round trip every time new connection string is needed + lock (s_dataSourceToPortCache) + { + int cachedPort; + string cacheKey = sb.DataSource; + if (s_dataSourceToPortCache.TryGetValue(cacheKey, out cachedPort)) + { + serverPort = cachedPort; + } + else + { + string originalServerNameWithInstance = sb.DataSource; + int protocolEndIndex = originalServerNameWithInstance.IndexOf(':'); + if (protocolEndIndex >= 0) + { + originalServerNameWithInstance = originalServerNameWithInstance.Substring(protocolEndIndex + 1); + } + + sb.DataSource = "tcp:" + originalServerNameWithInstance; + string tcpConnectionString = sb.ConnectionString; + using (SqlConnection con = new SqlConnection(tcpConnectionString)) + { + con.Open(); + + SqlCommand cmd = con.CreateCommand(); + cmd.CommandText = "select [local_tcp_port] from sys.dm_exec_connections where [session_id] = @@SPID"; + serverPort = Convert.ToInt32(cmd.ExecuteScalar()); + } + + s_dataSourceToPortCache[cacheKey] = serverPort.Value; + } + } + } + + // override it with user-provided one + string retDataSource; + if (replaceServerName != null) + { + retDataSource = replaceServerName; + } + else + { + retDataSource = originalServerName; + } + + // reconstruct the connection string (with the new server name and port) + // also, no protocol is needed since TCP is enforced anyway if MultiSubnetFailover is set to true + Debug.Assert(serverPort.HasValue, "Server port must be initialized"); + retDataSource += ", " + serverPort.Value; + + sb.DataSource = retDataSource; + sb.MultiSubnetFailover = true; + + return sb; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClient.Stress.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClient.Stress.Tests.csproj new file mode 100644 index 0000000000..6b03c2a5af --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClient.Stress.Tests.csproj @@ -0,0 +1,17 @@ + + + + Stress.Data.SqlClient + net481;net9.0 + latest + + + + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientStressFactory.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientStressFactory.cs new file mode 100644 index 0000000000..b8ea7def81 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientStressFactory.cs @@ -0,0 +1,226 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using Microsoft.Data.SqlClient; +using Microsoft.Test.Data.SqlClient; + +namespace Stress.Data.SqlClient +{ + public class SqlClientStressFactory : DataStressFactory + { + // scenarios + internal enum SqlClientScenario + { + Sql + } + + private SqlServerDataSource _source; + private SqlClientScenario _scenario; + + private MultiSubnetFailoverSetup _multiSubnetSetupHelper; + + internal SqlClientStressFactory() + : base(SqlClientFactory.Instance) + { + } + + internal void Initialize(ref string scenario, ref DataSource source) + { + // Ignore all asserts from known issues + var defaultTraceListener = Trace.Listeners["Default"] as DefaultTraceListener; + if (defaultTraceListener != null) + { + var newTraceListener = new FilteredDefaultTraceListener(defaultTraceListener); + Trace.Listeners.Remove(defaultTraceListener); + Trace.Listeners.Add(newTraceListener); + } + + // scenario <=> SqlClientScenario + if (string.IsNullOrEmpty(scenario)) + { + _scenario = SqlClientScenario.Sql; + } + else + { + _scenario = (SqlClientScenario)Enum.Parse(typeof(SqlClientScenario), scenario, true); + } + scenario = _scenario.ToString(); + + // initialize the source information + // SNAC/WDAC is using SqlServer sources; JET is using Access + switch (_scenario) + { + case SqlClientScenario.Sql: + if (source == null) + source = DataStressSettings.Instance.GetDefaultSourceByType(DataSourceType.SqlServer); + else if (source.Type != DataSourceType.SqlServer) + throw new ArgumentException(string.Format("Given source type is wrong: required {0}, received {1}", DataSourceType.SqlServer, source.Type)); + break; + + default: + throw new ArgumentException("Wrong scenario \"" + scenario + "\""); + } + + _source = (SqlServerDataSource)source; + + // Only try to add Multisubnet Failover host entries when the settings allow it in the source. + if (!_source.DisableMultiSubnetFailoverSetup) + { + _multiSubnetSetupHelper = new MultiSubnetFailoverSetup(_source); + _multiSubnetSetupHelper.InitializeFakeHostsForMultiSubnetFailover(); + } + } + + + + internal void Terminate() + { + if (_multiSubnetSetupHelper != null) + { + _multiSubnetSetupHelper.Terminate(); + } + } + + public sealed override string GetParameterName(string pName) + { + return "@" + pName; + } + + public override bool PrimaryKeyValueIsRequired + { + get { return false; } + } + + + public override string CreateBaseConnectionString(Random rnd, ConnectionStringOptions options) + { + SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(); + + switch (_scenario) + { + case SqlClientScenario.Sql: + builder.DataSource = _source.DataSource; + builder.InitialCatalog = _source.Database; + break; + + default: + throw new InvalidOperationException("missing case for " + _scenario); + } + + // Randomize between Windows Authentication and SQL Authentication + // Note that having 2 options here doubles the number of connection pools + bool integratedSecurity = false; + if (_source.SupportsWindowsAuthentication) + { + if (string.IsNullOrEmpty(_source.User)) // if sql login is not provided + integratedSecurity = true; + else + integratedSecurity = (rnd != null) ? (rnd.Next(2) == 0) : true; + } + + if (integratedSecurity) + { + builder.IntegratedSecurity = true; + } + else + { + builder.UserID = _source.User; + builder.Password = _source.Password; + } + + if (CurrentPoolingStressMode == PoolingStressMode.RandomizeConnectionStrings && rnd != null) + { + // Randomize connection string + + // Randomize packetsize + // Note that having 2 options here doubles the number of connection pools + if (rnd.NextBool()) + { + builder.PacketSize = 8192; + } + else + { + builder.PacketSize = 512; + } + + // If test case allows randomization and doesn't disallow MultiSubnetFailover, then enable MultiSubnetFailover 20% of the time + // Note that having 2 options here doubles the number of connection pools + + if (!_source.DisableMultiSubnetFailoverSetup && + !options.HasFlag(ConnectionStringOptions.DisableMultiSubnetFailover) && + rnd != null && + rnd.Next(5) == 0) + { + string msfHostName; + if (integratedSecurity) + { + msfHostName = _multiSubnetSetupHelper.MultiSubnetFailoverHostNameForIntegratedSecurity; + } + else + { + msfHostName = _multiSubnetSetupHelper.GetMultiSubnetFailoverHostName(rnd); + } + string serverName; + + // replace with build which has host name with multiple IP addresses + builder = NetUtils.GetMultiSubnetFailoverConnectionString(builder.ConnectionString, msfHostName, out serverName); + } + + // Randomize between using Named Pipes and TCP providers + // Note that having 2 options here doubles the number of connection pools + if (rnd != null) + { + if (rnd.Next(2) == 0) + { + builder.DataSource = "tcp:" + builder.DataSource; + } + else if (!_source.DisableNamedPipes) + { + // Named Pipes + if (builder.DataSource.Equals("(local)")) + builder.DataSource = "np:" + builder.DataSource; + else + builder.DataSource = @"np:\\" + builder.DataSource.Split(',')[0] + @"\pipe\sql\query"; + } + } + + // Set MARS if it is requested by the test case + if (options.HasFlag(ConnectionStringOptions.EnableMars)) + { + builder.MultipleActiveResultSets = true; + } + + // Disable connection resiliency, which is on by default, 20% of the time. + if (rnd != null && rnd.NextBool(.2)) + { + builder.ConnectRetryCount = 0; + } + } + else + { + // Minimal randomization of connection string + + // Enable MARS for all scenarios + builder.MultipleActiveResultSets = true; + } + builder.Encrypt = _source.Encrypt; + + // TODO - read from config file and randomize this option with required SQL server setup. + builder.TrustServerCertificate = true; + + builder.MaxPoolSize = 1000; + return builder.ToString(); + } + + protected override int GetNumDifferentApplicationNames() + { + // Return only 1 because the randomization in the base connection string above will give us more pools, so we don't need + // to also have many different application names. Getting connections from many different pools is not interesting to test + // because it reduces the amount of multithreadedness within each pool. + return 1; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientTestGroup.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientTestGroup.cs new file mode 100644 index 0000000000..b5b3b69e45 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientTestGroup.cs @@ -0,0 +1,610 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics; +using System.Data; +using Microsoft.Data.SqlClient; +using System.Xml; + +using DPStressHarness; +using System.IO; + +namespace Stress.Data.SqlClient +{ + public class SqlClientTestGroup : DataTestGroup + { + /// + /// SqlNotificationRequest options string + /// + private static string s_notificationOptions; + + /// + /// Connection string for SqlDependency.Start()/Stop() + /// + /// The connection string used for SqlDependency.Start() must always be exactly the same every time + /// if you are connecting to the same database with the same user and same application domain, so + /// don't randomise the connection string for calling SqlDependency.Start() + /// + private static string s_sqlDependencyConnString; + + /// + /// A thread which randomly calls SqlConnection.ClearAllPools. + /// This significantly increases the probability of hitting some bugs, such as: + /// vstfdevdiv 674236 (SqlConnection.Open() throws InvalidOperationException for absolutely valid connection request) + /// sqlbuvsts 328845 (InvalidOperationException: The requested operation cannot be completed because the connection has been broken.) (this is LSE QFE) + /// However, calling ClearAllPools all the time might also significantly decrease the probability of hitting some other bug, + /// so this thread will alternate between hammering on ClearAllPools for several minutes, and then doing nothing for several minutes. + /// + private static Thread s_clearAllPoolsThread; + + /// + /// Call .Set() on this to cleanly stop the ClearAllPoolsThread. + /// + private static ManualResetEvent s_clearAllPoolsThreadStop = new ManualResetEvent(false); + + private static void ClearAllPoolsThreadFunc() + { + Random rnd = new TrackedRandom((int)Environment.TickCount); + + // Swap between calling ClearAllPools and doing nothing every 5 minutes. + TimeSpan halfCycleTime = TimeSpan.FromMinutes(5); + + int minWait = 10; // milliseconds + int maxWait = 1000; // milliseconds + + bool active = true; // Start active so we can hit vstfdevdiv 674236 asap + Stopwatch stopwatch = Stopwatch.StartNew(); + while (!s_clearAllPoolsThreadStop.WaitOne(rnd.Next(minWait, maxWait))) + { + if (stopwatch.Elapsed > halfCycleTime) + { + active = !active; + stopwatch.Reset(); + stopwatch.Start(); + } + + if (active) + { + SqlConnection.ClearAllPools(); + } + } + } + + public override void GlobalTestSetup() + { + base.GlobalTestSetup(); + + s_clearAllPoolsThread = new Thread(ClearAllPoolsThreadFunc); + s_clearAllPoolsThread.Start(); + + // set the notification options for SqlNotificationRequest tests + s_notificationOptions = "service=StressNotifications;local database=" + ((SqlServerDataSource)Source).Database; + + s_sqlDependencyConnString = Factory.CreateBaseConnectionString(null, DataStressFactory.ConnectionStringOptions.DisableMultiSubnetFailover); + } + + public override void GlobalTestCleanup() + { + s_clearAllPoolsThreadStop.Set(); + s_clearAllPoolsThread.Join(); + + SqlClientStressFactory factory = Factory as SqlClientStressFactory; + if (factory != null) + { + factory.Terminate(); + } + + base.GlobalTestCleanup(); + } + + public override void GlobalExceptionHandler(Exception e) + { + base.GlobalExceptionHandler(e); + } + + protected override DataStressFactory CreateFactory(ref string scenario, ref DataSource source) + { + SqlClientStressFactory factory = new SqlClientStressFactory(); + factory.Initialize(ref scenario, ref source); + return factory; + } + + protected override bool IsCommandCancelledException(Exception e) + { + return + base.IsCommandCancelledException(e) || + ((e is SqlException || e is InvalidOperationException) && e.Message.ToLower().Contains("operation cancelled")) || + (e is SqlException && e.Message.StartsWith("A severe error occurred on the current command.")) || + (e is AggregateException && e.InnerException != null && IsCommandCancelledException(e.InnerException)) || + (e is System.Reflection.TargetInvocationException && e.InnerException != null && IsCommandCancelledException(e.InnerException)); + } + + protected override bool IsReaderClosedException(Exception e) + { + return + e is TaskCanceledException + || + ( + e is InvalidOperationException + && + ( + (e.Message.StartsWith("Invalid attempt to call") && e.Message.EndsWith("when reader is closed.")) + || + e.Message.Equals("Invalid attempt to read when no data is present.") + || + e.Message.Equals("Invalid operation. The connection is closed.") + ) + ) + || + ( + e is ObjectDisposedException + && + ( + e.Message.Equals("Cannot access a disposed object.\r\nObject name: 'SqlSequentialStream'.") + || + e.Message.Equals("Cannot access a disposed object.\r\nObject name: 'SqlSequentialTextReader'.") + ) + ); + } + + protected override bool AllowReaderCloseDuringReadAsync() + { + return true; + } + + /// + /// Utility function used by async tests + /// + /// SqlCommand to be executed. + /// Indicates if data is being queried + /// Indicates if the query should be executed as an Xml + /// + /// The Cancellation Token Source + /// The result of beginning of Async execution. + private IAsyncResult SqlCommandBeginExecute(SqlCommand com, bool query, bool xml, bool useBeginAPI, CancellationTokenSource cts = null) + { + DataStressErrors.Assert(!(useBeginAPI && cts != null), "Cannot use begin api with CancellationTokenSource"); + + CancellationToken token = (cts != null) ? cts.Token : CancellationToken.None; + + if (xml) + { + com.CommandText = com.CommandText + " FOR XML AUTO"; + return useBeginAPI ? null : com.ExecuteXmlReaderAsync(token); + } + else if (query) + { + return useBeginAPI ? null : com.ExecuteReaderAsync(token); + } + else + { + return useBeginAPI ? null : com.ExecuteNonQueryAsync(token); + } + } + + /// + /// Utility function used by async tests + /// + /// Used to randomize reader.Read() call, whether it should continue or break, and is passed down to ConsumeReaderAsync + /// The Async result from Begin operation. + /// The Sql Command to Execute + /// Indicates if data is being queried and where ExecuteQuery or Non-query to be used with the reader + /// Indicates if the query should be executed as an Xml + /// Indicates if command was cancelled and is used to throw exception if a Command cancellation related exception is encountered + /// The Cancellation Token Source + private void SqlCommandEndExecute(Random rnd, IAsyncResult result, SqlCommand com, bool query, bool xml, bool cancelled, CancellationTokenSource cts = null) + { + try + { + bool closeReader = ShouldCloseDataReader(); + if (xml) + { + XmlReader reader = null; + if (result != null && result is Task) + { + reader = AsyncUtils.GetResult(result); + } + else + { + reader = AsyncUtils.ExecuteXmlReader(com); + } + + while (reader.Read()) + { + if (rnd.Next(10) == 0) break; + if (rnd.Next(2) == 0) continue; + reader.ReadElementContentAsString(); + } + if (closeReader) reader.Dispose(); + } + else if (query) + { + DataStressReader reader = null; + if (result != null && result is Task) + { + reader = new DataStressReader(AsyncUtils.GetResult(result)); + } + else + { + reader = new DataStressReader(AsyncUtils.ExecuteReader(com)); + } + + CancellationToken token = (cts != null) ? cts.Token : CancellationToken.None; + + AsyncUtils.WaitAndUnwrapException(ConsumeReaderAsync(reader, false, token, rnd)); + + if (closeReader) reader.Close(); + } + else + { + if (result != null && result is Task) + { + int temp = AsyncUtils.GetResult(result); + } + else + { + AsyncUtils.ExecuteNonQuery(com); + } + } + } + catch (Exception e) + { + if (cancelled && IsCommandCancelledException(e)) + { + // expected exception, ignore + } + else + { + throw; + } + } + } + + + /// + /// Utility function for tests + /// + /// + /// + /// + /// + /// + private void TestSqlAsync(Random rnd, bool read, bool poll, bool handle, bool xml) + { + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + SqlCommand com = (SqlCommand)Factory.GetCommand(rnd, table, conn, read, xml); + bool useBeginAPI = rnd.NextBool(); + + IAsyncResult result = SqlCommandBeginExecute(com, read, xml, useBeginAPI); + // Cancel 1/10 commands + bool cancel = (rnd.Next(10) == 0); + if (cancel) + { + if (com.Connection.State != ConnectionState.Closed) com.Cancel(); + } + + if (result != null) + WaitForAsyncOpToComplete(rnd, result, poll, handle); + // At random end query or forget it + if (rnd.Next(2) == 0) + SqlCommandEndExecute(rnd, result, com, read, xml, cancel); + + // Randomly wait for the command to complete after closing the connection to verify devdiv bug 200550. + // This was fixed for .NET 4.5 Task-based API, but not for the older Begin/End IAsyncResult API. + conn.Close(); + if (!useBeginAPI && rnd.NextBool()) + result.AsyncWaitHandle.WaitOne(); + } + } + + private void WaitForAsyncOpToComplete(Random rnd, IAsyncResult result, bool poll, bool handle) + { + if (poll) + { + long ret = 0; + bool wait = !result.IsCompleted; + while (wait) + { + wait = !result.IsCompleted; + Thread.Sleep(100); + if (ret++ > 300) //30 second max wait time then exit + wait = false; + } + } + else if (handle) + { + WaitHandle wait = result.AsyncWaitHandle; + wait.WaitOne(rnd.Next(1000)); + } + } + + /// + /// SqlClient Async Non-blocking Read Test + /// + [StressTest("TestSqlAsyncNonBlockingRead", Weight = 10)] + public void TestSqlAsyncNonBlockingRead() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: true, poll: false, handle: false, xml: false); + } + + /// + /// SqlClient Async Non-blocking Write Test + /// + [StressTest("TestSqlAsyncNonBlockingWrite", Weight = 10)] + public void TestSqlAsyncNonBlockingWrite() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: false, poll: false, handle: false, xml: false); + } + + /// + /// SqlClient Async Polling Read Test + /// + [StressTest("TestSqlAsyncPollingRead", Weight = 10)] + public void TestSqlAsyncPollingRead() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: true, poll: true, handle: false, xml: false); + } + + /// + /// SqlClient Async Polling Write Test + /// + [StressTest("TestSqlAsyncPollingWrite", Weight = 10)] + public void TestSqlAsyncPollingWrite() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: false, poll: true, handle: false, xml: false); + } + + /// + /// SqlClient Async Event Read Test + /// + [StressTest("TestSqlAsyncEventRead", Weight = 10)] + public void TestSqlAsyncEventRead() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: true, poll: false, handle: true, xml: false); + } + + /// + /// SqlClient Async Event Write Test + /// + [StressTest("TestSqlAsyncEventWrite", Weight = 10)] + public void TestSqlAsyncEventWrite() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: false, poll: false, handle: true, xml: false); + } + + + /// + /// SqlClient Async Xml Non-blocking Read Test + /// + [StressTest("TestSqlXmlAsyncNonBlockingRead", Weight = 10)] + public void TestSqlXmlAsyncNonBlockingRead() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: true, poll: false, handle: false, xml: true); + } + + /// + /// SqlClient Async Xml Polling Read Test + /// + [StressTest("TestSqlXmlAsyncPollingRead", Weight = 10)] + public void TestSqlXmlAsyncPollingRead() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: true, poll: true, handle: false, xml: true); + } + + /// + /// SqlClient Async Xml Event Read Test + /// + [StressTest("TestSqlXmlAsyncEventRead", Weight = 10)] + public void TestSqlXmlAsyncEventRead() + { + Random rnd = RandomInstance; + TestSqlAsync(rnd, read: true, poll: false, handle: true, xml: true); + } + + + [StressTest("TestSqlXmlCommandReader", Weight = 10)] + public void TestSqlXmlCommandReader() + { + Random rnd = RandomInstance; + + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + SqlCommand com = (SqlCommand)Factory.GetCommand(rnd, table, conn, query: true, isXml: true); + com.CommandText = com.CommandText + " FOR XML AUTO"; + + // Cancel 1/10 commands + bool cancel = rnd.Next(10) == 0; + if (cancel) + { + ThreadPool.QueueUserWorkItem(new WaitCallback(CommandCancel), com); + } + + try + { + XmlReader reader = com.ExecuteXmlReader(); + + while (reader.Read()) + { + if (rnd.Next(10) == 0) break; + if (rnd.Next(2) == 0) continue; + reader.ReadElementContentAsString(); + } + if (rnd.Next(10) != 0) reader.Dispose(); + } + catch (Exception ex) + { + if (cancel && IsCommandCancelledException(ex)) + { + // expected, ignore + } + else + { + throw; + } + } + } + } + + + /// + /// Utility function used for testing cancellation on Execute*Async APIs. + /// + private void TestSqlAsyncCancellation(Random rnd, bool read, bool xml) + { + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + SqlCommand com = (SqlCommand)Factory.GetCommand(rnd, table, conn, read, xml); + + CancellationTokenSource cts = new CancellationTokenSource(); + Task t = (Task)SqlCommandBeginExecute(com, read, xml, false, cts); + + cts.CancelAfter(rnd.Next(2000)); + SqlCommandEndExecute(rnd, (IAsyncResult)t, com, read, xml, true, cts); + } + } + + /// + /// SqlClient Async Xml Event Read Test + /// + [StressTest("TestExecuteXmlReaderAsyncCancellation", Weight = 10)] + public void TestExecuteXmlReaderAsyncCancellation() + { + Random rnd = RandomInstance; + TestSqlAsyncCancellation(rnd, true, true); + } + + /// + /// SqlClient Async Xml Event Read Test + /// + [StressTest("TestExecuteReaderAsyncCancellation", Weight = 10)] + public void TestExecuteReaderAsyncCancellation() + { + Random rnd = RandomInstance; + TestSqlAsyncCancellation(rnd, true, false); + } + + /// + /// SqlClient Async Xml Event Read Test + /// + [StressTest("TestExecuteNonQueryAsyncCancellation", Weight = 10)] + public void TestExecuteNonQueryAsyncCancellation() + { + Random rnd = RandomInstance; + TestSqlAsyncCancellation(rnd, false, false); + } + + + private class MARSCommand + { + internal SqlCommand cmd; + internal IAsyncResult result; + internal bool query; + internal bool xml; + } + + [StressTest("TestSqlAsyncMARS", Weight = 10)] + public void TestSqlAsyncMARS() + { + const int MaxCmds = 11; + Random rnd = RandomInstance; + + using (DataStressConnection conn = Factory.CreateConnection(rnd, DataStressFactory.ConnectionStringOptions.EnableMars)) + { + if (!OpenConnection(conn)) return; + DataStressFactory.TableMetadata table = Factory.GetRandomTable(rnd); + + // MARS session cache is by default 10. + // This is documented here: https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/enabling-multiple-active-result-sets + // We want to stress test this by allowing 11 concurrent commands. Hence the max in rnd.Next below is 12. + MARSCommand[] cmds = new MARSCommand[rnd.Next(5, MaxCmds + 1)]; + + for (int i = 0; i < cmds.Length; i++) + { + cmds[i] = new MARSCommand(); + + // Make every 3rd query xml reader + if (i % 3 == 0) + { + cmds[i].query = true; + cmds[i].xml = true; + } + else + { + cmds[i].query = rnd.NextBool(); + cmds[i].xml = false; + } + + cmds[i].cmd = (SqlCommand)Factory.GetCommand(rnd, table, conn, cmds[i].query, cmds[i].xml); + cmds[i].result = SqlCommandBeginExecute(cmds[i].cmd, cmds[i].query, cmds[i].xml, rnd.NextBool()); + if (cmds[i].result != null) + WaitForAsyncOpToComplete(rnd, cmds[i].result, true, false); + } + + // After all commands have been launched, wait for them to complete now. + for (int i = 0; i < cmds.Length; i++) + { + SqlCommandEndExecute(rnd, cmds[i].result, cmds[i].cmd, cmds[i].query, cmds[i].xml, false); + } + } + } + + + [StressTest("TestStreamInputParameter", Weight = 10)] + public void TestStreamInputParameter() + { + Random rnd = RandomInstance; + int dataSize = 100000; + byte[] data = new byte[dataSize]; + rnd.NextBytes(data); + + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + SqlCommand cmd = (SqlCommand)conn.CreateCommand(); + cmd.CommandText = "SELECT @blob"; + SqlParameter param = cmd.Parameters.Add("@blob", SqlDbType.VarBinary, dataSize); + param.Direction = ParameterDirection.Input; + param.Value = new MemoryStream(data); + CommandExecute(rnd, cmd, true); + } + } + + [StressTest("TestTextReaderInputParameter", Weight = 10)] + public void TestTextReaderInputParameter() + { + Random rnd = RandomInstance; + int dataSize = 100000; + string data = new string('a', dataSize); + + using (DataStressConnection conn = Factory.CreateConnection(rnd)) + { + if (!OpenConnection(conn)) return; + SqlCommand cmd = (SqlCommand)conn.CreateCommand(); + cmd.CommandText = "SELECT @blob"; + SqlParameter param = cmd.Parameters.Add("@blob", SqlDbType.VarChar, dataSize); + param.Direction = ParameterDirection.Input; + param.Value = new StringReader(data); + CommandExecute(rnd, cmd, true); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/StressTests.slnx b/src/Microsoft.Data.SqlClient/tests/StressTests/StressTests.slnx new file mode 100644 index 0000000000..a0dfd5e503 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/StressTests/StressTests.slnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/LocalAppContextSwitchesTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/LocalAppContextSwitchesTest.cs index 90ee094f8f..c28fe18978 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/LocalAppContextSwitchesTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/LocalAppContextSwitchesTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using Xunit; namespace Microsoft.Data.SqlClient.UnitTests; @@ -22,11 +23,15 @@ public void TestDefaultAppContextSwitchValues() Assert.False(LocalAppContextSwitches.MakeReadAsyncBlocking); Assert.True(LocalAppContextSwitches.UseMinimumLoginTimeout); Assert.True(LocalAppContextSwitches.LegacyVarTimeZeroScaleBehaviour); - Assert.False(LocalAppContextSwitches.UseCompatibilityProcessSni); - Assert.False(LocalAppContextSwitches.UseCompatibilityAsyncBehaviour); + Assert.True(LocalAppContextSwitches.UseCompatibilityProcessSni); + Assert.True(LocalAppContextSwitches.UseCompatibilityAsyncBehaviour); Assert.False(LocalAppContextSwitches.UseConnectionPoolV2); - #if NETFRAMEWORK + Assert.False(LocalAppContextSwitches.TruncateScaledDecimal); +#if NETFRAMEWORK Assert.False(LocalAppContextSwitches.DisableTnirByDefault); - #endif + Assert.False(LocalAppContextSwitches.UseManagedNetworking); +#else + Assert.Equal(!OperatingSystem.IsWindows(), LocalAppContextSwitches.UseManagedNetworking); +#endif } } diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs new file mode 100644 index 0000000000..fa5bf9752b --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/UserAgentInfoTests.cs @@ -0,0 +1,333 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Data.Common; +using Microsoft.Data.SqlClient.UserAgent; +using Xunit; + +#nullable enable + +namespace Microsoft.Data.SqlClient.UnitTests +{ + /// + /// Unit tests for and its companion DTO. + /// Focus areas: + /// 1. Cached payload size and non-nullability + /// 2. Default expected value check for payload fields + /// 3. Payload size adjustment and field dropping(all low priority fields) + /// 4. Payload size adjustment and field dropping(drop particular low priority fields: arch, runtime and os.description) + /// 5. DTO JSON contract (key names and values) + /// 6. Combined truncation, adjustment, and serialization + /// + public class UserAgentInfoTests + { + // Cached payload is within the 2,047‑byte spec and never null + [Fact] + public void CachedPayload_IsNotNull_And_WithinSpecLimit() + { + ReadOnlyMemory payload = UserAgentInfo.UserAgentCachedJsonPayload; + Assert.False(payload.IsEmpty); + Assert.InRange(payload.Length, 1, UserAgentInfo.JsonPayloadMaxBytes); + } + + // Cached payload contains the expected values for driver name and version + [Fact] + public void CachedPayload_Contains_Correct_DriverName_And_Version() + { + // Arrange: retrieve the raw JSON payload bytes and determine what we expect + ReadOnlyMemory payload = UserAgentInfo.UserAgentCachedJsonPayload; + Assert.False(payload.IsEmpty); // guard against empty payload + + // compute the expected driver and version + string expectedDriver = UserAgentInfo.DriverName; + string expectedVersion = ADP.GetAssemblyVersion().ToString(); + + // Act: turn the bytes back into JSON and pull out the fields + using JsonDocument document = JsonDocument.Parse(payload); + JsonElement root = document.RootElement; + string actualDriver = root.GetProperty(UserAgentInfoDto.DriverJsonKey).GetString()!; + string actualVersion = root.GetProperty(UserAgentInfoDto.VersionJsonKey).GetString()!; + + // Assert: the driver and version in the payload match the expected values + Assert.Equal(expectedDriver, actualDriver); + Assert.Equal(expectedVersion, actualVersion); + } + + // TruncateOrDefault respects null, empty, fit, and overflow cases + [Theory] + [InlineData(null, 5, "Unknown")] // null returns default + [InlineData("", 5, "Unknown")] // empty returns default + [InlineData("abc", 5, "abc")] // within limit unchanged + [InlineData("abcde", 5, "abcde")] // exact max chars + [InlineData("abcdef", 5, "abcde")] // overflow truncated + public void TruncateOrDefault_Behaviour(string? input, int max, string expected) + { + string actual = UserAgentInfo.TruncateOrDefault(input!, max); + Assert.Equal(expected, actual); + } + + // AdjustJsonPayloadSize drops all low‑priority fields when required + + /// + /// Verifies that AdjustJsonPayloadSize truncates the DTO’s JSON when it exceeds the maximum size. + /// High-priority fields (Driver, Version) must remain, low-priority fields (Arch, Runtime and OS) are removed + /// + [Fact] + public void AdjustJsonPayloadSize_DropAllLowPriorityFields_When_PayloadTooLarge() + { + // Arrange: create a DTO whose serialized JSON is guaranteed to exceed the max size + string huge = new string('x', 20_000); + var dto = new UserAgentInfoDto + { + Driver = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.DriverNameMaxChars), + Version = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.VersionMaxChars), + OS = new UserAgentInfoDto.OsInfo + { + Type = huge, + Details = huge + }, + Arch = huge, + Runtime = huge + }; + + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = null, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false + }; + + string expectedDriverName = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.DriverNameMaxChars); + string expectedVersion = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.VersionMaxChars); + + // Capture the size before the helper mutates the DTO + byte[] original = JsonSerializer.SerializeToUtf8Bytes(dto, options); + Assert.True(original.Length > UserAgentInfo.JsonPayloadMaxBytes); + + // Act: apply the size-adjustment helper + byte[] payload = UserAgentInfo.AdjustJsonPayloadSize(dto); + + // Assert: payload is smaller and not empty + Assert.NotEmpty(payload); + Assert.True(payload.Length < original.Length); + + // Structural checks using JsonDocument + using JsonDocument doc = JsonDocument.Parse(payload); + JsonElement root = doc.RootElement; + + // High-priority fields must still be present(driver name and version) + Assert.Equal(expectedDriverName, root.GetProperty(UserAgentInfoDto.DriverJsonKey).GetString()); + Assert.Equal(expectedVersion, root.GetProperty(UserAgentInfoDto.VersionJsonKey).GetString()); + + // Low-priority fields should have been removed(arch and runtime) + Assert.False(root.TryGetProperty(UserAgentInfoDto.ArchJsonKey, out _)); + Assert.False(root.TryGetProperty(UserAgentInfoDto.RuntimeJsonKey, out _)); + + // OS block should have been removed entirely + Assert.False(root.TryGetProperty(UserAgentInfoDto.OsJsonKey, out _)); + } + + /// + /// Verifies that AdjustJsonPayloadSize truncates the DTO’s JSON when it exceeds the maximum size. + /// High-priority fields (Driver, Version) must remain, low-priority fields (Arch, Runtime and OS.details) are removed + /// Note that OS subfield(Type) is preserved, but OS.Details is dropped. + /// + [Fact] + public void AdjustJsonPayloadSize_DropSpecificPriorityFields_Excluding_OsType_When_PayloadTooLarge() + { + // Arrange: create a DTO whose serialized JSON is guaranteed to exceed the max size + string huge = new string('x', 20_000); + var dto = new UserAgentInfoDto + { + Driver = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.DriverNameMaxChars), + Version = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.VersionMaxChars), + OS = new UserAgentInfoDto.OsInfo + { + Type = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.OsTypeMaxChars), + Details = huge + }, + Arch = huge, + Runtime = huge + }; + + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = null, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false + }; + + string expectedDriverName = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.DriverNameMaxChars); + string expectedVersion = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.VersionMaxChars); + string expectedOsType = UserAgentInfo.TruncateOrDefault(huge, UserAgentInfo.OsTypeMaxChars); + + // Capture the size before the helper mutates the DTO + byte[] original = JsonSerializer.SerializeToUtf8Bytes(dto, options); + Assert.True(original.Length > UserAgentInfo.JsonPayloadMaxBytes); + + // Act: apply the size-adjustment helper + byte[] payload = UserAgentInfo.AdjustJsonPayloadSize(dto); + + // Assert: payload is smaller and not empty + Assert.NotEmpty(payload); + Assert.True(payload.Length < original.Length); + + // Structural checks using JsonDocument + using JsonDocument doc = JsonDocument.Parse(payload); + JsonElement root = doc.RootElement; + + // High-priority fields must still be present(driver name and version) and truncated to expected length + //Assert.True(root.TryGetProperty(UserAgentInfoDto.DriverJsonKey, out _)); + Assert.Equal(expectedDriverName, root.GetProperty(UserAgentInfoDto.DriverJsonKey).GetString()); + Assert.Equal(expectedVersion, root.GetProperty(UserAgentInfoDto.VersionJsonKey).GetString()); + //Assert.True(root.TryGetProperty(UserAgentInfoDto.VersionJsonKey, out _)); + + // Low-priority fields should have been removed(arch and runtime) + Assert.False(root.TryGetProperty(UserAgentInfoDto.ArchJsonKey, out _)); + Assert.False(root.TryGetProperty(UserAgentInfoDto.RuntimeJsonKey, out _)); + + Assert.True(root.TryGetProperty(UserAgentInfoDto.OsJsonKey, out JsonElement os)); + Assert.True(os.TryGetProperty(UserAgentInfoDto.OsInfo.TypeJsonKey, out JsonElement type)); + + Assert.Equal(expectedOsType, type.GetString()); + Assert.False(os.TryGetProperty(UserAgentInfoDto.OsInfo.DetailsJsonKey, out _)); + + } + + // DTO JSON contract - verify names and values(parameterized) + + /// + /// Verifies that UserAgentInfoDto serializes according to its JSON contract: + /// required fields always appear with correct values, optional fields + /// and the nested OS object are only emitted when non-null, + /// and all JSON property names match the defined constants. + /// + [Theory] + [InlineData("d", "v", "t", "dd", "a", "r")] + [InlineData("DeReaver", "1.2", "linux", "kernel", "", "")] + [InlineData("LongDrv", "2.0", "win", null, null, null)] + [InlineData("Driver", "Version", null, null, null, null)] // drop OsInfo entirely + public void Dto_JsonPropertyNames_MatchConstants( + string driver, + string version, + string? osType, + string? osDetails, + string? arch, + string? runtime) + { + // Arrange: build the DTO, dropping the OS object if osType is null + var dto = new UserAgentInfoDto + { + Driver = driver, + Version = version, + OS = osType == null + ? null + : new UserAgentInfoDto.OsInfo + { + Type = osType, + Details = string.IsNullOrEmpty(osDetails) ? null : osDetails + }, + Arch = string.IsNullOrEmpty(arch) ? null : arch, + Runtime = string.IsNullOrEmpty(runtime) ? null : runtime + }; + + // Arrange: configure JSON serialization to omit nulls and use exact property names + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = null, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = false + }; + + // Act: serialize the DTO and parse it back into a JsonDocument + string json = JsonSerializer.Serialize(dto, options); + using var doc = JsonDocument.Parse(json); + JsonElement root = doc.RootElement; + + // Assert: required properties always present with correct values + Assert.Equal(driver, root.GetProperty(UserAgentInfoDto.DriverJsonKey).GetString()); + Assert.Equal(version, root.GetProperty(UserAgentInfoDto.VersionJsonKey).GetString()); + + // Assert: Arch is only present if non-null + if (dto.Arch == null) + { + Assert.False(root.TryGetProperty(UserAgentInfoDto.ArchJsonKey, out _)); + } + else + { + Assert.Equal(dto.Arch, root.GetProperty(UserAgentInfoDto.ArchJsonKey).GetString()); + } + + // Assert: Runtime is only present if non-null + if (dto.Runtime == null) + { + Assert.False(root.TryGetProperty(UserAgentInfoDto.RuntimeJsonKey, out _)); + } + else + { + Assert.Equal(dto.Runtime, root.GetProperty(UserAgentInfoDto.RuntimeJsonKey).GetString()); + } + + // Assert: OS object may be omitted entirely + if (dto.OS == null) + { + Assert.False(root.TryGetProperty(UserAgentInfoDto.OsJsonKey, out _)); + } + else + { + JsonElement os = root.GetProperty(UserAgentInfoDto.OsJsonKey); + + // OS.Type must always be present when OS is not null + Assert.Equal(dto.OS.Type, os.GetProperty(UserAgentInfoDto.OsInfo.TypeJsonKey).GetString()); + + // OS.Details is optional + if (dto.OS.Details == null) + { + Assert.False(os.TryGetProperty(UserAgentInfoDto.OsInfo.DetailsJsonKey, out _)); + } + else + { + Assert.Equal(dto.OS.Details, os.GetProperty(UserAgentInfoDto.OsInfo.DetailsJsonKey).GetString()); + } + } + } + + // End-to-end test that combines truncation, adjustment, and serialization + [Fact] + public void EndToEnd_Truncate_Adjust_Serialize_Works() + { + string raw = new string('x', 2_000); + const int Max = 100; + + string driver = UserAgentInfo.TruncateOrDefault(raw, Max); + string version = UserAgentInfo.TruncateOrDefault(raw, Max); + string osType = UserAgentInfo.TruncateOrDefault(raw, Max); + + var dto = new UserAgentInfoDto + { + Driver = driver, + Version = version, + OS = new UserAgentInfoDto.OsInfo { Type = osType, Details = raw }, + Arch = raw, + Runtime = raw + }; + + byte[] payload = UserAgentInfo.AdjustJsonPayloadSize(dto); + string json = Encoding.UTF8.GetString(payload); + + using JsonDocument doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + Assert.Equal(driver, root.GetProperty(UserAgentInfoDto.DriverJsonKey).GetString()); + Assert.Equal(version, root.GetProperty(UserAgentInfoDto.VersionJsonKey).GetString()); + + JsonElement os = root.GetProperty(UserAgentInfoDto.OsJsonKey); + Assert.Equal(osType, os.GetProperty(UserAgentInfoDto.OsInfo.TypeJsonKey).GetString()); + } + } +} diff --git a/tools/GenAPI/Directory.Build.Props b/tools/GenAPI/Directory.Build.props similarity index 100% rename from tools/GenAPI/Directory.Build.Props rename to tools/GenAPI/Directory.Build.props diff --git a/tools/props/Versions.props b/tools/props/Versions.props index 67e8010809..ad42c4964a 100644 --- a/tools/props/Versions.props +++ b/tools/props/Versions.props @@ -2,7 +2,7 @@ - 6.1.0 + 7.0.0 0 - 6.0.0.0 + 7.0.0.0 $(AssemblyFileVersion) $(MdsVersionDefault)-dev diff --git a/tools/specs/Microsoft.Data.SqlClient.nuspec b/tools/specs/Microsoft.Data.SqlClient.nuspec index 0f029b4c19..bc177256cb 100644 --- a/tools/specs/Microsoft.Data.SqlClient.nuspec +++ b/tools/specs/Microsoft.Data.SqlClient.nuspec @@ -29,7 +29,8 @@ sqlclient microsoft.data.sqlclient - + + @@ -42,7 +43,8 @@ - + + @@ -54,7 +56,8 @@ - + + @@ -66,7 +69,8 @@ - + + diff --git a/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec b/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec index bb237f6df7..2140a32e7a 100644 --- a/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec +++ b/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec @@ -25,21 +25,21 @@ Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyStoreProvider.SqlColumnEncrypti sqlclient microsoft.data.sqlclient azurekeyvaultprovider akvprovider alwaysencrypted - - - + + + - - - + + + - - - + + + diff --git a/tools/targets/NotSupported.targets b/tools/targets/NotSupported.targets index 890c4ae048..124d86f87c 100644 --- a/tools/targets/NotSupported.targets +++ b/tools/targets/NotSupported.targets @@ -42,7 +42,7 @@ $(GenAPIArgs) -o:"$(NotSupportedSourceFile)" $(GenAPIArgs) -t:"$(GeneratePlatformNotSupportedAssemblyMessage)" $(GenAPIArgs) -global - "$(DotNetCmd) $(ToolsArtifactsDir)$(TargetFramework)\Microsoft.DotNet.GenAPI.dll" + "$(DotnetPath)dotnet" "$(ToolsArtifactsDir)net8.0/Microsoft.DotNet.GenAPI.dll" "$(ToolsArtifactsDir)net472\Microsoft.DotNet.GenAPI.exe"