diff --git a/.github/prompts/release-notes.prompt.md b/.github/prompts/release-notes.prompt.md index ca03933236..55cc7785e2 100644 --- a/.github/prompts/release-notes.prompt.md +++ b/.github/prompts/release-notes.prompt.md @@ -86,6 +86,10 @@ For each package that has relevant PRs in the milestone: - Look up dependencies using the Dependency Sources from the lookup table above. Resolve concrete versions from [Directory.Packages.props](Directory.Packages.props). - List dependencies per target framework. Use the project file's `` to determine which frameworks to list. - Omit the Contributors section for packages with no public contributors. + - **GA releases (all packages):** When the release is a stable (non-preview) version, structure the notes with two sections: + 1. **"Changes Since [last preview]"** — only the delta since the most recent preview of this package. + 2. **"Cumulative Changes Since [last stable]"** — all changes since the last stable release of this package, synthesized from all preview release notes plus the GA milestone. This applies to every package (MDS, AKV, Extensions.Azure, Abstractions, Internal.Logging, etc.), not just the core driver. Apply the cross-referencing from Step 3 to eliminate items already shipped in prior stable patch releases. + - **Preview releases:** Only include the delta since the previous release (preview or stable). No cumulative section is needed. 3. **Create or update the version README** at `/README.md`. Follow the existing format — see [release-notes/add-ons/AzureKeyVaultProvider/6.1/README.md](release-notes/add-ons/AzureKeyVaultProvider/6.1/README.md) for reference: diff --git a/.github/workflows/check-milestone.yml b/.github/workflows/check-milestone.yml new file mode 100644 index 0000000000..da4681fd7b --- /dev/null +++ b/.github/workflows/check-milestone.yml @@ -0,0 +1,24 @@ +name: Check Milestone + +on: + pull_request: + types: [opened, edited, synchronize, milestoned, demilestoned] + +jobs: + check-milestone: + name: Validate milestone + runs-on: ubuntu-latest + permissions: + pull-requests: read + steps: + - name: Check milestone is set + if: github.event.pull_request.milestone == null + run: | + echo "::error::This PR does not have a milestone set. Please assign a milestone before merging." + exit 1 + + - name: Check milestone is open + if: github.event.pull_request.milestone != null && github.event.pull_request.milestone.state != 'open' + run: | + echo "::error::Milestone '${{ github.event.pull_request.milestone.title }}' is ${{ github.event.pull_request.milestone.state }}. Please assign an open milestone." + exit 1 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 64584746df..b7d328547e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -12,21 +12,12 @@ name: "CodeQL Advanced" on: - # Scan on pushes to main only. push: - branches: - - main - - # Scan on PRs that target matching branches. + branches: [ "release/7.0" ] pull_request: - branches: - - main - - feat/** - - dev/** - - # Scan weekly on Saturdays at 23:33 UTC + branches: [ "release/7.0" ] schedule: - - cron: '33 23 * * 6' + - cron: '15 23 * * 6' jobs: analyze: diff --git a/BUILDGUIDE.md b/BUILDGUIDE.md index 8f13730f68..f0fd358a82 100644 --- a/BUILDGUIDE.md +++ b/BUILDGUIDE.md @@ -13,6 +13,14 @@ Tests and tools may require different .NET Runtimes that may be installed independently. For example, tests targeting .NET 8.0 will need that runtime installed. +### NuGet CLI + +The `GenerateMdsPackage` build target uses `nuget.exe` to create the Microsoft.Data.SqlClient NuGet +package, and is thus only supported on Windows build hosts. In CI, this is installed automatically +by the `NuGetToolInstaller@1` pipeline task. For local builds, install the [NuGet +CLI](https://learn.microsoft.com/nuget/install-nuget-client-tools) and ensure `nuget.exe` is on your +PATH. + ### Visual Studio This project should be built with Visual Studio 2019+ for the best compatibility. The required set of components are provided in the below file: diff --git a/CHANGELOG.md b/CHANGELOG.md index dc51c7d2bc..fedf1ad568 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,137 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) > **Note:** Releases are sorted in reverse chronological order (newest first). +## [Stable Release 7.0.0] - 2026-03-17 + +This section summarizes all changes across the 7.0 preview cycle for users upgrading from the latest 6.1 stable release. +See the [full release notes](release-notes/7.0/7.0.0.md) for detailed descriptions. + +Also released as part of this milestone: +- Released Microsoft.Data.SqlClient.Extensions.Abstractions 1.0.0. See [release notes](release-notes/Extensions/Abstractions/1.0/1.0.0.md). +- Released Microsoft.Data.SqlClient.Extensions.Azure 1.0.0. See [release notes](release-notes/Extensions/Azure/1.0/1.0.0.md). +- Released Microsoft.Data.SqlClient.Internal.Logging 1.0.0. See [release notes](release-notes/Internal/Logging/1.0/1.0.0.md). +- Released Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 7.0.0. See [release notes](release-notes/add-ons/AzureKeyVaultProvider/7.0/7.0.0.md). + +### Changed + +- **Breaking:** Removed Azure dependencies from the core package. Entra ID authentication (`ActiveDirectoryAuthenticationProvider` and related types) has been extracted into a new `Microsoft.Data.SqlClient.Extensions.Azure` package. The core `Microsoft.Data.SqlClient` package no longer depends on `Azure.Core`, `Azure.Identity`, or their transitive dependencies. Applications using Entra ID authentication must now install `Microsoft.Data.SqlClient.Extensions.Azure` separately. + ([#1108](https://github.com/dotnet/SqlClient/issues/1108), + [#3680](https://github.com/dotnet/SqlClient/pull/3680), + [#3902](https://github.com/dotnet/SqlClient/pull/3902), + [#3904](https://github.com/dotnet/SqlClient/pull/3904), + [#3908](https://github.com/dotnet/SqlClient/pull/3908), + [#3917](https://github.com/dotnet/SqlClient/pull/3917), + [#3982](https://github.com/dotnet/SqlClient/pull/3982), + [#3978](https://github.com/dotnet/SqlClient/pull/3978), + [#3986](https://github.com/dotnet/SqlClient/pull/3986)) + +- Two additional packages were introduced to support this separation: `Microsoft.Data.SqlClient.Extensions.Abstractions` (shared types between the core driver and extensions) and `Microsoft.Data.SqlClient.Internal.Logging` (shared ETW tracing infrastructure). + ([#3626](https://github.com/dotnet/SqlClient/pull/3626), + [#3628](https://github.com/dotnet/SqlClient/pull/3628), + [#3967](https://github.com/dotnet/SqlClient/pull/3967), + [#4038](https://github.com/dotnet/SqlClient/pull/4038)) + +- Deprecated `SqlAuthenticationMethod.ActiveDirectoryPassword` (ROPC flow). The method is now marked `[Obsolete]` and will generate compiler warnings. Migrate to `ActiveDirectoryInteractive`, `ActiveDirectoryServicePrincipal`, `ActiveDirectoryManagedIdentity`, or `ActiveDirectoryDefault`. + ([#3671](https://github.com/dotnet/SqlClient/pull/3671)) + +- Reverted public visibility of internal interop enums (`IoControlCodeAccess` and `IoControlTransferType`) that were accidentally made public during the project merge. + ([#3900](https://github.com/dotnet/SqlClient/pull/3900)) + +- Removed `Constrained Execution Region` error handling blocks and associated `SqlConnection` cleanup. + ([#3535](https://github.com/dotnet/SqlClient/pull/3535)) + +- Performance improvements across SqlStatistics timing, Always Encrypted scenarios, and connection opening: + ([#3609](https://github.com/dotnet/SqlClient/pull/3609), + [#3612](https://github.com/dotnet/SqlClient/pull/3612), + [#3732](https://github.com/dotnet/SqlClient/pull/3732), + [#3660](https://github.com/dotnet/SqlClient/pull/3660), + [#3791](https://github.com/dotnet/SqlClient/pull/3791), + [#3772](https://github.com/dotnet/SqlClient/pull/3772), + [#3554](https://github.com/dotnet/SqlClient/pull/3554)) + +- Allow `SqlBulkCopy` to operate on hidden columns. + ([#3590](https://github.com/dotnet/SqlClient/pull/3590)) + +- Updated UserAgent feature to use a pipe-delimited format, replacing the previous JSON format. + ([#3826](https://github.com/dotnet/SqlClient/pull/3826)) + +- Minor improvements to Managed SNI tracing to capture continuation events and errors. + ([#3859](https://github.com/dotnet/SqlClient/pull/3859)) + +### Added + +- Added `SspiContextProvider` abstract class and `SqlConnection.SspiContextProvider` property, enabling custom SSPI authentication for scenarios like cross-domain Kerberos negotiation and NTLM username/password authentication. + ([#2253](https://github.com/dotnet/SqlClient/issues/2253), + [#2494](https://github.com/dotnet/SqlClient/pull/2494)) + +- Continued refinement of packet multiplexing with bug fixes and stability improvements, plus new app context switches for opt-in control. + ([#3534](https://github.com/dotnet/SqlClient/pull/3534), + [#3537](https://github.com/dotnet/SqlClient/pull/3537), + [#3605](https://github.com/dotnet/SqlClient/pull/3605)) + +- Added support for enhanced routing, a TDS feature that allows the server to redirect connections to a specific server and database during login, enabling Azure SQL Hyperscale read replica load balancing. + ([#3641](https://github.com/dotnet/SqlClient/issues/3641), + [#3969](https://github.com/dotnet/SqlClient/pull/3969), + [#3970](https://github.com/dotnet/SqlClient/pull/3970), + [#3973](https://github.com/dotnet/SqlClient/pull/3973)) + +- Updated pipelines and test suites to compile the driver using the .NET 10 SDK. + ([#3686](https://github.com/dotnet/SqlClient/pull/3686)) + +- Added `SqlConfigurableRetryFactory.BaselineTransientErrors` static property exposing the default transient error codes list as a `ReadOnlyCollection`. + ([#3903](https://github.com/dotnet/SqlClient/pull/3903)) + +- Added app context switch `Switch.Microsoft.Data.SqlClient.EnableMultiSubnetFailoverByDefault` to set `MultiSubnetFailover=true` globally without modifying connection strings. + ([#3841](https://github.com/dotnet/SqlClient/pull/3841)) + +- Added app context switch `Switch.Microsoft.Data.SqlClient.IgnoreServerProvidedFailoverPartner` to let the client ignore server-provided failover partner info in Basic Availability Groups. + ([#3625](https://github.com/dotnet/SqlClient/pull/3625)) + +- Enabled `SqlClientDiagnosticListener` for `SqlCommand` on .NET Framework, closing a long-standing observability gap where diagnostic events were previously only available on .NET Core. + ([#3658](https://github.com/dotnet/SqlClient/pull/3658)) + +- Brought the 15 strongly-typed diagnostic event classes in the `Microsoft.Data.SqlClient.Diagnostics` namespace (e.g., `SqlClientCommandBefore`, `SqlClientConnectionOpenAfter`, `SqlClientTransactionCommitError`) to .NET Framework as part of the codebase merge. These types were originally introduced for .NET Core in 6.0. + ([#3493](https://github.com/dotnet/SqlClient/pull/3493)) + +- Enabled User Agent Feature Extension (opt-in via `Switch.Microsoft.Data.SqlClient.EnableUserAgent`). + ([#3606](https://github.com/dotnet/SqlClient/pull/3606)) + +- Added actionable error message when Entra ID authentication methods are used without the `Microsoft.Data.SqlClient.Extensions.Azure` package installed. + ([#3962](https://github.com/dotnet/SqlClient/issues/3962), + [#4046](https://github.com/dotnet/SqlClient/pull/4046)) + +### Fixed + +- Fixed a connection performance regression where SPN generation was triggered for non-integrated authentication modes (e.g., SQL authentication) on the native SNI path. + ([#3929](https://github.com/dotnet/SqlClient/pull/3929)) + +- Fixed `ExecuteScalar` to propagate errors when the server sends data followed by an error token. + ([#3912](https://github.com/dotnet/SqlClient/pull/3912)) + +- Fixed `NullReferenceException` in `SqlDataAdapter` when processing batch scenarios. + ([#3857](https://github.com/dotnet/SqlClient/pull/3857)) + +- Fixed reading of multiple app context switches from a single `AppContextSwitchOverrides` configuration field. + ([#3960](https://github.com/dotnet/SqlClient/pull/3960)) + +- Fixed an edge case in `TdsParserStateObject.TryReadPlpBytes` where zero-length reads returned `null` instead of an empty array. + ([#3872](https://github.com/dotnet/SqlClient/pull/3872)) + +- Fixed issue where extra connection deactivation was occurring. + ([#3758](https://github.com/dotnet/SqlClient/pull/3758)) + +- Fixed debug assertion in connection pool (no impact to production code). + ([#3587](https://github.com/dotnet/SqlClient/pull/3587)) + +- Prevented uninitialized performance counters escaping `CreatePerformanceCounters`. + ([#3623](https://github.com/dotnet/SqlClient/pull/3623)) + +- Fixed `SetProvider` to return immediately if user-defined authentication provider found. + ([#3620](https://github.com/dotnet/SqlClient/pull/3620)) + +- Fixed connection pool concurrency issue. + ([#3632](https://github.com/dotnet/SqlClient/pull/3632)) + ## [Preview Release 7.0.0-preview4.26064.3] - 2026-03-05 This update brings the below changes over the previous preview release: diff --git a/Directory.Packages.props b/Directory.Packages.props index d043543e78..2d150a29a4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,6 +14,20 @@ + + @@ -21,7 +35,7 @@ We never reference this package via its project, so we always need a version specified. --> - + @@ -51,7 +65,7 @@ - + @@ -83,17 +97,19 @@ + - - + + + diff --git a/doc/apps/AzureSqlConnector/AzureSqlConnector.csproj b/doc/apps/AzureSqlConnector/AzureSqlConnector.csproj new file mode 100644 index 0000000000..0ea12bd7d2 --- /dev/null +++ b/doc/apps/AzureSqlConnector/AzureSqlConnector.csproj @@ -0,0 +1,34 @@ + + + + + net481;net10.0-windows + net10.0-windows + + true + WinExe + Microsoft.Data.SqlClient.Samples.AzureSqlConnector + AzureSqlConnector + true + latest + disable + AnyCPU + true + false + + + + + + + + diff --git a/doc/apps/AzureSqlConnector/IdentityQuery.cs b/doc/apps/AzureSqlConnector/IdentityQuery.cs new file mode 100644 index 0000000000..6c6cff84a1 --- /dev/null +++ b/doc/apps/AzureSqlConnector/IdentityQuery.cs @@ -0,0 +1,25 @@ +namespace Microsoft.Data.SqlClient.Samples.AzureSqlConnector +{ + /// + /// Shared SQL text used by both (UI-thread variant) and + /// (worker-thread variant). Keeping the literal in one place + /// avoids drift when one variant gains a new column. + /// + internal static class IdentityQuery + { + public const string CommandText = + "SELECT " + + " SUSER_SNAME() AS LoggedInUser, " + + " ORIGINAL_LOGIN() AS OriginalLogin, " + + " USER_NAME() AS DatabaseUser, " + + " SUSER_ID() AS LoginSid, " + + " DB_NAME() AS DatabaseName, " + + " @@SERVERNAME AS ServerName, " + + " HOST_NAME() AS ClientHost, " + + " APP_NAME() AS AppName, " + + " SESSION_USER AS SessionUser, " + + " CURRENT_USER AS CurrentUser, " + + " @@SPID AS SessionId, " + + " @@VERSION AS ServerVersion;"; + } +} diff --git a/doc/apps/AzureSqlConnector/MainForm.Designer.cs b/doc/apps/AzureSqlConnector/MainForm.Designer.cs new file mode 100644 index 0000000000..4dd6c5a047 --- /dev/null +++ b/doc/apps/AzureSqlConnector/MainForm.Designer.cs @@ -0,0 +1,394 @@ +namespace Microsoft.Data.SqlClient.Samples.AzureSqlConnector +{ + partial class MainForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.lblServer = new System.Windows.Forms.Label(); + this.txtServer = new System.Windows.Forms.TextBox(); + this.lblDatabase = new System.Windows.Forms.Label(); + this.txtDatabase = new System.Windows.Forms.TextBox(); + this.lblAuthentication = new System.Windows.Forms.Label(); + this.cmbAuthentication = new System.Windows.Forms.ComboBox(); + this.lblUserId = new System.Windows.Forms.Label(); + this.txtUserId = new System.Windows.Forms.TextBox(); + this.lblPassword = new System.Windows.Forms.Label(); + this.txtPassword = new System.Windows.Forms.TextBox(); + this.lblEncrypt = new System.Windows.Forms.Label(); + this.cmbEncrypt = new System.Windows.Forms.ComboBox(); + this.chkTrustServerCertificate = new System.Windows.Forms.CheckBox(); + this.lblTimeout = new System.Windows.Forms.Label(); + this.numTimeout = new System.Windows.Forms.NumericUpDown(); + this.lblOpenMode = new System.Windows.Forms.Label(); + this.cmbOpenMode = new System.Windows.Forms.ComboBox(); + this.lblConnectionString = new System.Windows.Forms.Label(); + this.txtConnectionString = new System.Windows.Forms.TextBox(); + this.btnBuild = new System.Windows.Forms.Button(); + this.btnTest = new System.Windows.Forms.Button(); + this.btnCopy = new System.Windows.Forms.Button(); + this.btnClear = new System.Windows.Forms.Button(); + this.btnWhoAmI = new System.Windows.Forms.Button(); + this.lblStatus = new System.Windows.Forms.Label(); + this.txtStatus = new System.Windows.Forms.TextBox(); + this.statusStrip = new System.Windows.Forms.StatusStrip(); + this.statusLabel = new System.Windows.Forms.ToolStripStatusLabel(); + ((System.ComponentModel.ISupportInitialize)(this.numTimeout)).BeginInit(); + this.statusStrip.SuspendLayout(); + this.SuspendLayout(); + // + // lblServer + // + this.lblServer.AutoSize = true; + this.lblServer.Location = new System.Drawing.Point(16, 18); + this.lblServer.Name = "lblServer"; + this.lblServer.Size = new System.Drawing.Size(75, 13); + this.lblServer.TabIndex = 0; + this.lblServer.Text = "&Server name:"; + // + // txtServer + // + this.txtServer.Location = new System.Drawing.Point(150, 15); + this.txtServer.Name = "txtServer"; + this.txtServer.Size = new System.Drawing.Size(400, 20); + this.txtServer.TabIndex = 1; + // + // lblDatabase + // + this.lblDatabase.AutoSize = true; + this.lblDatabase.Location = new System.Drawing.Point(16, 48); + this.lblDatabase.Name = "lblDatabase"; + this.lblDatabase.Size = new System.Drawing.Size(86, 13); + this.lblDatabase.TabIndex = 2; + this.lblDatabase.Text = "&Database name:"; + // + // txtDatabase + // + this.txtDatabase.Location = new System.Drawing.Point(150, 45); + this.txtDatabase.Name = "txtDatabase"; + this.txtDatabase.Size = new System.Drawing.Size(400, 20); + this.txtDatabase.TabIndex = 3; + // + // lblAuthentication + // + this.lblAuthentication.AutoSize = true; + this.lblAuthentication.Location = new System.Drawing.Point(16, 78); + this.lblAuthentication.Name = "lblAuthentication"; + this.lblAuthentication.Size = new System.Drawing.Size(80, 13); + this.lblAuthentication.TabIndex = 4; + this.lblAuthentication.Text = "&Authentication:"; + // + // cmbAuthentication + // + this.cmbAuthentication.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cmbAuthentication.FormattingEnabled = true; + this.cmbAuthentication.Location = new System.Drawing.Point(150, 75); + this.cmbAuthentication.Name = "cmbAuthentication"; + this.cmbAuthentication.Size = new System.Drawing.Size(400, 21); + this.cmbAuthentication.TabIndex = 5; + this.cmbAuthentication.SelectedIndexChanged += new System.EventHandler(this.cmbAuthentication_SelectedIndexChanged); + // + // lblUserId + // + this.lblUserId.AutoSize = true; + this.lblUserId.Location = new System.Drawing.Point(16, 108); + this.lblUserId.Name = "lblUserId"; + this.lblUserId.Size = new System.Drawing.Size(45, 13); + this.lblUserId.TabIndex = 6; + this.lblUserId.Text = "&User ID:"; + // + // txtUserId + // + this.txtUserId.Location = new System.Drawing.Point(150, 105); + this.txtUserId.Name = "txtUserId"; + this.txtUserId.Size = new System.Drawing.Size(400, 20); + this.txtUserId.TabIndex = 7; + // + // lblPassword + // + this.lblPassword.AutoSize = true; + this.lblPassword.Location = new System.Drawing.Point(16, 138); + this.lblPassword.Name = "lblPassword"; + this.lblPassword.Size = new System.Drawing.Size(56, 13); + this.lblPassword.TabIndex = 8; + this.lblPassword.Text = "&Password:"; + // + // txtPassword + // + this.txtPassword.Location = new System.Drawing.Point(150, 135); + this.txtPassword.Name = "txtPassword"; + this.txtPassword.Size = new System.Drawing.Size(400, 20); + this.txtPassword.TabIndex = 9; + this.txtPassword.UseSystemPasswordChar = true; + // + // lblEncrypt + // + this.lblEncrypt.AutoSize = true; + this.lblEncrypt.Location = new System.Drawing.Point(16, 168); + this.lblEncrypt.Name = "lblEncrypt"; + this.lblEncrypt.Size = new System.Drawing.Size(46, 13); + this.lblEncrypt.TabIndex = 10; + this.lblEncrypt.Text = "&Encrypt:"; + // + // cmbEncrypt + // + this.cmbEncrypt.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cmbEncrypt.FormattingEnabled = true; + this.cmbEncrypt.Location = new System.Drawing.Point(150, 165); + this.cmbEncrypt.Name = "cmbEncrypt"; + this.cmbEncrypt.Size = new System.Drawing.Size(200, 21); + this.cmbEncrypt.TabIndex = 11; + // + // chkTrustServerCertificate + // + this.chkTrustServerCertificate.AutoSize = true; + this.chkTrustServerCertificate.Location = new System.Drawing.Point(370, 167); + this.chkTrustServerCertificate.Name = "chkTrustServerCertificate"; + this.chkTrustServerCertificate.Size = new System.Drawing.Size(149, 17); + this.chkTrustServerCertificate.TabIndex = 12; + this.chkTrustServerCertificate.Text = "&Trust server certificate"; + this.chkTrustServerCertificate.UseVisualStyleBackColor = true; + // + // lblTimeout + // + this.lblTimeout.AutoSize = true; + this.lblTimeout.Location = new System.Drawing.Point(16, 198); + this.lblTimeout.Name = "lblTimeout"; + this.lblTimeout.Size = new System.Drawing.Size(101, 13); + this.lblTimeout.TabIndex = 13; + this.lblTimeout.Text = "Connect timeout (s):"; + // + // numTimeout + // + this.numTimeout.Location = new System.Drawing.Point(150, 196); + this.numTimeout.Maximum = new decimal(new int[] { 600, 0, 0, 0 }); + this.numTimeout.Minimum = new decimal(new int[] { 1, 0, 0, 0 }); + this.numTimeout.Name = "numTimeout"; + this.numTimeout.Size = new System.Drawing.Size(80, 20); + this.numTimeout.TabIndex = 14; + this.numTimeout.Value = new decimal(new int[] { 30, 0, 0, 0 }); + // + // lblOpenMode + // + this.lblOpenMode.AutoSize = true; + this.lblOpenMode.Location = new System.Drawing.Point(260, 198); + this.lblOpenMode.Name = "lblOpenMode"; + this.lblOpenMode.Size = new System.Drawing.Size(67, 13); + this.lblOpenMode.TabIndex = 25; + this.lblOpenMode.Text = "&Open mode:"; + // + // cmbOpenMode + // + this.cmbOpenMode.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cmbOpenMode.FormattingEnabled = true; + this.cmbOpenMode.Location = new System.Drawing.Point(350, 195); + this.cmbOpenMode.Name = "cmbOpenMode"; + this.cmbOpenMode.Size = new System.Drawing.Size(200, 21); + this.cmbOpenMode.TabIndex = 26; + // + // lblConnectionString + // + this.lblConnectionString.AutoSize = true; + this.lblConnectionString.Location = new System.Drawing.Point(16, 230); + this.lblConnectionString.Name = "lblConnectionString"; + this.lblConnectionString.Size = new System.Drawing.Size(94, 13); + this.lblConnectionString.TabIndex = 15; + this.lblConnectionString.Text = "Connection string:"; + // + // txtConnectionString + // + this.txtConnectionString.Location = new System.Drawing.Point(16, 246); + this.txtConnectionString.Multiline = true; + this.txtConnectionString.Name = "txtConnectionString"; + this.txtConnectionString.ReadOnly = true; + this.txtConnectionString.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; + this.txtConnectionString.Size = new System.Drawing.Size(534, 60); + this.txtConnectionString.TabIndex = 16; + this.txtConnectionString.BackColor = System.Drawing.SystemColors.Info; + // + // btnBuild + // + this.btnBuild.Location = new System.Drawing.Point(16, 316); + this.btnBuild.Name = "btnBuild"; + this.btnBuild.Size = new System.Drawing.Size(140, 26); + this.btnBuild.TabIndex = 17; + this.btnBuild.Text = "&Build Connection String"; + this.btnBuild.UseVisualStyleBackColor = true; + this.btnBuild.Click += new System.EventHandler(this.btnBuild_Click); + // + // btnTest + // + this.btnTest.Location = new System.Drawing.Point(166, 316); + this.btnTest.Name = "btnTest"; + this.btnTest.Size = new System.Drawing.Size(120, 26); + this.btnTest.TabIndex = 18; + this.btnTest.Text = "Te&st Connection"; + this.btnTest.UseVisualStyleBackColor = true; + this.btnTest.Click += new System.EventHandler(this.btnTest_Click); + // + // btnCopy + // + this.btnCopy.Location = new System.Drawing.Point(296, 316); + this.btnCopy.Name = "btnCopy"; + this.btnCopy.Size = new System.Drawing.Size(120, 26); + this.btnCopy.TabIndex = 19; + this.btnCopy.Text = "Cop&y to Clipboard"; + this.btnCopy.UseVisualStyleBackColor = true; + this.btnCopy.Click += new System.EventHandler(this.btnCopy_Click); + // + // btnClear + // + this.btnClear.Location = new System.Drawing.Point(426, 316); + this.btnClear.Name = "btnClear"; + this.btnClear.Size = new System.Drawing.Size(124, 26); + this.btnClear.TabIndex = 20; + this.btnClear.Text = "Cl&ear All"; + this.btnClear.UseVisualStyleBackColor = true; + this.btnClear.Click += new System.EventHandler(this.btnClear_Click); + // + // btnWhoAmI + // + this.btnWhoAmI.Location = new System.Drawing.Point(16, 348); + this.btnWhoAmI.Name = "btnWhoAmI"; + this.btnWhoAmI.Size = new System.Drawing.Size(534, 26); + this.btnWhoAmI.TabIndex = 21; + this.btnWhoAmI.Text = "&Who Am I? (run identity query on the database)"; + this.btnWhoAmI.UseVisualStyleBackColor = true; + this.btnWhoAmI.Click += new System.EventHandler(this.btnWhoAmI_Click); + // + // lblStatus + // + this.lblStatus.AutoSize = true; + this.lblStatus.Location = new System.Drawing.Point(16, 386); + this.lblStatus.Name = "lblStatus"; + this.lblStatus.Size = new System.Drawing.Size(40, 13); + this.lblStatus.TabIndex = 22; + this.lblStatus.Text = "Result:"; + // + // txtStatus + // + this.txtStatus.Location = new System.Drawing.Point(16, 402); + this.txtStatus.Multiline = true; + this.txtStatus.Name = "txtStatus"; + this.txtStatus.ReadOnly = true; + this.txtStatus.ScrollBars = System.Windows.Forms.ScrollBars.Both; + this.txtStatus.Size = new System.Drawing.Size(534, 160); + this.txtStatus.TabIndex = 23; + this.txtStatus.WordWrap = false; + this.txtStatus.Font = new System.Drawing.Font("Consolas", 9F); + // + // statusStrip + // + this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.statusLabel}); + this.statusStrip.Location = new System.Drawing.Point(0, 578); + this.statusStrip.Name = "statusStrip"; + this.statusStrip.Size = new System.Drawing.Size(566, 22); + this.statusStrip.TabIndex = 24; + // + // statusLabel + // + this.statusLabel.Name = "statusLabel"; + this.statusLabel.Size = new System.Drawing.Size(39, 17); + this.statusLabel.Text = "Ready"; + // + // MainForm + // + this.AcceptButton = this.btnTest; + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(566, 600); + this.Controls.Add(this.statusStrip); + this.Controls.Add(this.txtStatus); + this.Controls.Add(this.lblStatus); + this.Controls.Add(this.btnWhoAmI); + this.Controls.Add(this.btnClear); + this.Controls.Add(this.btnCopy); + this.Controls.Add(this.btnTest); + this.Controls.Add(this.btnBuild); + this.Controls.Add(this.txtConnectionString); + this.Controls.Add(this.lblConnectionString); + this.Controls.Add(this.cmbOpenMode); + this.Controls.Add(this.lblOpenMode); + this.Controls.Add(this.numTimeout); + this.Controls.Add(this.lblTimeout); + this.Controls.Add(this.chkTrustServerCertificate); + this.Controls.Add(this.cmbEncrypt); + this.Controls.Add(this.lblEncrypt); + this.Controls.Add(this.txtPassword); + this.Controls.Add(this.lblPassword); + this.Controls.Add(this.txtUserId); + this.Controls.Add(this.lblUserId); + this.Controls.Add(this.cmbAuthentication); + this.Controls.Add(this.lblAuthentication); + this.Controls.Add(this.txtDatabase); + this.Controls.Add(this.lblDatabase); + this.Controls.Add(this.txtServer); + this.Controls.Add(this.lblServer); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.MaximizeBox = false; + this.Name = "MainForm"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "Azure SQL Connector"; + ((System.ComponentModel.ISupportInitialize)(this.numTimeout)).EndInit(); + this.statusStrip.ResumeLayout(false); + this.statusStrip.PerformLayout(); + this.ResumeLayout(false); + this.PerformLayout(); + } + + #endregion + + private System.Windows.Forms.Label lblServer; + private System.Windows.Forms.TextBox txtServer; + private System.Windows.Forms.Label lblDatabase; + private System.Windows.Forms.TextBox txtDatabase; + private System.Windows.Forms.Label lblAuthentication; + private System.Windows.Forms.ComboBox cmbAuthentication; + private System.Windows.Forms.Label lblUserId; + private System.Windows.Forms.TextBox txtUserId; + private System.Windows.Forms.Label lblPassword; + private System.Windows.Forms.TextBox txtPassword; + private System.Windows.Forms.Label lblEncrypt; + private System.Windows.Forms.ComboBox cmbEncrypt; + private System.Windows.Forms.CheckBox chkTrustServerCertificate; + private System.Windows.Forms.Label lblTimeout; + private System.Windows.Forms.NumericUpDown numTimeout; + private System.Windows.Forms.Label lblOpenMode; + private System.Windows.Forms.ComboBox cmbOpenMode; + private System.Windows.Forms.Label lblConnectionString; + private System.Windows.Forms.TextBox txtConnectionString; + private System.Windows.Forms.Button btnBuild; + private System.Windows.Forms.Button btnTest; + private System.Windows.Forms.Button btnCopy; + private System.Windows.Forms.Button btnClear; + private System.Windows.Forms.Button btnWhoAmI; + private System.Windows.Forms.Label lblStatus; + private System.Windows.Forms.TextBox txtStatus; + private System.Windows.Forms.StatusStrip statusStrip; + private System.Windows.Forms.ToolStripStatusLabel statusLabel; + } +} diff --git a/doc/apps/AzureSqlConnector/MainForm.cs b/doc/apps/AzureSqlConnector/MainForm.cs new file mode 100644 index 0000000000..9cc6d0648c --- /dev/null +++ b/doc/apps/AzureSqlConnector/MainForm.cs @@ -0,0 +1,590 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Windows.Forms; +using Microsoft.Data.SqlClient; +using Microsoft.Identity.Client; + +namespace Microsoft.Data.SqlClient.Samples.AzureSqlConnector +{ + /// + /// "UI-thread" variant of the connector form. Opens the SQL connection via + /// on the UI thread; the WinForms + /// keeps the message pump alive while + /// the async I/O completes, so the form remains responsive and MSAL.NET's embedded sign-in + /// browser (for ActiveDirectoryInteractive) parents itself correctly. + /// + public partial class MainForm : Form + { + // ────────────────────────────────────────────────────────────────── + #region Construction + + public MainForm() + { + InitializeComponent(); + this.Text = "Azure SQL Connector — UI thread"; + PopulateAuthenticationMethods(); + PopulateEncryptOptions(); + PopulateOpenModes(); + UpdateCredentialFieldsAvailability(); + + // Force the underlying Win32 window to be created NOW (on the UI thread) so we can + // safely hand its HWND to MSAL later. Even in async mode, MSAL.NET may invoke the + // parent-window callback from a worker thread (e.g. when the driver blocks on a + // synchronous Open()), and touching Form.Handle from a non-UI thread throws + // InvalidOperationException ("Cross-thread operation not valid"). + _ownerHwnd = this.Handle; + + RegisterActiveDirectoryProvider(); + } + + #endregion + + // ────────────────────────────────────────────────────────────────── + #region UI Initialization + + private void PopulateAuthenticationMethods() + { + foreach (SqlAuthenticationMethod method in Enum.GetValues(typeof(SqlAuthenticationMethod))) + { + cmbAuthentication.Items.Add(method); + } + + cmbAuthentication.SelectedItem = SqlAuthenticationMethod.SqlPassword; + } + + private void PopulateEncryptOptions() + { + cmbEncrypt.Items.Add(EncryptDisplay.Mandatory); + cmbEncrypt.Items.Add(EncryptDisplay.Optional); + cmbEncrypt.Items.Add(EncryptDisplay.Strict); + cmbEncrypt.SelectedIndex = 0; + } + + private void PopulateOpenModes() + { + cmbOpenMode.Items.Add(OpenModeDisplay.Async); + cmbOpenMode.Items.Add(OpenModeDisplay.Sync); + cmbOpenMode.SelectedIndex = 0; + } + + /// + /// Registers a single for every + /// Entra ID authentication method and gives it the form's captured HWND as the parent + /// window owner. Both callbacks intentionally use the HWND captured in the constructor + /// () rather than this.Handle, because MSAL.NET can invoke + /// them from a worker thread (e.g. when the driver blocks on a synchronous Open() + /// or when its internal continuations resume off-UI). + /// + private void RegisterActiveDirectoryProvider() + { + ActiveDirectoryAuthenticationProvider provider = new ActiveDirectoryAuthenticationProvider(); + IntPtr ownerHwnd = _ownerHwnd; + +#if NETFRAMEWORK + // .NET Framework: parent the embedded WebView via the legacy IWin32Window API. + provider.SetIWin32WindowFunc(() => new Win32WindowHandle(ownerHwnd)); +#endif + + // Modern API: works on both .NET Framework and .NET 8+, and is the one MSAL's WAM + // broker consults on Windows. + provider.SetParentActivityOrWindowFunc(() => ownerHwnd); + + // Without this, MSAL's default device-code callback writes the prompt to + // Console.WriteLine, which is invisible in a WinForms host — the connection + // appears to hang while MSAL polls for a code the user never sees. + provider.SetDeviceCodeFlowCallback(DeviceCodeFlowCallback); + + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryIntegrated, provider); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, provider); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal, provider); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, provider); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, provider); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryMSI, provider); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDefault, provider); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity, provider); + #pragma warning disable CS0618 // Type or member is obsolete + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, provider); + #pragma warning restore CS0618 // Type or member is obsolete + } + + /// + /// Device Code Flow callback. MSAL invokes this on a worker thread before it begins + /// polling the token endpoint. We surface the user code three ways so the user always + /// sees it: (1) appended to the log textbox via BeginInvoke (works whenever the UI + /// thread is pumping — async OpenAsync), (2) the verification URL launched in + /// the default browser, and (3) a modal owned by the MSAL worker thread (works even + /// when the UI thread is blocked by a synchronous Open()). MSAL polling waits + /// for the returned Task to complete, so dismissing the dialog also resumes polling. + /// + private Task DeviceCodeFlowCallback(DeviceCodeResult result) + { + string message = result.Message; + string url = result.VerificationUrl; + string code = result.UserCode; + + if (IsHandleCreated) + { + try + { + BeginInvoke((Action)(() => + { + AppendStatus(string.Empty); + AppendStatus("=== Device Code Flow ==="); + AppendStatus(message); + })); + } + catch (InvalidOperationException) + { + // Form is closing or handle was destroyed; fall through to the modal. + } + } + + try + { + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + } + catch + { + // Best-effort; the modal below still shows the URL and code. + } + + MessageBox.Show( + "Sign in to complete Device Code Flow:" + Environment.NewLine + Environment.NewLine + + " URL : " + url + Environment.NewLine + + " Code: " + code + Environment.NewLine + Environment.NewLine + + "A browser window has been opened. Enter the code above, complete sign-in," + + Environment.NewLine + "then click OK to resume the connection.", + "Device Code Flow", + MessageBoxButtons.OK, + MessageBoxIcon.Information); + + return Task.CompletedTask; + } + + #endregion + + // ────────────────────────────────────────────────────────────────── + #region Event Handlers + + private void cmbAuthentication_SelectedIndexChanged(object sender, EventArgs e) + { + UpdateCredentialFieldsAvailability(); + } + + private void btnBuild_Click(object sender, EventArgs e) + { + try + { + SqlConnectionStringBuilder builder = BuildConnectionString(); + txtConnectionString.Text = MaskPassword(builder); + SetStatus("Connection string built successfully.", isError: false); + AppendStatus("Connection string built:\r\n" + MaskPassword(builder)); + } + catch (Exception ex) + { + txtConnectionString.Text = string.Empty; + SetStatus("Failed to build connection string.", isError: true); + AppendStatus("ERROR: " + ex.Message); + } + } + + private async void btnTest_Click(object sender, EventArgs e) + { + SqlConnectionStringBuilder builder; + try + { + builder = BuildConnectionString(); + txtConnectionString.Text = MaskPassword(builder); + } + catch (Exception ex) + { + SetStatus("Failed to build connection string.", isError: true); + AppendStatus("ERROR: " + ex.Message); + return; + } + + bool useAsync = IsAsyncOpenSelected(); + SetBusy(true, useAsync ? "Testing connection (OpenAsync)..." : "Testing connection (Open)..."); + AppendStatus(string.Empty); + AppendStatus("Testing connectivity to " + builder.DataSource + " (" + + (useAsync ? "OpenAsync" : "sync Open") + ") ..."); + + try + { + string serverVersion; + using (SqlConnection connection = new SqlConnection(builder.ConnectionString)) + { + await OpenConnectionAsync(connection, useAsync).ConfigureAwait(true); + serverVersion = connection.ServerVersion; + } + + SetStatus("Connected successfully.", isError: false); + AppendStatus("Connected successfully! Server version: " + serverVersion); + } + catch (SqlException ex) + { + SetStatus("Connection failed (SqlException).", isError: true); + AppendStatus("SqlException [" + ex.Number + "]: " + ex.Message + "\r\n" + ex.StackTrace); + } + catch (Exception ex) + { + SetStatus("Connection failed.", isError: true); + AppendStatus(ex.GetType().Name + ": " + ex.Message + "\r\n" + ex.StackTrace); + } + finally + { + SetBusy(false, null); + } + } + + private async void btnWhoAmI_Click(object sender, EventArgs e) + { + SqlConnectionStringBuilder builder; + try + { + builder = BuildConnectionString(); + txtConnectionString.Text = MaskPassword(builder); + } + catch (Exception ex) + { + SetStatus("Failed to build connection string.", isError: true); + AppendStatus("ERROR: " + ex.Message); + return; + } + + bool useAsync = IsAsyncOpenSelected(); + SetBusy(true, useAsync + ? "Querying logged-in identity (OpenAsync)..." + : "Querying logged-in identity (Open)..."); + AppendStatus(string.Empty); + AppendStatus("Running identity query against " + builder.DataSource + " (" + + (useAsync ? "OpenAsync" : "sync Open") + ") ..."); + + try + { + // Same UI-thread reasoning as btnTest_Click — keep the message pump alive for any + // ActiveDirectoryInteractive sign-in that may be required. + using (SqlConnection connection = new SqlConnection(builder.ConnectionString)) + { + await OpenConnectionAsync(connection, useAsync).ConfigureAwait(true); + + using (SqlCommand command = connection.CreateCommand()) + { + command.CommandText = IdentityQuery.CommandText; + + using (SqlDataReader reader = await command.ExecuteReaderAsync().ConfigureAwait(true)) + { + if (await reader.ReadAsync().ConfigureAwait(true)) + { + AppendStatus("Identity:"); + for (int i = 0; i < reader.FieldCount; i++) + { + string name = reader.GetName(i); + object value = reader.IsDBNull(i) ? "(null)" : reader.GetValue(i); + AppendStatus(" " + name.PadRight(16) + ": " + value); + } + SetStatus("Identity query succeeded.", isError: false); + } + else + { + SetStatus("Identity query returned no rows.", isError: true); + AppendStatus("(no rows returned)"); + } + } + } + } + } + catch (SqlException ex) + { + SetStatus("Identity query failed (SqlException).", isError: true); + AppendStatus("SqlException [" + ex.Number + "]: " + ex.Message); + } + catch (Exception ex) + { + SetStatus("Identity query failed.", isError: true); + AppendStatus(ex.GetType().Name + ": " + ex.Message); + } + finally + { + SetBusy(false, null); + } + } + + private void btnCopy_Click(object sender, EventArgs e) + { + if (string.IsNullOrEmpty(txtConnectionString.Text)) + { + SetStatus("Nothing to copy. Build the connection string first.", isError: true); + return; + } + + try + { + Clipboard.SetText(BuildConnectionString().ConnectionString); + SetStatus("Connection string copied to clipboard.", isError: false); + } + catch (Exception ex) + { + SetStatus("Failed to copy to clipboard.", isError: true); + AppendStatus("ERROR: " + ex.Message); + } + } + + private void btnClear_Click(object sender, EventArgs e) + { + txtServer.Clear(); + txtDatabase.Clear(); + txtUserId.Clear(); + txtPassword.Clear(); + txtConnectionString.Clear(); + txtStatus.Clear(); + cmbAuthentication.SelectedItem = SqlAuthenticationMethod.SqlPassword; + cmbEncrypt.SelectedIndex = 0; + cmbOpenMode.SelectedIndex = 0; + chkTrustServerCertificate.Checked = false; + numTimeout.Value = 30; + SetStatus("Ready", isError: false); + } + + #endregion + + // ────────────────────────────────────────────────────────────────── + #region Connection String Construction + + private SqlConnectionStringBuilder BuildConnectionString() + { + string server = (txtServer.Text ?? string.Empty).Trim(); + if (string.IsNullOrEmpty(server)) + { + throw new InvalidOperationException("Server name is required."); + } + + SqlAuthenticationMethod authMethod = (SqlAuthenticationMethod)cmbAuthentication.SelectedItem; + + SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder + { + DataSource = server, + ConnectTimeout = (int)numTimeout.Value, + }; + + string database = (txtDatabase.Text ?? string.Empty).Trim(); + if (!string.IsNullOrEmpty(database)) + { + builder.InitialCatalog = database; + } + + if (authMethod != SqlAuthenticationMethod.NotSpecified) + { + builder.Authentication = authMethod; + } + + if (RequiresUserAndPassword(authMethod)) + { + string userId = (txtUserId.Text ?? string.Empty).Trim(); + if (string.IsNullOrEmpty(userId)) + { + throw new InvalidOperationException( + "User ID is required for " + authMethod + " authentication."); + } + + builder.UserID = userId; + builder.Password = txtPassword.Text ?? string.Empty; + } + else if (authMethod == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal + || authMethod == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity + || authMethod == SqlAuthenticationMethod.ActiveDirectoryMSI + || authMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive + || authMethod == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow + || authMethod == SqlAuthenticationMethod.ActiveDirectoryDefault + || authMethod == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity) + { + string userId = (txtUserId.Text ?? string.Empty).Trim(); + if (!string.IsNullOrEmpty(userId)) + { + builder.UserID = userId; + } + + if (authMethod == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal + && !string.IsNullOrEmpty(txtPassword.Text)) + { + builder.Password = txtPassword.Text; + } + } + + string encryptValue = cmbEncrypt.SelectedItem as string ?? EncryptDisplay.Mandatory; + switch (encryptValue) + { + case EncryptDisplay.Mandatory: + builder.Encrypt = SqlConnectionEncryptOption.Mandatory; + break; + case EncryptDisplay.Optional: + builder.Encrypt = SqlConnectionEncryptOption.Optional; + break; + case EncryptDisplay.Strict: + builder.Encrypt = SqlConnectionEncryptOption.Strict; + break; + } + + builder.TrustServerCertificate = chkTrustServerCertificate.Checked; + + return builder; + } + + private static bool RequiresUserAndPassword(SqlAuthenticationMethod method) + { + switch (method) + { + case SqlAuthenticationMethod.SqlPassword: +#pragma warning disable CS0618 // Type or member is obsolete + case SqlAuthenticationMethod.ActiveDirectoryPassword: +#pragma warning restore CS0618 + return true; + default: + return false; + } + } + + private static string MaskPassword(SqlConnectionStringBuilder builder) + { + if (string.IsNullOrEmpty(builder.Password)) + { + return builder.ConnectionString; + } + + SqlConnectionStringBuilder copy = new SqlConnectionStringBuilder(builder.ConnectionString) + { + Password = "********", + }; + return copy.ConnectionString; + } + + /// + /// Returns when the user picked Async (OpenAsync) in the + /// open-mode selector. Defaults to async if the selector has not been initialized yet. + /// + private bool IsAsyncOpenSelected() + { + return cmbOpenMode.SelectedItem as string != OpenModeDisplay.Sync; + } + + /// + /// Opens on the calling thread using either + /// or the synchronous + /// based on . The method itself is always async-returning so + /// callers can await uniformly; for the sync case it runs Open() inline on + /// the UI thread (which is supported with WAM broker because the broker dialog is hosted + /// by a separate process and does not need this thread's message pump). + /// + private static Task OpenConnectionAsync(SqlConnection connection, bool useAsync) + { + if (useAsync) + { + return connection.OpenAsync(); + } + + connection.Open(); + return Task.CompletedTask; + } + + #endregion + + // ────────────────────────────────────────────────────────────────── + #region UI Helpers + + private void UpdateCredentialFieldsAvailability() + { + if (cmbAuthentication.SelectedItem == null) + { + return; + } + + SqlAuthenticationMethod method = (SqlAuthenticationMethod)cmbAuthentication.SelectedItem; + + bool userEnabled = method != SqlAuthenticationMethod.ActiveDirectoryIntegrated; + bool passwordEnabled = RequiresUserAndPassword(method) + || method == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal; + + txtUserId.Enabled = userEnabled; + txtPassword.Enabled = passwordEnabled; + + if (!passwordEnabled) + { + txtPassword.Clear(); + } + } + + private void SetStatus(string text, bool isError) + { + statusLabel.Text = text; + statusLabel.ForeColor = isError ? System.Drawing.Color.Firebrick : System.Drawing.Color.Black; + } + + private void AppendStatus(string line) + { + if (txtStatus.TextLength > 0) + { + txtStatus.AppendText(Environment.NewLine); + } + txtStatus.AppendText(line ?? string.Empty); + } + + private void SetBusy(bool busy, string statusText) + { + btnBuild.Enabled = !busy; + btnTest.Enabled = !busy; + btnCopy.Enabled = !busy; + btnClear.Enabled = !busy; + btnWhoAmI.Enabled = !busy; + cmbOpenMode.Enabled = !busy; + Cursor = busy ? Cursors.WaitCursor : Cursors.Default; + + if (statusText != null) + { + SetStatus(statusText, isError: false); + } + } + + #endregion + + // ────────────────────────────────────────────────────────────────── + #region Nested Types + + private static class EncryptDisplay + { + public const string Mandatory = "Mandatory"; + public const string Optional = "Optional"; + public const string Strict = "Strict"; + } + + private static class OpenModeDisplay + { + public const string Async = "Async (OpenAsync)"; + public const string Sync = "Sync (Open)"; + } + +#if NETFRAMEWORK + // Tiny IWin32Window wrapper around a raw HWND captured on the UI thread so MSAL.NET's + // legacy IWin32WindowFunc callback can safely return a window owner from a worker thread + // without ever touching Control.Handle off-UI. + private sealed class Win32WindowHandle : IWin32Window + { + private readonly IntPtr _hwnd; + public Win32WindowHandle(IntPtr hwnd) => _hwnd = hwnd; + public IntPtr Handle => _hwnd; + } +#endif + + #endregion + + // ─────────────────────────────────────────────────────────────── + #region Private Fields + + // The form's Win32 window handle, captured on the UI thread in the constructor. + // Read from worker threads by the Entra ID provider callbacks to parent MSAL's sign-in + // / WAM broker UI without illegally touching Control.Handle. + private readonly IntPtr _ownerHwnd; + + #endregion + } +} diff --git a/doc/apps/AzureSqlConnector/MainFormWorker.Designer.cs b/doc/apps/AzureSqlConnector/MainFormWorker.Designer.cs new file mode 100644 index 0000000000..c872ee38ab --- /dev/null +++ b/doc/apps/AzureSqlConnector/MainFormWorker.Designer.cs @@ -0,0 +1,383 @@ +namespace Microsoft.Data.SqlClient.Samples.AzureSqlConnector +{ + partial class MainFormWorker + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.lblServer = new System.Windows.Forms.Label(); + this.txtServer = new System.Windows.Forms.TextBox(); + this.lblDatabase = new System.Windows.Forms.Label(); + this.txtDatabase = new System.Windows.Forms.TextBox(); + this.lblAuthentication = new System.Windows.Forms.Label(); + this.cmbAuthentication = new System.Windows.Forms.ComboBox(); + this.lblUserId = new System.Windows.Forms.Label(); + this.txtUserId = new System.Windows.Forms.TextBox(); + this.lblPassword = new System.Windows.Forms.Label(); + this.txtPassword = new System.Windows.Forms.TextBox(); + this.lblEncrypt = new System.Windows.Forms.Label(); + this.cmbEncrypt = new System.Windows.Forms.ComboBox(); + this.chkTrustServerCertificate = new System.Windows.Forms.CheckBox(); + this.lblTimeout = new System.Windows.Forms.Label(); + this.numTimeout = new System.Windows.Forms.NumericUpDown(); + this.chkClearTokenCache = new System.Windows.Forms.CheckBox(); + this.lblConnectionString = new System.Windows.Forms.Label(); + this.txtConnectionString = new System.Windows.Forms.TextBox(); + this.btnBuild = new System.Windows.Forms.Button(); + this.btnTest = new System.Windows.Forms.Button(); + this.btnCopy = new System.Windows.Forms.Button(); + this.btnClear = new System.Windows.Forms.Button(); + this.btnWhoAmI = new System.Windows.Forms.Button(); + this.lblStatus = new System.Windows.Forms.Label(); + this.txtStatus = new System.Windows.Forms.TextBox(); + this.statusStrip = new System.Windows.Forms.StatusStrip(); + this.statusLabel = new System.Windows.Forms.ToolStripStatusLabel(); + ((System.ComponentModel.ISupportInitialize)(this.numTimeout)).BeginInit(); + this.statusStrip.SuspendLayout(); + this.SuspendLayout(); + // + // lblServer + // + this.lblServer.AutoSize = true; + this.lblServer.Location = new System.Drawing.Point(16, 18); + this.lblServer.Name = "lblServer"; + this.lblServer.Size = new System.Drawing.Size(75, 13); + this.lblServer.TabIndex = 0; + this.lblServer.Text = "&Server name:"; + // + // txtServer + // + this.txtServer.Location = new System.Drawing.Point(150, 15); + this.txtServer.Name = "txtServer"; + this.txtServer.Size = new System.Drawing.Size(400, 20); + this.txtServer.TabIndex = 1; + // + // lblDatabase + // + this.lblDatabase.AutoSize = true; + this.lblDatabase.Location = new System.Drawing.Point(16, 48); + this.lblDatabase.Name = "lblDatabase"; + this.lblDatabase.Size = new System.Drawing.Size(86, 13); + this.lblDatabase.TabIndex = 2; + this.lblDatabase.Text = "&Database name:"; + // + // txtDatabase + // + this.txtDatabase.Location = new System.Drawing.Point(150, 45); + this.txtDatabase.Name = "txtDatabase"; + this.txtDatabase.Size = new System.Drawing.Size(400, 20); + this.txtDatabase.TabIndex = 3; + // + // lblAuthentication + // + this.lblAuthentication.AutoSize = true; + this.lblAuthentication.Location = new System.Drawing.Point(16, 78); + this.lblAuthentication.Name = "lblAuthentication"; + this.lblAuthentication.Size = new System.Drawing.Size(80, 13); + this.lblAuthentication.TabIndex = 4; + this.lblAuthentication.Text = "&Authentication:"; + // + // cmbAuthentication + // + this.cmbAuthentication.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cmbAuthentication.FormattingEnabled = true; + this.cmbAuthentication.Location = new System.Drawing.Point(150, 75); + this.cmbAuthentication.Name = "cmbAuthentication"; + this.cmbAuthentication.Size = new System.Drawing.Size(400, 21); + this.cmbAuthentication.TabIndex = 5; + this.cmbAuthentication.SelectedIndexChanged += new System.EventHandler(this.cmbAuthentication_SelectedIndexChanged); + // + // lblUserId + // + this.lblUserId.AutoSize = true; + this.lblUserId.Location = new System.Drawing.Point(16, 108); + this.lblUserId.Name = "lblUserId"; + this.lblUserId.Size = new System.Drawing.Size(45, 13); + this.lblUserId.TabIndex = 6; + this.lblUserId.Text = "&User ID:"; + // + // txtUserId + // + this.txtUserId.Location = new System.Drawing.Point(150, 105); + this.txtUserId.Name = "txtUserId"; + this.txtUserId.Size = new System.Drawing.Size(400, 20); + this.txtUserId.TabIndex = 7; + // + // lblPassword + // + this.lblPassword.AutoSize = true; + this.lblPassword.Location = new System.Drawing.Point(16, 138); + this.lblPassword.Name = "lblPassword"; + this.lblPassword.Size = new System.Drawing.Size(56, 13); + this.lblPassword.TabIndex = 8; + this.lblPassword.Text = "&Password:"; + // + // txtPassword + // + this.txtPassword.Location = new System.Drawing.Point(150, 135); + this.txtPassword.Name = "txtPassword"; + this.txtPassword.Size = new System.Drawing.Size(400, 20); + this.txtPassword.TabIndex = 9; + this.txtPassword.UseSystemPasswordChar = true; + // + // lblEncrypt + // + this.lblEncrypt.AutoSize = true; + this.lblEncrypt.Location = new System.Drawing.Point(16, 168); + this.lblEncrypt.Name = "lblEncrypt"; + this.lblEncrypt.Size = new System.Drawing.Size(46, 13); + this.lblEncrypt.TabIndex = 10; + this.lblEncrypt.Text = "&Encrypt:"; + // + // cmbEncrypt + // + this.cmbEncrypt.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cmbEncrypt.FormattingEnabled = true; + this.cmbEncrypt.Location = new System.Drawing.Point(150, 165); + this.cmbEncrypt.Name = "cmbEncrypt"; + this.cmbEncrypt.Size = new System.Drawing.Size(200, 21); + this.cmbEncrypt.TabIndex = 11; + // + // chkTrustServerCertificate + // + this.chkTrustServerCertificate.AutoSize = true; + this.chkTrustServerCertificate.Location = new System.Drawing.Point(370, 167); + this.chkTrustServerCertificate.Name = "chkTrustServerCertificate"; + this.chkTrustServerCertificate.Size = new System.Drawing.Size(149, 17); + this.chkTrustServerCertificate.TabIndex = 12; + this.chkTrustServerCertificate.Text = "&Trust server certificate"; + this.chkTrustServerCertificate.UseVisualStyleBackColor = true; + // + // lblTimeout + // + this.lblTimeout.AutoSize = true; + this.lblTimeout.Location = new System.Drawing.Point(16, 198); + this.lblTimeout.Name = "lblTimeout"; + this.lblTimeout.Size = new System.Drawing.Size(101, 13); + this.lblTimeout.TabIndex = 13; + this.lblTimeout.Text = "Connect timeout (s):"; + // + // numTimeout + // + this.numTimeout.Location = new System.Drawing.Point(150, 196); + this.numTimeout.Maximum = new decimal(new int[] { 600, 0, 0, 0 }); + this.numTimeout.Minimum = new decimal(new int[] { 1, 0, 0, 0 }); + this.numTimeout.Name = "numTimeout"; + this.numTimeout.Size = new System.Drawing.Size(80, 20); + this.numTimeout.TabIndex = 14; + this.numTimeout.Value = new decimal(new int[] { 30, 0, 0, 0 }); + // + // chkClearTokenCache + // + this.chkClearTokenCache.AutoSize = true; + this.chkClearTokenCache.Location = new System.Drawing.Point(260, 198); + this.chkClearTokenCache.Name = "chkClearTokenCache"; + this.chkClearTokenCache.Size = new System.Drawing.Size(290, 17); + this.chkClearTokenCache.TabIndex = 15; + this.chkClearTokenCache.Text = "Clear MSAL token &cache before connect (force prompt)"; + this.chkClearTokenCache.UseVisualStyleBackColor = true; + // + // lblConnectionString + // + this.lblConnectionString.AutoSize = true; + this.lblConnectionString.Location = new System.Drawing.Point(16, 230); + this.lblConnectionString.Name = "lblConnectionString"; + this.lblConnectionString.Size = new System.Drawing.Size(94, 13); + this.lblConnectionString.TabIndex = 15; + this.lblConnectionString.Text = "Connection string:"; + // + // txtConnectionString + // + this.txtConnectionString.Location = new System.Drawing.Point(16, 246); + this.txtConnectionString.Multiline = true; + this.txtConnectionString.Name = "txtConnectionString"; + this.txtConnectionString.ReadOnly = true; + this.txtConnectionString.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; + this.txtConnectionString.Size = new System.Drawing.Size(534, 60); + this.txtConnectionString.TabIndex = 16; + this.txtConnectionString.BackColor = System.Drawing.SystemColors.Info; + // + // btnBuild + // + this.btnBuild.Location = new System.Drawing.Point(16, 316); + this.btnBuild.Name = "btnBuild"; + this.btnBuild.Size = new System.Drawing.Size(140, 26); + this.btnBuild.TabIndex = 17; + this.btnBuild.Text = "&Build Connection String"; + this.btnBuild.UseVisualStyleBackColor = true; + this.btnBuild.Click += new System.EventHandler(this.btnBuild_Click); + // + // btnTest + // + this.btnTest.Location = new System.Drawing.Point(166, 316); + this.btnTest.Name = "btnTest"; + this.btnTest.Size = new System.Drawing.Size(120, 26); + this.btnTest.TabIndex = 18; + this.btnTest.Text = "Te&st Connection"; + this.btnTest.UseVisualStyleBackColor = true; + this.btnTest.Click += new System.EventHandler(this.btnTest_Click); + // + // btnCopy + // + this.btnCopy.Location = new System.Drawing.Point(296, 316); + this.btnCopy.Name = "btnCopy"; + this.btnCopy.Size = new System.Drawing.Size(120, 26); + this.btnCopy.TabIndex = 19; + this.btnCopy.Text = "Cop&y to Clipboard"; + this.btnCopy.UseVisualStyleBackColor = true; + this.btnCopy.Click += new System.EventHandler(this.btnCopy_Click); + // + // btnClear + // + this.btnClear.Location = new System.Drawing.Point(426, 316); + this.btnClear.Name = "btnClear"; + this.btnClear.Size = new System.Drawing.Size(124, 26); + this.btnClear.TabIndex = 20; + this.btnClear.Text = "Cl&ear All"; + this.btnClear.UseVisualStyleBackColor = true; + this.btnClear.Click += new System.EventHandler(this.btnClear_Click); + // + // btnWhoAmI + // + this.btnWhoAmI.Location = new System.Drawing.Point(16, 348); + this.btnWhoAmI.Name = "btnWhoAmI"; + this.btnWhoAmI.Size = new System.Drawing.Size(534, 26); + this.btnWhoAmI.TabIndex = 21; + this.btnWhoAmI.Text = "&Who Am I? (run identity query on the database)"; + this.btnWhoAmI.UseVisualStyleBackColor = true; + this.btnWhoAmI.Click += new System.EventHandler(this.btnWhoAmI_Click); + // + // lblStatus + // + this.lblStatus.AutoSize = true; + this.lblStatus.Location = new System.Drawing.Point(16, 386); + this.lblStatus.Name = "lblStatus"; + this.lblStatus.Size = new System.Drawing.Size(40, 13); + this.lblStatus.TabIndex = 22; + this.lblStatus.Text = "Result:"; + // + // txtStatus + // + this.txtStatus.Location = new System.Drawing.Point(16, 402); + this.txtStatus.Multiline = true; + this.txtStatus.Name = "txtStatus"; + this.txtStatus.ReadOnly = true; + this.txtStatus.ScrollBars = System.Windows.Forms.ScrollBars.Both; + this.txtStatus.Size = new System.Drawing.Size(534, 160); + this.txtStatus.TabIndex = 23; + this.txtStatus.WordWrap = false; + this.txtStatus.Font = new System.Drawing.Font("Consolas", 9F); + // + // statusStrip + // + this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.statusLabel}); + this.statusStrip.Location = new System.Drawing.Point(0, 578); + this.statusStrip.Name = "statusStrip"; + this.statusStrip.Size = new System.Drawing.Size(566, 22); + this.statusStrip.TabIndex = 24; + // + // statusLabel + // + this.statusLabel.Name = "statusLabel"; + this.statusLabel.Size = new System.Drawing.Size(39, 17); + this.statusLabel.Text = "Ready"; + // + // MainForm + // + this.AcceptButton = this.btnTest; + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(566, 600); + this.Controls.Add(this.statusStrip); + this.Controls.Add(this.txtStatus); + this.Controls.Add(this.lblStatus); + this.Controls.Add(this.btnWhoAmI); + this.Controls.Add(this.btnClear); + this.Controls.Add(this.btnCopy); + this.Controls.Add(this.btnTest); + this.Controls.Add(this.btnBuild); + this.Controls.Add(this.txtConnectionString); + this.Controls.Add(this.lblConnectionString); + this.Controls.Add(this.numTimeout); + this.Controls.Add(this.lblTimeout); + this.Controls.Add(this.chkClearTokenCache); + this.Controls.Add(this.chkTrustServerCertificate); + this.Controls.Add(this.cmbEncrypt); + this.Controls.Add(this.lblEncrypt); + this.Controls.Add(this.txtPassword); + this.Controls.Add(this.lblPassword); + this.Controls.Add(this.txtUserId); + this.Controls.Add(this.lblUserId); + this.Controls.Add(this.cmbAuthentication); + this.Controls.Add(this.lblAuthentication); + this.Controls.Add(this.txtDatabase); + this.Controls.Add(this.lblDatabase); + this.Controls.Add(this.txtServer); + this.Controls.Add(this.lblServer); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.MaximizeBox = false; + this.Name = "MainFormWorker"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "Azure SQL Connector — Worker thread (Task.Run + Open)"; + ((System.ComponentModel.ISupportInitialize)(this.numTimeout)).EndInit(); + this.statusStrip.ResumeLayout(false); + this.statusStrip.PerformLayout(); + this.ResumeLayout(false); + this.PerformLayout(); + } + + #endregion + + private System.Windows.Forms.Label lblServer; + private System.Windows.Forms.TextBox txtServer; + private System.Windows.Forms.Label lblDatabase; + private System.Windows.Forms.TextBox txtDatabase; + private System.Windows.Forms.Label lblAuthentication; + private System.Windows.Forms.ComboBox cmbAuthentication; + private System.Windows.Forms.Label lblUserId; + private System.Windows.Forms.TextBox txtUserId; + private System.Windows.Forms.Label lblPassword; + private System.Windows.Forms.TextBox txtPassword; + private System.Windows.Forms.Label lblEncrypt; + private System.Windows.Forms.ComboBox cmbEncrypt; + private System.Windows.Forms.CheckBox chkTrustServerCertificate; + private System.Windows.Forms.Label lblTimeout; + private System.Windows.Forms.NumericUpDown numTimeout; + private System.Windows.Forms.CheckBox chkClearTokenCache; + private System.Windows.Forms.Label lblConnectionString; + private System.Windows.Forms.TextBox txtConnectionString; + private System.Windows.Forms.Button btnBuild; + private System.Windows.Forms.Button btnTest; + private System.Windows.Forms.Button btnCopy; + private System.Windows.Forms.Button btnClear; + private System.Windows.Forms.Button btnWhoAmI; + private System.Windows.Forms.Label lblStatus; + private System.Windows.Forms.TextBox txtStatus; + private System.Windows.Forms.StatusStrip statusStrip; + private System.Windows.Forms.ToolStripStatusLabel statusLabel; + } +} diff --git a/doc/apps/AzureSqlConnector/MainFormWorker.cs b/doc/apps/AzureSqlConnector/MainFormWorker.cs new file mode 100644 index 0000000000..8d344cf6c4 --- /dev/null +++ b/doc/apps/AzureSqlConnector/MainFormWorker.cs @@ -0,0 +1,598 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Windows.Forms; +using Microsoft.Data.SqlClient; +using Microsoft.Identity.Client; + +namespace Microsoft.Data.SqlClient.Samples.AzureSqlConnector +{ + /// + /// "Worker thread" variant of the connector form. Opens the SQL connection synchronously + /// inside a call so the UI thread never blocks. + /// + /// + /// + /// The form's HWND is captured on the UI thread in the constructor and stashed in + /// . Both Entra ID parent-window callbacks return that captured + /// handle, so they are safe to invoke from the worker thread (touching Form.Handle + /// from a non-UI thread is illegal). + /// + /// + /// Compare with , which keeps Open on the UI thread and relies on + /// for responsiveness. + /// + /// + public partial class MainFormWorker : Form + { + // ────────────────────────────────────────────────────────────────── + #region Construction + + public MainFormWorker() + { + InitializeComponent(); + PopulateAuthenticationMethods(); + PopulateEncryptOptions(); + UpdateCredentialFieldsAvailability(); + + // Force the underlying Win32 window to be created NOW (on the UI thread) so we can + // safely capture its HWND for MSAL to use later from a worker thread. Touching + // Form.Handle from a non-UI thread is illegal, so we read it here once and stash it. + _ownerHwnd = this.Handle; + + RegisterActiveDirectoryProvider(); + } + + #endregion + + // ────────────────────────────────────────────────────────────────── + #region UI Initialization + + private void PopulateAuthenticationMethods() + { + foreach (SqlAuthenticationMethod method in Enum.GetValues(typeof(SqlAuthenticationMethod))) + { + cmbAuthentication.Items.Add(method); + } + + cmbAuthentication.SelectedItem = SqlAuthenticationMethod.SqlPassword; + } + + private void PopulateEncryptOptions() + { + cmbEncrypt.Items.Add(EncryptDisplay.Mandatory); + cmbEncrypt.Items.Add(EncryptDisplay.Optional); + cmbEncrypt.Items.Add(EncryptDisplay.Strict); + cmbEncrypt.SelectedIndex = 0; + } + + /// + /// Registers a single for every + /// Entra ID authentication method and gives it the form's captured HWND as the parent + /// window owner. Both callbacks intentionally use the HWND captured in the constructor + /// () rather than this.Handle; they are invoked by MSAL on + /// the worker thread that called . + /// + private void RegisterActiveDirectoryProvider() + { + ActiveDirectoryAuthenticationProvider provider = new ActiveDirectoryAuthenticationProvider(); + IntPtr ownerHwnd = _ownerHwnd; + +#if NETFRAMEWORK + // .NET Framework: parent the embedded WebView via the legacy IWin32Window API. + provider.SetIWin32WindowFunc(() => new Win32WindowHandle(ownerHwnd)); +#endif + + // Modern API: works on both .NET Framework and .NET 8+, and is the one MSAL's WAM + // broker consults on Windows. + provider.SetParentActivityOrWindowFunc(() => ownerHwnd); + + // Without this, MSAL's default device-code callback writes the prompt to + // Console.WriteLine, which is invisible in a WinForms host — the connection + // appears to hang while MSAL polls for a code the user never sees. + provider.SetDeviceCodeFlowCallback(DeviceCodeFlowCallback); + + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryIntegrated, provider); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, provider); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal, provider); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, provider); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, provider); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryMSI, provider); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDefault, provider); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity, provider); + #pragma warning disable CS0618 // Type or member is obsolete + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, provider); + #pragma warning restore CS0618 // Type or member is obsolete + } + + /// + /// Device Code Flow callback. MSAL invokes this on a worker thread before it begins + /// polling the token endpoint. We surface the user code three ways so the user always + /// sees it: (1) appended to the log textbox via BeginInvoke (the UI thread is free in + /// this variant because Open() runs on a Task.Run worker), (2) the verification URL + /// launched in the default browser, and (3) a modal owned by the MSAL worker thread. + /// MSAL polling waits for the returned Task to complete, so dismissing the dialog + /// also resumes polling. + /// + private Task DeviceCodeFlowCallback(DeviceCodeResult result) + { + string message = result.Message; + string url = result.VerificationUrl; + string code = result.UserCode; + + if (IsHandleCreated) + { + try + { + BeginInvoke((Action)(() => + { + AppendStatus(string.Empty); + AppendStatus("=== Device Code Flow ==="); + AppendStatus(message); + })); + } + catch (InvalidOperationException) + { + // Form is closing or handle was destroyed; fall through to the modal. + } + } + + try + { + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + } + catch + { + // Best-effort; the modal below still shows the URL and code. + } + + MessageBox.Show( + "Sign in to complete Device Code Flow:" + Environment.NewLine + Environment.NewLine + + " URL : " + url + Environment.NewLine + + " Code: " + code + Environment.NewLine + Environment.NewLine + + "A browser window has been opened. Enter the code above, complete sign-in," + + Environment.NewLine + "then click OK to resume the connection.", + "Device Code Flow", + MessageBoxButtons.OK, + MessageBoxIcon.Information); + + return Task.CompletedTask; + } + + #endregion + + // ────────────────────────────────────────────────────────────────── + #region Event Handlers + + private void cmbAuthentication_SelectedIndexChanged(object sender, EventArgs e) + { + UpdateCredentialFieldsAvailability(); + } + + private void btnBuild_Click(object sender, EventArgs e) + { + try + { + SqlConnectionStringBuilder builder = BuildConnectionString(); + txtConnectionString.Text = MaskPassword(builder); + SetStatus("Connection string built successfully.", isError: false); + AppendStatus("Connection string built:\r\n" + MaskPassword(builder)); + } + catch (Exception ex) + { + txtConnectionString.Text = string.Empty; + SetStatus("Failed to build connection string.", isError: true); + AppendStatus("ERROR: " + ex.Message); + } + } + + private async void btnTest_Click(object sender, EventArgs e) + { + SqlConnectionStringBuilder builder; + try + { + builder = BuildConnectionString(); + txtConnectionString.Text = MaskPassword(builder); + } + catch (Exception ex) + { + SetStatus("Failed to build connection string.", isError: true); + AppendStatus("ERROR: " + ex.Message); + return; + } + + SetBusy(true, "Testing connection..."); + AppendStatus(string.Empty); + AppendStatus("Testing connectivity to " + builder.DataSource + " ..."); + + MaybeClearTokenCache(); + + try + { + // Run Open() on a thread-pool worker so the UI thread never blocks. The await + // continuation hops back onto the UI thread automatically (the awaiter captures + // the current SynchronizationContext), so it is safe to touch the form's controls + // after the await. + // + // The Entra ID interactive / WAM flows still find a parent window because we + // captured the form's HWND on the UI thread in the constructor and the callbacks + // registered in RegisterActiveDirectoryProvider return that captured handle (no + // UI-thread-only Form.Handle access from the worker thread). + string connectionString = builder.ConnectionString; + string serverVersion = await Task.Run(() => + { + using (SqlConnection connection = new SqlConnection(connectionString)) + { + connection.Open(); + return connection.ServerVersion; + } + }).ConfigureAwait(true); + + SetStatus("Connected successfully.", isError: false); + AppendStatus("Connected successfully! Server version: " + serverVersion); + } + catch (SqlException ex) + { + SetStatus("Connection failed (SqlException).", isError: true); + AppendStatus("SqlException [" + ex.Number + "]: " + ex.Message); + } + catch (Exception ex) + { + SetStatus("Connection failed.", isError: true); + AppendStatus(ex.GetType().Name + ": " + ex.Message); + } + finally + { + SetBusy(false, null); + } + } + + private async void btnWhoAmI_Click(object sender, EventArgs e) + { + SqlConnectionStringBuilder builder; + try + { + builder = BuildConnectionString(); + txtConnectionString.Text = MaskPassword(builder); + } + catch (Exception ex) + { + SetStatus("Failed to build connection string.", isError: true); + AppendStatus("ERROR: " + ex.Message); + return; + } + + SetBusy(true, "Querying logged-in identity..."); + AppendStatus(string.Empty); + AppendStatus("Running identity query against " + builder.DataSource + " ..."); + + MaybeClearTokenCache(); + + try + { + // Run the whole open + query + read on a worker thread so the UI never blocks. + // We materialize the single result row into a List<(name, value)> on the worker + // and then format it on the UI thread once the await returns. + string connectionString = builder.ConnectionString; + List<(string Name, object Value)> row = await Task.Run(() => + { + using (SqlConnection connection = new SqlConnection(connectionString)) + { + connection.Open(); + + using (SqlCommand command = connection.CreateCommand()) + { + command.CommandText = IdentityQuery.CommandText; + + using (SqlDataReader reader = command.ExecuteReader()) + { + if (!reader.Read()) + { + return null; + } + + var fields = new List<(string, object)>(reader.FieldCount); + for (int i = 0; i < reader.FieldCount; i++) + { + object value = reader.IsDBNull(i) ? "(null)" : reader.GetValue(i); + fields.Add((reader.GetName(i), value)); + } + return fields; + } + } + } + }).ConfigureAwait(true); + + if (row is null) + { + SetStatus("Identity query returned no rows.", isError: true); + AppendStatus("(no rows returned)"); + } + else + { + AppendStatus("Identity:"); + foreach (var (name, value) in row) + { + AppendStatus(" " + name.PadRight(16) + ": " + value); + } + SetStatus("Identity query succeeded.", isError: false); + } + } + catch (SqlException ex) + { + SetStatus("Identity query failed (SqlException).", isError: true); + AppendStatus("SqlException [" + ex.Number + "]: " + ex.Message); + } + catch (Exception ex) + { + SetStatus("Identity query failed.", isError: true); + AppendStatus(ex.GetType().Name + ": " + ex.Message); + } + finally + { + SetBusy(false, null); + } + } + + private void btnCopy_Click(object sender, EventArgs e) + { + if (string.IsNullOrEmpty(txtConnectionString.Text)) + { + SetStatus("Nothing to copy. Build the connection string first.", isError: true); + return; + } + + try + { + Clipboard.SetText(BuildConnectionString().ConnectionString); + SetStatus("Connection string copied to clipboard.", isError: false); + } + catch (Exception ex) + { + SetStatus("Failed to copy to clipboard.", isError: true); + AppendStatus("ERROR: " + ex.Message); + } + } + + private void btnClear_Click(object sender, EventArgs e) + { + txtServer.Clear(); + txtDatabase.Clear(); + txtUserId.Clear(); + txtPassword.Clear(); + txtConnectionString.Clear(); + txtStatus.Clear(); + cmbAuthentication.SelectedItem = SqlAuthenticationMethod.SqlPassword; + cmbEncrypt.SelectedIndex = 0; + chkTrustServerCertificate.Checked = false; + numTimeout.Value = 30; + SetStatus("Ready", isError: false); + } + + #endregion + + // ────────────────────────────────────────────────────────────────── + #region Connection String Construction + + private SqlConnectionStringBuilder BuildConnectionString() + { + string server = (txtServer.Text ?? string.Empty).Trim(); + if (string.IsNullOrEmpty(server)) + { + throw new InvalidOperationException("Server name is required."); + } + + SqlAuthenticationMethod authMethod = (SqlAuthenticationMethod)cmbAuthentication.SelectedItem; + + SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder + { + DataSource = server, + ConnectTimeout = (int)numTimeout.Value, + }; + + string database = (txtDatabase.Text ?? string.Empty).Trim(); + if (!string.IsNullOrEmpty(database)) + { + builder.InitialCatalog = database; + } + + if (authMethod != SqlAuthenticationMethod.NotSpecified) + { + builder.Authentication = authMethod; + } + + if (RequiresUserAndPassword(authMethod)) + { + string userId = (txtUserId.Text ?? string.Empty).Trim(); + if (string.IsNullOrEmpty(userId)) + { + throw new InvalidOperationException( + "User ID is required for " + authMethod + " authentication."); + } + + builder.UserID = userId; + builder.Password = txtPassword.Text ?? string.Empty; + } + else if (authMethod == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal + || authMethod == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity + || authMethod == SqlAuthenticationMethod.ActiveDirectoryMSI + || authMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive + || authMethod == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow + || authMethod == SqlAuthenticationMethod.ActiveDirectoryDefault + || authMethod == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity) + { + string userId = (txtUserId.Text ?? string.Empty).Trim(); + if (!string.IsNullOrEmpty(userId)) + { + builder.UserID = userId; + } + + if (authMethod == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal + && !string.IsNullOrEmpty(txtPassword.Text)) + { + builder.Password = txtPassword.Text; + } + } + + string encryptValue = cmbEncrypt.SelectedItem as string ?? EncryptDisplay.Mandatory; + switch (encryptValue) + { + case EncryptDisplay.Mandatory: + builder.Encrypt = SqlConnectionEncryptOption.Mandatory; + break; + case EncryptDisplay.Optional: + builder.Encrypt = SqlConnectionEncryptOption.Optional; + break; + case EncryptDisplay.Strict: + builder.Encrypt = SqlConnectionEncryptOption.Strict; + break; + } + + builder.TrustServerCertificate = chkTrustServerCertificate.Checked; + + return builder; + } + + private static bool RequiresUserAndPassword(SqlAuthenticationMethod method) + { + switch (method) + { + case SqlAuthenticationMethod.SqlPassword: +#pragma warning disable CS0618 // Type or member is obsolete + case SqlAuthenticationMethod.ActiveDirectoryPassword: +#pragma warning restore CS0618 + return true; + default: + return false; + } + } + + private static string MaskPassword(SqlConnectionStringBuilder builder) + { + if (string.IsNullOrEmpty(builder.Password)) + { + return builder.ConnectionString; + } + + SqlConnectionStringBuilder copy = new SqlConnectionStringBuilder(builder.ConnectionString) + { + Password = "********", + }; + return copy.ConnectionString; + } + + #endregion + + // ────────────────────────────────────────────────────────────────── + #region UI Helpers + + private void UpdateCredentialFieldsAvailability() + { + if (cmbAuthentication.SelectedItem == null) + { + return; + } + + SqlAuthenticationMethod method = (SqlAuthenticationMethod)cmbAuthentication.SelectedItem; + + bool userEnabled = method != SqlAuthenticationMethod.ActiveDirectoryIntegrated; + bool passwordEnabled = RequiresUserAndPassword(method) + || method == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal; + + txtUserId.Enabled = userEnabled; + txtPassword.Enabled = passwordEnabled; + + if (!passwordEnabled) + { + txtPassword.Clear(); + } + } + + private void SetStatus(string text, bool isError) + { + statusLabel.Text = text; + statusLabel.ForeColor = isError ? System.Drawing.Color.Firebrick : System.Drawing.Color.Black; + } + + private void AppendStatus(string line) + { + if (txtStatus.TextLength > 0) + { + txtStatus.AppendText(Environment.NewLine); + } + txtStatus.AppendText(line ?? string.Empty); + } + + private void SetBusy(bool busy, string statusText) + { + btnBuild.Enabled = !busy; + btnTest.Enabled = !busy; + btnCopy.Enabled = !busy; + btnClear.Enabled = !busy; + btnWhoAmI.Enabled = !busy; + Cursor = busy ? Cursors.WaitCursor : Cursors.Default; + + if (statusText != null) + { + SetStatus(statusText, isError: false); + } + } + + // Only drops the in-process PCA / TokenCredential maps; MSAL's persistent on-disk cache + // and WAM broker accounts are untouched. Sufficient to demo a worker-thread interactive + // prompt when the persistent cache has already been cleared (fresh run or no WAM account + // bound), and a useful reset between back-to-back connects within a single session. + private void MaybeClearTokenCache() + { + if (!chkClearTokenCache.Checked) + { + return; + } + + ActiveDirectoryAuthenticationProvider.ClearUserTokenCache(); + AppendStatus("Cleared in-process MSAL token cache."); + } + + #endregion + + // ────────────────────────────────────────────────────────────────── + #region Nested Types + + private static class EncryptDisplay + { + public const string Mandatory = "Mandatory"; + public const string Optional = "Optional"; + public const string Strict = "Strict"; + } + + /// + /// Tiny wrapper around a raw HWND captured on the UI thread. + /// Used so that MSAL.NET's IWin32WindowFunc callback can safely return a window + /// owner from a worker thread without ever touching off-UI. + /// Only needed on .NET Framework where the legacy SetIWin32WindowFunc API is used. + /// +#if NETFRAMEWORK + private sealed class Win32WindowHandle : IWin32Window + { + private readonly IntPtr _hwnd; + public Win32WindowHandle(IntPtr hwnd) => _hwnd = hwnd; + public IntPtr Handle => _hwnd; + } +#endif + + #endregion + + // ────────────────────────────────────────────────────────────────── + #region Private Fields + + /// + /// The form's Win32 window handle, captured on the UI thread in the constructor. + /// Read from worker threads by the Entra ID provider callbacks to parent MSAL's + /// sign-in / WAM broker UI without illegally touching . + /// + private readonly IntPtr _ownerHwnd; + + #endregion + } +} diff --git a/doc/apps/AzureSqlConnector/ModeSelectorForm.cs b/doc/apps/AzureSqlConnector/ModeSelectorForm.cs new file mode 100644 index 0000000000..651412de32 --- /dev/null +++ b/doc/apps/AzureSqlConnector/ModeSelectorForm.cs @@ -0,0 +1,116 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace Microsoft.Data.SqlClient.Samples.AzureSqlConnector +{ + /// + /// Choice exposed by . + /// + internal enum ConnectionMode + { + /// + /// Use , which calls SqlConnection.OpenAsync() on the UI + /// thread. Relies on the WinForms SynchronizationContext to keep the message pump alive. + /// + UiThreadOpenAsync, + + /// + /// Use , which calls SqlConnection.Open() inside + /// Task.Run on a thread-pool worker. The captured form HWND is passed to MSAL. + /// + WorkerThreadOpen, + } + + /// + /// Tiny modal dialog shown at startup that lets the user pick which connector form + /// (UI-thread async or worker-thread sync) to launch. + /// + internal sealed class ModeSelectorForm : Form + { + private readonly RadioButton _rdoUiThread; + private readonly RadioButton _rdoWorker; + + internal ConnectionMode SelectedMode => + _rdoWorker.Checked ? ConnectionMode.WorkerThreadOpen : ConnectionMode.UiThreadOpenAsync; + + internal ModeSelectorForm() + { + Text = "Azure SQL Connector — Choose Mode"; + FormBorderStyle = FormBorderStyle.FixedDialog; + StartPosition = FormStartPosition.CenterScreen; + MaximizeBox = false; + MinimizeBox = false; + ClientSize = new Size(460, 200); + + Label lblHeader = new Label + { + AutoSize = false, + Text = "Select how SqlConnection.Open should be invoked:", + Location = new Point(16, 14), + Size = new Size(420, 20), + Font = new Font(Font, FontStyle.Bold), + }; + + _rdoUiThread = new RadioButton + { + Text = "&UI thread", + Location = new Point(20, 42), + Size = new Size(420, 20), + Checked = true, + }; + + Label lblUiHint = new Label + { + AutoSize = false, + Text = " Async/Sync open on the UI thread; SynchronizationContext keeps the form responsive.", + Location = new Point(20, 62), + Size = new Size(420, 18), + ForeColor = SystemColors.GrayText, + }; + + _rdoWorker = new RadioButton + { + Text = "&Worker thread", + Location = new Point(20, 90), + Size = new Size(420, 20), + }; + + Label lblWorkerHint = new Label + { + AutoSize = false, + Text = " Sync open on a thread-pool worker; HWND is captured up-front for MSAL.", + Location = new Point(20, 110), + Size = new Size(420, 18), + ForeColor = SystemColors.GrayText, + }; + + Button btnOk = new Button + { + Text = "&Launch", + DialogResult = DialogResult.OK, + Location = new Point(268, 152), + Size = new Size(82, 28), + }; + + Button btnCancel = new Button + { + Text = "Cancel", + DialogResult = DialogResult.Cancel, + Location = new Point(358, 152), + Size = new Size(82, 28), + }; + + AcceptButton = btnOk; + CancelButton = btnCancel; + + Controls.AddRange(new Control[] + { + lblHeader, + _rdoUiThread, lblUiHint, + _rdoWorker, lblWorkerHint, + btnOk, btnCancel, + }); + } + } +} diff --git a/doc/apps/AzureSqlConnector/Program.cs b/doc/apps/AzureSqlConnector/Program.cs new file mode 100644 index 0000000000..c4bfad836c --- /dev/null +++ b/doc/apps/AzureSqlConnector/Program.cs @@ -0,0 +1,39 @@ +using System; +using System.Windows.Forms; + +namespace Microsoft.Data.SqlClient.Samples.AzureSqlConnector +{ + /// + /// Application entry point for the Azure SQL Connector WinForms test app. + /// + internal static class Program + { + /// + /// The main entry point for the application. Shows a small chooser dialog at startup so + /// the user can pick between the UI-thread and the worker-thread + /// variant of the connector. + /// + [STAThread] + private static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + ConnectionMode mode; + using (ModeSelectorForm selector = new ModeSelectorForm()) + { + if (selector.ShowDialog() != DialogResult.OK) + { + return; + } + mode = selector.SelectedMode; + } + + Form main = mode == ConnectionMode.WorkerThreadOpen + ? (Form)new MainFormWorker() + : new MainForm(); + + Application.Run(main); + } + } +} diff --git a/doc/apps/AzureSqlConnector/README.md b/doc/apps/AzureSqlConnector/README.md new file mode 100644 index 0000000000..62f100e966 --- /dev/null +++ b/doc/apps/AzureSqlConnector/README.md @@ -0,0 +1,137 @@ +# Azure SQL Connector (WinForms) + +A small Windows Forms test application that lets a user fill in Azure SQL Database connection +parameters in a UI, builds the corresponding ADO.NET connection string via +`SqlConnectionStringBuilder`, and tests connectivity using `Microsoft.Data.SqlClient`. + +It is intended as a quick, repeatable scratch tool for manually validating connection-string +combinations (server / database / authentication mode / encryption / etc.) against an Azure SQL DB +or SQL Server instance, **and as a manual repro** for the WAM-broker behavior added in this +branch's `ActiveDirectoryAuthenticationProvider`. + +The sample multi-targets: + +| TFM | Purpose | +| ---------------- | ------------------------------------------------------------------------------------------------ | +| `net481` | Exercises the legacy `SetIWin32WindowFunc` API used by .NET Framework callers with WinForms. | +| `net10.0-windows` | Exercises the modern `SetParentActivityOrWindowFunc` API used on .NET 8+. | + +`net10.0-windows` restores and builds cleanly on Linux/macOS hosts even though the resulting +binary only runs on Windows, so the project no longer needs a separate no-op cross-platform +fallback. + +> **Note:** `SetParentActivityOrWindowFunc` is also available on `net481` and is the +> recommended API for new code on any framework. The sample wires `net481` up to +> `SetIWin32WindowFunc` only to keep coverage of that legacy code path; replacing the +> `SetIWin32WindowFunc(() => this)` call with `SetParentActivityOrWindowFunc(() => this.Handle)` +> on `net481` works the same way. + +## Mode selector + +When the app launches it shows a small `ModeSelectorForm` that picks between two top-level forms: + +| Mode | Form | What it exercises | +| ---------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------- | +| **UI thread (`OpenAsync`)** | `MainForm` | Calls `SqlConnection.OpenAsync()` on the UI thread so the Windows Forms message pump stays alive during MSAL sign-in. | +| **Worker thread (`Open`, sync)** | `MainFormWorker` | Calls `SqlConnection.Open()` on a background worker thread; the parent window handle is captured up-front on the UI thread. | + +Both forms demonstrate the supported patterns for parenting the WAM broker (or the legacy +embedded WebView on .NET Framework). + +## Form inputs + +| Field | Maps to connection string keyword | +| -------------------------- | ----------------------------------------------- | +| Server name | `Data Source` | +| Database name | `Initial Catalog` *(only added when non-empty)* | +| Authentication | `Authentication` *(SqlAuthenticationMethod)* | +| User ID | `User ID` | +| Password | `Password` | +| Encrypt | `Encrypt` *(Mandatory / Optional / Strict)* | +| Trust server certificate | `TrustServerCertificate` | +| Connect timeout (s) | `Connect Timeout` | + +The **Authentication** dropdown is populated from every member of +`Microsoft.Data.SqlClient.SqlAuthenticationMethod`. The User ID and Password fields are enabled / +disabled automatically based on the selected method: + +- **SqlPassword** / **ActiveDirectoryPassword** — both User ID and Password are required. +- **ActiveDirectoryServicePrincipal** — User ID = App (Client) ID, Password = client secret. +- **ActiveDirectoryManagedIdentity / MSI / Default / Interactive / DeviceCodeFlow / WorkloadIdentity** + — User ID is optional (e.g. user-assigned MI client id), Password is disabled. +- **ActiveDirectoryIntegrated** — credentials come from the OS, both fields disabled. + +## Buttons + +| Button | Action | +| ----------------------- | ---------------------------------------------------------------------- | +| Build Connection String | Builds the connection string from the form values and displays it. | +| Test Connection | Builds the connection string and opens the connection. | +| Copy to Clipboard | Copies the currently-built connection string to the clipboard. | +| Clear All | Resets every input field to its default state. | +| Who Am I? | Connects and runs an identity query (`SUSER_SNAME()`, `ORIGINAL_LOGIN()`, `USER_NAME()`, `DB_NAME()`, `@@SPID`, etc.) and prints the results. | + +The result pane shows the built connection string with the password masked, the test connection +outcome (including SQL error number when applicable), and the server version on success. + +## Prerequisites + +- Visual Studio 2026 (or any IDE / SDK with .NET Framework **4.8.1** Developer Pack installed) for + the `net481` target. The `net10.0-windows` target only needs the .NET 10 SDK. +- Network connectivity to your Azure SQL Database (server firewall must allow your client IP). +- For Entra ID authentication modes, valid credentials available through Azure CLI / environment + variables / managed identity / the WAM broker, depending on the chosen method. + +## Build & run + +From the project folder: + +```pwsh +dotnet build .\AzureSqlConnector.csproj +dotnet run --project .\AzureSqlConnector.csproj -f net10.0-windows # modern WAM API +dotnet run --project .\AzureSqlConnector.csproj -f net481 # legacy IWin32Window API +``` + +Or load `src\Microsoft.Data.SqlClient.slnx` in Visual Studio, set **AzureSqlConnector** as the +startup project, and press **F5**. + +## Example + +1. **Server name:** `myserver.database.windows.net` +2. **Database name:** `MyDb` +3. **Authentication:** `SqlPassword` +4. **User ID:** `sqladmin` +5. **Password:** *your password* +6. **Encrypt:** `Mandatory` +7. **Trust server certificate:** unchecked +8. Click **Test Connection** — the result pane should display + `Connected successfully! Server version: 12.00.xxxx`. + +## Entra ID parent-window plumbing + +For any `ActiveDirectory*` authentication method (especially **ActiveDirectoryInteractive**) the +app installs an `ActiveDirectoryAuthenticationProvider` and tells it which window should host the +sign-in UI: + +- On **`net481`** the form calls `provider.SetIWin32WindowFunc(() => this)`. This is the legacy + API used by .NET Framework callers with the embedded WebView. +- On **`net10.0-windows`** the form calls + `provider.SetParentActivityOrWindowFunc(() => this.Handle)`. This is the modern API that also + integrates with the WAM broker on Windows. + +The provider is registered for every `SqlAuthenticationMethod.ActiveDirectory*` value at startup. + +### Threading patterns + +| Form | Open mode | Parent window callback | +| ----------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `MainForm` | `OpenAsync` on UI thread | Callback runs on the UI thread when MSAL invokes it, so `this`/`this.Handle` is naturally safe to access. | +| `MainFormWorker` | `Open` (sync) on worker | The form captures `this.Handle` into a field on the UI thread before kicking off the worker; the callback closes over that captured value so it never needs to marshal back. | + +Without one of these patterns the WAM broker (or the embedded WebView on .NET Framework) can fail +to render or stay unresponsive while it waits for the user. + +## Notes + +- This is a sample / diagnostic tool, **not** a product. It does not persist credentials. +- From the repo root: `dotnet run --project .\doc\apps\AzureSqlConnector\AzureSqlConnector.csproj` diff --git a/doc/snippets/Microsoft.Data.SqlClient/ActiveDirectoryAuthenticationProvider.xml b/doc/snippets/Microsoft.Data.SqlClient/ActiveDirectoryAuthenticationProvider.xml index 36f10a6aab..4dd9f61d5a 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/ActiveDirectoryAuthenticationProvider.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/ActiveDirectoryAuthenticationProvider.xml @@ -1,4 +1,9 @@ - + + @@ -74,7 +79,12 @@ Clears cached user tokens from the token provider. - This will cause interactive authentication prompts to appear again if tokens were previously being obtained from the cache. + + This will cause interactive authentication prompts to appear again if tokens were previously being obtained from the cache. + + + The driver's per-pool federated-authentication token cache is also cleared, so subsequent calls will reacquire fed-auth tokens instead of reusing cached entries. + diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlCommand.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlCommand.xml index a3d87c843d..34c738b41e 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlCommand.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlCommand.xml @@ -357,35 +357,35 @@ Next, compile and execute the following: using System; using System.Data; using Microsoft.Data.SqlClient; - + class Class1 { static void Main() { - // This is a simple example that demonstrates the usage of the + // This is a simple example that demonstrates the usage of the // BeginExecuteNonQuery functionality. - // The WAITFOR statement simply adds enough time to prove the + // The WAITFOR statement simply adds enough time to prove the // asynchronous nature of the command. - + string commandText = "UPDATE Production.Product SET ReorderPoint = ReorderPoint + 1 " + "WHERE ReorderPoint Is Not Null;" + "WAITFOR DELAY '0:0:3';" + "UPDATE Production.Product SET ReorderPoint = ReorderPoint - 1 " + "WHERE ReorderPoint Is Not Null"; - + RunCommandAsynchronously(commandText, GetConnectionString()); - + Console.WriteLine("Press ENTER to continue."); Console.ReadLine(); } - + private static void RunCommandAsynchronously(string commandText, string connectionString) { // Given command text and connection string, asynchronously execute // the specified command against the connection. For this example, - // the code displays an indicator as it is working, verifying the - // asynchronous behavior. + // the code displays an indicator as it is working, verifying the + // asynchronous behavior. using (SqlConnection connection = new SqlConnection(connectionString)) { try @@ -393,17 +393,17 @@ Next, compile and execute the following: int count = 0; SqlCommand command = new SqlCommand(commandText, connection); connection.Open(); - + IAsyncResult result = command.BeginExecuteNonQuery(); while (!result.IsCompleted) { Console.WriteLine("Waiting ({0})", count++); // Wait for 1/10 second, so the counter - // does not consume all available resources + // does not consume all available resources // on the main thread. System.Threading.Thread.Sleep(100); } - + Console.WriteLine("Command complete. Affected {0} rows.", command.EndExecuteNonQuery(result)); } @@ -423,11 +423,11 @@ Next, compile and execute the following: } } } - + private static string GetConnectionString() { - // To avoid storing the connection string in your code, - // you can retrieve it from a configuration file. + // To avoid storing the connection string in your code, + // you can retrieve it from a configuration file. return "Data Source=(local);Integrated Security=SSPI;" + "Initial Catalog=AdventureWorks"; } @@ -527,7 +527,7 @@ Next, compile and execute the following: using System.Text; using System.Windows.Forms; using Microsoft.Data.SqlClient; - + namespace Microsoft.AdoDotNet.CodeSamples { public partial class Form1 : Form @@ -536,9 +536,9 @@ Next, compile and execute the following: { InitializeComponent(); } - - // Hook up the form's Load event handler (you can double-click on - // the form's design surface in Visual Studio), and then add + + // Hook up the form's Load event handler (you can double-click on + // the form's design surface in Visual Studio), and then add // this code to the form's class: private void Form1_Load(object sender, EventArgs e) { @@ -546,42 +546,42 @@ Next, compile and execute the following: this.FormClosing += new System.Windows.Forms. FormClosingEventHandler(this.Form1_FormClosing); } - + // You need this delegate in order to display text from a thread // other than the form's thread. See the HandleCallback // procedure for more information. - // This same delegate matches both the DisplayStatus + // This same delegate matches both the DisplayStatus // and DisplayResults methods. private delegate void DisplayInfoDelegate(string Text); - + // This flag ensures that the user does not attempt - // to restart the command or close the form while the + // to restart the command or close the form while the // asynchronous command is executing. private bool isExecuting; - - // This example maintains the connection object + + // This example maintains the connection object // externally, so that it is available for closing. private SqlConnection connection; - + private static string GetConnectionString() { - // To avoid storing the connection string in your code, + // To avoid storing the connection string in your code, // you can retrieve it from a configuration file. return "Data Source=(local);Integrated Security=true;" + "Initial Catalog=AdventureWorks"; } - + private void DisplayStatus(string Text) { this.label1.Text = Text; } - + private void DisplayResults(string Text) { this.label1.Text = Text; DisplayStatus("Ready"); } - + private void Form1_FormClosing(object sender, System.Windows.Forms.FormClosingEventArgs e) { if (isExecuting) @@ -591,7 +591,7 @@ Next, compile and execute the following: e.Cancel = true; } } - + private void button1_Click(object sender, System.EventArgs e) { if (isExecuting) @@ -608,7 +608,7 @@ Next, compile and execute the following: DisplayResults(""); DisplayStatus("Connecting..."); connection = new SqlConnection(GetConnectionString()); - // To emulate a long-running query, wait for + // To emulate a long-running query, wait for // a few seconds before working with the data. // This command does not do much, but that's the point-- // it does not change your data, in the long run. @@ -618,19 +618,19 @@ Next, compile and execute the following: "WHERE ReorderPoint Is Not Null;" + "UPDATE Production.Product SET ReorderPoint = ReorderPoint - 1 " + "WHERE ReorderPoint Is Not Null"; - + command = new SqlCommand(commandText, connection); connection.Open(); - + DisplayStatus("Executing..."); isExecuting = true; - // Although it is not required that you pass the - // SqlCommand object as the second parameter in the + // Although it is not required that you pass the + // SqlCommand object as the second parameter in the // BeginExecuteNonQuery call, doing so makes it easier // to call EndExecuteNonQuery in the callback procedure. AsyncCallback callback = new AsyncCallback(HandleCallback); command.BeginExecuteNonQuery(callback, command); - + } catch (Exception ex) { @@ -643,7 +643,7 @@ Next, compile and execute the following: } } } - + private void HandleCallback(IAsyncResult result) { try @@ -658,38 +658,38 @@ Next, compile and execute the following: { rowText = " row affected."; } - + rowText = rowCount + rowText; - + // You may not interact with the form and its contents // from a different thread, and this callback procedure // is all but guaranteed to be running from a different thread - // than the form. Therefore you cannot simply call code that + // than the form. Therefore you cannot simply call code that // displays the results, like this: // DisplayResults(rowText) - + // Instead, you must call the procedure from the form's thread. // One simple way to accomplish this is to call the Invoke // method of the form, which calls the delegate you supply - // from the form's thread. + // from the form's thread. DisplayInfoDelegate del = new DisplayInfoDelegate(DisplayResults); this.Invoke(del, rowText); - + } catch (Exception ex) { - // Because you are now running code in a separate thread, + // Because you are now running code in a separate thread, // if you do not handle the exception here, none of your other - // code catches the exception. Because none of + // code catches the exception. Because none of // your code is on the call stack in this thread, there is nothing - // higher up the stack to catch the exception if you do not - // handle it here. You can either log the exception or - // invoke a delegate (as in the non-error case in this + // higher up the stack to catch the exception if you do not + // handle it here. You can either log the exception or + // invoke a delegate (as in the non-error case in this // example) to display the error on the form. In no case // can you simply display the error without executing a delegate - // as in the try block here. - - // You can create the delegate instance as you + // as in the try block here. + + // You can create the delegate instance as you // invoke it, like this: this.Invoke(new DisplayInfoDelegate(DisplayStatus), String.Format("Ready(last error: {0}", ex.Message)); @@ -851,7 +851,7 @@ Next, compile and execute the following: // Display the data within the reader. while (reader.Read()) { - // Display all the columns. + // Display all the columns. for (int i = 0; i < reader.FieldCount; i++) { Console.Write("{0}\t", reader.GetValue(i)); @@ -862,8 +862,8 @@ Next, compile and execute the following: private static string GetConnectionString() { - // To avoid storing the connection string in your code, - // you can retrieve it from a configuration file. + // To avoid storing the connection string in your code, + // you can retrieve it from a configuration file. return "Data Source=(local);Integrated Security=true;" + "Initial Catalog=AdventureWorks"; @@ -953,54 +953,54 @@ Next, compile and execute the following: using System; using System.Data; using Microsoft.Data.SqlClient; - + class Class1 { static void Main() { // This example is not terribly useful, but it proves a point. - // The WAITFOR statement simply adds enough time to prove the + // The WAITFOR statement simply adds enough time to prove the // asynchronous nature of the command. string commandText = "WAITFOR DELAY '00:00:03';" + "SELECT ProductID, Name FROM Production.Product WHERE ListPrice < 100"; - + RunCommandAsynchronously(commandText, GetConnectionString()); - + Console.WriteLine("Press ENTER to continue."); Console.ReadLine(); } - + private static void RunCommandAsynchronously(string commandText, string connectionString) { // Given command text and connection string, asynchronously execute // the specified command against the connection. For this example, - // the code displays an indicator as it is working, verifying the - // asynchronous behavior. + // the code displays an indicator as it is working, verifying the + // asynchronous behavior. try { // The code does not need to handle closing the connection explicitly-- // the use of the CommandBehavior.CloseConnection option takes care - // of that for you. + // of that for you. SqlConnection connection = new SqlConnection(connectionString); SqlCommand command = new SqlCommand(commandText, connection); - + connection.Open(); IAsyncResult result = command.BeginExecuteReader(CommandBehavior.CloseConnection); - + // Although it is not necessary, the following code - // displays a counter in the console window, indicating that - // the main thread is not blocked while awaiting the command + // displays a counter in the console window, indicating that + // the main thread is not blocked while awaiting the command // results. int count = 0; while (!result.IsCompleted) { Console.WriteLine("Waiting ({0})", count++); // Wait for 1/10 second, so the counter - // does not consume all available resources + // does not consume all available resources // on the main thread. System.Threading.Thread.Sleep(100); } - + using (SqlDataReader reader = command.EndExecuteReader(result)) { DisplayResults(reader); @@ -1021,26 +1021,26 @@ Next, compile and execute the following: Console.WriteLine("Error: {0}", ex.Message); } } - + private static void DisplayResults(SqlDataReader reader) { // Display the data within the reader. while (reader.Read()) { - // Display all the columns. + // Display all the columns. for (int i = 0; i < reader.FieldCount; i++) { Console.Write("{0}\t", reader.GetValue(i)); } - + Console.WriteLine(); } } - + private static string GetConnectionString() { - // To avoid storing the connection string in your code, - // you can retrieve it from a configuration file. + // To avoid storing the connection string in your code, + // you can retrieve it from a configuration file. return "Data Source=(local);Integrated Security=true;" + "Initial Catalog=AdventureWorks"; } @@ -1138,54 +1138,54 @@ Next, compile and execute the following: using System; using System.Data; using Microsoft.Data.SqlClient; - + class Class1 { static void Main() { // This example is not terribly useful, but it proves a point. - // The WAITFOR statement simply adds enough time to prove the + // The WAITFOR statement simply adds enough time to prove the // asynchronous nature of the command. string commandText = "WAITFOR DELAY '00:00:03';" + "SELECT ProductID, Name FROM Production.Product WHERE ListPrice < 100"; - + RunCommandAsynchronously(commandText, GetConnectionString()); - + Console.WriteLine("Press ENTER to continue."); Console.ReadLine(); } - + private static void RunCommandAsynchronously(string commandText, string connectionString) { // Given command text and connection string, asynchronously execute // the specified command against the connection. For this example, - // the code displays an indicator as it is working, verifying the - // asynchronous behavior. + // the code displays an indicator as it is working, verifying the + // asynchronous behavior. try { // The code does not need to handle closing the connection explicitly-- // the use of the CommandBehavior.CloseConnection option takes care - // of that for you. + // of that for you. SqlConnection connection = new SqlConnection(connectionString); SqlCommand command = new SqlCommand(commandText, connection); - + connection.Open(); IAsyncResult result = command.BeginExecuteReader(CommandBehavior.CloseConnection); - + // Although it is not necessary, the following code - // displays a counter in the console window, indicating that - // the main thread is not blocked while awaiting the command + // displays a counter in the console window, indicating that + // the main thread is not blocked while awaiting the command // results. int count = 0; while (!result.IsCompleted) { Console.WriteLine("Waiting ({0})", count++); // Wait for 1/10 second, so the counter - // does not consume all available resources + // does not consume all available resources // on the main thread. System.Threading.Thread.Sleep(100); } - + using (SqlDataReader reader = command.EndExecuteReader(result)) { DisplayResults(reader); @@ -1206,26 +1206,26 @@ Next, compile and execute the following: Console.WriteLine("Error: {0}", ex.Message); } } - + private static void DisplayResults(SqlDataReader reader) { // Display the data within the reader. while (reader.Read()) { - // Display all the columns. + // Display all the columns. for (int i = 0; i < reader.FieldCount; i++) { Console.Write("{0}\t", reader.GetValue(i)); } - + Console.WriteLine(); } } - + private static string GetConnectionString() { - // To avoid storing the connection string in your code, - // you can retrieve it from a configuration file. + // To avoid storing the connection string in your code, + // you can retrieve it from a configuration file. return "Data Source=(local);Integrated Security=true;" + "Initial Catalog=AdventureWorks"; } @@ -1335,7 +1335,7 @@ Next, compile and execute the following: using System.Text; using System.Windows.Forms; using Microsoft.Data.SqlClient; - + namespace Microsoft.AdoDotNet.CodeSamples { public partial class Form1 : Form @@ -1344,28 +1344,28 @@ Next, compile and execute the following: { InitializeComponent(); } - - // Hook up the form's Load event handler (you can double-click on - // the form's design surface in Visual Studio), and then add + + // Hook up the form's Load event handler (you can double-click on + // the form's design surface in Visual Studio), and then add // this code to the form's class: // You need this delegate in order to fill the grid from // a thread other than the form's thread. See the HandleCallback // procedure for more information. private delegate void FillGridDelegate(SqlDataReader reader); - + // You need this delegate to update the status bar. private delegate void DisplayStatusDelegate(string Text); - + // This flag ensures that the user does not attempt - // to restart the command or close the form while the + // to restart the command or close the form while the // asynchronous command is executing. private bool isExecuting; - + private void DisplayStatus(string Text) { this.label1.Text = Text; } - + private void FillGrid(SqlDataReader reader) { try @@ -1385,7 +1385,7 @@ Next, compile and execute the following: finally { // Closing the reader also closes the connection, - // because this reader was created using the + // because this reader was created using the // CommandBehavior.CloseConnection value. if (reader != null) { @@ -1393,7 +1393,7 @@ Next, compile and execute the following: } } } - + private void HandleCallback(IAsyncResult result) { try @@ -1403,37 +1403,37 @@ Next, compile and execute the following: // of the IAsyncResult parameter. SqlCommand command = (SqlCommand)result.AsyncState; SqlDataReader reader = command.EndExecuteReader(result); - + // You may not interact with the form and its contents // from a different thread, and this callback procedure // is all but guaranteed to be running from a different thread - // than the form. Therefore you cannot simply call code that + // than the form. Therefore you cannot simply call code that // fills the grid, like this: // FillGrid(reader); // Instead, you must call the procedure from the form's thread. // One simple way to accomplish this is to call the Invoke // method of the form, which calls the delegate you supply - // from the form's thread. + // from the form's thread. FillGridDelegate del = new FillGridDelegate(FillGrid); this.Invoke(del, reader); - - // Do not close the reader here, because it is being used in + + // Do not close the reader here, because it is being used in // a separate thread. Instead, have the procedure you have // called close the reader once it is done with it. } catch (Exception ex) { - // Because you are now running code in a separate thread, + // Because you are now running code in a separate thread, // if you do not handle the exception here, none of your other - // code catches the exception. Because there is none of + // code catches the exception. Because there is none of // your code on the call stack in this thread, there is nothing - // higher up the stack to catch the exception if you do not - // handle it here. You can either log the exception or - // invoke a delegate (as in the non-error case in this + // higher up the stack to catch the exception if you do not + // handle it here. You can either log the exception or + // invoke a delegate (as in the non-error case in this // example) to display the error on the form. In no case // can you simply display the error without executing a delegate - // as in the try block here. - // You can create the delegate instance as you + // as in the try block here. + // You can create the delegate instance as you // invoke it, like this: this.Invoke(new DisplayStatusDelegate(DisplayStatus), "Error: " + ex.Message); } @@ -1442,15 +1442,15 @@ Next, compile and execute the following: isExecuting = false; } } - + private string GetConnectionString() { - // To avoid storing the connection string in your code, - // you can retrieve it from a configuration file. + // To avoid storing the connection string in your code, + // you can retrieve it from a configuration file. return "Data Source=(local);Integrated Security=true;" + "Initial Catalog=AdventureWorks"; } - + private void button1_Click(object sender, System.EventArgs e) { if (isExecuting) @@ -1467,17 +1467,17 @@ Next, compile and execute the following: { DisplayStatus("Connecting..."); connection = new SqlConnection(GetConnectionString()); - // To emulate a long-running query, wait for + // To emulate a long-running query, wait for // a few seconds before retrieving the real data. command = new SqlCommand("WAITFOR DELAY '0:0:5';" + "SELECT ProductID, Name, ListPrice, Weight FROM Production.Product", connection); connection.Open(); - + DisplayStatus("Executing..."); isExecuting = true; - // Although it is not required that you pass the - // SqlCommand object as the second parameter in the + // Although it is not required that you pass the + // SqlCommand object as the second parameter in the // BeginExecuteReader call, doing so makes it easier // to call EndExecuteReader in the callback procedure. AsyncCallback callback = new AsyncCallback(HandleCallback); @@ -1494,13 +1494,13 @@ Next, compile and execute the following: } } } - + private void Form1_Load(object sender, System.EventArgs e) { this.button1.Click += new System.EventHandler(this.button1_Click); this.FormClosing += new FormClosingEventHandler(Form1_FormClosing); } - + void Form1_FormClosing(object sender, FormClosingEventArgs e) { if (isExecuting) @@ -1605,58 +1605,58 @@ Next, compile and execute the following: using System.Data; using Microsoft.Data.SqlClient; using System.Xml; - + class Class1 { static void Main() { // This example is not terribly effective, but it proves a point. - // The WAITFOR statement simply adds enough time to prove the + // The WAITFOR statement simply adds enough time to prove the // asynchronous nature of the command. string commandText = "WAITFOR DELAY '00:00:03';" + "SELECT Name, ListPrice FROM Production.Product " + "WHERE ListPrice < 100 " + "FOR XML AUTO, XMLDATA"; - + RunCommandAsynchronously(commandText, GetConnectionString()); - + Console.WriteLine("Press ENTER to continue."); Console.ReadLine(); } - + private static void RunCommandAsynchronously(string commandText, string connectionString) { // Given command text and connection string, asynchronously execute // the specified command against the connection. For this example, - // the code displays an indicator as it is working, verifying the - // asynchronous behavior. + // the code displays an indicator as it is working, verifying the + // asynchronous behavior. using (SqlConnection connection = new SqlConnection(connectionString)) { SqlCommand command = new SqlCommand(commandText, connection); - + connection.Open(); IAsyncResult result = command.BeginExecuteXmlReader(); - + // Although it is not necessary, the following procedure - // displays a counter in the console window, indicating that - // the main thread is not blocked while awaiting the command + // displays a counter in the console window, indicating that + // the main thread is not blocked while awaiting the command // results. int count = 0; while (!result.IsCompleted) { Console.WriteLine("Waiting ({0})", count++); // Wait for 1/10 second, so the counter - // does not consume all available resources + // does not consume all available resources // on the main thread. System.Threading.Thread.Sleep(100); } - + XmlReader reader = command.EndExecuteXmlReader(result); DisplayProductInfo(reader); } } - + private static void DisplayProductInfo(XmlReader reader) { // Display the data within the reader. @@ -1670,11 +1670,11 @@ Next, compile and execute the following: } } } - + private static string GetConnectionString() { - // To avoid storing the connection string in your code, - // you can retrieve it from a configuration file. + // To avoid storing the connection string in your code, + // you can retrieve it from a configuration file. return "Data Source=(local);Integrated Security=true;" + "Initial Catalog=AdventureWorks"; } @@ -1788,49 +1788,49 @@ Next, compile and execute the following: using System.Windows.Forms; using System.Xml; using Microsoft.Data.SqlClient; - + namespace Microsoft.AdoDotNet.CodeSamples { public partial class Form1 : Form { - // Hook up the form's Load event handler and then add + // Hook up the form's Load event handler and then add // this code to the form's class: // You need these delegates in order to display text from a thread // other than the form's thread. See the HandleCallback // procedure for more information. private delegate void DisplayInfoDelegate(string Text); private delegate void DisplayReaderDelegate(XmlReader reader); - + private bool isExecuting; - - // This example maintains the connection object + + // This example maintains the connection object // externally, so that it is available for closing. private SqlConnection connection; - + public Form1() { InitializeComponent(); } - + private string GetConnectionString() { - // To avoid storing the connection string in your code, - // you can retrieve it from a configuration file. + // To avoid storing the connection string in your code, + // you can retrieve it from a configuration file. return "Data Source=(local);Integrated Security=true;" + "Initial Catalog=AdventureWorks"; } - + private void DisplayStatus(string Text) { this.label1.Text = Text; } - + private void ClearProductInfo() { // Clear the list box. this.listBox1.Items.Clear(); } - + private void DisplayProductInfo(XmlReader reader) { // Display the data within the reader. @@ -1845,7 +1845,7 @@ Next, compile and execute the following: } DisplayStatus("Ready"); } - + private void Form1_FormClosing(object sender, System.Windows.Forms.FormClosingEventArgs e) { @@ -1856,7 +1856,7 @@ Next, compile and execute the following: e.Cancel = true; } } - + private void button1_Click(object sender, System.EventArgs e) { if (isExecuting) @@ -1873,27 +1873,27 @@ Next, compile and execute the following: ClearProductInfo(); DisplayStatus("Connecting..."); connection = new SqlConnection(GetConnectionString()); - - // To emulate a long-running query, wait for + + // To emulate a long-running query, wait for // a few seconds before working with the data. string commandText = "WAITFOR DELAY '00:00:03';" + "SELECT Name, ListPrice FROM Production.Product " + "WHERE ListPrice < 100 " + "FOR XML AUTO, XMLDATA"; - + command = new SqlCommand(commandText, connection); connection.Open(); - + DisplayStatus("Executing..."); isExecuting = true; - // Although it is not required that you pass the - // SqlCommand object as the second parameter in the + // Although it is not required that you pass the + // SqlCommand object as the second parameter in the // BeginExecuteXmlReader call, doing so makes it easier // to call EndExecuteXmlReader in the callback procedure. AsyncCallback callback = new AsyncCallback(HandleCallback); command.BeginExecuteXmlReader(callback, command); - + } catch (Exception ex) { @@ -1906,7 +1906,7 @@ Next, compile and execute the following: } } } - + private void HandleCallback(IAsyncResult result) { try @@ -1916,34 +1916,34 @@ Next, compile and execute the following: // of the IAsyncResult parameter. SqlCommand command = (SqlCommand)result.AsyncState; XmlReader reader = command.EndExecuteXmlReader(result); - + // You may not interact with the form and its contents // from a different thread, and this callback procedure // is all but guaranteed to be running from a different thread - // than the form. - + // than the form. + // Instead, you must call the procedure from the form's thread. // One simple way to accomplish this is to call the Invoke // method of the form, which calls the delegate you supply - // from the form's thread. + // from the form's thread. DisplayReaderDelegate del = new DisplayReaderDelegate(DisplayProductInfo); this.Invoke(del, reader); - + } catch (Exception ex) { - // Because you are now running code in a separate thread, + // Because you are now running code in a separate thread, // if you do not handle the exception here, none of your other - // code catches the exception. Because none of + // code catches the exception. Because none of // your code is on the call stack in this thread, there is nothing - // higher up the stack to catch the exception if you do not - // handle it here. You can either log the exception or - // invoke a delegate (as in the non-error case in this + // higher up the stack to catch the exception if you do not + // handle it here. You can either log the exception or + // invoke a delegate (as in the non-error case in this // example) to display the error on the form. In no case // can you simply display the error without executing a delegate - // as in the try block here. - - // You can create the delegate instance as you + // as in the try block here. + + // You can create the delegate instance as you // invoke it, like this: this.Invoke(new DisplayInfoDelegate(DisplayStatus), String.Format("Ready(last error: {0}", ex.Message)); @@ -1957,7 +1957,7 @@ Next, compile and execute the following: } } } - + private void Form1_Load(object sender, System.EventArgs e) { this.button1.Click += new System.EventHandler(this.button1_Click); @@ -2031,22 +2031,22 @@ Next, compile and execute the following: using System.Data; using System.Threading; using Microsoft.Data.SqlClient; - + class Program { private static SqlCommand m_rCommand; - + public static SqlCommand Command { get { return m_rCommand; } set { m_rCommand = value; } } - + public static void Thread_Cancel() { Command.Cancel(); } - + static void Main() { string connectionString = GetConnectionString(); @@ -2055,7 +2055,7 @@ Next, compile and execute the following: using (SqlConnection connection = new SqlConnection(connectionString)) { connection.Open(); - + Command = connection.CreateCommand(); Command.CommandText = "DROP TABLE TestCancel"; try @@ -2063,19 +2063,19 @@ Next, compile and execute the following: Command.ExecuteNonQuery(); } catch { } - + Command.CommandText = "CREATE TABLE TestCancel(co1 int, co2 char(10))"; Command.ExecuteNonQuery(); Command.CommandText = "INSERT INTO TestCancel VALUES (1, '1')"; Command.ExecuteNonQuery(); - + Command.CommandText = "SELECT * FROM TestCancel"; SqlDataReader reader = Command.ExecuteReader(); - + Thread rThread2 = new Thread(new ThreadStart(Thread_Cancel)); rThread2.Start(); rThread2.Join(); - + reader.Read(); System.Console.WriteLine(reader.FieldCount); reader.Close(); @@ -2088,7 +2088,7 @@ Next, compile and execute the following: } static private string GetConnectionString() { - // To avoid storing the connection string in your code, + // To avoid storing the connection string in your code, // you can retrieve it from a configuration file. return "Data Source=(local);Initial Catalog=AdventureWorks;" + "Integrated Security=SSPI"; @@ -2528,7 +2528,7 @@ If the option is enabled and a parameter with Direction Output or InputOutput is Although the returns no rows, any output parameters or return values mapped to parameters are populated with data. - For UPDATE, INSERT, and DELETE statements, the return value is the number of rows affected by the command. For all other types of statements, the return value is -1. When a trigger exists on a table being inserted or updated, the return value includes the number of rows affected by both the insert or update operation and the number of rows affected by the trigger or triggers. When SET NOCOUNT ON is set on the connection (before or as part of executing the command, or as part of a trigger initiated by the execution of the command) the rows affected by individual statements stop contributing to the count of rows affected that is returned by this method. If no statements are detected that contribute to the count, the return value is -1. If a rollback occurs, the return value is also -1. + For UPDATE, INSERT, and DELETE statements, the return value is the number of rows affected by the command. For all other types of statements, the return value is -1. When a trigger exists on a table being inserted or updated, the return value includes the number of rows affected by both the insert or update operation and the number of rows affected by the trigger or triggers. When SET NOCOUNT ON is set on the connection (before or as part of executing the command, or as part of a trigger initiated by the execution of the command) the rows affected by individual statements stop contributing to the count of rows affected that is returned by this method. If no statements are detected that contribute to the count, the return value is -1. If a rollback occurs, the return value is also -1. @@ -2540,7 +2540,7 @@ If the option is enabled and a parameter with Direction Output or InputOutput is using System; using System.Data; using Microsoft.Data.SqlClient; - + namespace SqlCommandCS { class Program @@ -2682,7 +2682,7 @@ If you use or , and then executes it by passing a string that is a Transact-SQL SELECT statement, and a string to use to connect to the data source. -[!code-csharp[SqlCommand_ExecuteReader](~/../sqlclient/doc/samples/SqlCommand_ExecuteReader.cs)] +[!code-csharp[SqlCommand_ExecuteReader](~/../sqlclient/doc/samples/SqlCommand_ExecuteReader.cs#1)] ]]> @@ -2750,45 +2750,9 @@ If you use or , and then executes it by passing a string that is a Transact-SQL SELECT statement, and a string to use to connect to the data source. is set to . -[!code-csharp[SqlCommand_ExecuteReader2](~/../sqlclient/doc/samples/SqlCommand_ExecuteReader2.cs#1)] +[!code-csharp[SqlCommand_ExecuteReader2#1](~/../sqlclient/doc/samples/SqlCommand_ExecuteReader2.cs#1)] ]]> - - - The following example creates a , and then executes it by passing a string that is a Transact-SQL SELECT statement, and a string to use to connect to the data source. is set to . - - - - using System; - using System.Data; - using Microsoft.Data.SqlClient; - - class Program - { - static void Main() - { - string str = "Data Source=(local);Initial Catalog=Northwind;" - + "Integrated Security=SSPI"; - string qs = "SELECT OrderID, CustomerID FROM dbo.Orders;"; - CreateCommand(qs, str); - } - - private static void CreateCommand(string queryString, string connectionString) - { - using (SqlConnection connection = new SqlConnection(connectionString)) - { - SqlCommand command = new SqlCommand(queryString, connection); - connection.Open(); - SqlDataReader reader = command.ExecuteReader(CommandBehavior.CloseConnection); - while (reader.Read()) - { - Console.WriteLine(String.Format("{0}", reader[0])); - } - } - } - } - - @@ -3076,7 +3040,7 @@ For more information about asynchronous programming in the .NET Framework Data P using System; using System.Data; using Microsoft.Data.SqlClient; - + public class Sample { public void CreateSqlCommand(string queryString, SqlConnection connection) @@ -3218,7 +3182,7 @@ For more information about asynchronous programming in the .NET Framework Data P using System; using System.Data; using Microsoft.Data.SqlClient; - + private static void CreateXMLReader(string queryString, string connectionString) { using (SqlConnection connection = new SqlConnection(connectionString)) @@ -3474,9 +3438,9 @@ The blocking connection simulates a situation like a command still running in th ### How to use with legacy asynchronous commands Besides assigning the provider to the command and executing the command, it's possible to run it directly using the following methods: -- +- - -- +- [!code-csharp[SqlConfigurableRetryLogic_SqlCommand#4](~/../sqlclient/doc/samples/SqlConfigurableRetryLogic_SqlCommand.cs#4)] @@ -3526,67 +3490,6 @@ The following example demonstrates how to create a - - - The following example demonstrates how to create a and add parameters to the . - - - - using System; - using System.Data; - using Microsoft.Data.SqlClient; - - class Program - { - static void Main() - { - string connectionString = GetConnectionString(); - string demo = @"<StoreSurvey xmlns=""http://schemas.microsoft.com/sqlserver/2004/07/adventure-works/StoreSurvey""><AnnualSales>1500000</AnnualSales><AnnualRevenue>150000</AnnualRevenue><BankName>Primary International</BankName><BusinessType>OS</BusinessType><YearOpened>1974</YearOpened><Specialty>Road</Specialty><SquareFeet<38000</SquareFeet><Brands>3</Brands><Internet>DSL</Internet><NumberEmployees>40</NumberEmployees></StoreSurvey>"; - Int32 id = 3; - UpdateDemographics(id, demo, connectionString); - Console.ReadLine(); - } - private static void UpdateDemographics(Int32 customerID, - string demoXml, string connectionString) - { - // Update the demographics for a store, which is stored - // in an xml column. - string commandText = "UPDATE Sales.Store SET Demographics = @demographics " - + "WHERE CustomerID = @ID;"; - - using (SqlConnection connection = new SqlConnection(connectionString)) - { - SqlCommand command = new SqlCommand(commandText, connection); - command.Parameters.Add("@ID", SqlDbType.Int); - command.Parameters["@ID"].Value = customerID; - - // Use AddWithValue to assign Demographics. - // SQL Server will implicitly convert strings into XML. - command.Parameters.AddWithValue("@demographics", demoXml); - - try - { - connection.Open(); - Int32 rowsAffected = command.ExecuteNonQuery(); - Console.WriteLine("RowsAffected: {0}", rowsAffected); - } - catch (Exception ex) - { - Console.WriteLine(ex.Message); - } - } - } - - static private string GetConnectionString() - { - // To avoid storing the connection string in your code, - // you can retrieve it from a configuration file. - return "Data Source=(local);Initial Catalog=AdventureWorks;" - + "Integrated Security=SSPI"; - } - } - - @@ -3606,7 +3509,7 @@ If you call an `Execute` method after calling . -For vector data types, the property is ignored. The size of the vector is inferred from the of type . +For vector data types, the property is ignored. The size of the vector is inferred from the of type . Prior to Visual Studio 2010, threw an exception. Beginning in Visual Studio 2010, this method does not throw an exception. diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlDataAdapter.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlDataAdapter.xml index 7a2f32a0bf..468c5046a6 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlDataAdapter.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlDataAdapter.xml @@ -72,7 +72,7 @@ The following example uses the , , , , , , , , , , , , , , , , , , , The values in the are moved to the parameter values. - The event is raised. + The event is raised. The command executes. If the command is set to FirstReturnedRecord, the first returned result is placed in the . If there are output parameters, they are placed in the . - The event is raised. + The event is raised. is called. @@ -853,55 +853,55 @@ The following example uses the , , , The values in the are moved to the parameter values. - The event is raised. + The event is raised. The command executes. If the command is set to FirstReturnedRecord, the first returned result is placed in the . If there are output parameters, they are placed in the . - The event is raised. + The event is raised. is called. @@ -966,55 +966,55 @@ The following example uses the , , , method retur - Gets the value of the specified column as a . + Gets the value of the specified column as a . - A object representing the column at the given ordinal. + A object representing the column at the given ordinal. The index passed was outside the range of 0 to - 1 @@ -979,7 +979,7 @@ The method retur An attempt was made to read or access columns in a closed . - The retrieved data is not compatible with the type. + The retrieved data is not compatible with the type. No conversions are performed; therefore, the data retrieved must already be a vector value, or an exception is generated. @@ -1193,7 +1193,7 @@ The method retur There is no data ready to be read (for example, the first hasn't been called, or returned false). Tried to read a previously-read column in sequential mode. There was an asynchronous operation in progress. This applies to all Get* methods when running in sequential mode, as they could be called while reading a stream. - + Trying to read a column that does not exist. @@ -1282,21 +1282,21 @@ The method retur // enough for all the columns. Object[] values = new Object[reader.FieldCount]; int fieldCount = reader.GetValues(values); - + Console.WriteLine("reader.GetValues retrieved {0} columns.", fieldCount); for (int i = 0; i < fieldCount; i++) { Console.WriteLine(values[i]); } - + Console.WriteLine(); - - // Now repeat, using an array that may contain a different + + // Now repeat, using an array that may contain a different // number of columns than the original data. This should work correctly, - // whether the size of the array is larger or smaller than + // whether the size of the array is larger or smaller than // the number of columns. - + // Attempt to retrieve three columns of data. values = new Object[3]; fieldCount = reader.GetValues(values); @@ -1334,7 +1334,7 @@ The method retur There is no data ready to be read (for example, the first hasn't been called, or returned false). Trying to read a previously read column in sequential mode. There was an asynchronous operation in progress. This applies to all Get* methods when running in sequential mode, as they could be called while reading a stream. - + Trying to read a column that does not exist. @@ -1394,7 +1394,7 @@ The method retur using System; using System.Data; using Microsoft.Data.SqlClient; - + class Program { static void Main(string[] args) @@ -1448,7 +1448,7 @@ The method retur There is no data ready to be read (for example, the first hasn't been called, or returned false). Trying to read a previously read column in sequential mode. There was an asynchronous operation in progress. This applies to all Get* methods when running in sequential mode, as they could be called while reading a stream. - + Trying to read a column that does not exist. diff --git a/eng/dashboards/ado.net-pipelines-ci-builds-by-branch.md b/eng/dashboards/ado.net-pipelines-ci-builds-by-branch.md index 03a32d4e78..ef6ed4a268 100644 --- a/eng/dashboards/ado.net-pipelines-ci-builds-by-branch.md +++ b/eng/dashboards/ado.net-pipelines-ci-builds-by-branch.md @@ -13,6 +13,8 @@ The branches listed below indicate the repo branch used for the triggered runs. |-|-|-|-|-|-|-| |MDS Main CI|[internal/main](https://sqlclientdrivers.visualstudio.com/ADO.Net/_git/dotnet-sqlclient)|Release|No|Yes|Weekdays 01:00 UTC|YAML| |MDS Main CI-Package|[internal/main](https://sqlclientdrivers.visualstudio.com/ADO.Net/_git/dotnet-sqlclient)|Release|No|Yes|Weekdays 01:00 UTC|YAML| +|MDS Main CI|[internal/release/7.0](https://sqlclientdrivers.visualstudio.com/ADO.Net/_git/dotnet-sqlclient?path=%2F&version=GBinternal%2Frelease%2F7.0&_a=contents)|Release|No|Yes|Sunday 06:00 UTC|YAML| +|MDS Main CI-Package|[internal/release/7.0](https://sqlclientdrivers.visualstudio.com/ADO.Net/_git/dotnet-sqlclient?path=%2F&version=GBinternal%2Frelease%2F7.0&_a=contents)|Release|No|Yes|Sunday 06:30 UTC|YAML| |MDS Main CI|[internal/release/6.1](https://sqlclientdrivers.visualstudio.com/ADO.Net/_git/dotnet-sqlclient?path=%2F&version=GBinternal%2Frelease%2F6.1&_a=contents)|Release|No|Yes|Weekdays 01:00 UTC|YAML| |MDS Main CI-Package|[internal/release/6.1](https://sqlclientdrivers.visualstudio.com/ADO.Net/_git/dotnet-sqlclient?path=%2F&version=GBinternal%2Frelease%2F6.1&_a=contents)|Release|No|Yes|Weekdays 01:00 UTC|YAML| |MDS Main CI|[internal/release/6.0](https://sqlclientdrivers.visualstudio.com/ADO.Net/_git/dotnet-sqlclient?path=%2F&version=GBinternal%2Frelease%2F6.0&_a=contents)|Release|No|Yes|Weekdays 01:00 UTC|YAML| diff --git a/eng/dashboards/public-pipelines-ci-builds-by-branch.md b/eng/dashboards/public-pipelines-ci-builds-by-branch.md index ab6f3eb4e4..8dfad35070 100644 --- a/eng/dashboards/public-pipelines-ci-builds-by-branch.md +++ b/eng/dashboards/public-pipelines-ci-builds-by-branch.md @@ -16,6 +16,8 @@ PR pipelines run on the topic branch associated to the PR. |PR-SqlClient-Package|N/A|Debug|Yes|No|None|YAML| |CI-SqlClient|[main](https://github.com/dotnet/SqlClient)|Release|No|Yes|Weekdays 01:00 UTC|YAML| |CI-SqlClient-Package|[main](https://github.com/dotnet/SqlClient)|Release|No|Yes|Weekdays 03:00 UTC|YAML| +|CI-SqlClient|[release/7.0](https://github.com/dotnet/SqlClient/tree/release/7.0)|Release|No|Yes|Sunday 05:00 UTC|YAML| +|CI-SqlClient-Package|[release/7.0](https://github.com/dotnet/SqlClient/tree/release/7.0)|Release|No|Yes|Sunday 05:30 UTC|YAML| |CI-SqlClient|[release/6.1](https://github.com/dotnet/SqlClient/tree/release/6.1)|Release|No|Yes|Sunday 04:00 UTC|YAML| |CI-SqlClient-Package|[release/6.1](https://github.com/dotnet/SqlClient/tree/release/6.1)|Release|No|Yes|Sunday 04:30 UTC|YAML| |CI-SqlClient|[release/6.0](https://github.com/dotnet/SqlClient/tree/release/6.0)|Release|No|Yes|Sunday 06:00 UTC|YAML| diff --git a/eng/pipelines/common/templates/jobs/ci-build-nugets-job.yml b/eng/pipelines/common/templates/jobs/ci-build-nugets-job.yml index 5e17b506c1..43791bb0d7 100644 --- a/eng/pipelines/common/templates/jobs/ci-build-nugets-job.yml +++ b/eng/pipelines/common/templates/jobs/ci-build-nugets-job.yml @@ -143,16 +143,29 @@ jobs: abstractionsPackageVersion: ${{parameters.abstractionsPackageVersion}} loggingPackageVersion: ${{ parameters.loggingPackageVersion }} - - template: /eng/pipelines/common/templates/steps/generate-nuget-package-step.yml@self - parameters: - buildConfiguration: ${{ parameters.buildConfiguration }} - displayName: 'Create MDS NuGet Package' - generateSymbolsPackage: true - nuspecPath: 'tools/specs/Microsoft.Data.SqlClient.nuspec' - outputDirectory: $(packagePath) - packageVersion: ${{ parameters.mdsPackageVersion }} - properties: 'AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }};LoggingPackageVersion=${{ parameters.loggingPackageVersion }}' - referenceType: ${{ parameters.referenceType }} + # Create the MDS NuGet package via build.proj's GenerateMdsPackage target. + # This target has access to the version range properties computed in + # Versions.props, so it passes them through to nuget pack automatically. + - task: NuGetToolInstaller@1 + displayName: 'Install NuGet' + inputs: + checkLatest: true + + - task: DotNetCoreCLI@2 + displayName: 'Create MDS NuGet Package' + inputs: + command: build + projects: build.proj + arguments: >- + -t:GenerateMdsPackage + -p:GenerateNuget=true + -p:Configuration=${{ parameters.buildConfiguration }} + -p:ReferenceType=${{ parameters.referenceType }} + -p:MdsPackageVersion=${{ parameters.mdsPackageVersion }} + -p:AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }} + -p:LoggingPackageVersion=${{ parameters.loggingPackageVersion }} + -p:PackagesDir=$(packagePath) + -p:NuGetCmd=nuget.exe # When building in Package mode, the AKV Provider restore needs to find the MDS package # we just built. Copy it to the local NuGet feed so NuGet.config can resolve it. diff --git a/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml b/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml index 76a3bf3c75..8666cd0dbb 100644 --- a/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml +++ b/eng/pipelines/common/templates/jobs/ci-run-tests-job.yml @@ -326,6 +326,33 @@ jobs: ${{ if parameters.configProperties.FileStreamDirectory }}: fileStreamDirectory: ${{ parameters.configProperties.FileStreamDirectory }} + # Set up for x86 tests by manually installing dotnet for x86 to an alternative location. This + # is only used to execute the test runtime (framework to test is specified in build params), so + # it should be acceptable to just install a specific version in all cases. + # @TODO: This setup is very confusing. Ideally we should just be utilizing the dotnet installation + # earlier in the job. There has to be a cleaner way of doing this. + - ${{ if and(eq(parameters.enableX86Test, true), eq(parameters.operatingSystem, 'Windows')) }}: + - ${{ if ne(variables['dotnetx86RootPath'], '') }}: + # Install the .NET SDK and Runtimes for x86. + - template: /eng/pipelines/steps/install-dotnet.yml@self + parameters: + architecture: x86 + debug: ${{ parameters.debug }} + installDir: $(dotnetx86RootPath) + runtimes: [8.x, 9.x] + + # Ensure TestResults directory exists so that publish steps don't fail when + # tests are skipped due to a setup failure. + - pwsh: New-Item -ItemType Directory -Path TestResults -Force | Out-Null + displayName: 'Create TestResults Directory' + + # Gate: record that all setup steps (SDK install, build, SQL config, x86 SDK + # install, etc.) completed successfully. Test steps condition on this variable + # so they are skipped when setup fails, yet remain independent of each + # other's results. + - pwsh: Write-Host '##vso[task.setvariable variable=setupSucceeded]true' + displayName: 'Gate: Mark Setup Succeeded' + - ${{ if eq(parameters.enableX64Test, true) }}: # run native tests - template: /eng/pipelines/common/templates/steps/run-all-tests-step.yml@self # run tests parameters: @@ -340,20 +367,6 @@ jobs: mdsPackageVersion: ${{ parameters.mdsPackageVersion }} - ${{ if and(eq(parameters.enableX86Test, true), eq(parameters.operatingSystem, 'Windows')) }}: - # Set up for x86 tests by manually installing dotnet for x86 to an alternative location. This - # is only used to execute the test runtime (framework to test is specified in build params), so - # it should be acceptable to just install a specific version in all cases. - # @TODO: This setup is very confusing. Ideally we should just be utilizing the dotnet installation - # earlier in the job. There has to be a cleaner way of doing this. - - ${{ if ne(variables['dotnetx86RootPath'], '') }}: - # Install the .NET SDK and Runtimes for x86. - - template: /eng/pipelines/steps/install-dotnet.yml@self - parameters: - architecture: x86 - debug: ${{ parameters.debug }} - installDir: $(dotnetx86RootPath) - runtimes: [8.x, 9.x] - - template: /eng/pipelines/common/templates/steps/run-all-tests-step.yml@self parameters: debug: ${{ parameters.debug }} diff --git a/eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml b/eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml index c1b788717d..6e1541f939 100644 --- a/eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml +++ b/eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml @@ -29,36 +29,53 @@ steps: export PS4='+ [$(date "+%Y-%m-%d %H:%M:%S")] ' set -x - # Install Docker and SQLCMD tools. + # Install Docker CLI (not Desktop — Colima provides the daemon) and SQLCMD tools. brew install colima - brew install --cask docker + brew install docker brew tap microsoft/mssql-release https://github.com/Microsoft/homebrew-mssql-release brew update + # Homebrew 5.2+ requires explicit trust for third-party taps. Run this + # after 'brew update' so the trust command is available even if the runner + # image shipped an older Homebrew version. + brew trust microsoft/mssql-release HOMEBREW_ACCEPT_EULA=Y brew install mssql-tools18 - colima start --arch x86_64 + + # Fail fast if sqlcmd was not installed (e.g. tap-trust or formula error). + # Without this check the script would loop for ~6 minutes trying to connect. + if ! command -v sqlcmd &>/dev/null; then + echo "ERROR: sqlcmd is not on PATH after brew install. Check the mssql-tools18 installation above." + exit 1 + fi + + # Start Colima with Virtualization.framework for x86_64 binary translation + # on Apple Silicon. Rosetta/binfmt emulation is enabled by default in + # Colima >= 0.8 when using --vm-type vz, which is dramatically faster than + # --arch x86_64 (full QEMU VM emulation). + # Requires macOS >= 13 (Ventura). + colima start --vm-type vz --cpu 4 --memory 4 docker --version - docker pull mcr.microsoft.com/mssql/server:2025-latest + docker pull --platform linux/amd64 mcr.microsoft.com/mssql/server:2025-latest # Password for the SA user (required) MSSQL_SA_PW="${{ parameters.saPassword }}" - docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$MSSQL_SA_PW" -p 1433:1433 -p 1434:1434 --name sql1 --hostname sql1 -d mcr.microsoft.com/mssql/server:2025-latest + docker run --platform linux/amd64 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$MSSQL_SA_PW" -p 1433:1433 -p 1434:1434 --name sql1 --hostname sql1 -d mcr.microsoft.com/mssql/server:2025-latest - sleep 5 + sleep 10 docker ps -a # Connect to the SQL Server container and get its version. # - # It can take a while for the docker container to start listening and be - # ready for connections, so we will wait for up to 2 minutes, checking every - # 3 seconds. + # With Rosetta 2 emulation, SQL Server starts much faster than under full + # QEMU emulation, but it can still take a minute or two. We allow up to + # 6 minutes (72 attempts × 5 seconds) as a generous upper bound. - # Wait 3 seconds between attempts. - delay=3 + # Wait 5 seconds between attempts. + delay=5 - # Try up to 40 times (2 minutes) to connect. - maxAttempts=40 + # Try up to 72 times (~6 minutes) to connect. + maxAttempts=72 # Attempt counter. attempt=1 @@ -71,7 +88,8 @@ steps: echo "Waiting for SQL Server to start (attempt #$attempt of $maxAttempts)..." - sqlcmd -S 127.0.0.1 -No -U sa -P "$MSSQL_SA_PW" -Q "SELECT @@VERSION" >> $SQLCMD_ERRORS 2>&1 + # -C trusts the self-signed certificate inside the container. + sqlcmd -S 127.0.0.1 -No -C -U sa -P "$MSSQL_SA_PW" -Q "SELECT @@VERSION" >> $SQLCMD_ERRORS 2>&1 # If the command was successful, then the SQL Server is ready. if [ $? -eq 0 ]; then @@ -79,6 +97,16 @@ steps: break fi + # Verify the container is still running; no point retrying if it crashed. + if ! docker ps --filter "name=^/sql1$" --filter "status=running" --format '{{.Names}}' | grep -Fxq 'sql1'; then + echo "ERROR: sql1 container is no longer running." + docker ps -a --filter "name=^/sql1$" + echo "--- Container logs ---" + docker logs sql1 2>&1 | tail -50 + rm -f $SQLCMD_ERRORS + exit 1 + fi + # Increment the attempt counter. ((attempt++)) @@ -91,8 +119,13 @@ steps: if [ $ready -eq 0 ] then # No, so report the error(s) and exit. - echo Cannot connect to SQL Server; installation aborted; errors were: + echo "Cannot connect to SQL Server after $maxAttempts attempts; installation aborted." + echo "--- sqlcmd errors ---" cat $SQLCMD_ERRORS + echo "--- Container status ---" + docker ps -a --filter "name=^/sql1$" + echo "--- Container logs (last 80 lines) ---" + docker logs sql1 2>&1 | tail -80 rm -f $SQLCMD_ERRORS exit 1 fi @@ -101,18 +134,18 @@ steps: echo "Use sqlcmd to show which IP addresses are being listened on..." echo 0.0.0.0 - sqlcmd -S 0.0.0.0 -No -U sa -P "$MSSQL_SA_PW" -Q "SELECT @@VERSION" -l 2 + sqlcmd -S 0.0.0.0 -No -C -U sa -P "$MSSQL_SA_PW" -Q "SELECT @@VERSION" -l 2 echo 127.0.0.1 - sqlcmd -S 127.0.0.1 -No -U sa -P "$MSSQL_SA_PW" -Q "SELECT @@VERSION" -l 2 + sqlcmd -S 127.0.0.1 -No -C -U sa -P "$MSSQL_SA_PW" -Q "SELECT @@VERSION" -l 2 echo ::1 - sqlcmd -S ::1 -No -U sa -P "$MSSQL_SA_PW" -Q "SELECT @@VERSION" -l 2 + sqlcmd -S ::1 -No -C -U sa -P "$MSSQL_SA_PW" -Q "SELECT @@VERSION" -l 2 echo localhost - sqlcmd -S localhost -No -U sa -P "$MSSQL_SA_PW" -Q "SELECT @@VERSION" -l 2 + sqlcmd -S localhost -No -C -U sa -P "$MSSQL_SA_PW" -Q "SELECT @@VERSION" -l 2 echo "(sqlcmd default / not specified)" - sqlcmd -No -U sa -P "$MSSQL_SA_PW" -Q "SELECT @@VERSION" -l 2 + sqlcmd -No -C -U sa -P "$MSSQL_SA_PW" -Q "SELECT @@VERSION" -l 2 echo "Configuring Dedicated Administer Connections to allow remote connections..." - sqlcmd -S 127.0.0.1 -No -U sa -P "$MSSQL_SA_PW" -Q "sp_configure 'remote admin connections', 1; RECONFIGURE;" + sqlcmd -S 127.0.0.1 -No -C -U sa -P "$MSSQL_SA_PW" -Q "sp_configure 'remote admin connections', 1; RECONFIGURE;" if [ $? = 1 ] then echo "Error configuring DAC for remote access." diff --git a/eng/pipelines/common/templates/steps/configure-sql-server-win-step.yml b/eng/pipelines/common/templates/steps/configure-sql-server-win-step.yml index 749c30a79a..50925d00d5 100644 --- a/eng/pipelines/common/templates/steps/configure-sql-server-win-step.yml +++ b/eng/pipelines/common/templates/steps/configure-sql-server-win-step.yml @@ -162,10 +162,28 @@ steps: #Change the access level for FileStream for SQLServer Set-ExecutionPolicy Unrestricted Import-Module "sqlps" - Invoke-Sqlcmd -ServerInstance "$machineName" @" - EXEC sp_configure filestream_access_level, 2; - RECONFIGURE; + + # Retry loop: SQL Server may be temporarily unavailable after enabling FileStream via WMI. + # Worst-case budget: 10 attempts x (5s connection timeout + 5s sleep) = ~100s + $tries = 0 + while ($true) { + $tries++ + try { + Invoke-Sqlcmd -ServerInstance "$machineName" -ConnectionTimeout 5 @" + EXEC sp_configure filestream_access_level, 2; + RECONFIGURE; "@ + Write-Host "FileStream access level configured successfully." + break + } catch { + if ($tries -ge 10) { + Write-Host "##[error]Failed to configure FileStream access level after $tries tries." + throw + } + Write-Host "Failed to connect to SQL Server (attempt $tries/10). Retrying in 5 seconds..." + Start-Sleep -Seconds 5 + } + } displayName: 'Enable FileStream [Win]' env: SQL_USER: ${{parameters.user }} @@ -250,6 +268,31 @@ steps: Restart-Service -Name "$serviceName" -Force Restart-Service -Name MSSQLSERVER* -Force + # Wait for SQL Server to be ready to accept connections after restart. + # Worst-case budget: 20 attempts x (5s connection timeout + 3s sleep) = ~160s + $machineName = $env:COMPUTERNAME + if ("${{parameters.instanceName }}" -ne "MSSQLSERVER") { + $machineName += "\${{parameters.instanceName }}" + } + + Import-Module "sqlps" + $tries = 0 + while ($true) { + $tries++ + try { + Invoke-Sqlcmd -ServerInstance "$machineName" -Query "SELECT @@VERSION" -ConnectionTimeout 5 + Write-Host "SQL Server is ready after restart (attempt $tries)." + break + } catch { + if ($tries -ge 20) { + Write-Host "##[error]SQL Server did not become ready after $tries attempts." + throw + } + Write-Host "Waiting for SQL Server to start (attempt $tries/20)..." + Start-Sleep -Seconds 3 + } + } + displayName: 'Restart SQL Server [Win]' - powershell: | diff --git a/eng/pipelines/common/templates/steps/generate-nuget-package-step.yml b/eng/pipelines/common/templates/steps/generate-nuget-package-step.yml deleted file mode 100644 index 46c5c61492..0000000000 --- a/eng/pipelines/common/templates/steps/generate-nuget-package-step.yml +++ /dev/null @@ -1,80 +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. # -################################################################################# -parameters: - - name: nuspecPath - type: string - - - name: packageVersion - type: string - - - name: outputDirectory - type: string - default: '$(Build.SourcesDirectory)/output' - - # The C# build configuration (e.g. Debug or Release) to use when building the NuGet package. - - name: buildConfiguration - type: string - default: Debug - values: - - Debug - - Release - - - name: generateSymbolsPackage - type: boolean - - - name: displayName - type: string - - - name: installNuget - type: boolean - default: true - - # The C# project reference type to use when building and packing the packages. - - name: referenceType - type: string - values: - # Reference sibling packages as NuGet packages. - - Package - # Reference sibling packages as C# projects. - - Project - - # Semi-colon separated properties to pass to nuget pack via the -properties argument. - - name: properties - type: string - default: '' - -steps: -- ${{ if parameters.installNuget }}: - - task: NuGetToolInstaller@1 - displayName: 'Install Latest Nuget' - inputs: - checkLatest: true - -- powershell: | - $Commit=git rev-parse HEAD - Write-Host "##vso[task.setvariable variable=CommitHead;]$Commit" - displayName: CommitHead - -- task: NuGetCommand@2 - displayName: ${{parameters.displayName }} - inputs: - command: custom - ${{ if parameters.generateSymbolsPackage }}: - arguments: >- - pack - -Symbols - -SymbolPackageFormat snupkg - ${{parameters.nuspecPath}} - -Version ${{parameters.packageVersion}} - -OutputDirectory ${{parameters.outputDirectory}} - -properties "COMMITID=$(CommitHead);Configuration=${{parameters.buildConfiguration}};ReferenceType=${{parameters.referenceType}};${{parameters.properties}}" - ${{else }}: - arguments: >- - pack - ${{parameters.nuspecPath}} - -Version ${{parameters.packageVersion}} - -OutputDirectory ${{parameters.outputDirectory}} - -properties "COMMITID=$(CommitHead);Configuration=${{parameters.buildConfiguration}};ReferenceType=${{parameters.referenceType}};${{parameters.properties}}" diff --git a/eng/pipelines/common/templates/steps/run-all-tests-step.yml b/eng/pipelines/common/templates/steps/run-all-tests-step.yml index b1d0261abc..9a8981bb85 100644 --- a/eng/pipelines/common/templates/steps/run-all-tests-step.yml +++ b/eng/pipelines/common/templates/steps/run-all-tests-step.yml @@ -79,7 +79,7 @@ steps: - ${{if eq(parameters.referenceType, 'Project')}}: - task: MSBuild@1 displayName: 'Run Unit Tests ${{parameters.msbuildArchitecture }}' - condition: succeededOrFailed() + condition: and(eq(variables['setupSucceeded'], 'true'), succeededOrFailed()) inputs: solution: build.proj msbuildArchitecture: ${{parameters.msbuildArchitecture }} @@ -99,7 +99,7 @@ steps: - task: MSBuild@1 displayName: 'Run Flaky Unit Tests ${{parameters.msbuildArchitecture }}' - condition: succeededOrFailed() + condition: and(eq(variables['setupSucceeded'], 'true'), succeededOrFailed()) inputs: solution: build.proj msbuildArchitecture: ${{parameters.msbuildArchitecture }} @@ -124,7 +124,7 @@ steps: - task: MSBuild@1 displayName: 'Run Functional Tests ${{parameters.msbuildArchitecture }}' - condition: succeededOrFailed() + condition: and(eq(variables['setupSucceeded'], 'true'), succeededOrFailed()) inputs: solution: build.proj msbuildArchitecture: ${{parameters.msbuildArchitecture }} @@ -151,7 +151,7 @@ steps: -p:DotnetPath=${{ parameters.dotnetx86RootPath }} - task: MSBuild@1 - condition: succeededOrFailed() + condition: and(eq(variables['setupSucceeded'], 'true'), succeededOrFailed()) displayName: 'Run Flaky Functional Tests ${{parameters.msbuildArchitecture }}' inputs: solution: build.proj @@ -184,7 +184,7 @@ steps: continueOnError: true - task: MSBuild@1 - condition: succeededOrFailed() + condition: and(eq(variables['setupSucceeded'], 'true'), succeededOrFailed()) displayName: 'Run Manual Tests ${{parameters.msbuildArchitecture }}' inputs: solution: build.proj @@ -213,7 +213,7 @@ steps: retryCountOnTaskFailure: ${{parameters.retryCountOnManualTests }} - task: MSBuild@1 - condition: succeededOrFailed() + condition: and(eq(variables['setupSucceeded'], 'true'), succeededOrFailed()) displayName: 'Run Flaky Manual Tests ${{parameters.msbuildArchitecture }}' inputs: solution: build.proj @@ -249,7 +249,7 @@ steps: - ${{if eq(parameters.referenceType, 'Project')}}: - task: DotNetCoreCLI@2 displayName: 'Run Unit Tests' - condition: succeededOrFailed() + condition: and(eq(variables['setupSucceeded'], 'true'), succeededOrFailed()) inputs: command: custom projects: build.proj @@ -269,7 +269,7 @@ steps: - task: DotNetCoreCLI@2 displayName: 'Run Flaky Unit Tests' - condition: succeededOrFailed() + condition: and(eq(variables['setupSucceeded'], 'true'), succeededOrFailed()) inputs: command: custom projects: build.proj @@ -292,7 +292,7 @@ steps: - task: DotNetCoreCLI@2 displayName: 'Run Functional Tests' - condition: succeededOrFailed() + condition: and(eq(variables['setupSucceeded'], 'true'), succeededOrFailed()) inputs: command: custom projects: build.proj @@ -311,7 +311,7 @@ steps: verbosityPack: Detailed - task: DotNetCoreCLI@2 - condition: succeededOrFailed() + condition: and(eq(variables['setupSucceeded'], 'true'), succeededOrFailed()) displayName: 'Run Flaky Functional Tests' inputs: command: custom @@ -335,7 +335,7 @@ steps: - task: DotNetCoreCLI@2 displayName: 'Run Manual Tests' - condition: succeededOrFailed() + condition: and(eq(variables['setupSucceeded'], 'true'), succeededOrFailed()) inputs: command: custom projects: build.proj @@ -356,7 +356,7 @@ steps: - task: DotNetCoreCLI@2 displayName: 'Run Flaky Manual Tests' - condition: succeededOrFailed() + condition: and(eq(variables['setupSucceeded'], 'true'), succeededOrFailed()) inputs: command: custom projects: build.proj diff --git a/eng/pipelines/dotnet-sqlclient-ci-core.yml b/eng/pipelines/dotnet-sqlclient-ci-core.yml index f9ead686ff..7cedad97f6 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-core.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-core.yml @@ -149,6 +149,9 @@ stages: buildConfiguration: ${{ parameters.buildConfiguration }} debug: ${{ parameters.debug }} dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + loggingArtifactsName: $(loggingArtifactsName) + loggingPackageVersion: $(loggingPackageVersion) + referenceType: ${{ parameters.referenceType }} # When building Abstractions via packages, we must depend on the Logging # package. ${{ if eq(parameters.referenceType, 'Package') }}: @@ -198,6 +201,8 @@ stages: - build_logging_package_stage - build_sqlclient_package_stage dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + loggingArtifactsName: $(loggingArtifactsName) + loggingPackageVersion: $(loggingPackageVersion) mdsArtifactsName: $(mdsArtifactsName) mdsPackageVersion: $(mdsPackageVersion) referenceType: ${{ parameters.referenceType }} diff --git a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml index 262c2b909a..67c991f0ec 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml @@ -14,18 +14,18 @@ # It runs via CI push triggers and schedules and uses the Release build # configuration: # -# - Commits to GitHub main -# - Commits to ADO internal/main -# - Weekdays at 03:00 UTC on GitHub main -# - Thursdays at 07:00 UTC on ADO internal/main +# - Commits to GitHub release/7.0 +# - Commits to ADO internal/release/7.0 +# - Sundays at 05:30 UTC on GitHub release/7.0 +# - Sundays at 06:30 UTC on ADO internal/release/7.0 # # GOTCHA: This pipeline definition is triggered by GitHub _and_ ADO CI. We are # able to define different triggers and schedules using branch filters: # -# - Only the GitHub repo has a 'main' branch, so its presence indicates that -# the pipeline run was triggered via GitHub. +# - Only the GitHub repo has a 'release/7.0' branch, so its presence indicates +# that the pipeline run was triggered via GitHub. # -# - Only the ADO repo has an 'internal/main' branch. +# - Only the ADO repo has an 'internal/release/7.0' branch. # # Changes are batched together to ensure that the pipline never runs # concurrently. @@ -55,29 +55,29 @@ trigger: branches: include: - # GitHub main branch. - - main + # GitHub release/7.0 branch. + - release/7.0 - # ADO internal/main branch. - - internal/main + # ADO internal/release/7.0 branch. + - internal/release/7.0 # Trigger this pipline on a schedule. schedules: - # GitHub main on weekdays - - cron: '0 3 * * Mon-Fri' - displayName: Weekday Run (Release Config) + # GitHub release/7.0 on Sundays (1 hour after release/6.1's 04:30 UTC run). + - cron: '30 5 * * Sun' + displayName: Sunday Run GitHub (Release Config) branches: include: - - main + - release/7.0 always: true - # ADO internal/main on Thursdays. - - cron: '0 7 * * Thu' - displayName: Thursday Run (Release Config) + # ADO internal/release/7.0 on Sundays (1 hour after release/6.1's 05:30 UTC run). + - cron: '30 6 * * Sun' + displayName: Sunday Run ADO Internal (Release Config) branches: include: - - internal/main + - internal/release/7.0 always: true # Pipeline parameters, visible in the Azure DevOps UI. diff --git a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml index a5a78f0f8b..f3974f541b 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml @@ -14,18 +14,18 @@ # It runs via CI push triggers and schedules and uses the Release build # configuration: # -# - Commits to GitHub main -# - Commits to ADO internal/main -# - Weekdays at 01:00 UTC on GitHub main -# - Thursdays at 05:00 UTC on ADO internal/main +# - Commits to GitHub release/7.0 +# - Commits to ADO internal/release/7.0 +# - Sundays at 05:00 UTC on GitHub release/7.0 +# - Sundays at 06:00 UTC on ADO internal/release/7.0 # # GOTCHA: This pipeline definition is triggered by GitHub _and_ ADO CI. We are # able to define different triggers and schedules using branch filters: # -# - Only the GitHub repo has a 'main' branch, so its presence indicates that -# the pipeline run was triggered via GitHub. +# - Only the GitHub repo has a 'release/7.0' branch, so its presence indicates +# that the pipeline run was triggered via GitHub. # -# - Only the ADO repo has an 'internal/main' branch. +# - Only the ADO repo has an 'internal/release/7.0' branch. # # Changes are batched together to ensure that the pipline never runs # concurrently. @@ -55,29 +55,29 @@ trigger: branches: include: - # GitHub main branch. - - main + # GitHub release/7.0 branch. + - release/7.0 - # ADO internal/main branch. - - internal/main + # ADO internal/release/7.0 branch. + - internal/release/7.0 # Trigger this pipline on a schedule. schedules: - # GitHub main on weekdays - - cron: '0 1 * * Mon-Fri' - displayName: Weekday Run (Release Config) + # GitHub release/7.0 on Sundays (1 hour after release/6.1's 04:00 UTC run). + - cron: '0 5 * * Sun' + displayName: Sunday Run GitHub (Release Config) branches: include: - - main + - release/7.0 always: true - # ADO internal/main on Thursdays. - - cron: '0 5 * * Thu' - displayName: Thursday Run (Release Config) + # ADO internal/release/7.0 on Sundays (1 hour after release/6.1's 05:00 UTC run). + - cron: '0 6 * * Sun' + displayName: Sunday Run ADO Internal (Release Config) branches: include: - - internal/main + - internal/release/7.0 always: true # Pipeline parameters, visible in the Azure DevOps UI. diff --git a/eng/pipelines/jobs/pack-abstractions-package-ci-job.yml b/eng/pipelines/jobs/pack-abstractions-package-ci-job.yml index 9db1c05696..adfe91dd7b 100644 --- a/eng/pipelines/jobs/pack-abstractions-package-ci-job.yml +++ b/eng/pipelines/jobs/pack-abstractions-package-ci-job.yml @@ -53,6 +53,30 @@ parameters: - detailed - diagnostic + # The name of the Logging pipeline artifacts to download. + # + # This is used when the referenceType is 'Package'. + - name: loggingArtifactsName + type: string + default: Logging.Artifacts + + # The Logging package version to depend on. + # + # This is used when the referenceType is 'Package'. + - name: loggingPackageVersion + type: string + default: '' + + # The C# project reference type to use when building and packing the packages. + - name: referenceType + type: string + default: Project + values: + # Reference sibling packages as NuGet packages. + - Package + # Reference sibling packages as C# projects. + - Project + jobs: - job: pack_abstractions_package_job @@ -104,21 +128,46 @@ jobs: - pwsh: 'Get-ChildItem Env: | Sort-Object Name' displayName: '[Debug] Print Environment Variables' + # For Package reference builds, we must first download the dependency + # package artifacts. + - ${{ if eq(parameters.referenceType, 'Package') }}: + - task: DownloadPipelineArtifact@2 + displayName: Download Logging Package Artifacts + inputs: + artifactName: ${{ parameters.loggingArtifactsName }} + targetPath: $(Build.SourcesDirectory)/packages + # Install the .NET SDK. - template: /eng/pipelines/steps/install-dotnet.yml@self parameters: debug: ${{ parameters.debug }} # Create the NuGet packages. - - task: DotNetCoreCLI@2 - displayName: Create NuGet Package - inputs: - command: pack - packagesToPack: $(project) - configurationToPack: ${{ parameters.buildConfiguration }} - packDirectory: $(dotnetPackagesDir) - verbosityToPack: ${{ parameters.dotnetVerbosity }} - buildProperties: AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }};AbstractionsAssemblyFileVersion=${{ parameters.abstractionsAssemblyFileVersion }} + # + # When referenceType is Package, we must pass ReferenceType and the + # dependency version so that Directory.Packages.props applies version + # ranges to sibling package dependencies. + - ${{ if eq(parameters.referenceType, 'Package') }}: + - task: DotNetCoreCLI@2 + displayName: Create NuGet Package + inputs: + command: pack + packagesToPack: $(project) + configurationToPack: ${{ parameters.buildConfiguration }} + packDirectory: $(dotnetPackagesDir) + verbosityToPack: ${{ parameters.dotnetVerbosity }} + buildProperties: AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }};AbstractionsAssemblyFileVersion=${{ parameters.abstractionsAssemblyFileVersion }};ReferenceType=Package;LoggingPackageVersion=${{ parameters.loggingPackageVersion }} + + - ${{ else }}: + - task: DotNetCoreCLI@2 + displayName: Create NuGet Package + inputs: + command: pack + packagesToPack: $(project) + configurationToPack: ${{ parameters.buildConfiguration }} + packDirectory: $(dotnetPackagesDir) + verbosityToPack: ${{ parameters.dotnetVerbosity }} + buildProperties: AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }};AbstractionsAssemblyFileVersion=${{ parameters.abstractionsAssemblyFileVersion }} # Publish the NuGet packages as a named pipeline artifact. - task: PublishPipelineArtifact@1 diff --git a/eng/pipelines/jobs/pack-azure-package-ci-job.yml b/eng/pipelines/jobs/pack-azure-package-ci-job.yml index 95853d43e0..4afa8ceaae 100644 --- a/eng/pipelines/jobs/pack-azure-package-ci-job.yml +++ b/eng/pipelines/jobs/pack-azure-package-ci-job.yml @@ -26,12 +26,19 @@ parameters: type: string default: Logging.Artifacts - # The Abstractions package verion to depend on. + # The Abstractions package version to depend on. # # This is used when the referenceType is 'Package'. - name: abstractionsPackageVersion type: string + # The Logging package version to depend on. + # + # This is used when the referenceType is 'Package'. + - name: loggingPackageVersion + type: string + default: '' + # The name of the pipeline artifacts to publish. - name: azureArtifactsName type: string @@ -153,15 +160,31 @@ jobs: debug: ${{ parameters.debug }} # Create the NuGet packages. - - task: DotNetCoreCLI@2 - displayName: Create NuGet Package - inputs: - command: pack - packagesToPack: $(project) - configurationToPack: ${{ parameters.buildConfiguration }} - packDirectory: $(dotnetPackagesDir) - verbosityToPack: ${{ parameters.dotnetVerbosity }} - buildProperties: AzurePackageVersion=${{ parameters.azurePackageVersion }};AzureAssemblyFileVersion=${{ parameters.azureAssemblyFileVersion }} + # + # When referenceType is Package, we must pass ReferenceType and the + # dependency versions so that Directory.Packages.props applies version + # ranges to sibling package dependencies. + - ${{ if eq(parameters.referenceType, 'Package') }}: + - task: DotNetCoreCLI@2 + displayName: Create NuGet Package + inputs: + command: pack + packagesToPack: $(project) + configurationToPack: ${{ parameters.buildConfiguration }} + packDirectory: $(dotnetPackagesDir) + verbosityToPack: ${{ parameters.dotnetVerbosity }} + buildProperties: AzurePackageVersion=${{ parameters.azurePackageVersion }};AzureAssemblyFileVersion=${{ parameters.azureAssemblyFileVersion }};ReferenceType=Package;LoggingPackageVersion=${{ parameters.loggingPackageVersion }};AbstractionsPackageVersion=${{ parameters.abstractionsPackageVersion }} + + - ${{ else }}: + - task: DotNetCoreCLI@2 + displayName: Create NuGet Package + inputs: + command: pack + packagesToPack: $(project) + configurationToPack: ${{ parameters.buildConfiguration }} + packDirectory: $(dotnetPackagesDir) + verbosityToPack: ${{ parameters.dotnetVerbosity }} + buildProperties: AzurePackageVersion=${{ parameters.azurePackageVersion }};AzureAssemblyFileVersion=${{ parameters.azureAssemblyFileVersion }} # Publish the NuGet packages as a named pipeline artifact. - task: PublishPipelineArtifact@1 diff --git a/eng/pipelines/kerberos/build-and-test-steps.yml b/eng/pipelines/kerberos/build-and-test-steps.yml new file mode 100644 index 0000000000..8ba9ccdee8 --- /dev/null +++ b/eng/pipelines/kerberos/build-and-test-steps.yml @@ -0,0 +1,144 @@ +################################################################################# +# 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. # +################################################################################# + +# Shared build-and-test steps used by both the Windows and Linux Kerberos jobs. +# +# Parameters: +# buildTarget — The build.proj target that builds SqlClient for the current +# OS (e.g. BuildSqlClientWindows or BuildSqlClientUnix). +# testFramework — The TFM to test against (e.g. net9.0, net462). +# testRunTitle — Title for the published test results (displayed in the ADO +# Tests tab). +# artifactName — Name of the published pipeline artifact that carries the +# test results and coverage files. + +parameters: + + # build.proj target to build SqlClient for the current OS. + - name: buildTarget + type: string + + # TFM to pass to the test targets (-p:TestFramework). + - name: testFramework + type: string + + # Title shown in the ADO Tests tab for this run. + - name: testRunTitle + type: string + + # Pipeline artifact name for test results and coverage. + - name: artifactName + type: string + +steps: + + # --------------------------------------------------------------------------- + # Build + # --------------------------------------------------------------------------- + + # Build the given target. + # + # The test stages build as part of their targets, but this separate step isolates build failures + # so we can fail fast before running tests. Retries are enabled intentionally (1 attempt for + # build, 2 attempts for test steps) to reduce transient infrastructure-related failures. + # + - task: DotNetCoreCLI@2 + displayName: Build SqlClient + retryCountOnTaskFailure: 1 + inputs: + command: build + projects: build2.proj + arguments: >- + -t:${{ parameters.buildTarget }} + -p:Configuration=Release + + # --------------------------------------------------------------------------- + # Run tests in separate steps to permit focused retries. + # --------------------------------------------------------------------------- + + # Run the Unit Test suite. + - task: DotNetCoreCLI@2 + displayName: Run Unit Tests (${{ parameters.testFramework }}) + retryCountOnTaskFailure: 2 + inputs: + command: build + projects: build2.proj + arguments: > + -t:TestMdsUnit + -p:TestFramework=${{ parameters.testFramework }} + -p:Configuration=Release + + # Run the Functional Test suite. + - task: DotNetCoreCLI@2 + displayName: Run Functional Tests (${{ parameters.testFramework }}) + retryCountOnTaskFailure: 2 + inputs: + command: build + projects: build2.proj + arguments: > + -t:TestMdsFunctional + -p:TestFramework=${{ parameters.testFramework }} + -p:Configuration=Release + + # Run the Manual Test suite. + - task: DotNetCoreCLI@2 + displayName: Run Manual Tests (${{ parameters.testFramework }}) + retryCountOnTaskFailure: 2 + inputs: + command: build + projects: build2.proj + arguments: > + -t:TestMdsManual + -p:TestFramework=${{ parameters.testFramework }} + -p:Configuration=Release + + # --------------------------------------------------------------------------- + # Publish results & coverage + # --------------------------------------------------------------------------- + + # Publish the TRX test results to the pipeline run. + - task: PublishTestResults@2 + displayName: Publish Test Results + condition: succeededOrFailed() + inputs: + testResultsFormat: VSTest + # build.proj defines TestResultsFolderPath which defaults to + # $(Build.SourcesDirectory)/test_results, so we look there for the results and coverage files. + testResultsFiles: $(Build.SourcesDirectory)/test_results/**/*.trx + mergeTestResults: true + testRunTitle: ${{ parameters.testRunTitle }} + buildConfiguration: Release + + # Azure Pipelines task conditions do not support path existence checks directly, + # so compute this once and gate later steps on the variable. + - pwsh: | + $resultsDir = "$(Build.SourcesDirectory)/test_results" + if (Test-Path -LiteralPath $resultsDir) { + Write-Host "##vso[task.setvariable variable=HasTestResultsDir]true" + } + else { + Write-Host "##vso[task.setvariable variable=HasTestResultsDir]false" + } + displayName: Detect test_results directory + condition: succeededOrFailed() + + # Give our coverage files a unique name to make it clear where they originated when we download + # the artifacts from all jobs in the merge stage. + - pwsh: | + cd $(Build.SourcesDirectory)/test_results + Get-ChildItem -Filter "*.coverage" -Recurse | + Rename-Item -NewName { "${{ parameters.testFramework }}" + $_.Name } + displayName: Rename coverage files + condition: and(succeededOrFailed(), eq(variables['HasTestResultsDir'], 'true')) + + # Publish TRX test results and coverage files as pipeline artifacts. The merge stage needs the + # coverage files from all of the jobs. + - task: PublishPipelineArtifact@1 + displayName: Publish Test Artifacts + condition: and(succeededOrFailed(), eq(variables['HasTestResultsDir'], 'true')) + inputs: + targetPath: $(Build.SourcesDirectory)/test_results + artifact: ${{ parameters.artifactName }} diff --git a/eng/pipelines/kerberos/linux-cleanup-step.yml b/eng/pipelines/kerberos/linux-cleanup-step.yml new file mode 100644 index 0000000000..d3491bb337 --- /dev/null +++ b/eng/pipelines/kerberos/linux-cleanup-step.yml @@ -0,0 +1,45 @@ +################################################################################# +# 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. # +################################################################################# + +# This template leaves the Active Directory domain and destroys Kerberos +# credentials. It should be referenced at the end of any job that called +# linux-init-step.yml. +# +# All steps use condition: always() so that cleanup runs even when previous +# steps fail. + +parameters: + + # The Active Directory domain to leave (e.g. mydomain.contoso.com). + - name: kerberosDomain + type: string + + # The domain user account used during the join. + - name: kerberosDomainUser + type: string + + # The password for the domain user account. + - name: kerberosDomainPassword + type: string + +steps: + + - bash: | + set -uo pipefail + + DOMAIN="${{ parameters.kerberosDomain }}" + DOMAIN_USER="${{ parameters.kerberosDomainUser }}" + DOMAIN_PASSWORD="${{ parameters.kerberosDomainPassword }}" + DOMAIN_UPPER=$(echo "$DOMAIN" | tr '[:lower:]' '[:upper:]') + + # Leave the domain + echo "$DOMAIN_PASSWORD" | sudo realm leave "$DOMAIN_UPPER" --verbose \ + -U "$DOMAIN_USER@$DOMAIN_UPPER" || true + + # Destroy the TGT and credential cache + kdestroy || true + displayName: Clean up Kerberos (domain leave + kdestroy) + condition: always() diff --git a/eng/pipelines/kerberos/linux-init-step.yml b/eng/pipelines/kerberos/linux-init-step.yml new file mode 100644 index 0000000000..c0c4603232 --- /dev/null +++ b/eng/pipelines/kerberos/linux-init-step.yml @@ -0,0 +1,117 @@ +################################################################################# +# 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. # +################################################################################# + +# This template joins a Linux agent to an Active Directory domain using Kerberos +# and acquires a TGT (Ticket-Granting Ticket) for the specified domain user. +# +# Prerequisites: +# - The agent must be running on Ubuntu/Debian (uses apt-get). +# - The domain controller must be reachable from the agent network. +# +# After this step completes successfully, the agent will have: +# - Kerberos packages installed (krb5-user, realmd, sssd, adcli, etc.) +# - Hostname set to FQDN within the domain +# - NTP synchronized with the domain controller +# - Machine joined to the AD domain +# - A valid Kerberos TGT for the specified user + +parameters: + + # The Active Directory domain to join (e.g. mydomain.contoso.com). + - name: kerberosDomain + type: string + + # The Organizational Unit in which to place the computer account. + - name: kerberosDomainOU + type: string + + # The domain user account to authenticate with (sAMAccountName, without @realm). + - name: kerberosDomainUser + type: string + + # The password for the domain user account. + - name: kerberosDomainPassword + type: string + +steps: + + - bash: | + set -euo pipefail + + DOMAIN="${{ parameters.kerberosDomain }}" + DOMAIN_OU="${{ parameters.kerberosDomainOU }}" + DOMAIN_USER="${{ parameters.kerberosDomainUser }}" + DOMAIN_PASSWORD="${{ parameters.kerberosDomainPassword }}" + DOMAIN_UPPER=$(echo "$DOMAIN" | tr '[:lower:]' '[:upper:]') + + echo "Domain: $DOMAIN" + echo "Realm: $DOMAIN_UPPER" + echo "User: $DOMAIN_USER" + echo "OU: $DOMAIN_OU" + + if [ -z "$DOMAIN_PASSWORD" ]; then + echo "##vso[task.logissue type=error]KerberosDomainPassword is empty" + exit 1 + fi + + # ----------------------------------------------------------------------- + # Install Kerberos and AD integration packages + # ----------------------------------------------------------------------- + echo 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections + + sudo apt-get -y update + sudo apt-get install -y dialog apt-utils + sudo apt-get install -y \ + krb5-user samba sssd sssd-tools libnss-sss libpam-sss \ + ntp ntpdate realmd adcli + + # ----------------------------------------------------------------------- + # Set the hostname to FQDN within the domain + # ----------------------------------------------------------------------- + CURRENT_HOSTNAME="$(hostname)" + if [ "$CURRENT_HOSTNAME" = "$DOMAIN" ] || [[ "$CURRENT_HOSTNAME" == *".$DOMAIN" ]]; then + echo "Hostname already uses domain suffix '.$DOMAIN': $CURRENT_HOSTNAME" + else + sudo hostnamectl set-hostname "$CURRENT_HOSTNAME.$DOMAIN" + fi + + # ----------------------------------------------------------------------- + # Synchronize time with the domain controller (required for Kerberos) + # ----------------------------------------------------------------------- + if ! sudo grep -Fqx "server $DOMAIN" /etc/ntp.conf; then + echo "server $DOMAIN" | sudo tee -a /etc/ntp.conf + fi + sudo systemctl stop ntp + sudo ntpdate "$DOMAIN" + sudo systemctl start ntp + + # ----------------------------------------------------------------------- + # Configure Kerberos realm + # ----------------------------------------------------------------------- + echo "[libdefaults] + default_realm = $DOMAIN_UPPER + rdns = false" | sudo tee /etc/krb5.conf + + # ----------------------------------------------------------------------- + # Discover and join the domain + # ----------------------------------------------------------------------- + sudo realm discover "$DOMAIN_UPPER" + + echo "$DOMAIN_PASSWORD" | sudo realm join --verbose "$DOMAIN_UPPER" \ + -U "$DOMAIN_USER@$DOMAIN_UPPER" \ + --computer-ou "OU=$DOMAIN_OU" + + realm list + + # ----------------------------------------------------------------------- + # Acquire a Kerberos TGT + # ----------------------------------------------------------------------- + echo "$DOMAIN_PASSWORD" | kinit "$DOMAIN_USER@$DOMAIN_UPPER" + + klist + sudo ip addr + sudo ip route + displayName: Initialize Kerberos (domain join + kinit) diff --git a/eng/pipelines/kerberos/sqlclient-kerberos.yml b/eng/pipelines/kerberos/sqlclient-kerberos.yml new file mode 100644 index 0000000000..8770326493 --- /dev/null +++ b/eng/pipelines/kerberos/sqlclient-kerberos.yml @@ -0,0 +1,290 @@ +################################################################################# +# 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. # +################################################################################# + +# ============================================================================= +# sqlclient-kerberos +# ============================================================================= +# Daily Kerberos authentication test pipeline for Microsoft.Data.SqlClient. +# Replaces the Classic "Test-SqlClient-Kerberos-Azure" pipeline. +# +# Schedule: daily at 07:30 UTC on internal/release/7.0. +# +# Job breakdown (10 total): +# Stage: windows → 7 jobs (net462 + net8/9/10 × NativeSNI/ManagedSNI) +# Stage: linux → 3 jobs (net8, net9, net10 — ManagedSNI only) +# Stage: Merge-Code-Coverage → 1 job +# +# Shared steps: +# Build and test steps are defined in build-and-test-steps.yml and reused +# by both stages. Each stage passes its OS-specific build target and the +# per-job testFramework matrix variable. +# +# Required ADO variable groups (link in pipeline UI): +# - kv-sqldrivers-shared (provides the agent-at-sqldrv-ad secret) +# +# Variables defined in this file (no pipeline UI configuration needed): +# - KerberosDomain sqldrv.ad +# - KerberosDomainOU agents +# - KerberosDomainUser agent +# - KerberosDomainPassword $(agent-at-sqldrv-ad) from kv-sqldrivers-shared +# - REMOTE_TCP_CONN_STRING TCP connection string to sqldrv-sql22 +# - REMOTE_NP_CONN_STRING Named Pipe connection string to sqldrv-sql22 +# ============================================================================= + +trigger: none # Scheduled runs only — no CI trigger +pr: none # Not triggered by PRs + +schedules: + - cron: '30 7 * * *' + displayName: Daily run (07:30 UTC) + branches: + include: + - internal/release/7.0 + always: true + +name: $(date:yyyyMMdd)$(rev:.r) + +variables: + # Our Kerberos environment doesn't change often, so we can define these variables here rather than + # in the Pipeline UI or in Libraries. + - name: KerberosDomain + value: sqldrv.ad + + - name: KerberosDomainOU + value: agents + + - name: KerberosDomainUser + value: agent + + - name: KerberosDomainPassword + value: $(agent-at-sqldrv-ad) # Secret from kv-sqldrivers-shared variable group + + - name: REMOTE_TCP_CONN_STRING + value: Data Source=tcp:sqldrv-sql22.sqldrv.ad\sql2022;Initial Catalog=Northwind;Integrated Security=true;Encrypt=false;TrustServerCertificate=true + + - name: REMOTE_NP_CONN_STRING + value: Data Source=np:sqldrv-sql22.sqldrv.ad\sql2022;Initial Catalog=Northwind;Integrated Security=true;Encrypt=false;TrustServerCertificate=true + +# ============================================================================= +# STAGES +# ============================================================================= +stages: + + # =========================================================================== + # Stage 1 - Windows (7 jobs = net462 + 3 TFs × 2 ManagedSNI) + # =========================================================================== + - stage: windows + displayName: Windows + dependsOn: [] + variables: + # KerberosDomainPassword expands at runtime in this stage from the agent-at-sqldrv-ad secret + # exposed by this variable group. + - group: kv-sqldrivers-shared + jobs: + - job: windows + displayName: Windows + timeoutInMinutes: 90 + workspace: + clean: all # Purge obj/artifacts from prior runs on self-hosted agents + strategy: + matrix: + # Azure Pipelines exposes matrix variables as environment variables for each step. + # Do not use the name targetFramework here: MSBuild imports environment variables as + # properties, and TargetFramework would leak into dotnet build build.proj, forcing + # transitive project references onto an invalid TFM (for example SqlServer.Server -> net9.0). + net462_NativeSNI: + testFramework: net462 + managedSNI: 'false' + net8_NativeSNI: + testFramework: net8.0 + managedSNI: 'false' + net8_ManagedSNI: + testFramework: net8.0 + managedSNI: 'true' + net9_NativeSNI: + testFramework: net9.0 + managedSNI: 'false' + net9_ManagedSNI: + testFramework: net9.0 + managedSNI: 'true' + net10_NativeSNI: + testFramework: net10.0 + managedSNI: 'false' + net10_ManagedSNI: + testFramework: net10.0 + managedSNI: 'true' + pool: + name: ADO-Trusted-Domain-Win-WestUS2 + demands: + - ImageOverride -equals ADO-MMS22-SQL19 + steps: + + - checkout: self + clean: true + fetchDepth: 1 + fetchTags: false + + - template: /eng/pipelines/steps/install-dotnet.yml@self + parameters: + runtimes: [8.x, 9.x] + + # --- Update test configuration --- + # Uses runtime variables from the matrix ($(managedSNI)) so we use + # inline PowerShell instead of the shared config template which + # requires compile-time parameters. + - pwsh: | + $managedSni = [System.Convert]::ToBoolean($env:MANAGED_SNI) + $jdata = Get-Content -Raw "config.default.json" | ConvertFrom-Json + foreach ($p in $jdata) { + $p.TCPConnectionString = $env:REMOTE_TCP_CONN_STRING + $p.NPConnectionString = $env:REMOTE_NP_CONN_STRING + $p.SupportsIntegratedSecurity = $true + $p.UseManagedSNIOnWindows = $managedSni + } + $jdata | ConvertTo-Json | Set-Content "config.json" + workingDirectory: src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities + displayName: Update test config.json + env: + REMOTE_TCP_CONN_STRING: $(REMOTE_TCP_CONN_STRING) + REMOTE_NP_CONN_STRING: $(REMOTE_NP_CONN_STRING) + MANAGED_SNI: $(managedSNI) + + # --- Prepare Windows services --- + - powershell: | + $svc = Get-Service -Name SQLBrowser -ErrorAction SilentlyContinue + if ($null -ne $svc) { + Set-Service -StartupType Automatic SQLBrowser + if ($svc.Status -ne 'Running') { Start-Service SQLBrowser } + Get-Service SQLBrowser | Select-Object Name, StartType, Status + } + displayName: Start SQL Server Browser + + - powershell: | + Set-DtcNetworkSetting -DtcName "Local" ` + -InboundTransactionsEnabled $true ` + -OutboundTransactionsEnabled $true ` + -RemoteClientAccessEnabled $true ` + -Confirm:$false + + Get-NetFirewallRule -DisplayName "Distributed Transaction Coordinator (RPC)" | Set-NetFirewallRule -Profile Domain -Action Allow -Enabled True + Get-NetFirewallRule -DisplayName "Distributed Transaction Coordinator (RPC-EPMAP)" | Set-NetFirewallRule -Profile Domain -Action Allow -Enabled True + Get-NetFirewallRule -DisplayName "Distributed Transaction Coordinator (TCP-Out)" | Set-NetFirewallRule -Profile Domain -Action Allow -Enabled True + Get-NetFirewallRule -DisplayName "Distributed Transaction Coordinator (TCP-In)" | Set-NetFirewallRule -Profile Domain -Action Allow -Enabled True + displayName: Enable Network DTC Access + + # --- Build and test --- + - template: /eng/pipelines/kerberos/build-and-test-steps.yml@self + parameters: + buildTarget: BuildMdsWindows + testFramework: $(testFramework) + testRunTitle: Windows-$(testFramework)-ManagedSNI_$(managedSNI) + artifactName: $(testFramework)-ManagedSNI_$(managedSNI)-$(System.JobId) + + # =========================================================================== + # Stage 2 - Linux - .NET Core + Kerberos (3 jobs = 3 TFs) + # =========================================================================== + - stage: linux + displayName: Linux + dependsOn: [] + variables: + # KerberosDomainPassword expands at runtime in this stage from the agent-at-sqldrv-ad secret + # exposed by this variable group. + - group: kv-sqldrivers-shared + jobs: + - job: linux + displayName: Linux + timeoutInMinutes: 90 + workspace: + clean: all # Purge leftovers on self-hosted agents to reduce cross-run flakiness + strategy: + matrix: + net8: + testFramework: net8.0 + net9: + testFramework: net9.0 + net10: + testFramework: net10.0 + pool: + name: ADO-Trusted-Linux-WestUS2 + demands: + - ImageOverride -equals ADO-UB20-SQL22 + + steps: + + - checkout: self + clean: true + fetchDepth: 1 + fetchTags: false + + # --- Install .NET SDK and runtimes --- + - template: /eng/pipelines/steps/install-dotnet.yml@self + parameters: + runtimes: [8.x, 9.x] + + # --- Update test configuration (with Kerberos credentials) --- + - pwsh: | + $jdata = Get-Content -Raw "config.default.json" | ConvertFrom-Json + foreach ($p in $jdata) { + $p.TCPConnectionString = $env:REMOTE_TCP_CONN_STRING + $p.NPConnectionString = $env:REMOTE_NP_CONN_STRING + $p.SupportsIntegratedSecurity = $true + } + $jdata | Add-Member -NotePropertyName "KerberosDomainUser" -NotePropertyValue $env:KERBEROS_DOMAIN_USER -Force + $jdata | Add-Member -NotePropertyName "KerberosDomainPassword" -NotePropertyValue $env:KERBEROS_DOMAIN_PASSWORD -Force + $jdata | ConvertTo-Json | Set-Content "config.json" + workingDirectory: src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities + displayName: Update test config.json (Kerberos) + env: + REMOTE_TCP_CONN_STRING: $(REMOTE_TCP_CONN_STRING) + REMOTE_NP_CONN_STRING: $(REMOTE_NP_CONN_STRING) + KERBEROS_DOMAIN_USER: $(KerberosDomainUser) + KERBEROS_DOMAIN_PASSWORD: $(KerberosDomainPassword) + + # --- Kerberos domain join --- + - template: /eng/pipelines/kerberos/linux-init-step.yml@self + parameters: + kerberosDomain: $(KerberosDomain) + kerberosDomainOU: $(KerberosDomainOU) + kerberosDomainUser: $(KerberosDomainUser) + kerberosDomainPassword: $(KerberosDomainPassword) + + # --- Verify SQL connectivity --- + - pwsh: | + Install-Module -Name SqlServer -Force -Confirm:$false + Import-Module SqlServer + Invoke-Sqlcmd -Query "SELECT @@VERSION, @@SERVERNAME" -ConnectionString $env:REMOTE_TCP_CONN_STRING + displayName: Verify SQL connectivity + env: + REMOTE_TCP_CONN_STRING: $(REMOTE_TCP_CONN_STRING) + + # --- Build and test --- + - template: /eng/pipelines/kerberos/build-and-test-steps.yml@self + parameters: + buildTarget: BuildMdsUnix + testFramework: $(testFramework) + testRunTitle: Linux-$(testFramework) + artifactName: $(testFramework)-linux-$(System.JobId) + + # --- Kerberos cleanup (always runs) --- + - template: /eng/pipelines/kerberos/linux-cleanup-step.yml@self + parameters: + kerberosDomain: $(KerberosDomain) + kerberosDomainUser: $(KerberosDomainUser) + kerberosDomainPassword: $(KerberosDomainPassword) + + # =========================================================================== + # Stage 3 — Merge Code Coverage (1 job) + # =========================================================================== + - stage: merge + displayName: Merge Code Coverage + dependsOn: + - windows + - linux + condition: succeededOrFailed() + jobs: + - template: /eng/pipelines/common/templates/jobs/ci-code-coverage-job.yml@self + parameters: + upload: false diff --git a/eng/pipelines/libraries/ci-build-variables.yml b/eng/pipelines/libraries/ci-build-variables.yml index 2779277678..9d058ce078 100644 --- a/eng/pipelines/libraries/ci-build-variables.yml +++ b/eng/pipelines/libraries/ci-build-variables.yml @@ -24,43 +24,43 @@ variables: # Abstractions library assembly file version - name: abstractionsAssemblyFileVersion - value: 1.0.0.$(assemblyBuildNumber) + value: 7.0.2.$(assemblyBuildNumber) # Abstractions library NuGet package version - name: abstractionsPackageVersion - value: 1.0.0.$(Build.BuildNumber)-ci + value: 7.0.2.$(Build.BuildNumber)-ci # AKV provider assembly file version - name: akvAssemblyFileVersion - value: 7.0.0.$(assemblyBuildNumber) + value: 7.0.2.$(assemblyBuildNumber) # AKV provider NuGet package version - name: akvPackageVersion - value: 7.0.0.$(Build.BuildNumber)-ci + value: 7.0.2.$(Build.BuildNumber)-ci # Azure library assembly file version - name: azureAssemblyFileVersion - value: 1.0.0.$(assemblyBuildNumber) + value: 7.0.2.$(assemblyBuildNumber) # Azure library NuGet package version - name: azurePackageVersion - value: 1.0.0.$(Build.BuildNumber)-ci + value: 7.0.2.$(Build.BuildNumber)-ci # Logging library assembly file version - name: loggingAssemblyFileVersion - value: 1.0.0.$(assemblyBuildNumber) + value: 7.0.2.$(assemblyBuildNumber) # Logging library NuGet package version - name: loggingPackageVersion - value: 1.0.0.$(Build.BuildNumber)-ci + value: 7.0.2.$(Build.BuildNumber)-ci # MDS library assembly file version - name: mdsAssemblyFileVersion - value: 7.0.0.$(assemblyBuildNumber) + value: 7.0.2.$(assemblyBuildNumber) # MDS library NuGet package version - name: mdsPackageVersion - value: 7.0.0.$(Build.BuildNumber)-ci + value: 7.0.2.$(Build.BuildNumber)-ci # Local NuGet feed directory where downloaded pipeline artifacts are placed. # NuGet.config references this as a local package source for restore. diff --git a/eng/pipelines/onebranch/jobs/build-signed-csproj-package-job.yml b/eng/pipelines/onebranch/jobs/build-signed-csproj-package-job.yml index 809690a886..01d9b61da1 100644 --- a/eng/pipelines/onebranch/jobs/build-signed-csproj-package-job.yml +++ b/eng/pipelines/onebranch/jobs/build-signed-csproj-package-job.yml @@ -191,6 +191,7 @@ jobs: authSignCertName: ${{ parameters.authSignCertName }} esrpClientId: ${{ parameters.esrpClientId }} esrpConnectedServiceName: ${{ parameters.esrpConnectedServiceName }} + pattern: '${{ parameters.packageFullName }}.*nupkg' # Publish symbols to servers - ${{ if eq(parameters.publishSymbols, true) }}: diff --git a/eng/pipelines/onebranch/jobs/build-signed-sqlclient-package-job.yml b/eng/pipelines/onebranch/jobs/build-signed-sqlclient-package-job.yml index a422a5351c..d3a6245782 100644 --- a/eng/pipelines/onebranch/jobs/build-signed-sqlclient-package-job.yml +++ b/eng/pipelines/onebranch/jobs/build-signed-sqlclient-package-job.yml @@ -93,21 +93,34 @@ jobs: sourceRoot: $(BUILD_OUTPUT) dllPattern: 'Microsoft.Data.SqlClient.resources.dll' - - template: /eng/pipelines/common/templates/steps/generate-nuget-package-step.yml@self - parameters: - buildConfiguration: Release - displayName: 'Create MDS NuGet Package' - generateSymbolsPackage: true - installNuget: false - nuspecPath: $(nuspecPath) - outputDirectory: $(PACK_OUTPUT) - packageVersion: $(mdsPackageVersion) - properties: 'AbstractionsPackageVersion=$(abstractionsPackageVersion);LoggingPackageVersion=$(loggingPackageVersion)' - referenceType: Package + # Create the MDS NuGet package via build.proj's GenerateMdsPackage target. + # This target has access to the version range properties computed in + # Versions.props, so it passes them through to nuget pack automatically. + - task: NuGetToolInstaller@1 + displayName: 'Install NuGet' + inputs: + checkLatest: true + + - task: DotNetCoreCLI@2 + displayName: 'Create MDS NuGet Package' + inputs: + command: build + projects: $(REPO_ROOT)/build.proj + arguments: >- + -t:GenerateMdsPackage + -p:GenerateNuget=true + -p:Configuration=Release + -p:ReferenceType=Package + -p:MdsPackageVersion=$(mdsPackageVersion) + -p:AbstractionsPackageVersion=$(abstractionsPackageVersion) + -p:LoggingPackageVersion=$(loggingPackageVersion) + -p:PackagesDir=$(PACK_OUTPUT) + -p:NuGetCmd=nuget.exe - template: /eng/pipelines/onebranch/steps/esrp-code-signing-step.yml@self parameters: artifactType: pkg + nupkgPattern: 'Microsoft.Data.SqlClient.$(mdsPackageVersion).*nupkg' # Copy signed DLLs and PDBs to APIScan folders. - task: CopyFiles@2 diff --git a/eng/pipelines/onebranch/sqlclient-official.yml b/eng/pipelines/onebranch/sqlclient-official.yml index c9548b25d6..4bc17988eb 100644 --- a/eng/pipelines/onebranch/sqlclient-official.yml +++ b/eng/pipelines/onebranch/sqlclient-official.yml @@ -9,20 +9,20 @@ name: $(Year:YY)$(DayOfYear)$(Rev:.r) # No PR-based triggers. pr: none -# We trigger runs on activity on the internal/main branch, but not on any other branches. This +# We trigger runs on activity on the internal/release/7.0 branch, but not on any other branches. This # pipeline is intended to only run against the ADO.Net dotnet-sqlclient repo. trigger: branches: include: - - internal/main + - internal/release/7.0 -# We run this pipeline on a daily schedule. +# We run this pipeline weekly on Wednesdays at 03:00 UTC. schedules: - - cron: "30 4 * * *" - displayName: Daily 04:30 UTC Build + - cron: "0 3 * * Wed" + displayName: Weekly Wednesday 03:00 UTC Build branches: include: - - internal/main + - internal/release/7.0 always: true # These parameters are visible in the Azure DevOps pipeline UI when a new run is queued. diff --git a/eng/pipelines/onebranch/steps/compound-esrp-nuget-signing-step.yml b/eng/pipelines/onebranch/steps/compound-esrp-nuget-signing-step.yml index 34e903465f..e86964cf3d 100644 --- a/eng/pipelines/onebranch/steps/compound-esrp-nuget-signing-step.yml +++ b/eng/pipelines/onebranch/steps/compound-esrp-nuget-signing-step.yml @@ -30,6 +30,11 @@ parameters: - name: esrpClientId type: string + # Glob pattern to match NuGet packages for scanning and signing. + - name: pattern + type: string + default: '*.*nupkg' + steps: # See: https://aka.ms/esrp.scantask - task: EsrpMalwareScanning@6 @@ -41,7 +46,7 @@ steps: ConnectedServiceName: '${{ parameters.esrpConnectedServiceName }}' EsrpClientId: '${{ parameters.esrpClientId }}' FolderPath: '$(PACK_OUTPUT)' - Pattern: '*.*nupkg' + Pattern: '${{ parameters.pattern }}' UseMSIAuthentication: true VerboseLogin: 1 @@ -56,7 +61,7 @@ steps: AuthAKVName: '${{ parameters.authAkvName }}' AuthSignCertName: '${{ parameters.authSignCertName }}' FolderPath: '$(PACK_OUTPUT)' - Pattern: '*.*nupkg' + Pattern: '${{ parameters.pattern }}' signConfigType: 'inlineSignParams' UseMSIAuthentication: true inlineOperation: | diff --git a/eng/pipelines/onebranch/steps/esrp-code-signing-step.yml b/eng/pipelines/onebranch/steps/esrp-code-signing-step.yml index 59322d67aa..83053a8d29 100644 --- a/eng/pipelines/onebranch/steps/esrp-code-signing-step.yml +++ b/eng/pipelines/onebranch/steps/esrp-code-signing-step.yml @@ -17,6 +17,10 @@ parameters: type: string default: 'Microsoft.Data.SqlClient*.dll' + - name: nupkgPattern + type: string + default: '*.*nupkg' + - name: artifactDirectory type: string default: $(PACK_OUTPUT) @@ -125,7 +129,7 @@ steps: EsrpClientId: '${{parameters.EsrpClientId }}' UseMSIAuthentication: true FolderPath: '${{parameters.artifactDirectory }}' - Pattern: '*.*nupkg' + Pattern: '${{ parameters.nupkgPattern }}' CleanupTempStorage: 1 VerboseLogin: 1 @@ -133,7 +137,6 @@ steps: - task: EsrpCodeSigning@6 displayName: 'ESRP CodeSigning Nuget Package' inputs: - inputs: ConnectedServiceName: '${{parameters.ESRPConnectedServiceName }}' AppRegistrationClientId: '${{parameters.appRegistrationClientId }}' AppRegistrationTenantId: '${{parameters.appRegistrationTenantId }}' @@ -142,7 +145,7 @@ steps: AuthAKVName: '${{parameters.AuthAKVName }}' AuthSignCertName: '${{parameters.AuthSignCertName }}' FolderPath: '${{parameters.artifactDirectory }}' - Pattern: '*.*nupkg' + Pattern: '${{ parameters.nupkgPattern }}' signConfigType: inlineSignParams inlineOperation: | [ diff --git a/eng/pipelines/onebranch/variables/common-variables.yml b/eng/pipelines/onebranch/variables/common-variables.yml index feddd2c6ce..b4d0ca470b 100644 --- a/eng/pipelines/onebranch/variables/common-variables.yml +++ b/eng/pipelines/onebranch/variables/common-variables.yml @@ -66,15 +66,15 @@ variables: # The NuGet package version for GA releases (non-preview). - name: abstractionsPackageVersion - value: '1.0.0' + value: '7.0.2' # The NuGet package version for preview releases. - name: abstractionsPackagePreviewVersion - value: 1.0.0-preview1.$(Build.BuildNumber) + value: 7.0.2-preview1.$(Build.BuildNumber) # The AssemblyFileVersion for all assemblies in the Abstractions package. - name: abstractionsAssemblyFileVersion - value: 1.0.0.$(assemblyBuildNumber) + value: 7.0.2.$(assemblyBuildNumber) # ---------------------------------------------------------------------------- # MDS Package Versions @@ -84,15 +84,20 @@ variables: # The NuGet package version for GA releases (non-preview). - name: mdsPackageVersion - value: '7.0.0' + value: '7.0.2' # The NuGet package version for preview releases. + # + # Since we're on the release/7.0 branch, there will never be another legitimate preview release. + # However, we routinely want to run preview builds to test other changes, so we continue to + # maintain this variable such that these testing preview builds receive newer version numbers + # than previous builds. - name: mdsPackagePreviewVersion - value: 7.0.0-preview4.$(Build.BuildNumber) + value: 7.0.2-preview1.$(Build.BuildNumber) # The AssemblyFileVersion for all assemblies in the MDS package. - name: mdsAssemblyFileVersion - value: 7.0.0.$(assemblyBuildNumber) + value: 7.0.2.$(assemblyBuildNumber) # The path to the NuGet packaging specification file used to generate the MDS NuGet package. - name: nuspecPath @@ -106,15 +111,15 @@ variables: # The NuGet package version for GA releases (non-preview). - name: loggingPackageVersion - value: '1.0.0' + value: '7.0.2' # The NuGet package version for preview releases. - name: loggingPackagePreviewVersion - value: 1.0.0-preview1.$(Build.BuildNumber) + value: 7.0.2-preview1.$(Build.BuildNumber) # The AssemblyFileVersion for all assemblies in the Logging package. - name: loggingAssemblyFileVersion - value: 1.0.0.$(assemblyBuildNumber) + value: 7.0.2.$(assemblyBuildNumber) # ---------------------------------------------------------------------------- # Azure Package Versions @@ -124,15 +129,15 @@ variables: # The NuGet package version for GA releases (non-preview). - name: azurePackageVersion - value: '1.0.0' + value: '7.0.2' # The NuGet package version for preview releases. - name: azurePackagePreviewVersion - value: 1.0.0-preview1.$(Build.BuildNumber) + value: 7.0.2-preview1.$(Build.BuildNumber) # The AssemblyFileVersion for all assemblies in the Azure package. - name: azureAssemblyFileVersion - value: 1.0.0.$(assemblyBuildNumber) + value: 7.0.2.$(assemblyBuildNumber) # ---------------------------------------------------------------------------- # SqlServer.Server Package Versions @@ -160,15 +165,15 @@ variables: # The NuGet package version for GA releases (non-preview). - name: akvPackageVersion - value: '7.0.0' + value: '7.0.2' # The NuGet package version for preview releases. - name: akvPackagePreviewVersion - value: 7.0.0-preview1.$(Build.BuildNumber) + value: 7.0.2-preview1.$(Build.BuildNumber) # The AssemblyFileVersion for all assemblies in the AKV Provider package. - name: akvAssemblyFileVersion - value: 7.0.0.$(assemblyBuildNumber) + value: 7.0.2.$(assemblyBuildNumber) # ---------------------------------------------------------------------------- # MDS Symbols Publishing diff --git a/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml b/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml index 663532b905..a6646d12af 100644 --- a/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml +++ b/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml @@ -12,7 +12,7 @@ # - Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider # # It is triggered by pushes to PRs that target dev/ and feature/ branches, and -# the main branch in GitHub. +# the release/7.0 branch in GitHub. # # It maps to the "PR-SqlClient-Package" pipeline in the Public project: # @@ -31,8 +31,7 @@ pr: include: # GitHub repo branch targets that will trigger PR validation builds. - dev/* - - feat/* - - main + - release/7.0 paths: include: @@ -49,6 +48,7 @@ pr: - global.json - NuGet.config exclude: + - eng/pipelines/kerberos/* - eng/pipelines/onebranch/* # Do not trigger commit or schedule runs for this pipeline. diff --git a/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml b/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml index 868b3010ff..5069bbea5e 100644 --- a/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml +++ b/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml @@ -12,7 +12,7 @@ # - Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider # # It is triggered by pushes to PRs that target dev/ and feature/ branches, and -# the main branch in GitHub. +# the release/7.0 branch in GitHub. # # It maps to the "PR-SqlClient-Project" pipeline in the Public project: # @@ -31,8 +31,7 @@ pr: include: # GitHub repo branch targets that will trigger PR validation builds. - dev/* - - feat/* - - main + - release/7.0 paths: include: @@ -49,6 +48,7 @@ pr: - global.json - NuGet.config exclude: + - eng/pipelines/kerberos/* - eng/pipelines/onebranch/* # Do not trigger commit or schedule runs for this pipeline. diff --git a/eng/pipelines/stages/build-abstractions-package-ci-stage.yml b/eng/pipelines/stages/build-abstractions-package-ci-stage.yml index 6aae1703b8..8579b79caf 100644 --- a/eng/pipelines/stages/build-abstractions-package-ci-stage.yml +++ b/eng/pipelines/stages/build-abstractions-package-ci-stage.yml @@ -67,6 +67,30 @@ parameters: - detailed - diagnostic + # The name of the Logging pipeline artifacts to download. + # + # This is used when the referenceType is 'Package'. + - name: loggingArtifactsName + type: string + default: Logging.Artifacts + + # The Logging package version to depend on. + # + # This is used when the referenceType is 'Package'. + - name: loggingPackageVersion + type: string + default: '' + + # The C# project reference type to use when building and packing the packages. + - name: referenceType + type: string + default: Project + values: + # Reference sibling packages as NuGet packages. + - Package + # Reference sibling packages as C# projects. + - Project + stages: - stage: build_abstractions_package_stage @@ -141,3 +165,6 @@ stages: - test_abstractions_package_job_windows - test_abstractions_package_job_macos dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + loggingArtifactsName: ${{ parameters.loggingArtifactsName }} + loggingPackageVersion: ${{ parameters.loggingPackageVersion }} + referenceType: ${{ parameters.referenceType }} diff --git a/eng/pipelines/stages/build-azure-package-ci-stage.yml b/eng/pipelines/stages/build-azure-package-ci-stage.yml index 3b57ce8c3b..36d754b15f 100644 --- a/eng/pipelines/stages/build-azure-package-ci-stage.yml +++ b/eng/pipelines/stages/build-azure-package-ci-stage.yml @@ -101,6 +101,20 @@ parameters: - detailed - diagnostic + # The name of the Logging pipeline artifacts to download. + # + # This is used when the referenceType is 'Package'. + - name: loggingArtifactsName + type: string + default: Logging.Artifacts + + # The Logging package version to depend on. + # + # This is used when the referenceType is 'Package'. + - name: loggingPackageVersion + type: string + default: '' + # The name of the MDS pipeline artifacts to download. # # This is used when the referenceType is 'Package'. @@ -282,4 +296,6 @@ stages: - test_azure_package_job_windows_integration - test_azure_package_job_macos dotnetVerbosity: ${{ parameters.dotnetVerbosity }} + loggingArtifactsName: ${{ parameters.loggingArtifactsName }} + loggingPackageVersion: ${{ parameters.loggingPackageVersion }} referenceType: ${{ parameters.referenceType }} diff --git a/eng/pipelines/steps/install-dotnet-arm64.ps1 b/eng/pipelines/steps/install-dotnet-arm64.ps1 index b8057a703a..95d580543b 100644 --- a/eng/pipelines/steps/install-dotnet-arm64.ps1 +++ b/eng/pipelines/steps/install-dotnet-arm64.ps1 @@ -4,15 +4,15 @@ .DESCRIPTION Special handling is required for ARM64 due to a bug in UseDotNet@2: - + [BUG]: UseDotNet@2 task installs x86 build https://github.com/microsoft/azure-pipelines-tasks/issues/20300 - + The downloaded dotnet-install.ps1 script is kept in the $InstallDir to avoid downloading it multiple times during the pipeline job. - + The following environment variables are set for subsequent steps in the pipeline: - + DOTNET_ROOT: Set to $InstallDir. PATH: $DOTNET_ROOT is prepended to the PATH environment variable. @@ -58,6 +58,72 @@ param # Stop on all errors. $ErrorActionPreference = 'Stop' +# Maximum number of retry attempts for transient install failures (e.g. +# corrupt downloads, network timeouts). +$maxAttempts = 3 +$retryDelaySec = 10 + +#------------------------------------------------------------------------------ +# Invoke dotnet-install.ps1 with retry logic. On each attempt the script is +# called with the supplied $Params. If a non-zero exit code is returned, or +# if the optional $Verify script-block throws, the attempt is considered failed +# and will be retried after a short delay. + +function Invoke-DotNetInstall +{ + param + ( + [Parameter(Mandatory)] + [string]$Description, + + [Parameter(Mandatory)] + [hashtable]$Params, + + [scriptblock]$Verify = $null + ) + + for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) + { + try + { + Write-Host "$Description (attempt $attempt of $maxAttempts)" + + $global:LASTEXITCODE = 0 + & "$InstallDir/dotnet-install.ps1" -Verbose:$Debug -DryRun:$DryRun @Params + $installSucceeded = $? + + if (-not $installSucceeded) + { + throw "dotnet-install.ps1 failed." + } + + if ($global:LASTEXITCODE -ne 0) + { + throw "dotnet-install.ps1 failed with exit code $global:LASTEXITCODE" + } + + if ($Verify) + { + & $Verify + } + + return + } + catch + { + Write-Warning "Attempt $attempt failed: $_" + + if ($attempt -ge $maxAttempts) + { + throw "$Description failed after $maxAttempts attempts. Last error: $_" + } + + Write-Host "Retrying in $retryDelaySec seconds..." + Start-Sleep -Seconds $retryDelaySec + } + } +} + #------------------------------------------------------------------------------ # Emit our command-line arguments. @@ -80,7 +146,7 @@ if (-not (Test-Path -Path "$InstallDir/dotnet-install.ps1" -PathType Leaf)) } Write-Host "Downloading dotnet-install.ps1..." - + $params = @{ Uri = "https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.ps1" @@ -113,8 +179,6 @@ if ($Debug) #------------------------------------------------------------------------------ # Install the SDK. -Write-Host "Installing .NET SDK version: $sdkVersion" - $installParams = @{ Architecture = "arm64" @@ -128,15 +192,40 @@ if ($Debug) Write-Host ($installParams | ConvertTo-Json -Depth 1) } -& "$InstallDir/dotnet-install.ps1" -Verbose:$Debug -DryRun:$DryRun @installParams +# Verify the SDK was actually installed. dotnet-install.ps1 can silently fail +# with exit code 0 when the package download is corrupt or the size cannot be +# measured. +$verifySdk = + if (-not $DryRun) + { + { + $dotnetExe = Join-Path $InstallDir "dotnet" + $installedSdks = & $dotnetExe --list-sdks 2>&1 + $installedSdksText = [string]::Join("`n", @($installedSdks)) + + Write-Host "Installed SDKs:`n$installedSdksText" + + $sdkPattern = "(?m)^$([regex]::Escape($sdkVersion))\s+\[" + + if (-not [regex]::IsMatch($installedSdksText, $sdkPattern)) + { + throw "SDK $sdkVersion is not present after installation." + } + + Write-Host "Verified SDK $sdkVersion is installed." + } + } + +Invoke-DotNetInstall ` + -Description "Installing .NET SDK version: $sdkVersion" ` + -Params $installParams ` + -Verify $verifySdk #------------------------------------------------------------------------------ # Install the Runtimes, if any. foreach ($channel in $Runtimes) { - Write-Host "Installing .NET Runtime GA channel: $channel" - $installParams = @{ Architecture = "arm64" @@ -152,7 +241,36 @@ foreach ($channel in $Runtimes) Write-Host ($installParams | ConvertTo-Json -Depth 1) } - & "$InstallDir/dotnet-install.ps1" -Verbose:$Debug -DryRun:$DryRun @installParams + # Verify the runtime was actually installed. Use the same guard against + # silent corruption that we use for the SDK. + $verifyRuntime = + if (-not $DryRun) + { + # Capture $channel in a local variable so the script-block closure + # binds to the current iteration value. + $ch = $channel + { + $dotnetExe = Join-Path $InstallDir "dotnet" + $installedRuntimes = & $dotnetExe --list-runtimes 2>&1 + $installedRuntimesText = [string]::Join("`n", @($installedRuntimes)) + + Write-Host "Installed runtimes:`n$installedRuntimesText" + + $runtimePattern = "Microsoft\.NETCore\.App $([regex]::Escape($ch))\." + + if (-not [regex]::IsMatch($installedRuntimesText, $runtimePattern)) + { + throw "Runtime $ch is not present after installation." + } + + Write-Host "Verified runtime $ch is installed." + } + } + + Invoke-DotNetInstall ` + -Description "Installing .NET Runtime GA channel: $channel" ` + -Params $installParams ` + -Verify $verifyRuntime } #------------------------------------------------------------------------------ diff --git a/eng/pipelines/steps/install-dotnet.yml b/eng/pipelines/steps/install-dotnet.yml index e6a50ee03f..5774984663 100644 --- a/eng/pipelines/steps/install-dotnet.yml +++ b/eng/pipelines/steps/install-dotnet.yml @@ -55,8 +55,12 @@ steps: - ${{ if ne(parameters.architecture, 'arm64') }}: # Install the SDK listed in the global.json file. + # + # retryCountOnTaskFailure is set because UseDotNet@2 fails intermittently + # in CI due to transient network/CDN issues when downloading the SDK. - task: UseDotNet@2 displayName: Install .NET SDK (global.json) + retryCountOnTaskFailure: 3 inputs: installationPath: ${{ parameters.installDir }} packageType: sdk @@ -69,6 +73,7 @@ steps: - ${{ each version in parameters.runtimes }}: - task: UseDotNet@2 displayName: Install .NET ${{ version }} Runtime + retryCountOnTaskFailure: 3 inputs: installationPath: ${{ parameters.installDir }} packageType: runtime @@ -81,8 +86,13 @@ steps: - ${{ else }}: # Use the install script for ARM64. + # + # retryCountOnTaskFailure provides an outer retry around the script's + # internal retry loop, in case the script download itself or the entire + # step fails due to transient issues. - task: PowerShell@2 displayName: Install .NET SDK and Runtimes for ARM64 + retryCountOnTaskFailure: 3 inputs: targetType: filePath pwsh: true diff --git a/global.json b/global.json index 303d578bc6..3c51141e58 100644 --- a/global.json +++ b/global.json @@ -3,22 +3,15 @@ { // We currently require the .NET 10 SDK to build and test the project. // - // We specify the most recent release, and let rollForward pick the latest - // available suitable SDK per the rollForward setting. + // GOTCHA: Our CI infrastructure for Windows uses VS 2022 which comes with MSBuild 17.x. The + // .NET 10 SDK versions in the 10.0.2xx series require MSBuild 18.x, so we stick with the + // 10.0.1xx feature band which is compatible with MSBuild 17.x. // - // We must specify a complete version here since this file is also used by - // the Azure Pipelines UseDotNet@2 task, which doesn't support wildcards, - // and won't roll-forward. It uses the version verbatim. - // - // GOTCHA: This file is only used by the dotnet CLI and related tools. - // Other toolchains (IDEs like Visual Studio, the .NET Framework MSBuild - // system, etc.) may use their own installed SDKs unless configured - // otherwise. - // - "version": "10.0.104", + "version": "10.0.109", - // Any 10.x version is acceptable. - "rollForward": "latestMinor", + // Allow roll-forward within the 10.0.1xx feature band so servicing patches + // are picked up automatically without requiring a PR for each bump. + "rollForward": "patch", // Do not allow pre-release versions. // diff --git a/release-notes/7.0/7.0.0.md b/release-notes/7.0/7.0.0.md new file mode 100644 index 0000000000..f62178076b --- /dev/null +++ b/release-notes/7.0/7.0.0.md @@ -0,0 +1,410 @@ +# Release Notes + +## Stable Release 7.0.0 - 2026-03-17 + +This is the general availability release of **Microsoft.Data.SqlClient 7.0**, a major milestone for the .NET data provider for SQL Server. This release addresses the most upvoted issue in the repository's history — extracting Azure dependencies from the core package — introduces pluggable SSPI authentication, adds enhanced routing for Azure SQL Hyperscale, and delivers async read performance improvements. + +Also released as part of this milestone: +- Released Microsoft.Data.SqlClient.Extensions.Abstractions 1.0.0. See [release notes](../Extensions/Abstractions/1.0/1.0.0.md). +- Released Microsoft.Data.SqlClient.Extensions.Azure 1.0.0. See [release notes](../Extensions/Azure/1.0/1.0.0.md). +- Released Microsoft.Data.SqlClient.Internal.Logging 1.0.0. See [release notes](../Internal/Logging/1.0/1.0.0.md). +- Released Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 7.0.0. See [release notes](../add-ons/AzureKeyVaultProvider/7.0/7.0.0.md). + +## Changes Since [7.0.0-preview4](7.0.0-preview4.md) + +### Added + +- Added actionable error message when Entra ID authentication methods are used without the `Microsoft.Data.SqlClient.Extensions.Azure` package installed, guiding users to install the correct package. + ([#3962](https://github.com/dotnet/SqlClient/issues/3962), + [#4046](https://github.com/dotnet/SqlClient/pull/4046)) + +- Added Azure authentication sample application. + ([#3988](https://github.com/dotnet/SqlClient/pull/3988)) + +### Changed + +#### Other changes + +- Renamed the `Microsoft.Data.SqlClient.Extensions.Logging` package to `Microsoft.Data.SqlClient.Internal.Logging` to indicate it is for internal use only and should not be referenced directly by application code. + ([#4038](https://github.com/dotnet/SqlClient/pull/4038)) + +- Fixed non-localized exception strings. + ([#4022](https://github.com/dotnet/SqlClient/pull/4022)) + +- Codebase merge and cleanup: + ([#3997](https://github.com/dotnet/SqlClient/pull/3997), + [#4052](https://github.com/dotnet/SqlClient/pull/4052)) + +- Various test improvements: + ([#3891](https://github.com/dotnet/SqlClient/pull/3891), + [#3996](https://github.com/dotnet/SqlClient/pull/3996), + [#4002](https://github.com/dotnet/SqlClient/pull/4002), + [#4034](https://github.com/dotnet/SqlClient/pull/4034), + [#4041](https://github.com/dotnet/SqlClient/pull/4041), + [#4044](https://github.com/dotnet/SqlClient/pull/4044)) + +- Documentation improvements (including Entra ID branding updates): + ([#4021](https://github.com/dotnet/SqlClient/pull/4021), + [#4047](https://github.com/dotnet/SqlClient/pull/4047), + [#4049](https://github.com/dotnet/SqlClient/pull/4049)) + +- Updated Dependencies + ([#4045](https://github.com/dotnet/SqlClient/pull/4045)): + - Updated `Azure.Core` to v1.51.1 + - Updated `Azure.Identity` to v1.18.0 + - Updated `Azure.Security.KeyVault.Keys` to v4.9.0 + - Updated `Microsoft.Extensions.Caching.Memory` to v9.0.13 (.NET 9.0) + - Updated `Microsoft.IdentityModel.JsonWebTokens` to v8.16.0 + - Updated `Microsoft.IdentityModel.Protocols.OpenIdConnect` to v8.16.0 + - Updated `Microsoft.Bcl.Cryptography` to v9.0.13 (.NET 9.0) + - Updated `System.Configuration.ConfigurationManager` to v9.0.13 (.NET 9.0) + - Updated `System.Diagnostics.DiagnosticSource` to v10.0.3 + - Updated `System.Security.Cryptography.Pkcs` to v9.0.13 (.NET 9.0) + - Updated `System.Text.Json` to v10.0.3 + - Updated `System.Threading.Channels` to v10.0.3 + - Updated `System.ValueTuple` to v4.6.2 + +## Cumulative Changes Since [6.1](../6.1/README.md) + +This section summarizes all changes across the 7.0 preview cycle for users upgrading from the latest 6.1 stable release. + +### Changed + +#### Azure Dependencies Removed from Core Package + +*What Changed:* + +- The core `Microsoft.Data.SqlClient` package no longer depends on `Azure.Core`, `Azure.Identity`, or their transitive dependencies (e.g., `Microsoft.Identity.Client`, `Microsoft.Web.WebView2`). Azure Active Directory / Entra ID authentication functionality (`ActiveDirectoryAuthenticationProvider` and related types) has been extracted into a new `Microsoft.Data.SqlClient.Extensions.Azure` package. + ([#1108](https://github.com/dotnet/SqlClient/issues/1108), + [#3680](https://github.com/dotnet/SqlClient/pull/3680), + [#3902](https://github.com/dotnet/SqlClient/pull/3902), + [#3904](https://github.com/dotnet/SqlClient/pull/3904), + [#3908](https://github.com/dotnet/SqlClient/pull/3908), + [#3917](https://github.com/dotnet/SqlClient/pull/3917), + [#3982](https://github.com/dotnet/SqlClient/pull/3982), + [#3978](https://github.com/dotnet/SqlClient/pull/3978), + [#3986](https://github.com/dotnet/SqlClient/pull/3986)) +- Two additional packages were introduced to support this separation: `Microsoft.Data.SqlClient.Extensions.Abstractions` (shared types between the core driver and extensions) and `Microsoft.Data.SqlClient.Internal.Logging` (shared ETW tracing infrastructure). + ([#3626](https://github.com/dotnet/SqlClient/pull/3626), + [#3628](https://github.com/dotnet/SqlClient/pull/3628), + [#3967](https://github.com/dotnet/SqlClient/pull/3967), + [#4038](https://github.com/dotnet/SqlClient/pull/4038)) + +*Who Benefits:* + +- All users benefit from a significantly lighter core package. Previously, the Azure dependency chain pulled in numerous assemblies even for applications that only needed basic SQL Server connectivity. This was the [most upvoted open issue](https://github.com/dotnet/SqlClient/issues/1108) in the repository. +- Users who do not use Entra ID authentication no longer carry Azure-related assemblies in their build output. +- Users who do use Entra ID authentication can now manage Azure dependency versions independently from the core driver. + +*Impact:* + +- Applications using Entra ID authentication (e.g., `ActiveDirectoryInteractive`, `ActiveDirectoryDefault`, `ActiveDirectoryManagedIdentity`, etc.) must now install the `Microsoft.Data.SqlClient.Extensions.Azure` NuGet package separately: + +``` +dotnet add package Microsoft.Data.SqlClient.Extensions.Azure +``` + +- No code changes are required beyond adding the package reference. +- If an Entra ID authentication method is used without the Azure package installed, the driver now provides an actionable error message guiding users to install the correct package. + +### Added + +#### Pluggable Authentication with SspiContextProvider + +*What Changed:* + +- Added a public `SspiContextProvider` property on `SqlConnection`, completing the SSPI extensibility work begun in 6.1.0. Applications can now supply a custom SSPI context provider for integrated authentication, enabling custom Kerberos ticket negotiation and NTLM username/password authentication scenarios. + ([#2253](https://github.com/dotnet/SqlClient/issues/2253), + [#2494](https://github.com/dotnet/SqlClient/pull/2494)) + +*Who Benefits:* + +- Users authenticating across untrusted domains, non-domain-joined machines, or cross-platform environments where configuring integrated authentication is difficult. +- Users running in containers who need manual Kerberos negotiation without deploying sidecars or external ticket-refresh mechanisms. +- Users who need NTLM username/password authentication to SQL Server, which the driver does not provide natively. + +*Impact:* + +- Applications can set a custom `SspiContextProvider` on `SqlConnection` before opening the connection: + +```c# +var connection = new SqlConnection(connectionString); +connection.SspiContextProvider = new MyKerberosProvider(); +connection.Open(); +``` + +- The provider handles the authentication token exchange during integrated authentication. Existing authentication behavior is unchanged when no custom provider is set. See [SspiContextProvider_CustomProvider.cs](../../doc/samples/SspiContextProvider_CustomProvider.cs) for a sample implementation. +- **Note:** The `SspiContextProvider` is part of the connection pool key. Care should be taken when using this property to ensure the implementation returns a stable identity per resource. + +#### Async Read Performance: Packet Multiplexing (Preview) + +*What Changed:* + +- Continued refinement of packet multiplexing with bug fixes and stability improvements since 6.1.0, plus new app context switches for opt-in control. + ([#3534](https://github.com/dotnet/SqlClient/pull/3534), + [#3537](https://github.com/dotnet/SqlClient/pull/3537), + [#3605](https://github.com/dotnet/SqlClient/pull/3605)) + +*Who Benefits:* + +- Applications performing large async reads (`ExecuteReaderAsync` with big result sets, streaming scenarios, or bulk data retrieval). + +*Impact:* + +- Packet multiplexing ships behind two opt-in feature switches: + +```c# +AppContext.SetSwitch("Switch.Microsoft.Data.SqlClient.UseCompatibilityAsyncBehaviour", false); +AppContext.SetSwitch("Switch.Microsoft.Data.SqlClient.UseCompatibilityProcessSni", false); +``` + +- Setting both switches to `false` enables the new async processing path. By default, the driver uses the existing (compatible) behavior. + +#### Enhanced Routing Support + +*What Changed:* + +- Added support for enhanced routing, a TDS feature that allows the server to redirect connections to a specific server *and* database during login. + ([#3641](https://github.com/dotnet/SqlClient/issues/3641), + [#3969](https://github.com/dotnet/SqlClient/pull/3969), + [#3970](https://github.com/dotnet/SqlClient/pull/3970), + [#3973](https://github.com/dotnet/SqlClient/pull/3973)) + +*Who Benefits:* + +- Users connecting to Azure SQL Hyperscale environments that use named read replicas and gateway-based load balancing. + +*Impact:* + +- Enhanced routing is negotiated automatically during login when the server supports it. No application code changes are required. + +#### Support for .NET 10 + +*What Changed:* + +- Updated pipelines and test suites to compile the driver using the .NET 10 SDK. + ([#3686](https://github.com/dotnet/SqlClient/pull/3686)) + +*Who Benefits:* + +- Developers targeting .NET 10 on day one. + +*Impact:* + +- SqlClient 7.0 compiles and tests against .NET 10, ensuring compatibility. + +#### Strongly-Typed Diagnostic Events on .NET Framework + +*What Changed:* + +- Enabled `SqlClientDiagnosticListener` for `SqlCommand` on .NET Framework, closing a long-standing observability gap where diagnostic events were previously only emitted on .NET Core. + ([#3658](https://github.com/dotnet/SqlClient/pull/3658)) + +- Brought the 15 strongly-typed diagnostic event classes in the `Microsoft.Data.SqlClient.Diagnostics` namespace — originally introduced for .NET Core in 6.0 — to .NET Framework as part of the codebase merge. Both platforms now use the same strongly-typed event model. The types cover command, connection, and transaction lifecycle events: + - `SqlClientCommandBefore`, `SqlClientCommandAfter`, `SqlClientCommandError` + - `SqlClientConnectionOpenBefore`, `SqlClientConnectionOpenAfter`, `SqlClientConnectionOpenError` + - `SqlClientConnectionCloseBefore`, `SqlClientConnectionCloseAfter`, `SqlClientConnectionCloseError` + - `SqlClientTransactionCommitBefore`, `SqlClientTransactionCommitAfter`, `SqlClientTransactionCommitError` + - `SqlClientTransactionRollbackBefore`, `SqlClientTransactionRollbackAfter`, `SqlClientTransactionRollbackError` + + ([#3493](https://github.com/dotnet/SqlClient/pull/3493)) + +*Who Benefits:* + +- .NET Framework users subscribing to `SqlClientDiagnosticListener` events for observability, distributed tracing, or custom telemetry. These users now have parity with .NET Core, gaining IntelliSense, compile-time safety, and eliminating the need to access diagnostic payloads via reflection or dictionary lookups. + +*Impact:* + +- On .NET Framework, `SqlCommand` now emits the same diagnostic events that were previously only available on .NET Core. Subscribers to `DiagnosticListener` events (e.g., `Microsoft.Data.SqlClient.WriteCommandBefore`) receive the strongly-typed objects: + +```c# +listener.Subscribe(new Observer>(kvp => +{ + if (kvp.Value is SqlClientCommandBefore before) + { + Console.WriteLine($"Executing: {before.Command.CommandText}"); + } +})); +``` + +- The types implement `IReadOnlyList>` for backward compatibility with code that iterates properties generically. + +#### Other Additions + +- Added `SqlConfigurableRetryFactory.BaselineTransientErrors` static property exposing the default transient error codes list as a `ReadOnlyCollection`, making it easier to extend the default list with application-specific error codes. + ([#3903](https://github.com/dotnet/SqlClient/pull/3903)) + +- Added app context switch `Switch.Microsoft.Data.SqlClient.EnableMultiSubnetFailoverByDefault` to set `MultiSubnetFailover=true` globally without modifying connection strings. + ([#3841](https://github.com/dotnet/SqlClient/pull/3841)) + +- Added app context switch `Switch.Microsoft.Data.SqlClient.IgnoreServerProvidedFailoverPartner` to let the client ignore server-provided failover partner info in Basic Availability Groups. + ([#3625](https://github.com/dotnet/SqlClient/pull/3625)) + +- Enabled User Agent Feature Extension (opt-in via `Switch.Microsoft.Data.SqlClient.EnableUserAgent`). + ([#3606](https://github.com/dotnet/SqlClient/pull/3606)) + +### Changed + +#### Deprecation of `SqlAuthenticationMethod.ActiveDirectoryPassword` + +*What Changed:* + +- `SqlAuthenticationMethod.ActiveDirectoryPassword` (the ROPC flow) is now marked `[Obsolete]` and will generate compiler warnings. This aligns with Microsoft's move toward [mandatory multifactor authentication](https://learn.microsoft.com/entra/identity/authentication/concept-mandatory-multifactor-authentication). + ([#3671](https://github.com/dotnet/SqlClient/pull/3671)) + +*Who Benefits:* + +- Teams moving toward stronger, passwordless or MFA-compliant authentication. + +*Impact:* + +- If you use `Authentication=Active Directory Password`, migrate to a supported alternative: + +| Scenario | Recommended Authentication | +|----------|---------------------------| +| Interactive / desktop apps | `Active Directory Interactive` | +| Service-to-service | `Active Directory Service Principal` | +| Azure-hosted workloads | `Active Directory Managed Identity` | +| Developer / CI environments | `Active Directory Default` | + +- See [Connect to Azure SQL with Microsoft Entra authentication](https://learn.microsoft.com/sql/connect/ado-net/sql/azure-active-directory-authentication) for more information. + +#### Breaking Changes + +- Reverted public visibility of internal interop enums (`IoControlCodeAccess` and `IoControlTransferType`) that were accidentally made public during the project merge. + ([#3900](https://github.com/dotnet/SqlClient/pull/3900)) + +#### Other Changes + +- Removed `Constrained Execution Region` error handling blocks and associated `SqlConnection` cleanup. + ([#3535](https://github.com/dotnet/SqlClient/pull/3535)) + +- Performance improvements across SqlStatistics timing, Always Encrypted scenarios, and connection opening: + ([#3609](https://github.com/dotnet/SqlClient/pull/3609), + [#3612](https://github.com/dotnet/SqlClient/pull/3612), + [#3732](https://github.com/dotnet/SqlClient/pull/3732), + [#3660](https://github.com/dotnet/SqlClient/pull/3660), + [#3791](https://github.com/dotnet/SqlClient/pull/3791), + [#3772](https://github.com/dotnet/SqlClient/pull/3772), + [#3554](https://github.com/dotnet/SqlClient/pull/3554)) + +- Allow `SqlBulkCopy` to operate on hidden columns. + ([#3590](https://github.com/dotnet/SqlClient/pull/3590)) + +- Updated UserAgent feature to use a pipe-delimited format, replacing the previous JSON format. + ([#3826](https://github.com/dotnet/SqlClient/pull/3826)) + +- Minor improvements to Managed SNI tracing to capture continuation events and errors. + ([#3859](https://github.com/dotnet/SqlClient/pull/3859)) + +### Fixed + +- Fixed a connection performance regression where SPN generation was triggered for non-integrated authentication modes (e.g., SQL authentication) on the native SNI path. + ([#3929](https://github.com/dotnet/SqlClient/pull/3929)) + +- Fixed `ExecuteScalar` to propagate errors when the server sends data followed by an error token. + ([#3912](https://github.com/dotnet/SqlClient/pull/3912)) + +- Fixed `NullReferenceException` in `SqlDataAdapter` when processing batch scenarios. + ([#3857](https://github.com/dotnet/SqlClient/pull/3857)) + +- Fixed reading of multiple app context switches from a single `AppContextSwitchOverrides` configuration field. + ([#3960](https://github.com/dotnet/SqlClient/pull/3960)) + +- Fixed an edge case in `TdsParserStateObject.TryReadPlpBytes` where zero-length reads returned `null` instead of an empty array. + ([#3872](https://github.com/dotnet/SqlClient/pull/3872)) + +- Fixed issue where extra connection deactivation was occurring. + ([#3758](https://github.com/dotnet/SqlClient/pull/3758)) + +- Fixed debug assertion in connection pool (no impact to production code). + ([#3587](https://github.com/dotnet/SqlClient/pull/3587)) + +- Prevented uninitialized performance counters escaping `CreatePerformanceCounters`. + ([#3623](https://github.com/dotnet/SqlClient/pull/3623)) + +- Fixed `SetProvider` to return immediately if user-defined authentication provider found. + ([#3620](https://github.com/dotnet/SqlClient/pull/3620)) + +- Fixed connection pool concurrency issue. + ([#3632](https://github.com/dotnet/SqlClient/pull/3632)) + + +## 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) +- [frankbuckley](https://github.com/frankbuckley) +- [MatthiasHuygelen](https://github.com/MatthiasHuygelen) +- [ShreyaLaxminarayan](https://github.com/ShreyaLaxminarayan) +- [tetolv](https://github.com/tetolv) +- [twsouthwick](https://github.com/twsouthwick) +- [Wraith2](https://github.com/Wraith2) + +## Target Platform Support + +- .NET Framework 4.6.2+ (Windows x86, Windows x64, Windows ARM64) +- .NET 8.0+ (Windows x86, Windows x64, Windows ARM, Windows ARM64, Linux, macOS) + +### Dependencies + +#### .NET 9.0 + +- Microsoft.Bcl.Cryptography 9.0.13 +- Microsoft.Data.SqlClient.Extensions.Abstractions 1.0.0 +- Microsoft.Data.SqlClient.Internal.Logging 1.0.0 +- Microsoft.Data.SqlClient.SNI.runtime 6.0.2 +- Microsoft.Extensions.Caching.Memory 9.0.13 +- Microsoft.IdentityModel.JsonWebTokens 8.16.0 +- Microsoft.IdentityModel.Protocols.OpenIdConnect 8.16.0 +- Microsoft.SqlServer.Server 1.0.0 +- System.Configuration.ConfigurationManager 9.0.13 +- System.Security.Cryptography.Pkcs 9.0.13 + +#### .NET 8.0 + +- Microsoft.Bcl.Cryptography 8.0.0 +- Microsoft.Data.SqlClient.Extensions.Abstractions 1.0.0 +- Microsoft.Data.SqlClient.Internal.Logging 1.0.0 +- Microsoft.Data.SqlClient.SNI.runtime 6.0.2 +- Microsoft.Extensions.Caching.Memory 8.0.1 +- Microsoft.IdentityModel.JsonWebTokens 8.16.0 +- Microsoft.IdentityModel.Protocols.OpenIdConnect 8.16.0 +- Microsoft.SqlServer.Server 1.0.0 +- System.Configuration.ConfigurationManager 8.0.1 +- System.Security.Cryptography.Pkcs 8.0.1 + +#### .NET Standard 2.0 + +- Microsoft.Bcl.Cryptography 8.0.0 +- Microsoft.Data.SqlClient.Extensions.Abstractions 1.0.0 +- Microsoft.Data.SqlClient.Internal.Logging 1.0.0 +- Microsoft.Data.SqlClient.SNI.runtime 6.0.2 +- Microsoft.Extensions.Caching.Memory 8.0.1 +- Microsoft.IdentityModel.JsonWebTokens 8.16.0 +- Microsoft.IdentityModel.Protocols.OpenIdConnect 8.16.0 +- Microsoft.SqlServer.Server 1.0.0 +- System.Configuration.ConfigurationManager 8.0.1 +- System.Security.Cryptography.Pkcs 8.0.1 +- System.Text.Json 10.0.3 +- System.Threading.Channels 10.0.3 + +#### .NET Framework 4.6.2+ + +- Microsoft.Bcl.Cryptography 8.0.0 +- Microsoft.Data.SqlClient.Extensions.Abstractions 1.0.0 +- Microsoft.Data.SqlClient.Internal.Logging 1.0.0 +- Microsoft.Data.SqlClient.SNI 6.0.2 +- Microsoft.Extensions.Caching.Memory 8.0.1 +- Microsoft.IdentityModel.JsonWebTokens 8.16.0 +- Microsoft.IdentityModel.Protocols.OpenIdConnect 8.16.0 +- System.Buffers 4.6.1 +- System.Diagnostics.DiagnosticSource 10.0.3 +- System.Memory 4.6.3 +- System.Runtime.InteropServices.RuntimeInformation 4.3.0 +- System.Security.Cryptography.Pkcs 8.0.1 +- System.Text.Json 10.0.3 +- System.Threading.Channels 10.0.3 +- System.ValueTuple 4.6.2 diff --git a/release-notes/7.0/README.md b/release-notes/7.0/README.md index e8bfcc25f3..70cfdc3835 100644 --- a/release-notes/7.0/README.md +++ b/release-notes/7.0/README.md @@ -4,6 +4,7 @@ The following Microsoft.Data.SqlClient 7.0 releases have been shipped: | Release Date | Version | Notes | | :-- | :-- | :--: | +| 2026-03-17 | 7.0.0 | [Release Notes](7.0.0.md) | | 2026-03-05 | 7.0.0-preview4.26064.3 | [Release Notes](7.0.0-preview4.md) | | 2025-12-08 | 7.0.0-preview3.25342.7 | [Release Notes](7.0.0-preview3.md) | | 2025-10-16 | 7.0.0-preview2.25289.6 | [Release Notes](7.0.0-preview2.md) | diff --git a/release-notes/Extensions/Abstractions/1.0/1.0.0.md b/release-notes/Extensions/Abstractions/1.0/1.0.0.md new file mode 100644 index 0000000000..82157224ff --- /dev/null +++ b/release-notes/Extensions/Abstractions/1.0/1.0.0.md @@ -0,0 +1,30 @@ +# Release Notes + +## Stable Release 1.0.0 - 2026-03-17 + +This is the first stable release of `Microsoft.Data.SqlClient.Extensions.Abstractions`, providing shared types and interfaces between the core `Microsoft.Data.SqlClient` driver and its extension packages (e.g., `Microsoft.Data.SqlClient.Extensions.Azure`). + +## Changes Since [1.0.0-preview1](1.0.0-preview1.md) + +No changes to this package since preview1. + +## Cumulative Changes (First Stable Release) + +This is the first stable release of this package. The following summarizes all changes across the preview cycle. + +### Added + +- Shared abstraction types for the SqlClient extensions model. + ([#3626](https://github.com/dotnet/SqlClient/pull/3626), + [#3628](https://github.com/dotnet/SqlClient/pull/3628), + [#3967](https://github.com/dotnet/SqlClient/pull/3967)) + +## Target Platform Support + +- .NET Standard 2.0 + +### Dependencies + +#### .NET Standard 2.0 + +- Microsoft.Data.SqlClient.Internal.Logging 1.0.0 diff --git a/release-notes/Extensions/Abstractions/1.0/README.md b/release-notes/Extensions/Abstractions/1.0/README.md index 2e91111742..347ae77335 100644 --- a/release-notes/Extensions/Abstractions/1.0/README.md +++ b/release-notes/Extensions/Abstractions/1.0/README.md @@ -5,4 +5,5 @@ The following `Microsoft.Data.SqlClient.Extensions.Abstractions` | Release Date | Description | Notes | | :-- | :-- | :--: | +| 2026-03-17 | 1.0.0 | [Release Notes](1.0.0.md) | | 2026-03-05 | 1.0.0-preview1.26064.3 | [Release Notes](1.0.0-preview1.md) | diff --git a/release-notes/Extensions/Azure/1.0/1.0.0.md b/release-notes/Extensions/Azure/1.0/1.0.0.md new file mode 100644 index 0000000000..c371669493 --- /dev/null +++ b/release-notes/Extensions/Azure/1.0/1.0.0.md @@ -0,0 +1,66 @@ +# Release Notes + +## Stable Release 1.0.0 - 2026-03-17 + +This is the first stable release of `Microsoft.Data.SqlClient.Extensions.Azure`, providing Microsoft Entra ID (Azure Active Directory) authentication support for Microsoft.Data.SqlClient. This package was extracted from the core driver to address the [most requested feature](https://github.com/dotnet/SqlClient/issues/1108) — making the core package lighter by removing Azure dependencies. + +## Changes Since [1.0.0-preview1](1.0.0-preview1.md) + +### Changed + +- Renamed dependency from `Microsoft.Data.SqlClient.Extensions.Logging` to `Microsoft.Data.SqlClient.Internal.Logging`. + ([#4038](https://github.com/dotnet/SqlClient/pull/4038)) + +- Updated Dependencies + ([#4045](https://github.com/dotnet/SqlClient/pull/4045)): + - Updated `Azure.Core` to v1.51.1 + - Updated `Azure.Identity` to v1.18.0 + - Updated `Microsoft.Identity.Client` to v4.83.0 + +## Cumulative Changes (First Stable Release) + +This is the first stable release of this package. The following summarizes all changes across the preview cycle. + +### Added + +- Entra ID authentication provider (`ActiveDirectoryAuthenticationProvider`) supporting: + - `ActiveDirectoryInteractive` + - `ActiveDirectoryDefault` + - `ActiveDirectoryManagedIdentity` + - `ActiveDirectoryServicePrincipal` + - `ActiveDirectoryDeviceCodeFlow` + - `ActiveDirectoryWorkloadIdentity` + - `ActiveDirectoryPassword` (deprecated) + ([#3680](https://github.com/dotnet/SqlClient/pull/3680), + [#3902](https://github.com/dotnet/SqlClient/pull/3902), + [#3904](https://github.com/dotnet/SqlClient/pull/3904), + [#3908](https://github.com/dotnet/SqlClient/pull/3908), + [#3917](https://github.com/dotnet/SqlClient/pull/3917), + [#3982](https://github.com/dotnet/SqlClient/pull/3982), + [#3978](https://github.com/dotnet/SqlClient/pull/3978), + [#3986](https://github.com/dotnet/SqlClient/pull/3986)) + +## Target Platform Support + +- .NET Standard 2.0 +- .NET Framework 4.6.2+ + +### Dependencies + +#### .NET Standard 2.0 + +- Azure.Core 1.51.1 +- Azure.Identity 1.18.0 +- Microsoft.Data.SqlClient.Extensions.Abstractions 1.0.0 +- Microsoft.Data.SqlClient.Internal.Logging 1.0.0 +- Microsoft.Extensions.Caching.Memory 8.0.1 +- Microsoft.Identity.Client 4.83.0 + +#### .NET Framework 4.6.2+ + +- Azure.Core 1.51.1 +- Azure.Identity 1.18.0 +- Microsoft.Data.SqlClient.Extensions.Abstractions 1.0.0 +- Microsoft.Data.SqlClient.Internal.Logging 1.0.0 +- Microsoft.Extensions.Caching.Memory 8.0.1 +- Microsoft.Identity.Client 4.83.0 diff --git a/release-notes/Extensions/Azure/1.0/README.md b/release-notes/Extensions/Azure/1.0/README.md index 19dc060723..dd0030f110 100644 --- a/release-notes/Extensions/Azure/1.0/README.md +++ b/release-notes/Extensions/Azure/1.0/README.md @@ -5,4 +5,5 @@ The following `Microsoft.Data.SqlClient.Extensions.Azure` | Release Date | Description | Notes | | :-- | :-- | :--: | +| 2026-03-17 | 1.0.0 | [Release Notes](1.0.0.md) | | 2026-03-05 | 1.0.0-preview1.26064.3 | [Release Notes](1.0.0-preview1.md) | diff --git a/release-notes/Extensions/Logging/1.0/README.md b/release-notes/Extensions/Logging/1.0/README.md index b31406665b..f5cbea90ea 100644 --- a/release-notes/Extensions/Logging/1.0/README.md +++ b/release-notes/Extensions/Logging/1.0/README.md @@ -1,5 +1,8 @@ # Microsoft.Data.SqlClient.Extensions.Logging 1.0 Releases +> **Note:** This package has been renamed to `Microsoft.Data.SqlClient.Internal.Logging` starting +> with the 1.0.0 stable release. See [Internal/Logging release notes](../../../Internal/Logging/1.0/1.0.0.md). + > **Note:** This package is for internal use by other Microsoft.Data.SqlClient packages only > and should not be referenced directly by application code. diff --git a/release-notes/Internal/Logging/1.0/1.0.0.md b/release-notes/Internal/Logging/1.0/1.0.0.md new file mode 100644 index 0000000000..5cbf4d886b --- /dev/null +++ b/release-notes/Internal/Logging/1.0/1.0.0.md @@ -0,0 +1,37 @@ +# Release Notes + +## Stable Release 1.0.0 - 2026-03-17 + +> **Note:** This package is for internal use by other Microsoft.Data.SqlClient packages only +> and should not be referenced directly by application code. + +This is the first stable release of `Microsoft.Data.SqlClient.Internal.Logging`, providing shared ETW EventSource tracing infrastructure for the Microsoft.Data.SqlClient package family. + +## Changes Since [1.0.0-preview1](../../../Extensions/Logging/1.0/1.0.0-preview1.md) + +### Changed + +- Renamed from `Microsoft.Data.SqlClient.Extensions.Logging` to `Microsoft.Data.SqlClient.Internal.Logging` to clarify that this package is for internal use only. + ([#4038](https://github.com/dotnet/SqlClient/pull/4038)) + +## Cumulative Changes (First Stable Release) + +This is the first stable release of this package. The following summarizes all changes across the preview cycle. + +### Added + +- Shared ETW EventSource tracing infrastructure for internal use by `Microsoft.Data.SqlClient` and its extension packages. + ([#3626](https://github.com/dotnet/SqlClient/pull/3626), + [#3628](https://github.com/dotnet/SqlClient/pull/3628), + [#3967](https://github.com/dotnet/SqlClient/pull/3967)) + + +## Target Platform Support + +- .NET Standard 2.0 + +### Dependencies + +#### .NET Standard 2.0 + +- None diff --git a/release-notes/Internal/Logging/1.0/README.md b/release-notes/Internal/Logging/1.0/README.md new file mode 100644 index 0000000000..15978c4843 --- /dev/null +++ b/release-notes/Internal/Logging/1.0/README.md @@ -0,0 +1,11 @@ +# Microsoft.Data.SqlClient.Internal.Logging 1.0 Releases + +> **Note:** This package is for internal use by other Microsoft.Data.SqlClient packages only +> and should not be referenced directly by application code. + +The following `Microsoft.Data.SqlClient.Internal.Logging` +1.0 releases have been shipped: + +| Release Date | Description | Notes | +| :-- | :-- | :--: | +| 2026-03-17 | 1.0.0 | [Release Notes](1.0.0.md) | diff --git a/release-notes/README.md b/release-notes/README.md index 4e2c67bc33..a511a589bc 100644 --- a/release-notes/README.md +++ b/release-notes/README.md @@ -1,6 +1,6 @@ # Microsoft.Data.SqlClient Release Notes -The latest stable release is [Microsoft.Data.SqlClient 6.1](6.1). +The latest stable release is [Microsoft.Data.SqlClient 7.0](7.0). ## Release Information @@ -22,7 +22,7 @@ The latest stable release is [Microsoft.Data.SqlClient 6.1](6.1). # Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider Release Notes The latest stable release is -[Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 6.1](add-ons/AzureKeyVaultProvider/6.1). +[Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 7.0](add-ons/AzureKeyVaultProvider/7.0). ## Release Information @@ -54,14 +54,15 @@ The latest release is - [Microsoft.Data.SqlClient.Extensions.Azure 1.0](Extensions/Azure/1.0) -# Microsoft.Data.SqlClient.Extensions.Logging Release Notes + +# Microsoft.Data.SqlClient.Internal.Logging Release Notes The latest release is -[Microsoft.Data.SqlClient.Extensions.Logging 1.0](Extensions/Logging/1.0). +[Microsoft.Data.SqlClient.Internal.Logging 1.0](Internal/Logging/1.0). ## Release Information -- [Microsoft.Data.SqlClient.Extensions.Logging 1.0](Extensions/Logging/1.0) +- [Microsoft.Data.SqlClient.Internal.Logging 1.0](Internal/Logging/1.0) # Microsoft.SqlServer.Server Release Notes diff --git a/release-notes/add-ons/AzureKeyVaultProvider/7.0/7.0.0.md b/release-notes/add-ons/AzureKeyVaultProvider/7.0/7.0.0.md new file mode 100644 index 0000000000..6a6f31054b --- /dev/null +++ b/release-notes/add-ons/AzureKeyVaultProvider/7.0/7.0.0.md @@ -0,0 +1,54 @@ +# Release Notes + +## Stable Release 7.0.0 - 2026-03-17 + +## Changes Since [7.0.0-preview1](7.0.0-preview1.md) + +### Changed + +- Changed AKV Provider to target .NET Standard 2.0 instead of multi-targeting .NET Framework and .NET Core. Since the package contains no framework-specific code, this simplifies the package and reduces its footprint. + ([#4036](https://github.com/dotnet/SqlClient/pull/4036)) + +- Renamed dependency from `Microsoft.Data.SqlClient.Extensions.Logging` to `Microsoft.Data.SqlClient.Internal.Logging`. + ([#4038](https://github.com/dotnet/SqlClient/pull/4038)) + +- Updated Dependencies + ([#4045](https://github.com/dotnet/SqlClient/pull/4045)): + - Updated `Azure.Core` to v1.51.1 + - Updated `Azure.Security.KeyVault.Keys` to v4.9.0 + +## Cumulative Changes Since [6.1](../6.1/README.md) + +This section summarizes all changes across the 7.0 preview cycle for users upgrading from the latest 6.1 stable release. + +### Changed + +- Changed AKV Provider to target .NET Standard 2.0 instead of multi-targeting .NET Framework and .NET Core. Since the package contains no framework-specific code, this simplifies the package and reduces its footprint. + ([#4036](https://github.com/dotnet/SqlClient/pull/4036)) + +- Performance improvements for all built-in `SqlColumnEncryptionKeyStoreProvider` implementations, including `SqlColumnEncryptionAzureKeyVaultProvider`. + ([#3554](https://github.com/dotnet/SqlClient/pull/3554)) + +- Added dependency on `Microsoft.Data.SqlClient.Internal.Logging` to align with the new extensions infrastructure. + ([#3626](https://github.com/dotnet/SqlClient/pull/3626), + [#3628](https://github.com/dotnet/SqlClient/pull/3628), + [#3967](https://github.com/dotnet/SqlClient/pull/3967), + [#4038](https://github.com/dotnet/SqlClient/pull/4038)) + +- Updated Dependencies: + - Updated `Azure.Core` to v1.51.1 + - Updated `Azure.Security.KeyVault.Keys` to v4.9.0 + +## Target Platform Support + +- .NET Standard 2.0 + +### Dependencies + +#### .NET Standard 2.0 + +- Azure.Core 1.51.1 +- Azure.Security.KeyVault.Keys 4.9.0 +- Microsoft.Data.SqlClient (>= 7.0.0) +- Microsoft.Data.SqlClient.Internal.Logging 1.0.0 +- Microsoft.Extensions.Caching.Memory 8.0.1 diff --git a/release-notes/add-ons/AzureKeyVaultProvider/7.0/README.md b/release-notes/add-ons/AzureKeyVaultProvider/7.0/README.md index c63a0bc71f..22593ef05c 100644 --- a/release-notes/add-ons/AzureKeyVaultProvider/7.0/README.md +++ b/release-notes/add-ons/AzureKeyVaultProvider/7.0/README.md @@ -5,4 +5,5 @@ The following `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` | Release Date | Description | Notes | | :-- | :-- | :--: | +| 2026-03-17 | 7.0.0 | [Release Notes](7.0.0.md) | | 2026-03-05 | 7.0.0-preview1.26064.3 | [Release Notes](7.0.0-preview1.md) | diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AbstractionsVersions.props b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AbstractionsVersions.props index 2bd328fb92..5ebc995a32 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AbstractionsVersions.props +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AbstractionsVersions.props @@ -41,7 +41,7 @@ --> - 1 + 7 <_OurPackageVersion Condition="'$(AbstractionsPackageVersion)' != ''">$(AbstractionsPackageVersion) diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.Internal.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.Internal.cs index e0adf34e49..5007384465 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.Internal.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.Internal.cs @@ -59,7 +59,7 @@ static Internal() // Look for the manager class. const string className = "Microsoft.Data.SqlClient.SqlAuthenticationProviderManager"; - var manager = assembly.GetType(className); + Type? manager = assembly.GetType(className); if (manager is null) { diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/doc/ActiveDirectoryAuthenticationProvider.xml b/src/Microsoft.Data.SqlClient.Extensions/Azure/doc/ActiveDirectoryAuthenticationProvider.xml index 84b6343497..2b58a8676f 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Azure/doc/ActiveDirectoryAuthenticationProvider.xml +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/doc/ActiveDirectoryAuthenticationProvider.xml @@ -1,4 +1,4 @@ - + - 1 + 7 <_OurPackageVersion Condition="'$(AzurePackageVersion)' != ''">$(AzurePackageVersion) diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Interop/Interop.GetAncestor.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Interop/Interop.GetAncestor.cs new file mode 100644 index 0000000000..af255844fc --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Interop/Interop.GetAncestor.cs @@ -0,0 +1,42 @@ +// 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.Runtime.InteropServices; + +namespace Microsoft.Data.SqlClient; + +/// +/// Win32 P/Invoke wrappers used by for +/// console-window discovery. Follows the .NET runtime's Interop convention: one Win32 +/// import per file, grouped into a nested Interop.<module> static class that mirrors +/// the Win32 DLL it targets. Only the internal helper is exposed; the raw +/// DllImport stays private. +/// +internal static partial class Interop +{ + internal static partial class User32 + { + /// + /// GA_ROOTOWNER flag value for GetAncestor — "Retrieves the owned root + /// window by walking the chain of parent and owner windows returned by GetParent." + /// + private const uint GA_ROOTOWNER = 3; + + /// + /// Raw user32!GetAncestor P/Invoke. Documented by Windows to return + /// rather than throw when the input handle is invalid. + /// + [DllImport("user32.dll")] + private static extern IntPtr GetAncestor(IntPtr hwnd, uint gaFlags); + + /// + /// Walks the parent/owner chain of and returns the root owner + /// window, or when none can be found. + /// + internal static IntPtr GetRootOwner(IntPtr hwnd) + { + return GetAncestor(hwnd, GA_ROOTOWNER); + } + } +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Interop/Interop.GetConsoleWindow.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Interop/Interop.GetConsoleWindow.cs new file mode 100644 index 0000000000..4638e1458d --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/Interop/Interop.GetConsoleWindow.cs @@ -0,0 +1,27 @@ +// 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.Runtime.InteropServices; + +namespace Microsoft.Data.SqlClient; + +/// +/// Win32 P/Invoke wrappers used by for +/// console-window discovery. Follows the .NET runtime's Interop convention: one Win32 +/// import per file, grouped into a nested Interop.<module> static class that mirrors +/// the Win32 DLL it targets. +/// +internal static partial class Interop +{ + internal static partial class Kernel32 + { + /// + /// Raw kernel32!GetConsoleWindow P/Invoke. Documented by Windows to return + /// when the calling process is not attached to a console + /// (and to never throw). + /// + [DllImport("kernel32.dll")] + internal static extern IntPtr GetConsoleWindow(); + } +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADAuthenticationTests.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADAuthenticationTests.cs index 5c7572ec84..e90dcce506 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADAuthenticationTests.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADAuthenticationTests.cs @@ -10,6 +10,7 @@ namespace Microsoft.Data.SqlClient.Extensions.Azure.Test; // These tests were moved from MDS FunctionalTests AADAuthenticationTests.cs. +[Collection("SqlAuthenticationProvider")] public class AADAuthenticationTests { [Fact] diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADConnectionTest.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADConnectionTest.cs index 8c4006c5b4..5dd3ac336e 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADConnectionTest.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/AADConnectionTest.cs @@ -26,120 +26,6 @@ public static void KustoDatabaseTest() Assert.Equal(System.Data.ConnectionState.Open, connection.State); } - [ConditionalFact( - typeof(Config), - nameof(Config.HasPasswordConnectionString))] - public static void AADPasswordWithWrongPassword() - { - string[] credKeys = { "Password", "PWD" }; - string connStr = RemoveKeysInConnStr(Config.PasswordConnectionString, credKeys) + "Password=TestPassword;"; - - Assert.Throws(() => ConnectAndDisconnect(connStr)); - - // We cannot verify error message with certainty as driver may cache token from other tests for current user - // and error message may change accordingly. - } - - [ConditionalFact( - typeof(Config), - nameof(Config.HasPasswordConnectionString))] - public static void TestADPasswordAuthentication() - { - // Connect to Azure DB with password and retrieve user name. - using (SqlConnection conn = new SqlConnection(Config.PasswordConnectionString)) - { - conn.Open(); - using (SqlCommand sqlCommand = new SqlCommand - ( - cmdText: $"SELECT SUSER_SNAME();", - connection: conn, - transaction: null - )) - { - string customerId = (string)sqlCommand.ExecuteScalar(); - string expected = RetrieveValueFromConnStr(Config.PasswordConnectionString, new string[] { "User ID", "UID" }); - Assert.Equal(expected, customerId); - } - } - } - - [ConditionalFact( - typeof(Config), - nameof(Config.HasPasswordConnectionString))] - public static void EmptyPasswordInConnStrAADPassword() - { - // connection fails with expected error message. - string[] pwdKey = { "Password", "PWD" }; - string connStr = RemoveKeysInConnStr(Config.PasswordConnectionString, pwdKey) + "Password=;"; - SqlException e = Assert.Throws(() => ConnectAndDisconnect(connStr)); - - string? user = FetchKeyInConnStr(Config.PasswordConnectionString, new string[] { "User Id", "UID" }); - string expectedMessage = string.Format("Failed to authenticate the user {0} in Active Directory (Authentication=ActiveDirectoryPassword).", user); - Assert.Contains(expectedMessage, e.Message); - } - - [ConditionalFact( - typeof(Config), - nameof(Config.OnWindows), - nameof(Config.HasPasswordConnectionString))] - public static void EmptyCredInConnStrAADPassword() - { - // connection fails with expected error message. - string[] removeKeys = { "User ID", "Password", "UID", "PWD" }; - string connStr = RemoveKeysInConnStr(Config.PasswordConnectionString, removeKeys) + "User ID=; Password=;"; - SqlException e = Assert.Throws(() => ConnectAndDisconnect(connStr)); - - string expectedMessage = "Failed to authenticate the user in Active Directory (Authentication=ActiveDirectoryPassword)."; - Assert.Contains(expectedMessage, e.Message); - } - - [ConditionalFact( - typeof(Config), - nameof(Config.OnUnix), - nameof(Config.HasPasswordConnectionString))] - public static void EmptyCredInConnStrAADPasswordAnyUnix() - { - // connection fails with expected error message. - string[] removeKeys = { "User ID", "Password", "UID", "PWD" }; - string connStr = RemoveKeysInConnStr(Config.PasswordConnectionString, removeKeys) + "User ID=; Password=;"; - SqlException e = Assert.Throws(() => ConnectAndDisconnect(connStr)); - - string expectedMessage = "MSAL cannot determine the username (UPN) of the currently logged in user.For Integrated Windows Authentication and Username/Password flows, please use .WithUsername() before calling ExecuteAsync()."; - Assert.Contains(expectedMessage, e.Message); - } - - [ConditionalFact( - typeof(Config), - nameof(Config.HasPasswordConnectionString))] - public static void AADPasswordWithInvalidUser() - { - // connection fails with expected error message. - string[] removeKeys = { "User ID", "UID" }; - string user = "testdotnet@domain.com"; - string connStr = RemoveKeysInConnStr(Config.PasswordConnectionString, removeKeys) + $"User ID={user}"; - SqlException e = Assert.Throws(() => ConnectAndDisconnect(connStr)); - - string expectedMessage = string.Format("Failed to authenticate the user {0} in Active Directory (Authentication=ActiveDirectoryPassword).", user); - Assert.Contains(expectedMessage, e.Message); - } - - [ConditionalFact( - typeof(Config), - nameof(Config.HasPasswordConnectionString))] - public static void NoCredentialsActiveDirectoryPassword() - { - // test Passes with correct connection string. - ConnectAndDisconnect(Config.PasswordConnectionString); - - // connection fails with expected error message. - string[] credKeys = { "User ID", "Password", "UID", "PWD" }; - string connStrWithNoCred = RemoveKeysInConnStr(Config.PasswordConnectionString, credKeys); - InvalidOperationException e = Assert.Throws(() => ConnectAndDisconnect(connStrWithNoCred)); - - string expectedMessage = "Either Credential or both 'User ID' and 'Password' (or 'UID' and 'PWD') connection string keywords must be specified, if 'Authentication=Active Directory Password'."; - Assert.Contains(expectedMessage, e.Message); - } - [ConditionalFact( typeof(Config), nameof(Config.HasPasswordConnectionString), diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Config.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Config.cs index f153096d9f..1fe30b979e 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Config.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/Config.cs @@ -40,6 +40,7 @@ internal static class Config internal static bool DebugEmit { get; } = false; internal static bool IntegratedSecuritySupported { get; } = false; internal static bool ManagedIdentitySupported { get; } = false; + // @TODO Remove PasswordConnectionString from config; AAD Password auth is deprecated internal static string PasswordConnectionString { get; } = string.Empty; internal static string ServicePrincipalId { get; } = string.Empty; internal static string ServicePrincipalSecret { get; } = string.Empty; diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/DefaultAuthProviderTests.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/DefaultAuthProviderTests.cs index 84d32651d5..fa426141c9 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/DefaultAuthProviderTests.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/DefaultAuthProviderTests.cs @@ -4,6 +4,7 @@ namespace Microsoft.Data.SqlClient.Extensions.Azure.Test; +[Collection("SqlAuthenticationProvider")] public class DefaultAuthProviderTests { // Verify that our auth provider has been installed for all AAD/Entra diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/SqlAuthenticationProviderCollection.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/SqlAuthenticationProviderCollection.cs new file mode 100644 index 0000000000..2bf23f7873 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/SqlAuthenticationProviderCollection.cs @@ -0,0 +1,14 @@ +// 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 Microsoft.Data.SqlClient.Extensions.Azure.Test; + +/// +/// Defines a test collection that serializes execution of test classes +/// which mutate the global registry. +/// +[CollectionDefinition("SqlAuthenticationProvider")] +public class SqlAuthenticationProviderCollection +{ +} diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/test/WamBrokerTests.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/WamBrokerTests.cs new file mode 100644 index 0000000000..9b2aa5dcf7 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/test/WamBrokerTests.cs @@ -0,0 +1,316 @@ +// 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.Reflection; + +namespace Microsoft.Data.SqlClient.Extensions.Azure.Test; + +[Collection("SqlAuthenticationProvider")] +public class WamBrokerTests +{ + // The SqlClient first-party application client id that is hard-coded in the provider. + private const string SqlClientApplicationId = "2fd908ad-0664-4344-b9be-cd3e8b574c38"; + + // A fixed, deterministic stand-in for a caller-supplied application id. Hard-coded (instead + // of Guid.NewGuid()) so test outcomes don't depend on RNG and so a single point asserts + // that this value differs from the SqlClient first-party id. + private const string TestCustomAppId = "11111111-2222-3333-4444-555555555555"; + + // Reads the private _parentActivityOrWindowFunc field. Used to assert downstream effects + // of SetParentActivityOrWindowFunc without triggering a live MSAL flow. + private static Func? GetParentActivityOrWindowFunc(ActiveDirectoryAuthenticationProvider provider) + { + FieldInfo? field = typeof(ActiveDirectoryAuthenticationProvider).GetField( + "_parentActivityOrWindowFunc", + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(field); + return (Func?)field!.GetValue(provider); + } + + /// + /// A callback is treated as "clear any previously installed callback" + /// and must not throw. This is a deliberate API contract change from the original + /// behavior so callers can opt out without recreating + /// the provider. Asserts the underlying field is reset to so the + /// provider's downstream consumer (MSAL parameters builder) sees the cleared state. + /// + [Fact] + public void SetParentActivityOrWindowFunc_Null_ClearsCallback() + { + var provider = new ActiveDirectoryAuthenticationProvider(); + Func first = () => IntPtr.Zero; + provider.SetParentActivityOrWindowFunc(first); + Assert.Same(first, GetParentActivityOrWindowFunc(provider)); + + provider.SetParentActivityOrWindowFunc(null); + Assert.Null(GetParentActivityOrWindowFunc(provider)); + } + + /// + /// The constructor uses the SqlClient first-party application id, which always + /// enables WAM broker mode regardless of any opt-in flag. + /// + [Fact] + public void Ctor_ApplicationClientId_EnablesWamBroker() + { + var provider = new ActiveDirectoryAuthenticationProvider(SqlClientApplicationId); + Assert.Equal(SqlClientApplicationId, provider.ApplicationClientId); + Assert.True(provider.UseWamBroker, + "Constructor with SqlClient first-party application id must enable WAM broker."); + } + + /// + /// The parameterless constructor uses the SqlClient first-party application id, which always + /// enables WAM broker mode regardless of any opt-in flag. + /// + [Fact] + public void Ctor_Default_EnablesWamBroker() + { + var provider = new ActiveDirectoryAuthenticationProvider(); + Assert.Equal(SqlClientApplicationId, provider.ApplicationClientId); + Assert.True(provider.UseWamBroker, + "Default ctor must enable WAM broker (uses SqlClient first-party application id)."); + } + + /// A caller-supplied application id without explicit opt-in must NOT enable WAM broker. + [Fact] + public void Ctor_AppClientId_DefaultsUseWamBrokerToFalse() + { + var provider = new ActiveDirectoryAuthenticationProvider(TestCustomAppId); + + Assert.Equal(TestCustomAppId, provider.ApplicationClientId); + Assert.False(provider.UseWamBroker, + "Custom application id without useWamBroker=true must keep WAM broker disabled."); + } + + /// + /// Mirrors the previous test for the + /// constructor: a caller (or app.config) that sets only ApplicationClientId and skips + /// UseWamBroker must get the documented default of . This is + /// the contract SqlAuthenticationProviderManager relies on when reflecting onto the + /// Options ctor and only forwarding the properties that were explicitly configured. + /// + [Fact] + public void Ctor_Options_AppClientIdOnly_DefaultsUseWamBrokerToFalse() + { + var provider = new ActiveDirectoryAuthenticationProvider( + new ActiveDirectoryAuthenticationProviderOptions + { + ApplicationClientId = TestCustomAppId, + // UseWamBroker intentionally left at its default (false). + }); + + Assert.Equal(TestCustomAppId, provider.ApplicationClientId); + Assert.False(provider.UseWamBroker, + "Options ctor with ApplicationClientId set and UseWamBroker omitted must keep WAM broker disabled."); + } + + /// + /// Passing the SqlClient first-party application id to the single-string constructor must + /// enable WAM broker. The first-party app id is hard-wired to the WAM broker redirect URI, + /// so callers that opt into it explicitly should get the same behavior as the parameterless + /// constructor. + /// + [Fact] + public void Ctor_AppClientId_SqlClientId_EnablesWamBroker() + { + var provider = new ActiveDirectoryAuthenticationProvider(SqlClientApplicationId); + + Assert.Equal(SqlClientApplicationId, provider.ApplicationClientId); + Assert.True(provider.UseWamBroker, + "Single-string ctor with the SqlClient first-party id must enable WAM broker."); + } + + /// A caller-supplied application id with explicit opt-in must enable WAM broker. + [Fact] + public void Ctor_AppClientId_UseWamBrokerTrue_EnablesWamBroker() + { + var provider = new ActiveDirectoryAuthenticationProvider( + new ActiveDirectoryAuthenticationProviderOptions + { + ApplicationClientId = TestCustomAppId, + UseWamBroker = true, + }); + + Assert.Equal(TestCustomAppId, provider.ApplicationClientId); + Assert.True(provider.UseWamBroker, + "Custom application id with UseWamBroker=true must enable WAM broker."); + } + + /// A caller-supplied application id with explicit opt-out keeps WAM broker disabled. + [Fact] + public void Ctor_AppClientId_UseWamBrokerFalse_DisablesWamBroker() + { + var provider = new ActiveDirectoryAuthenticationProvider( + new ActiveDirectoryAuthenticationProviderOptions + { + ApplicationClientId = TestCustomAppId, + UseWamBroker = false, + }); + + Assert.Equal(TestCustomAppId, provider.ApplicationClientId); + Assert.False(provider.UseWamBroker); + } + + /// + /// Even when the SqlClient first-party application id is passed explicitly with + /// UseWamBroker=false, WAM broker mode must remain enabled because the first-party + /// app id is hard-wired to the WAM broker redirect URI. This guards the OR-condition in + /// the provider's constructor. + /// + [Fact] + public void Ctor_SqlClientAppIdExplicit_UseWamBrokerFalse_StillEnablesWamBroker() + { + var provider = new ActiveDirectoryAuthenticationProvider( + new ActiveDirectoryAuthenticationProviderOptions + { + ApplicationClientId = SqlClientApplicationId, + UseWamBroker = false, + }); + + Assert.Equal(SqlClientApplicationId, provider.ApplicationClientId); + Assert.True(provider.UseWamBroker, + "SqlClient first-party application id must always enable WAM broker, regardless of the UseWamBroker option."); + } + + /// + /// Passing a device-code callback together with a custom application id and + /// UseWamBroker=true via + /// must enable WAM broker mode. + /// + [Fact] + public void Ctor_WithDeviceCodeCallback_UseWamBrokerTrue_EnablesWamBroker() + { + var provider = new ActiveDirectoryAuthenticationProvider( + new ActiveDirectoryAuthenticationProviderOptions + { + DeviceCodeFlowCallback = static _ => Task.CompletedTask, + ApplicationClientId = TestCustomAppId, + UseWamBroker = true, + }); + + Assert.Equal(TestCustomAppId, provider.ApplicationClientId); + Assert.True(provider.UseWamBroker); + } + + /// + /// The two-arg device-code constructor (deviceCodeCallback, applicationClientId) must default + /// useWamBroker to for caller-supplied application ids. + /// + [Fact] + public void Ctor_WithDeviceCodeCallback_AppClientIdOnly_DefaultsUseWamBrokerToFalse() + { + var provider = new ActiveDirectoryAuthenticationProvider( + deviceCodeFlowCallbackMethod: static _ => Task.CompletedTask, + applicationClientId: TestCustomAppId); + + Assert.False(provider.UseWamBroker); + Assert.NotEqual(SqlClientApplicationId, provider.ApplicationClientId); + } + + /// + /// When the device-code callback constructor is invoked without an application id, the + /// provider falls back to the SqlClient first-party id and must enable WAM broker. + /// + [Fact] + public void Ctor_WithDeviceCodeCallback_NoAppClientId_EnablesWamBroker() + { + var provider = new ActiveDirectoryAuthenticationProvider( + deviceCodeFlowCallbackMethod: static _ => Task.CompletedTask); + + Assert.True(provider.UseWamBroker); + Assert.Equal(SqlClientApplicationId, provider.ApplicationClientId); + } + + /// + /// The -based constructor + /// is the recommended overload for new code. It must honor + /// the same way the positional-argument overloads do. + /// + [Fact] + public void Ctor_Options_CustomAppId_UseWamBrokerTrue_EnablesWamBroker() + { + var provider = new ActiveDirectoryAuthenticationProvider( + new ActiveDirectoryAuthenticationProviderOptions + { + ApplicationClientId = TestCustomAppId, + UseWamBroker = true, + }); + + Assert.Equal(TestCustomAppId, provider.ApplicationClientId); + Assert.True(provider.UseWamBroker); + } + + /// + /// Options with ApplicationClientId = null falls back to the SqlClient first-party + /// id, which always enables WAM broker, regardless of UseWamBroker. + /// + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Ctor_Options_NullAppId_AlwaysEnablesWamBroker(bool useWamBroker) + { + var provider = new ActiveDirectoryAuthenticationProvider( + new ActiveDirectoryAuthenticationProviderOptions + { + ApplicationClientId = null, + UseWamBroker = useWamBroker, + }); + + Assert.Equal(SqlClientApplicationId, provider.ApplicationClientId); + Assert.True(provider.UseWamBroker); + } + + /// + /// The Options-based constructor must reject a options instance with + /// so misuse fails fast at construction. + /// + [Fact] + public void Ctor_Options_Null_ThrowsArgumentNullException() + { + Assert.Throws( + () => new ActiveDirectoryAuthenticationProvider((ActiveDirectoryAuthenticationProviderOptions)null!)); + } + + /// + /// Registering an instance via must not + /// wrap or replace the instance, so its WAM broker setting survives registration. + /// + /// + /// Provider registration mutates global state shared across this test class collection + /// (and any other test that depends on the default provider being installed). Save and + /// restore the original provider in a finally block to keep cross-test isolation. + /// + [Fact] + public void Ctor_RegisteredAsProvider_PreservesUseWamBrokerSetting() + { + var provider = new ActiveDirectoryAuthenticationProvider( + new ActiveDirectoryAuthenticationProviderOptions + { + ApplicationClientId = TestCustomAppId, + UseWamBroker = true, + }); + + SqlAuthenticationProvider? original = + SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive); + try + { + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, provider); + + var retrieved = SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive) + as ActiveDirectoryAuthenticationProvider; + Assert.NotNull(retrieved); + Assert.Same(provider, retrieved); + Assert.Equal(TestCustomAppId, retrieved!.ApplicationClientId); + Assert.True(retrieved.UseWamBroker); + } + finally + { + if (original is not null) + { + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, original); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient.Internal/Logging/src/LoggingVersions.props b/src/Microsoft.Data.SqlClient.Internal/Logging/src/LoggingVersions.props index 597a692457..c48f8861e5 100644 --- a/src/Microsoft.Data.SqlClient.Internal/Logging/src/LoggingVersions.props +++ b/src/Microsoft.Data.SqlClient.Internal/Logging/src/LoggingVersions.props @@ -41,7 +41,7 @@ --> - 1 + 7 <_OurPackageVersion Condition="'$(LoggingPackageVersion)' != ''">$(LoggingPackageVersion) diff --git a/src/Microsoft.Data.SqlClient.sln b/src/Microsoft.Data.SqlClient.sln index 0a80e8f0ca..9df63c8ad3 100644 --- a/src/Microsoft.Data.SqlClient.sln +++ b/src/Microsoft.Data.SqlClient.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31912.275 +# Visual Studio Version 18 +VisualStudioVersion = 18.7.11911.148 stable MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Data.SqlClient", "Microsoft.Data.SqlClient\netfx\src\Microsoft.Data.SqlClient.csproj", "{407890AC-9876-4FEF-A6F1-F36A876BAADE}" EndProject @@ -262,7 +262,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "steps", "steps", "{EABE3A3E ..\eng\pipelines\common\templates\steps\configure-sql-server-win-step.yml = ..\eng\pipelines\common\templates\steps\configure-sql-server-win-step.yml ..\eng\pipelines\common\templates\steps\copy-dlls-for-test-step.yml = ..\eng\pipelines\common\templates\steps\copy-dlls-for-test-step.yml ..\eng\pipelines\common\templates\steps\esrp-code-signing-step.yml = ..\eng\pipelines\common\templates\steps\esrp-code-signing-step.yml - ..\eng\pipelines\common\templates\steps\generate-nuget-package-step.yml = ..\eng\pipelines\common\templates\steps\generate-nuget-package-step.yml ..\eng\pipelines\common\templates\steps\override-sni-version.yml = ..\eng\pipelines\common\templates\steps\override-sni-version.yml ..\eng\pipelines\common\templates\steps\pre-build-step.yml = ..\eng\pipelines\common\templates\steps\pre-build-step.yml ..\eng\pipelines\common\templates\steps\prepare-test-db-step.yml = ..\eng\pipelines\common\templates\steps\prepare-test-db-step.yml @@ -308,8 +307,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "steps", "steps", "{AD738BD4 ..\eng\pipelines\steps\compound-extract-akv-apiscan-files-step.yml = ..\eng\pipelines\steps\compound-extract-akv-apiscan-files-step.yml ..\eng\pipelines\steps\compound-nuget-pack-step.yml = ..\eng\pipelines\steps\compound-nuget-pack-step.yml ..\eng\pipelines\steps\compound-publish-symbols-step.yml = ..\eng\pipelines\steps\compound-publish-symbols-step.yml - ..\eng\pipelines\steps\install-dotnet.yml = ..\eng\pipelines\steps\install-dotnet.yml ..\eng\pipelines\steps\install-dotnet-arm64.ps1 = ..\eng\pipelines\steps\install-dotnet-arm64.ps1 + ..\eng\pipelines\steps\install-dotnet.yml = ..\eng\pipelines\steps\install-dotnet.yml ..\eng\pipelines\steps\roslyn-analyzers-akv-step.yml = ..\eng\pipelines\steps\roslyn-analyzers-akv-step.yml ..\eng\pipelines\steps\script-output-environment-variables-step.yml = ..\eng\pipelines\steps\script-output-environment-variables-step.yml EndProjectSection @@ -391,6 +390,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.DotNet.GenAPI", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Cci.Extensions", "..\tools\GenAPI\Microsoft.Cci.Extensions\Microsoft.Cci.Extensions.csproj", "{2F900B8A-EA19-4274-9AE9-3E6A59856FCC}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "apps", "apps", "{CCD7F253-A1EF-462D-A8CE-FBF04AB9EA0C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureSqlConnector", "..\doc\apps\AzureSqlConnector\AzureSqlConnector.csproj", "{8B299DCD-C728-231A-747E-254252866A0F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -839,6 +842,18 @@ Global {2F900B8A-EA19-4274-9AE9-3E6A59856FCC}.Release|Any CPU.ActiveCfg = Release|Any CPU {2F900B8A-EA19-4274-9AE9-3E6A59856FCC}.Release|x64.ActiveCfg = Release|Any CPU {2F900B8A-EA19-4274-9AE9-3E6A59856FCC}.Release|x86.ActiveCfg = Release|Any CPU + {8B299DCD-C728-231A-747E-254252866A0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B299DCD-C728-231A-747E-254252866A0F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B299DCD-C728-231A-747E-254252866A0F}.Debug|x64.ActiveCfg = Debug|Any CPU + {8B299DCD-C728-231A-747E-254252866A0F}.Debug|x64.Build.0 = Debug|Any CPU + {8B299DCD-C728-231A-747E-254252866A0F}.Debug|x86.ActiveCfg = Debug|Any CPU + {8B299DCD-C728-231A-747E-254252866A0F}.Debug|x86.Build.0 = Debug|Any CPU + {8B299DCD-C728-231A-747E-254252866A0F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B299DCD-C728-231A-747E-254252866A0F}.Release|Any CPU.Build.0 = Release|Any CPU + {8B299DCD-C728-231A-747E-254252866A0F}.Release|x64.ActiveCfg = Release|Any CPU + {8B299DCD-C728-231A-747E-254252866A0F}.Release|x64.Build.0 = Release|Any CPU + {8B299DCD-C728-231A-747E-254252866A0F}.Release|x86.ActiveCfg = Release|Any CPU + {8B299DCD-C728-231A-747E-254252866A0F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -914,10 +929,13 @@ Global {020A7E7B-04C9-4326-985F-045B42CC2200} = {7289C27E-D7DF-2C71-84B4-151F3A162493} {D433ED2D-5E47-4A4B-B94A-EC71482715C7} = {020A7E7B-04C9-4326-985F-045B42CC2200} {AA77C107-9A78-4A99-98BB-21FF7A1E0B01} = {2B71F605-037E-5629-6E23-0FA3C297446D} + {C3FE67C1-D288-45ED-A35C-08107396F8BB} = {CCD7F253-A1EF-462D-A8CE-FBF04AB9EA0C} {351BE847-A0BF-450C-A5BC-8337AFA49EAA} = {7289C27E-D7DF-2C71-84B4-151F3A162493} {1DB299CE-95EA-4566-84DD-171768758291} = {351BE847-A0BF-450C-A5BC-8337AFA49EAA} {583E2B51-A90B-4F34-AD8B-4061504E855E} = {351BE847-A0BF-450C-A5BC-8337AFA49EAA} {2F900B8A-EA19-4274-9AE9-3E6A59856FCC} = {351BE847-A0BF-450C-A5BC-8337AFA49EAA} + {CCD7F253-A1EF-462D-A8CE-FBF04AB9EA0C} = {ED952CF7-84DF-437A-B066-F516E9BE1C2C} + {8B299DCD-C728-231A-747E-254252866A0F} = {CCD7F253-A1EF-462D-A8CE-FBF04AB9EA0C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {01D48116-37A2-4D33-B9EC-94793C702431} 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 f3d5bdc844..3215e42068 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj @@ -31,6 +31,9 @@ + + TypeForwards.Abstractions.cs + 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 6c54000f9c..47bffc2948 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -15,7 +15,7 @@ true Core $(BaseProduct) true - $(NoWarn);IL2026;IL2057;IL2072;IL2075 + $(NoWarn);IL2026;IL2057;IL2067;IL2070;IL2072;IL2075 false @@ -1023,6 +1023,9 @@ TypeForwards.netcore.cs + + TypeForwards.Abstractions.cs + 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 6765e009db..b6a8fac3a1 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj @@ -26,6 +26,9 @@ + + TypeForwards.Abstractions.cs + @@ -43,6 +46,13 @@ + + 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 05a4284565..8408dabc0d 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -1006,6 +1006,9 @@ System\Runtime\CompilerServices\IsExternalInit.netfx.cs + + TypeForwards.Abstractions.cs + diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj index d0713280e6..9520356e8f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj @@ -117,6 +117,13 @@ + + diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs index cf0a296b30..c9b069cd21 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs @@ -1354,6 +1354,11 @@ internal void OnFeatureExtAck(int featureId, byte[] data) len = bLen; } + if (len < 0 || len > data.Length - i) + { + throw SQL.ParsingErrorLength(ParsingErrorState.CorruptedTdsStream, len); + } + byte[] stateData = new byte[len]; Buffer.BlockCopy(data, i, stateData, 0, len); i += len; @@ -2233,7 +2238,6 @@ private void AttemptOneLogin( // @TODO: Rename to meet naming conventions private bool AttemptRetryADAuthWithTimeoutError( SqlException sqlex, - SqlConnectionString connectionOptions, // @TODO: this is not used TimeoutTimer timeout) { if (!_activeDirectoryAuthTimeoutRetryHelper.CanRetryWithSqlException(sqlex)) @@ -2243,8 +2247,10 @@ private bool AttemptRetryADAuthWithTimeoutError( // Reset client-side timeout. timeout.Reset(); - // When server timeout, the auth context key was already created. Clean it up here. + // Clear fed-auth state captured by the failed attempt so OnFedAuthInfo's cache-reuse branch starts from null on the retry. _dbConnectionPoolAuthenticationContextKey = null; + _fedAuthToken = null; + _newDbConnectionPoolAuthenticationContext = null; // When server timeouts, connection is doomed. Reset here to allow reconnection. UnDoomThisConnection(); @@ -3343,7 +3349,7 @@ private void LoginNoFailover( } catch (SqlException sqlex) { - if (AttemptRetryADAuthWithTimeoutError(sqlex, connectionOptions, timeout)) + if (AttemptRetryADAuthWithTimeoutError(sqlex, timeout)) { continue; } @@ -3657,7 +3663,7 @@ private void LoginWithFailover( } catch (SqlException sqlex) { - if (AttemptRetryADAuthWithTimeoutError(sqlex, connectionOptions, timeout)) + if (AttemptRetryADAuthWithTimeoutError(sqlex, timeout)) { continue; } 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 7694092907..16e0abb7ec 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -36,13 +36,6 @@ internal static class LocalAppContextSwitches private const string EnableMultiSubnetFailoverByDefaultString = "Switch.Microsoft.Data.SqlClient.EnableMultiSubnetFailoverByDefault"; - /// - /// The name of the app context switch that controls whether - /// the user agent feature is enabled. - /// - private const string EnableUserAgentString = - "Switch.Microsoft.Data.SqlClient.EnableUserAgent"; - #if NET /// /// The name of the app context switch that controls whether @@ -177,11 +170,6 @@ private enum SwitchValue : byte /// private static SwitchValue s_enableMultiSubnetFailoverByDefault = SwitchValue.None; - /// - /// The cached value of the EnableUserAgent switch. - /// - private static SwitchValue s_enableUserAgent = SwitchValue.None; - #if NET /// /// The cached value of the GlobalizationInvariantMode switch. @@ -319,18 +307,6 @@ static LocalAppContextSwitches() defaultValue: false, ref s_enableMultiSubnetFailoverByDefault); - /// - /// When set to true, the user agent feature is enabled and the driver will - /// send the user agent string to the server. - /// - /// The default value of this switch is false. - /// - public static bool EnableUserAgent => - AcquireAndReturn( - EnableUserAgentString, - defaultValue: false, - ref s_enableUserAgent); - #if NET /// /// .NET Core 2.0 and up supports Globalization Invariant mode, which diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs index 8d1ce98df1..dc086d4610 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs @@ -9,6 +9,29 @@ namespace Microsoft.Data.SqlClient { + /// + /// Tri-state result returned by . + /// Distinguishes a cache miss from a cached negative result so callers cannot conflate the two. + /// + internal enum SignatureVerificationResult + { + /// + /// No cached entry exists for the requested CMK metadata. + /// The caller must verify the signature with the key store provider. + /// + NotFound, + + /// + /// A cached entry exists and indicates that signature verification previously failed. + /// + False, + + /// + /// A cached entry exists and indicates that signature verification previously succeeded. + /// + True, + } + /// /// Cache for storing result of signature verification of CMK Metadata /// @@ -16,138 +39,159 @@ internal class ColumnMasterKeyMetadataSignatureVerificationCache { private const int CacheSize = 2000; // Cache size in number of entries. private const int CacheTrimThreshold = 300; // Threshold above the cache size when we start trimming. - - private const string _className = "ColumnMasterKeyMetadataSignatureVerificationCache"; - private const string _getSignatureVerificationResultMethodName = "GetSignatureVerificationResult"; - private const string _addSignatureVerificationResultMethodName = "AddSignatureVerificationResult"; - private const string _masterkeypathArgumentName = "masterKeyPath"; - private const string _keyStoreNameArgumentName = "keyStoreName"; - private const string _signatureName = "signature"; private const string _cacheLookupKeySeparator = ":"; - private static readonly ColumnMasterKeyMetadataSignatureVerificationCache _signatureVerificationCache = new ColumnMasterKeyMetadataSignatureVerificationCache(); private static readonly TimeSpan s_verificationCacheTimeout = TimeSpan.FromDays(10); - //singleton instance - internal static ColumnMasterKeyMetadataSignatureVerificationCache Instance { get { return _signatureVerificationCache; } } + /// + /// Gets the process-wide singleton instance of the signature verification cache. + /// + internal static ColumnMasterKeyMetadataSignatureVerificationCache Instance { get; } = new(); private readonly MemoryCache _cache; - private int _inTrim = 0; + private int _inTrim; private ColumnMasterKeyMetadataSignatureVerificationCache() { _cache = new MemoryCache(new MemoryCacheOptions()); - _inTrim = 0; } /// - /// Get signature verification result for given CMK metadata (KeystoreName, MasterKeyPath, allowEnclaveComputations) and a given signature + /// Get signature verification result for given CMK metadata + /// (KeystoreName, MasterKeyPath, allowEnclaveComputations) and a given signature /// /// Key Store name for CMK /// Key Path for CMK /// boolean indicating whether the key can be sent to enclave /// Signature for the CMK metadata - internal bool GetSignatureVerificationResult(string keyStoreName, string masterKeyPath, bool allowEnclaveComputations, byte[] signature) + /// Tri-state result indicating whether signature verification succeeded, failed, or was not found in cache + /// + /// Thrown when , , + /// or is . + /// + /// + /// Thrown when or + /// is empty or whitespace, or when has length zero. + /// + internal SignatureVerificationResult GetSignatureVerificationResult(string keyStoreName, string masterKeyPath, bool allowEnclaveComputations, byte[] signature) { - ValidateStringArgumentNotNullOrEmpty(masterKeyPath, _masterkeypathArgumentName, _getSignatureVerificationResultMethodName); - ValidateStringArgumentNotNullOrEmpty(keyStoreName, _keyStoreNameArgumentName, _getSignatureVerificationResultMethodName); - ValidateSignatureNotNullOrEmpty(signature, _getSignatureVerificationResultMethodName); + ValidateStringArgumentNotNullOrEmpty(masterKeyPath, nameof(masterKeyPath), nameof(GetSignatureVerificationResult)); + ValidateStringArgumentNotNullOrEmpty(keyStoreName, nameof(keyStoreName), nameof(GetSignatureVerificationResult)); + ValidateSignatureNotNullOrEmpty(signature, nameof(GetSignatureVerificationResult)); string cacheLookupKey = GetCacheLookupKey(masterKeyPath, allowEnclaveComputations, signature, keyStoreName); - return _cache.TryGetValue(cacheLookupKey, out bool value); + if (!_cache.TryGetValue(cacheLookupKey, out bool value)) + { + return SignatureVerificationResult.NotFound; + } + + return value ? SignatureVerificationResult.True : SignatureVerificationResult.False; } /// - /// Add signature verification result for given CMK metadata (KeystoreName, MasterKeyPath, allowEnclaveComputations) and a given signature in the cache + /// Add signature verification result for given CMK metadata (KeystoreName, + /// MasterKeyPath, allowEnclaveComputations) and a given signature in the cache /// /// Key Store name for CMK /// Key Path for CMK /// boolean indicating whether the key can be sent to enclave /// Signature for the CMK metadata /// result indicating signature verification success/failure + /// + /// Thrown when , , + /// or is . + /// + /// + /// Thrown when or is empty or whitespace, + /// or when has length zero. + /// internal void AddSignatureVerificationResult(string keyStoreName, string masterKeyPath, bool allowEnclaveComputations, byte[] signature, bool result) { - ValidateStringArgumentNotNullOrEmpty(masterKeyPath, _masterkeypathArgumentName, _addSignatureVerificationResultMethodName); - ValidateStringArgumentNotNullOrEmpty(keyStoreName, _keyStoreNameArgumentName, _addSignatureVerificationResultMethodName); - ValidateSignatureNotNullOrEmpty(signature, _addSignatureVerificationResultMethodName); + ValidateStringArgumentNotNullOrEmpty(masterKeyPath, nameof(masterKeyPath), nameof(AddSignatureVerificationResult)); + ValidateStringArgumentNotNullOrEmpty(keyStoreName, nameof(keyStoreName), nameof(AddSignatureVerificationResult)); + ValidateSignatureNotNullOrEmpty(signature, nameof(AddSignatureVerificationResult)); string cacheLookupKey = GetCacheLookupKey(masterKeyPath, allowEnclaveComputations, signature, keyStoreName); TrimCacheIfNeeded(); // By default evict after 10 days. - _cache.Set(cacheLookupKey, result, absoluteExpirationRelativeToNow: s_verificationCacheTimeout); + _cache.Set(cacheLookupKey, result, absoluteExpirationRelativeToNow: s_verificationCacheTimeout); } - private void ValidateSignatureNotNullOrEmpty(byte[] signature, string methodName) + private static void ValidateSignatureNotNullOrEmpty(byte[] signature, string methodName) { - if (signature == null || signature.Length == 0) + if (signature is null) + { + throw SQL.NullArgumentInternal(nameof(signature), nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName); + } + if (signature.Length == 0) { - if (signature == null) - { - throw SQL.NullArgumentInternal(_signatureName, _className, methodName); - } - else - { - throw SQL.EmptyArgumentInternal(_signatureName, _className, methodName); - } + throw SQL.EmptyArgumentInternal(nameof(signature), nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName); } } - private void ValidateStringArgumentNotNullOrEmpty(string stringArgValue, string stringArgName, string methodName) + private static void ValidateStringArgumentNotNullOrEmpty(string value, string argumentName, string methodName) { - if (string.IsNullOrWhiteSpace(stringArgValue)) + if (value is null) + { + throw SQL.NullArgumentInternal(argumentName, nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName); + } + if (string.IsNullOrWhiteSpace(value)) { - if (stringArgValue == null) - { - throw SQL.NullArgumentInternal(stringArgName, _className, methodName); - } - else - { - throw SQL.EmptyArgumentInternal(stringArgName, _className, methodName); - } + throw SQL.EmptyArgumentInternal(argumentName, nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName); } } + private void TrimCacheIfNeeded() { // If the size of the cache exceeds the threshold, set that we are in trimming and trim the cache accordingly. long currentCacheSize = _cache.Count; - if ((currentCacheSize > CacheSize + CacheTrimThreshold) && (0 == Interlocked.CompareExchange(ref _inTrim, 1, 0))) + if (currentCacheSize <= CacheSize + CacheTrimThreshold || Interlocked.CompareExchange(ref _inTrim, 1, 0) != 0) { - try - { - // Example: 2301 - 2000 = 301; 301 / 2301 = 0.1308 * 100 = 13% compacting - _cache.Compact((((double)(currentCacheSize - CacheSize) / (double)currentCacheSize) * 100)); - } - finally - { - // Reset _inTrim flag - Interlocked.CompareExchange(ref _inTrim, 0, 1); - } + return; + } + + try + { + // Example: 2301 - 2000 = 301; 301 / 2301 = 0.1308 * 100 = 13% compacting + _cache.Compact((double)(currentCacheSize - CacheSize) / currentCacheSize * 100); + } + finally + { + Interlocked.Exchange(ref _inTrim, 0); } } - private string GetCacheLookupKey(string masterKeyPath, bool allowEnclaveComputations, byte[] signature, string keyStoreName) + /// + /// Generates a cache key for the given CMK metadata and signature. The key is a + /// concatenation of the key store name, master key path, allowEnclaveComputations value, and signature, separated by a delimiter. + /// + /// The master key path. + /// Whether enclave computations are allowed. + /// The signature. + /// The key store name. + /// A string that can be used as a cache key. + private static string GetCacheLookupKey(string masterKeyPath, bool allowEnclaveComputations, byte[] signature, string keyStoreName) { - StringBuilder cacheLookupKeyBuilder = new StringBuilder(keyStoreName, - capacity: - keyStoreName.Length + - masterKeyPath.Length + - SqlSecurityUtility.GetBase64LengthFromByteLength(signature.Length) + - 3 /*separators*/ + - 10 /*boolean value + somebuffer*/); - - cacheLookupKeyBuilder.Append(_cacheLookupKeySeparator); - cacheLookupKeyBuilder.Append(masterKeyPath); - cacheLookupKeyBuilder.Append(_cacheLookupKeySeparator); - cacheLookupKeyBuilder.Append(allowEnclaveComputations); - cacheLookupKeyBuilder.Append(_cacheLookupKeySeparator); - cacheLookupKeyBuilder.Append(Convert.ToBase64String(signature)); - cacheLookupKeyBuilder.Append(_cacheLookupKeySeparator); - string cacheLookupKey = cacheLookupKeyBuilder.ToString(); - return cacheLookupKey; + int cacheCapacity = + keyStoreName.Length + + masterKeyPath.Length + + SqlSecurityUtility.GetBase64LengthFromByteLength(signature.Length) + + 4 * _cacheLookupKeySeparator.Length + + 10 /* boolean value + buffer */; + + return new StringBuilder(keyStoreName, capacity: cacheCapacity) + .Append(_cacheLookupKeySeparator) + .Append(masterKeyPath) + .Append(_cacheLookupKeySeparator) + .Append(allowEnclaveComputations) + .Append(_cacheLookupKeySeparator) + .Append(Convert.ToBase64String(signature)) + .Append(_cacheLookupKeySeparator) + .ToString(); } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs index 54468782bd..4c77fbe8df 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs @@ -32,7 +32,7 @@ internal sealed class SqlAuthenticationProviderManager // The public key token of our Azure extension assembly, used to avoid loading imposter // assemblies. - private static readonly byte[] azurePublicKeyToken = [ 0x23, 0xec, 0x7f, 0xc2, 0xd6, 0xea, 0xa4, 0xa5 ]; + private static readonly byte[] s_azurePublicKeyToken = [ 0x23, 0xec, 0x7f, 0xc2, 0xd6, 0xea, 0xa4, 0xa5 ]; static SqlAuthenticationProviderManager() { @@ -70,10 +70,10 @@ static SqlAuthenticationProviderManager() nameof(SqlAuthenticationProviderManager) + $": Attempting to load Azure extension assembly={azureAssemblyName} with " + "expected public key token=" + - BitConverter.ToString(azurePublicKeyToken).Replace("-", "")); + BitConverter.ToString(s_azurePublicKeyToken).Replace("-", "")); var qualifiedName = new AssemblyName(azureAssemblyName); - qualifiedName.SetPublicKeyToken(azurePublicKeyToken); + qualifiedName.SetPublicKeyToken(s_azurePublicKeyToken); // The .NET Framework runtime will enforce the token during binding, causing Load() // to throw. This prevents an untrusted assembly from being loaded and having its @@ -92,7 +92,7 @@ static SqlAuthenticationProviderManager() { byte[]? actualToken = assembly.GetName().GetPublicKeyToken(); - if (actualToken is null || !actualToken.AsSpan().SequenceEqual(azurePublicKeyToken)) + if (actualToken is null || !actualToken.AsSpan().SequenceEqual(s_azurePublicKeyToken)) { SqlClientEventSource.Log.TryTraceEvent( nameof(SqlAuthenticationProviderManager) + @@ -132,7 +132,7 @@ static SqlAuthenticationProviderManager() // Look for the authentication provider class. const string className = "Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProvider"; - var type = assembly.GetType(className); + Type? type = assembly.GetType(className); if (type is null) { @@ -144,11 +144,28 @@ static SqlAuthenticationProviderManager() return; } - // Try to instantiate it. - var instance = Activator.CreateInstance( + // Try to instantiate it. Behavior depends on what the app + // configured in : + // * Neither applicationClientId nor useWamBroker -> use the + // parameterless constructor (defaults to the SqlClient + // first-party app id and enables WAM brokering on Windows). + // * applicationClientId only -> prefer the + // (ActiveDirectoryAuthenticationProviderOptions) constructor + // when the Azure extension exposes it; otherwise fall back + // to the legacy (string applicationClientId) constructor so + // older Azure extension versions keep working. + // * useWamBroker (with or without applicationClientId) -> + // requires the (Options) constructor because there is no + // positional analog. If the Azure extension is too old to + // expose Options, throw to surface the misconfiguration. + const string optionsTypeName = "Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions"; + Type? optionsType = assembly.GetType(optionsTypeName); + + SqlAuthenticationProvider? instance = CreateAzureAuthenticationProvider( type, - [Instance._applicationClientId]) - as SqlAuthenticationProvider; + optionsType, + Instance._applicationClientId, + Instance._useWamBroker); if (instance is null) { @@ -188,17 +205,19 @@ static SqlAuthenticationProviderManager() // simply have no default and the app must provide one if they // attempt to use Active Directory authentication. catch (Exception ex) - when (ex is ArgumentNullException || - ex is ArgumentException || - ex is BadImageFormatException || - ex is FileLoadException || - ex is FileNotFoundException || - ex is MemberAccessException || - ex is MethodAccessException || - ex is MissingMethodException || - ex is NotSupportedException || - ex is TargetInvocationException || - ex is TypeLoadException) + when (ex is + AmbiguousMatchException or + ArgumentException or + BadImageFormatException or + FileLoadException or + FileNotFoundException or + MemberAccessException or + MethodAccessException or + MissingMethodException or + NotSupportedException or + TargetInvocationException or + TypeInitializationException or + TypeLoadException) { SqlClientEventSource.Log.TryTraceEvent( nameof(SqlAuthenticationProviderManager) + @@ -216,6 +235,13 @@ ex is TargetInvocationException || private readonly SqlClientLogger _sqlAuthLogger = new SqlClientLogger(); private readonly string? _applicationClientId = null; + // Optional override for ActiveDirectoryAuthenticationProviderOptions.UseWamBroker + // read from the app.config attribute. + // null means the app did not configure the value, in which case we leave the + // provider's default behavior (WAM is implied by the SqlClient first-party app id and + // off otherwise) untouched. + private readonly bool? _useWamBroker = null; + /// /// Constructor. /// @@ -239,6 +265,23 @@ private SqlAuthenticationProviderManager(SqlAuthenticationProviderConfigurationS _sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, "No user-defined Application Client Id found."); } + if (!string.IsNullOrEmpty(configSection.UseWamBroker)) + { + if (bool.TryParse(configSection.UseWamBroker, out bool useWamBroker)) + { + _useWamBroker = useWamBroker; + _sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, $"Received user-defined UseWamBroker={useWamBroker}."); + } + else + { + _sqlAuthLogger.LogError(nameof(SqlAuthenticationProviderManager), methodName, $"Ignoring user-defined UseWamBroker='{configSection.UseWamBroker}': not a valid boolean."); + } + } + else + { + _sqlAuthLogger.LogInfo(nameof(SqlAuthenticationProviderManager), methodName, "No user-defined UseWamBroker found."); + } + // Create user-defined auth initializer, if any. if (!string.IsNullOrEmpty(configSection.InitializerType)) { @@ -312,8 +355,81 @@ private SqlAuthenticationProviderManager(SqlAuthenticationProviderConfigurationS /// Authentication provider or null if not found. internal static SqlAuthenticationProvider? GetProvider(SqlAuthenticationMethod authenticationMethod) { - SqlAuthenticationProvider? value; - return Instance._providers.TryGetValue(authenticationMethod, out value) ? value : null; + return Instance._providers.TryGetValue(authenticationMethod, out SqlAuthenticationProvider? value) ? value : null; + } + + // Reflectively constructs the Azure extension's ActiveDirectoryAuthenticationProvider, + // selecting the constructor that matches what the app configured. Extracted from the + // static initializer so it can be unit-tested with stub provider/options shapes. + // + // Returns null when no compatible constructor is available (e.g. a custom assembly + // that lacks both the (Options) and (string) ctors). + // + // Throws InvalidOperationException when useWamBroker is configured but the Azure + // extension is too old to expose ActiveDirectoryAuthenticationProviderOptions; that + // signals user-actionable misconfiguration and intentionally escapes the static ctor's + // catch-when filter so it surfaces as a TypeInitializationException. + internal static SqlAuthenticationProvider? CreateAzureAuthenticationProvider( + Type providerType, + Type? optionsType, + string? applicationClientId, + bool? useWamBroker) + { + if (applicationClientId is null && useWamBroker is null) + { + return Activator.CreateInstance(providerType) as SqlAuthenticationProvider; + } + + ConstructorInfo? optionsCtor = optionsType is null + ? null + : providerType.GetConstructor([optionsType]); + + if (useWamBroker is bool useWam) + { + if (optionsType is null || optionsCtor is null) + { + throw SQL.UseWamBrokerRequiresAzureExtensionUpgrade(); + } + + var options = Activator.CreateInstance(optionsType); + if (options is null) + { + return null; + } + + if (applicationClientId is not null) + { + optionsType.GetProperty("ApplicationClientId") + ?.SetValue(options, applicationClientId); + } + optionsType.GetProperty("UseWamBroker") + ?.SetValue(options, useWam); + + return optionsCtor.Invoke([options]) as SqlAuthenticationProvider; + } + + // applicationClientId-only: prefer Options when the extension exposes it, + // otherwise fall back to the legacy (string) ctor for backward compatibility + // with older Azure extension versions. + if (optionsType is not null && optionsCtor is not null) + { + var options = Activator.CreateInstance(optionsType); + if (options is null) + { + return null; + } + optionsType.GetProperty("ApplicationClientId") + ?.SetValue(options, applicationClientId); + return optionsCtor.Invoke([options]) as SqlAuthenticationProvider; + } + + ConstructorInfo? legacyCtor = providerType.GetConstructor([typeof(string)]); + if (legacyCtor is not null) + { + return legacyCtor.Invoke([applicationClientId]) as SqlAuthenticationProvider; + } + + return null; } /// @@ -448,6 +564,15 @@ internal class SqlAuthenticationProviderConfigurationSection : ConfigurationSect /// [ConfigurationProperty("applicationClientId", IsRequired = false)] public string ApplicationClientId => this["applicationClientId"] as string ?? string.Empty; + + /// + /// Forwarded to ActiveDirectoryAuthenticationProviderOptions.UseWamBroker + /// when the Azure extension's default provider is auto-installed. Stored as a string so + /// that an unset attribute can be distinguished from useWamBroker="false"; the + /// runtime parses it with . + /// + [ConfigurationProperty("useWamBroker", IsRequired = false)] + public string UseWamBroker => this["useWamBroker"] as string ?? string.Empty; } /// 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 710d81045f..48d2e38d32 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs @@ -472,29 +472,78 @@ private string CreateInitialQuery() } else if (!string.IsNullOrEmpty(CatalogName)) { - CatalogName = SqlServerEscapeHelper.EscapeIdentifier(CatalogName); + CatalogName = SqlServerEscapeHelper.EscapeStringAsLiteral(SqlServerEscapeHelper.EscapeIdentifier(CatalogName)); } string objectName = ADP.BuildMultiPartName(parts); string escapedObjectName = SqlServerEscapeHelper.EscapeStringAsLiteral(objectName); - // Specify the column names explicitly. This is to ensure that we can map to hidden columns (e.g. columns in temporal tables.) - // If the target table doesn't exist, OBJECT_ID will return NULL and @Column_Names will remain non-null. The subsequent SELECT * - // query will then continue to fail with "Invalid object name" rather than with an unusual error because the query being executed - // is NULL. - // Some hidden columns (e.g. SQL Graph columns) cannot be selected, so we need to exclude them explicitly. + // Specify the column names explicitly. This is to ensure that we can map to hidden + // columns (e.g. columns in temporal tables.) If the target table doesn't exist, + // OBJECT_ID will return NULL and @Column_Names will remain non-null. The subsequent + // SELECT * query will then continue to fail with "Invalid object name" rather than with + // an unusual error because the query being executed is NULL. + // + // Some hidden columns (e.g. SQL Graph columns) cannot be selected, so we need to + // exclude them explicitly. The graph_type values excluded below are internal graph + // columns that cannot be selected directly: + // + // 1 = GRAPH_ID + // 3 = GRAPH_FROM_ID + // 4 = GRAPH_FROM_OBJ_ID + // 6 = GRAPH_TO_ID + // 7 = GRAPH_TO_OBJ_ID + // + // See: https://learn.microsoft.com/sql/relational-databases/graphs/sql-graph-architecture#syscolumns + // + // The column-name query is built as dynamic SQL and executed via sp_executesql so + // that it is not compiled (and rejected) on SQL Server versions that lack the + // graph_type column (e.g. SQL 2016). CatalogName and escapedObjectName are + // interpolated directly into the SQL string because SQL Server does not allow + // identifiers (database/schema/table names) to be passed as parameters. Both + // values are escaped via SqlServerEscapeHelper before interpolation. + // + // SqlBulkCopy must remain compatible with Azure Synapse Analytics dedicated SQL pools + // and with SQL Server 2016. Azure Synapse Analytics does not allow variables assigned + // in the SELECT statement to appear in an expression, which prevents the consistent use of + // SELECT @Column_Names = COALESCE(@Column_Names + ', ', '') + QUOTENAME([name]) + // The alternative is to use STRING_AGG, but this was only introduced in SQL Server + // 2017. To meet both criteria, we review the EngineEdition server property. A value + // of 6 indicates that the bulk copy is going to run against Azure Synapse Analytics; + // we use STRING_AGG in that case and the COALESCE method otherwise. + // + // See: https://learn.microsoft.com/en-us/sql/t-sql/functions/serverproperty-transact-sql return $""" SELECT @@TRANCOUNT; +DECLARE @Object_ID INT = OBJECT_ID('{escapedObjectName}'); +DECLARE @Column_Name_Query_SELECT NVARCHAR(MAX); +DECLARE @Column_Name_Query_FILTER NVARCHAR(MAX); +DECLARE @Column_Name_Query_SORT NVARCHAR(MAX); +DECLARE @Column_Name_Query NVARCHAR(MAX); DECLARE @Column_Names NVARCHAR(MAX) = NULL; + +IF CAST(SERVERPROPERTY('EngineEdition') AS INT) = 6 +BEGIN + SET @Column_Name_Query_SELECT = N'SELECT @Column_Names = STRING_AGG(CAST(QUOTENAME([name]) AS NVARCHAR(MAX)), '', '') WITHIN GROUP (ORDER BY [column_id] ASC)'; + SET @Column_Name_Query_SORT = N''; +END +ELSE +BEGIN + SET @Column_Name_Query_SELECT = 'SELECT @Column_Names = COALESCE(@Column_Names + '', '', '''') + QUOTENAME([name])'; + SET @Column_Name_Query_SORT = N'ORDER BY [column_id] ASC'; +END + IF EXISTS (SELECT TOP 1 * FROM sys.all_columns WHERE [object_id] = OBJECT_ID('sys.all_columns') AND [name] = 'graph_type') BEGIN - SELECT @Column_Names = COALESCE(@Column_Names + ', ', '') + QUOTENAME([name]) FROM {CatalogName}.[sys].[all_columns] WHERE [object_id] = OBJECT_ID('{escapedObjectName}') AND COALESCE([graph_type], 0) NOT IN (1, 3, 4, 6, 7) ORDER BY [column_id] ASC; + SET @Column_Name_Query_FILTER = N'WHERE [object_id] = @Object_ID AND COALESCE([graph_type], 0) NOT IN (1, 3, 4, 6, 7)'; END ELSE BEGIN - SELECT @Column_Names = COALESCE(@Column_Names + ', ', '') + QUOTENAME([name]) FROM {CatalogName}.[sys].[all_columns] WHERE [object_id] = OBJECT_ID('{escapedObjectName}') ORDER BY [column_id] ASC; + SET @Column_Name_Query_FILTER = N'WHERE [object_id] = @Object_ID'; END +SET @Column_Name_Query = @Column_Name_Query_SELECT + ' FROM {CatalogName}.[sys].[all_columns] ' + @Column_Name_Query_FILTER + ' ' + @Column_Name_Query_SORT + ';' +EXEC sp_executesql @Column_Name_Query, N'@Object_ID INT, @Column_Names NVARCHAR(MAX) OUTPUT', @Object_ID = @Object_ID, @Column_Names = @Column_Names OUTPUT; SELECT @Column_Names = COALESCE(@Column_Names, '*'); SET FMTONLY ON; @@ -624,7 +673,7 @@ private string AnalyzeTargetAndCreateUpdateBulkCommand(BulkCopySimpleResultSet i bool matched = false; bool rejected = false; - + // Look for a local match for the remote column. for (int j = 0; j < _localColumnMappings.Count; ++j) { @@ -644,7 +693,7 @@ private string AnalyzeTargetAndCreateUpdateBulkCommand(BulkCopySimpleResultSet i // Remove it from our unmatched set. unmatchedColumns.Remove(localColumn.DestinationColumn); - + // Check for column types that we refuse to bulk load, even // though we found a match. // @@ -1437,7 +1486,7 @@ private void RunParserReliably(BulkCopySimpleResultSet bulkCopyHandler = null) try { // @TODO: CER Exception Handling was removed here (see GH#3581) - _parser.Run(RunBehavior.UntilDone, null, null, bulkCopyHandler, _stateObj); + _parser.Run(RunBehavior.UntilDone, null, null, bulkCopyHandler, _stateObj); } finally { @@ -1760,7 +1809,7 @@ public void WriteToServer(DbDataReader reader) try { statistics = SqlStatistics.StartTimer(Statistics); - + ResetWriteToServerGlobalVariables(); _rowSource = reader; _dbDataReaderRowSource = reader; @@ -1796,13 +1845,13 @@ public void WriteToServer(IDataReader reader) try { statistics = SqlStatistics.StartTimer(Statistics); - + ResetWriteToServerGlobalVariables(); _rowSource = reader; _sqlDataReaderRowSource = _rowSource as SqlDataReader; _dbDataReaderRowSource = _rowSource as DbDataReader; _rowSourceType = ValueSourceType.IDataReader; - + WriteRowSourceToServerAsync(reader.FieldCount, CancellationToken.None); //It returns null since _isAsyncBulkCopy = false; } finally @@ -1918,7 +1967,7 @@ public Task WriteToServerAsync(DataRow[] rows, CancellationToken cancellationTok try { statistics = SqlStatistics.StartTimer(Statistics); - + ResetWriteToServerGlobalVariables(); if (rows.Length == 0) { @@ -1935,9 +1984,9 @@ public Task WriteToServerAsync(DataRow[] rows, CancellationToken cancellationTok _rowSourceType = ValueSourceType.RowArray; _rowEnumerator = rows.GetEnumerator(); _isAsyncBulkCopy = true; - + // It returns Task since _isAsyncBulkCopy = true; - return WriteRowSourceToServerAsync(table.Columns.Count, cancellationToken); + return WriteRowSourceToServerAsync(table.Columns.Count, cancellationToken); } finally { @@ -1964,19 +2013,19 @@ public Task WriteToServerAsync(DbDataReader reader, CancellationToken cancellati { throw SQL.BulkLoadPendingOperation(); } - + SqlStatistics statistics = Statistics; try { statistics = SqlStatistics.StartTimer(Statistics); - + ResetWriteToServerGlobalVariables(); _rowSource = reader; _sqlDataReaderRowSource = reader as SqlDataReader; _dbDataReaderRowSource = reader; _rowSourceType = ValueSourceType.DbDataReader; _isAsyncBulkCopy = true; - + // It returns Task since _isAsyncBulkCopy = true; return WriteRowSourceToServerAsync(reader.FieldCount, cancellationToken); } @@ -2016,7 +2065,7 @@ public Task WriteToServerAsync(IDataReader reader, CancellationToken cancellatio _dbDataReaderRowSource = _rowSource as DbDataReader; _rowSourceType = ValueSourceType.IDataReader; _isAsyncBulkCopy = true; - + // It returns Task since _isAsyncBulkCopy = true; return WriteRowSourceToServerAsync(reader.FieldCount, cancellationToken); } @@ -2056,7 +2105,7 @@ public Task WriteToServerAsync(DataTable table, DataRowState rowState, Cancellat try { statistics = SqlStatistics.StartTimer(Statistics); - + ResetWriteToServerGlobalVariables(); _rowStateToSkip = ((rowState == 0) || (rowState == DataRowState.Deleted)) ? DataRowState.Deleted : ~rowState | DataRowState.Deleted; _rowSource = table; @@ -2064,7 +2113,7 @@ public Task WriteToServerAsync(DataTable table, DataRowState rowState, Cancellat _rowSourceType = ValueSourceType.DataTable; _rowEnumerator = table.Rows.GetEnumerator(); _isAsyncBulkCopy = true; - + // It returns Task since _isAsyncBulkCopy = true; return WriteRowSourceToServerAsync(table.Columns.Count, cancellationToken); } @@ -2114,7 +2163,7 @@ private Task WriteRowSourceToServerAsync(int columnCount, CancellationToken ctok bool finishedSynchronously = true; _isBulkCopyingInProgress = true; - + CreateOrValidateConnection(nameof(WriteToServer)); SqlConnectionInternal internalConnection = _connection.GetOpenTdsConnection(); @@ -3065,11 +3114,11 @@ private void WriteToServerInternalRestAsync(CancellationToken cts, TaskCompletio // No need to cancel timer since SqlBulkCopy creates specific task source for reconnection. AsyncHelper.SetTimeoutExceptionWithState( - completion: cancellableReconnectTS, + completion: cancellableReconnectTS, timeout: BulkCopyTimeout, state: _destinationTableName, - onFailure: static state => - SQL.BulkLoadInvalidDestinationTable((string)state, SQL.CR_ReconnectTimeout()), + onFailure: static state => + SQL.BulkLoadInvalidDestinationTable((string)state, SQL.CR_ReconnectTimeout()), cancellationToken: CancellationToken.None ); @@ -3242,7 +3291,7 @@ private Task WriteToServerInternalAsync(CancellationToken ctoken) } return resultTask; } - + private void ResetWriteToServerGlobalVariables() { _dataTableSource = null; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs index 71845ddeb9..d276f27738 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -1068,7 +1068,7 @@ public override void Cancel() "SqlCommand.Cancel | API | Correlation | " + $"Object Id {ObjectID}, " + $"Activity Id {ActivityCorrelator.Current}, " + - $"Client Connection Id {_activeConnection.ClientConnectionId}, " + + $"Client Connection Id {_activeConnection?.ClientConnectionId}, " + $"Command Text '{CommandText}'"); SqlStatistics statistics = null; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs index 0d6203c08d..f6d255e45a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs @@ -1307,6 +1307,10 @@ private Type GetFieldTypeInternal(_SqlMetaData metaData) Connection.CheckGetExtendedUDTInfo(metaData, false); fieldType = metaData.udt?.Type; } + else if (metaData.type == SqlDbTypeExtensions.Vector) + { + fieldType = GetVectorFieldType(metaData.scale); + } else { // For all other types, including Xml - use data in MetaType. if (metaData.cipherMD != null) @@ -1329,6 +1333,19 @@ private Type GetFieldTypeInternal(_SqlMetaData metaData) return fieldType; } +#if !NETFRAMEWORK + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] +#endif + private static Type GetVectorFieldType(byte vectorElementType) + { + MetaType.SqlVectorElementType elementType = (MetaType.SqlVectorElementType)vectorElementType; + return elementType switch + { + MetaType.SqlVectorElementType.Float32 => typeof(SqlVector), + _ => throw SQL.VectorTypeNotSupported(elementType.ToString()), + }; + } + virtual internal int GetLocaleId(int i) { _SqlMetaData sqlMetaData = MetaData[i]; @@ -1422,6 +1439,10 @@ private Type GetProviderSpecificFieldTypeInternal(_SqlMetaData metaData) Connection.CheckGetExtendedUDTInfo(metaData, false); providerSpecificFieldType = metaData.udt?.Type; } + else if (metaData.type == SqlDbTypeExtensions.Vector) + { + providerSpecificFieldType = GetVectorFieldType(metaData.scale); + } else { // For all other types, including Xml - use data in MetaType. @@ -2116,7 +2137,7 @@ override public long GetChars(int i, long dataIndex, char[] buffer, int bufferIn // if bad buffer index, throw if ((bufferIndex < 0) || (buffer != null && bufferIndex >= buffer.Length)) { - throw ADP.InvalidDestinationBufferIndex(buffer.Length, bufferIndex, nameof(bufferIndex)); + throw ADP.InvalidDestinationBufferIndex(buffer?.Length ?? 0, bufferIndex, nameof(bufferIndex)); } // if there is not enough room in the buffer for data @@ -2598,7 +2619,7 @@ virtual public SqlXml GetSqlXml(int i) return sx; } - /// + /// virtual public SqlJson GetSqlJson(int i) { ReadColumn(i); @@ -2606,7 +2627,7 @@ virtual public SqlJson GetSqlJson(int i) return json; } - /// + /// virtual public SqlVector GetSqlVector(int i) where T : unmanaged { if (typeof(T) != typeof(float)) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs index ae20c27254..26e1a07594 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs @@ -331,18 +331,20 @@ internal static void VerifyColumnMasterKeySignature(string keyStoreName, string } else { - bool signatureVerificationResult = ColumnMasterKeyMetadataSignatureVerificationCache.GetSignatureVerificationResult(keyStoreName, keyPath, isEnclaveEnabled, CMKSignature); - if (signatureVerificationResult == false) - { - // We will simply bubble up the exception from VerifyColumnMasterKeyMetadata function. - isValidSignature = provider.VerifyColumnMasterKeyMetadata(keyPath, isEnclaveEnabled, - CMKSignature); + SignatureVerificationResult cachedResult = ColumnMasterKeyMetadataSignatureVerificationCache.Instance + .GetSignatureVerificationResult(keyStoreName, keyPath, isEnclaveEnabled, CMKSignature); - ColumnMasterKeyMetadataSignatureVerificationCache.AddSignatureVerificationResult(keyStoreName, keyPath, isEnclaveEnabled, CMKSignature, isValidSignature); + if (cachedResult == SignatureVerificationResult.NotFound) + { + // Cache miss: verify with the provider and cache the result. + // Exceptions from VerifyColumnMasterKeyMetadata bubble up to the outer catch. + isValidSignature = provider.VerifyColumnMasterKeyMetadata(keyPath, isEnclaveEnabled, CMKSignature); + ColumnMasterKeyMetadataSignatureVerificationCache.Instance + .AddSignatureVerificationResult(keyStoreName, keyPath, isEnclaveEnabled, CMKSignature, isValidSignature); } else { - isValidSignature = signatureVerificationResult; + isValidSignature = cachedResult == SignatureVerificationResult.True; } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs index 8f0d7fcba0..d91616fd38 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs @@ -511,6 +511,11 @@ internal static Exception UnsupportedAuthenticationByProvider(string authenticat return ADP.NotSupported(StringsHelper.GetString(Strings.SQL_UnsupportedAuthenticationByProvider, type, authentication)); } + internal static Exception UseWamBrokerRequiresAzureExtensionUpgrade() + { + return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_UseWamBrokerRequiresAzureExtensionUpgrade)); + } + internal static Exception CannotFindAuthProvider(SqlAuthenticationMethod authentication) { string authName = authentication.ToString(); @@ -746,6 +751,10 @@ internal static Exception ParsingErrorLength(ParsingErrorState state, int length { return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_ParsingErrorLength, ((int)state).ToString(CultureInfo.InvariantCulture), length)); } + internal static Exception ParsingErrorLength(ParsingErrorState state, uint length) + { + return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_ParsingErrorLength, ((int)state).ToString(CultureInfo.InvariantCulture), length)); + } internal static Exception ParsingErrorStatus(ParsingErrorState state, int status) { return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_ParsingErrorStatus, ((int)state).ToString(CultureInfo.InvariantCulture), status)); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs index cf8f4b3893..fbdf2dbb53 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -84,6 +84,17 @@ internal static class TdsEnums public const int MAX_PACKET_SIZE = 32768; public const int MAX_SERVER_USER_NAME = 256; // obtained from luxor + // Maximum allowed data length for token payloads (feature ext ack, + // session state, fedauth info). Prevents a malicious server from causing + // unbounded memory allocation via spoofed token length fields. + internal const int MaxTokenDataLength = 1 << 20; // 1 MB + + // Maximum allowed data length for a DTC promote transaction propagation token. + internal const int MaxPromoteTransactionLength = 1 << 16; // 64 KB + + // Maximum valid wire size for datetime types (DateTimeOffset = 5 time + 3 date + 2 offset). + internal const int MaxDateTimeLength = 10; + // Severity 0 - 10 indicates informational (non-error) messages // Severity 11 - 16 indicates errors that can be corrected by user (syntax errors, etc...) // Severity 17 - 19 indicates failure due to insufficient resources in the server diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs index 94281b4998..c2b17b22d7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -208,7 +208,7 @@ static TdsParser() { // For CoreCLR, we need to register the ANSI Code Page encoding provider before attempting to get an Encoding from a CodePage // For a default installation of SqlServer the encoding exchanged during Login is 1252. This encoding is not loaded by default - // See Remarks at https://msdn.microsoft.com/en-us/library/system.text.encodingprovider(v=vs.110).aspx + // See Remarks at https://msdn.microsoft.com/en-us/library/system.text.encodingprovider(v=vs.110).aspx // SqlClient needs to register the encoding providers to make sure that even basic scenarios work with Sql Server. Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } @@ -683,7 +683,7 @@ internal void RemoveEncryption() // create a new packet encryption changes the internal packet size Bug# 228403 _physicalStateObj.ClearAllWritePackets(); - } + } internal void EnableMars() { @@ -1376,11 +1376,11 @@ internal void TdsLogin( int feOffset = length; // calculate and reserve the required bytes for the featureEx length = ApplyFeatureExData( - requestedFeatures, - recoverySessionData, + requestedFeatures, + recoverySessionData, fedAuthFeatureExtensionData, UserAgent.Ucs2Bytes, - useFeatureExt, + useFeatureExt, length ); @@ -2792,7 +2792,7 @@ internal TdsOperationStatus TryRun(RunBehavior runBehavior, SqlCommand cmdHandle { _connHandler._federatedAuthenticationInfoReceived = true; SqlFedAuthInfo info; - + result = TryProcessFedAuthInfo(stateObj, tokenLength, out info); if (result != TdsOperationStatus.Done) { @@ -3348,6 +3348,10 @@ private TdsOperationStatus TryProcessEnvChange(int tokenLength, TdsParserStateOb // new value has 4 byte length return result; } + if (env._newLength < 0 || env._newLength > TdsEnums.MaxPromoteTransactionLength) + { + throw SQL.ParsingErrorLength(ParsingErrorState.CorruptedTdsStream, env._newLength); + } // read new value with 4 byte length env._newBinValue = new byte[env._newLength]; result = stateObj.TryReadByteArray(env._newBinValue, env._newLength); @@ -3846,10 +3850,15 @@ private TdsOperationStatus TryProcessFeatureExtAck(TdsParserStateObject stateObj { return result; } - byte[] data = new byte[dataLen]; - if (dataLen > 0) + if (dataLen > (uint)TdsEnums.MaxTokenDataLength) { - result = stateObj.TryReadByteArray(data, checked((int)dataLen)); + throw SQL.ParsingErrorLength(ParsingErrorState.CorruptedTdsStream, dataLen); + } + int dataLength = (int)dataLen; + byte[] data = new byte[dataLength]; + if (dataLength > 0) + { + result = stateObj.TryReadByteArray(data, dataLength); if (result != TdsOperationStatus.Done) { return result; @@ -4169,6 +4178,10 @@ private TdsOperationStatus TryProcessSessionState(TdsParserStateObject stateObj, { throw SQL.ParsingErrorLength(ParsingErrorState.SessionStateLengthTooShort, length); } + if (length > TdsEnums.MaxTokenDataLength) + { + throw SQL.ParsingErrorLength(ParsingErrorState.CorruptedTdsStream, length); + } uint seqNum; TdsOperationStatus result = stateObj.TryReadUInt32(out seqNum); if (result != TdsOperationStatus.Done) @@ -4218,6 +4231,10 @@ private TdsOperationStatus TryProcessSessionState(TdsParserStateObject stateObj, return result; } } + if (stateLen < 0 || stateLen > TdsEnums.MaxTokenDataLength) + { + throw SQL.ParsingErrorLength(ParsingErrorState.CorruptedTdsStream, stateLen); + } byte[] buffer = null; lock (sdata._delta) { @@ -4435,6 +4452,10 @@ private TdsOperationStatus TryProcessFedAuthInfo(TdsParserStateObject stateObj, SqlClientEventSource.Log.TryTraceEvent(" FEDAUTHINFO token stream length too short for CountOfInfoIDs."); throw SQL.ParsingErrorLength(ParsingErrorState.FedAuthInfoLengthTooShortForCountOfInfoIds, tokenLen); } + if (tokenLen > TdsEnums.MaxTokenDataLength) + { + throw SQL.ParsingErrorLength(ParsingErrorState.CorruptedTdsStream, tokenLen); + } // read how many FedAuthInfo options there are uint optionsCount; @@ -4912,14 +4933,20 @@ internal TdsOperationStatus TryProcessReturnValue(int length, } // always read as sql types - Debug.Assert(valLen < (ulong)(int.MaxValue), "ProcessReturnValue received data size > 2Gb"); - - int intlen = valLen > (ulong)(int.MaxValue) ? int.MaxValue : (int)valLen; + int intlen; if (rec.metaType.IsPlp) { intlen = int.MaxValue; // If plp data, read it all } + else if (valLen > (ulong)int.MaxValue) + { + throw SQL.ParsingErrorLength(ParsingErrorState.CorruptedTdsStream, unchecked((int)valLen)); + } + else + { + intlen = (int)valLen; + } if (rec.type == SqlDbTypeExtensions.Vector) { @@ -5790,7 +5817,7 @@ private TdsOperationStatus TryCommonProcessMetaData(TdsParserStateObject stateOb { return result; } - + // read flags and set appropriate flags in structure byte flags; result = stateObj.TryReadByte(out flags); @@ -7119,7 +7146,7 @@ internal TdsOperationStatus TryReadSqlValue(SqlBuffer value, return result; } - // Internally, we use Sqlbinary to deal with varbinary data and store it in + // Internally, we use Sqlbinary to deal with varbinary data and store it in // SqlBuffer as SqlBinary value. #if NET value.SqlBinary = SqlBinary.WrapBytes(b); @@ -7188,9 +7215,20 @@ internal TdsOperationStatus TryReadSqlValue(SqlBuffer value, return TdsOperationStatus.Done; } + // length originates as a single byte on the wire (nullable datetime length prefix), + // but is kept as int to match the TDS parsing API surface where all lengths are int. + // Using byte here would require casts at all call sites and silently truncate values + // from the sql_variant path where lenData is computed arithmetically. private TdsOperationStatus TryReadSqlDateTime(SqlBuffer value, byte tdsType, int length, byte scale, TdsParserStateObject stateObj) { - Span datetimeBuffer = ((uint)length <= 16) ? stackalloc byte[16] : new byte[length]; + // DateTimeOffset is the largest datetime type at 10 bytes (5 time + 3 date + 2 offset). + // Reject anything larger to prevent heap allocation from spoofed metadata. + if (length < 0 || length > TdsEnums.MaxDateTimeLength) + { + throw SQL.ParsingErrorLength(ParsingErrorState.CorruptedTdsStream, length); + } + + Span datetimeBuffer = stackalloc byte[TdsEnums.MaxDateTimeLength]; TdsOperationStatus result = stateObj.TryReadByteArray(datetimeBuffer, length); if (result != TdsOperationStatus.Done) @@ -7446,9 +7484,11 @@ internal TdsOperationStatus TryReadSqlValueInternal(SqlBuffer value, byte tdsTyp case TdsEnums.SQLVECTOR: { // Note: Better not come here with plp data!! - Debug.Assert(length <= TdsEnums.MAXSIZE); - byte[] b = new byte[length]; - result = stateObj.TryReadByteArrayWithContinue(length, isPlp: false, out b); + if (length < 0 || length > TdsEnums.MAXSIZE) + { + throw SQL.ParsingErrorLength(ParsingErrorState.CorruptedTdsStream, length); + } + result = stateObj.TryReadByteArrayWithContinue(length, isPlp: false, out byte[] b); if (result != TdsOperationStatus.Done) { return result; @@ -9278,7 +9318,7 @@ internal int WriteFedAuthFeatureRequest(FederatedAuthenticationFeatureExtensionD /// internal int WriteVectorSupportFeatureRequest(bool write) { - const int len = 6; + const int len = 6; if (write) { @@ -9663,7 +9703,7 @@ private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures, checked { // NOTE: As part of TDS spec UserAgent feature extension should be the first feature extension in the list. - if (LocalAppContextSwitches.EnableUserAgent && ((requestedFeatures & TdsEnums.FeatureExtension.UserAgent) != 0)) + if ((requestedFeatures & TdsEnums.FeatureExtension.UserAgent) != 0) { length += WriteUserAgentFeatureRequest(userAgent, write); } @@ -10476,7 +10516,7 @@ private Task TDSExecuteRPCAddParameter(TdsParserStateObject stateObj, SqlParamet { isSqlVal = param.ParameterIsSqlType; // We have to forward the TYPE info, we need to know what type we are returning. Once we null the parameter we will no longer be able to distinguish what type were seeing. - // Output parameter of SqlDbType vector are defined through SqlParameter.Value. + // Output parameter of SqlDbType vector are defined through SqlParameter.Value. // This check ensures that we do not discard the parameter value when SqlDbType is vector. if (mt.SqlDbType != SqlDbTypeExtensions.Vector) { @@ -10761,7 +10801,7 @@ private Task TDSExecuteRPCAddParameter(TdsParserStateObject stateObj, SqlParamet Debug.Assert(udtVal != null, "GetBytes returned null instance. Make sure that it always returns non-null value"); size = udtVal.Length; - + if (size >= maxSupportedSize && maxsize != -1) { throw SQL.UDTInvalidSize(maxsize, maxSupportedSize); @@ -13263,7 +13303,7 @@ private Task WriteUnterminatedValue(object value, MetaType type, byte scale, int { if (type.NullableType == TdsEnums.SQLJSON) { - // TODO : Performance and BOM check. Saurabh + // TODO : Performance and BOM check. Saurabh byte[] jsonAsBytes = Encoding.UTF8.GetBytes((string)value); WriteInt(jsonAsBytes.Length, stateObj); return stateObj.WriteByteArray(jsonAsBytes, jsonAsBytes.Length, 0, canAccumulate: false); @@ -13921,13 +13961,13 @@ internal TdsOperationStatus TryReadPlpUnicodeCharsWithContinue(TdsParserStateObj } TdsOperationStatus result = TryReadPlpUnicodeChars( - ref temp, - 0, - length >> 1, - stateObj, - out length, + ref temp, + 0, + length >> 1, + stateObj, + out length, supportRentedBuff: !canContinue, // do not use the arraypool if we are going to keep the buffer in the snapshot - rentedBuff: ref buffIsRented, + rentedBuff: ref buffIsRented, startOffset, canContinue ); @@ -14137,7 +14177,7 @@ bool writeDataSizeToSnapshot stateObj._longlenleft--; if (writeDataSizeToSnapshot) { - // we need to write the single b1 byte to the array because we may run out of data + // we need to write the single b1 byte to the array because we may run out of data // and need to wait for another packet buff[offst] = (char)((b1 & 0xff)); currentPacketId = IncrementSnapshotDataSize(stateObj, restartingDataSizeCount, currentPacketId, 1); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlDbTypeExtensions.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlDbTypeExtensions.cs index 96244fb7a8..0b6780f900 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlDbTypeExtensions.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlDbTypeExtensions.cs @@ -6,16 +6,16 @@ namespace Microsoft.Data { - /// + /// public static class SqlDbTypeExtensions { - /// + /// #if NET9_0_OR_GREATER public const SqlDbType Json = SqlDbType.Json; #else public const SqlDbType Json = (SqlDbType)35; #endif - /// + /// #if NET10_0_OR_GREATER public const SqlDbType Vector = SqlDbType.Vector; #else diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlJson.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlJson.cs index 8d0ee74cc6..5ca1ecee5f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlJson.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlJson.cs @@ -12,18 +12,18 @@ namespace Microsoft.Data.SqlTypes { - /// + /// public class SqlJson : INullable { // Our serialized JSON string, or null. private readonly string? _jsonString = null; - /// + /// public SqlJson() { } - /// + /// #if NET public SqlJson([StringSyntax(StringSyntaxAttribute.Json)] string? jsonString) #else @@ -45,7 +45,7 @@ public SqlJson(string? jsonString) _jsonString = jsonString; } - /// + /// public SqlJson(JsonDocument? jsonDoc) { if (jsonDoc == null) @@ -57,13 +57,13 @@ public SqlJson(JsonDocument? jsonDoc) _jsonString = jsonDoc.RootElement.GetRawText(); } - /// + /// public bool IsNull => _jsonString is null; - /// + /// public static SqlJson Null => new(); - /// + /// public string Value { get @@ -77,7 +77,7 @@ public string Value } } - /// + /// public override string? ToString() { return _jsonString; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs index cf04ff8636..3c334fe061 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs @@ -15,7 +15,7 @@ namespace Microsoft.Data.SqlTypes; -/// +/// public readonly struct SqlVector : INullable, ISqlVector where T : unmanaged { @@ -55,10 +55,10 @@ private SqlVector(int length) Memory = new(); } - /// + /// public static SqlVector CreateNull(int length) => new(length); - /// + /// public SqlVector(ReadOnlyMemory memory) { (_elementType, _elementSize) = GetTypeFieldsOrThrow(); @@ -101,16 +101,16 @@ internal string GetString() #region Properties - /// + /// public bool IsNull { get; } - /// + /// public static SqlVector? Null => null; - /// + /// public int Length { get; } - /// + /// public ReadOnlyMemory Memory { get; } #endregion diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs index 2cae12cb6c..2226209a13 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs @@ -9325,7 +9325,16 @@ internal static string SQL_CannotFindActiveDirectoryAuthProvider { return ResourceManager.GetString("SQL_CannotFindActiveDirectoryAuthProvider", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Setting 'useWamBroker' requires the 'Microsoft.Data.SqlClient.Extensions.Azure' package to expose 'Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions'. Upgrade the 'Microsoft.Data.SqlClient.Extensions.Azure' package to a version that includes this type.. + /// + internal static string SQL_UseWamBrokerRequiresAzureExtensionUpgrade { + get { + return ResourceManager.GetString("SQL_UseWamBrokerRequiresAzureExtensionUpgrade", resourceCulture); + } + } + /// /// Looks up a localized string similar to Failed to read the config section for authentication providers.. /// diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx index 0006d91c9d..179c9590a8 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx @@ -2589,6 +2589,9 @@ Failed to read the config section for authentication providers. + + Setting 'useWamBroker' requires the 'Microsoft.Data.SqlClient.Extensions.Azure' package to expose 'Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProviderOptions'. Upgrade the 'Microsoft.Data.SqlClient.Extensions.Azure' package to at least v1.1.0 that includes this type. + Parameter '{0}' cannot be null or empty. diff --git a/src/Microsoft.Data.SqlClient/src/TypeForwards.Abstractions.cs b/src/Microsoft.Data.SqlClient/src/TypeForwards.Abstractions.cs new file mode 100644 index 0000000000..f8272c5d2b --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/TypeForwards.Abstractions.cs @@ -0,0 +1,9 @@ +// 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. + +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(Microsoft.Data.SqlClient.SqlAuthenticationMethod))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(Microsoft.Data.SqlClient.SqlAuthenticationParameters))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(Microsoft.Data.SqlClient.SqlAuthenticationProvider))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(Microsoft.Data.SqlClient.SqlAuthenticationProviderException))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(Microsoft.Data.SqlClient.SqlAuthenticationToken))] diff --git a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs index 4a0cdf67de..8102f47d51 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs @@ -43,7 +43,6 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable private readonly bool? _disableTnirByDefaultOriginal; #endif private readonly bool? _enableMultiSubnetFailoverByDefaultOriginal; - private readonly bool? _enableUserAgentOriginal; #if NET private readonly bool? _globalizationInvariantModeOriginal; #endif @@ -93,8 +92,6 @@ public LocalAppContextSwitchesHelper() #endif _enableMultiSubnetFailoverByDefaultOriginal = GetSwitchValue("s_enableMultiSubnetFailoverByDefault"); - _enableUserAgentOriginal = - GetSwitchValue("s_enableUserAgent"); #if NET _globalizationInvariantModeOriginal = GetSwitchValue("s_globalizationInvariantMode"); @@ -149,9 +146,6 @@ public void Dispose() SetSwitchValue( "s_enableMultiSubnetFailoverByDefault", _enableMultiSubnetFailoverByDefaultOriginal); - SetSwitchValue( - "s_enableUserAgent", - _enableUserAgentOriginal); #if NET SetSwitchValue( "s_globalizationInvariantMode", @@ -228,15 +222,6 @@ public bool? EnableMultiSubnetFailoverByDefault set => SetSwitchValue("s_enableMultiSubnetFailoverByDefault", value); } - /// - /// Get or set the EnableUserAgent switch value. - /// - public bool? EnableUserAgent - { - get => GetSwitchValue("s_enableUserAgent"); - set => SetSwitchValue("s_enableUserAgent", value); - } - #if NET /// /// Get or set the GlobalizationInvariantMode switch value. diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/LocalizationTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/LocalizationTest.cs index d78d801fde..250c009b27 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/LocalizationTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/LocalizationTest.cs @@ -40,6 +40,7 @@ public class LocalizationTest [InlineData("ru-RU")] [InlineData("zh-Hans")] [InlineData("zh-Hant")] + [Trait("Category", "flaky")] public void Localization_Tests(string culture) { string localized = GetLocalizedErrorMessage(culture); diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlCommandTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlCommandTest.cs index 80ceba25af..59f35b4941 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlCommandTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlCommandTest.cs @@ -562,6 +562,20 @@ public void ParameterCollectionTest() } } + /// + /// Verifies that SqlCommand.Cancel() is a no-op when Connection is null, + /// rather than throwing a NullReferenceException. Regression test for #4327. + /// + [Fact] + public void Cancel_WithNullConnection_DoesNotThrow() + { + using SqlCommand cmd = new SqlCommand(); + Assert.Null(cmd.Connection); + + // Should be a no-op, not throw NullReferenceException + cmd.Cancel(); + } + private static SqlConnection GetNonConnectingConnection() => new SqlConnection("Initial Catalog=a;Server=b;User ID=c;Password=d"); } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index 3b4245d84f..fc310bf3d3 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -86,7 +86,7 @@ public static class DataTestUtility internal static readonly string KerberosDomainPassword = null; // SQL server Version - private static string s_sQLServerVersion = string.Empty; + private static string s_sqlServerVersion; //SQL Server EngineEdition private static string s_sqlServerEngineEdition; @@ -125,9 +125,9 @@ public static string SQLServerVersion { if (!string.IsNullOrEmpty(TCPConnectionString)) { - s_sQLServerVersion ??= GetSqlServerProperty(TCPConnectionString, ServerProperty.ProductMajorVersion); + s_sqlServerVersion ??= GetSqlServerProperty(TCPConnectionString, ServerProperty.ProductMajorVersion); } - return s_sQLServerVersion; + return s_sqlServerVersion; } } @@ -302,15 +302,17 @@ private static Task AcquireTokenAsync(string authorityURL, string userID SecureString securePassword = new SecureString(); securePassword.MakeReadOnly(); -#pragma warning disable CS0618 // Type or member is obsolete + #pragma warning disable CS0618 // Type or member is obsolete result = app.AcquireTokenByUsernamePassword(scopes, userID, password).ExecuteAsync().Result; -#pragma warning restore CS0618 // Type or member is obsolete + #pragma warning restore CS0618 // Type or member is obsolete return result.AccessToken; }); public static bool IsKerberosTest => !string.IsNullOrEmpty(KerberosDomainUser) && !string.IsNullOrEmpty(KerberosDomainPassword); + public static bool IsNotKerberosTest => !IsKerberosTest; + #nullable enable /// @@ -389,7 +391,7 @@ public static string GetSqlServerProperty(SqlConnection connection, ServerProper } } - #nullable disable + #nullable restore private static bool GetSQLServerStatusOnTDS8(string connectionString) { @@ -491,7 +493,14 @@ public static bool AreConnStringsSetup() public static bool IsSQL2019() => string.Equals("15", SQLServerVersion.Trim()); - public static bool IsSQL2016() => string.Equals("14", s_sQLServerVersion.Trim()); + public static bool IsSQL2017() => string.Equals("14", SQLServerVersion.Trim()); + + public static bool IsSQL2016() => string.Equals("13", SQLServerVersion.Trim()); + + // "At least" version checks for use as ConditionalFact/ConditionalTheory conditions. + public static bool IsAtLeastSQL2017() => int.TryParse(SQLServerVersion?.Trim(), out int major) && major >= 14; + + public static bool IsAtLeastSQL2019() => int.TryParse(SQLServerVersion?.Trim(), out int major) && major >= 15; public static bool IsSQLAliasSetup() { @@ -951,35 +960,48 @@ public static void AssertEqualsWithDescription(object expectedValue, object actu } } - public static TException AssertThrowsWrapper(Action actionThatFails, string exceptionMessage = null, bool innerExceptionMustBeNull = false, Func customExceptionVerifier = null) where TException : Exception + #nullable enable + + /// + /// Asserts that throws an exception of type + /// and optionally verifies that its message contains + /// . + /// + public static TException AssertThrows( + Action actionThatFails, + string? exceptionMessage = null) + where TException : Exception { TException ex = Assert.Throws(actionThatFails); + if (exceptionMessage != null) { Assert.True(ex.Message.Contains(exceptionMessage), string.Format("FAILED: Exception did not contain expected message.\nExpected: {0}\nActual: {1}", exceptionMessage, ex.Message)); } - if (innerExceptionMustBeNull) - { - Assert.True(ex.InnerException == null, "FAILED: Expected InnerException to be null."); - } - - if (customExceptionVerifier != null) - { - Assert.True(customExceptionVerifier(ex), "FAILED: Custom exception verifier returned false for this exception."); - } - return ex; } - public static TException AssertThrowsWrapper(Action actionThatFails, string exceptionMessage = null, string innerExceptionMessage = null, bool innerExceptionMustBeNull = false, Func customExceptionVerifier = null) where TException : Exception + /// + /// Asserts that throws + /// whose is of type . + /// Optionally verifies message text on both the outer and inner exceptions. + /// + public static TException AssertThrowsInner( + Action actionThatFails, + string? exceptionMessage = null, + string? innerExceptionMessage = null) + where TException : Exception + where TInnerException : Exception { - TException ex = AssertThrowsWrapper(actionThatFails, exceptionMessage, innerExceptionMustBeNull, customExceptionVerifier); + TException ex = AssertThrows(actionThatFails, exceptionMessage); + + Assert.NotNull(ex.InnerException); + Assert.IsAssignableFrom(ex.InnerException); if (innerExceptionMessage != null) { - Assert.True(ex.InnerException != null, "FAILED: Cannot check innerExceptionMessage because InnerException is null."); Assert.True(ex.InnerException.Message.Contains(innerExceptionMessage), string.Format("FAILED: Inner Exception did not contain expected message.\nExpected: {0}\nActual: {1}", innerExceptionMessage, ex.InnerException.Message)); } @@ -987,26 +1009,40 @@ public static TException AssertThrowsWrapper(Action return ex; } - public static TException AssertThrowsWrapper(Action actionThatFails, string exceptionMessage = null, string innerExceptionMessage = null, string innerInnerExceptionMessage = null, bool innerInnerInnerExceptionMustBeNull = false) where TException : Exception where TInnerException : Exception where TInnerInnerException : Exception + /// + /// Asserts that throws + /// whose is either + /// or . Use this when a race condition + /// (e.g. disposal during an async read) may cause the inner exception type to vary + /// between runs. The is only verified when the + /// inner exception is . + /// + public static TException AssertThrowsInnerWithAlternate( + Action actionThatFails, + string? exceptionMessage = null, + string? innerExceptionMessage = null) + where TException : Exception + where TInnerException : Exception + where TAlternateInnerException : Exception { - TException ex = AssertThrowsWrapper(actionThatFails, exceptionMessage, innerExceptionMessage); - if (innerInnerInnerExceptionMustBeNull) - { - Assert.True(ex.InnerException != null, "FAILED: Cannot check innerInnerInnerExceptionMustBeNull since InnerException is null"); - Assert.True(ex.InnerException.InnerException == null, "FAILED: Expected InnerInnerException to be null."); - } + TException ex = AssertThrows(actionThatFails, exceptionMessage); + + Assert.NotNull(ex.InnerException); + Assert.True( + ex.InnerException is TInnerException or TAlternateInnerException, + $"Expected {typeof(TInnerException).Name} or {typeof(TAlternateInnerException).Name}, got: {ex.InnerException?.GetType()}"); - if (innerInnerExceptionMessage != null) + if (innerExceptionMessage != null && ex.InnerException is TInnerException) { - Assert.True(ex.InnerException != null, "FAILED: Cannot check innerInnerExceptionMessage since InnerException is null"); - Assert.True(ex.InnerException.InnerException != null, "FAILED: Cannot check innerInnerExceptionMessage since InnerInnerException is null"); - Assert.True(ex.InnerException.InnerException.Message.Contains(innerInnerExceptionMessage), - string.Format("FAILED: Inner Exception did not contain expected message.\nExpected: {0}\nActual: {1}", innerInnerExceptionMessage, ex.InnerException.InnerException.Message)); + Assert.True(ex.InnerException.Message.Contains(innerExceptionMessage), + string.Format("FAILED: Inner Exception did not contain expected message.\nExpected: {0}\nActual: {1}", innerExceptionMessage, ex.InnerException.Message)); } return ex; } + #nullable restore + public static TException ExpectFailure(Action actionThatFails, string[] exceptionMessages, bool innerExceptionMustBeNull = false, Func customExceptionVerifier = null) where TException : Exception { try @@ -1320,7 +1356,7 @@ public static string GetMachineFQDN(string hostname) } return fqdn.ToString(); } - } - #nullable disable + #nullable restore + } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AdapterTest/AdapterTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AdapterTest/AdapterTest.cs index ed9d9ab541..9bf5351a83 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AdapterTest/AdapterTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AdapterTest/AdapterTest.cs @@ -1318,13 +1318,13 @@ public void TestDeriveParameters() using (SqlCommand cmd = new SqlCommand(procName, connection)) { string errorMessage = string.Format(SystemDataResourceManager.Instance.ADP_DeriveParametersNotSupported, "SqlCommand", cmd.CommandType); - DataTestUtility.AssertThrowsWrapper( + DataTestUtility.AssertThrows( () => SqlCommandBuilder.DeriveParameters(cmd), errorMessage); errorMessage = string.Format(SystemDataResourceManager.Instance.ADP_OpenConnectionRequired, "DeriveParameters", ""); cmd.CommandType = CommandType.StoredProcedure; - DataTestUtility.AssertThrowsWrapper( + DataTestUtility.AssertThrows( () => SqlCommandBuilder.DeriveParameters(cmd), errorMessage); @@ -1335,7 +1335,7 @@ public void TestDeriveParameters() cmd.CommandText = "Test_EmployeeSalesBy"; errorMessage = string.Format(SystemDataResourceManager.Instance.ADP_NoStoredProcedureExists, cmd.CommandText); - DataTestUtility.AssertThrowsWrapper( + DataTestUtility.AssertThrows( () => SqlCommandBuilder.DeriveParameters(cmd), errorMessage); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs index 9dd87d8f21..00bba56fc2 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs @@ -25,7 +25,10 @@ public class AsyncCancelledConnectionsTest private Random _random; // Disabled on Azure since this test fails on concurrent runs on same database. - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] + // Disabled on Kerberos and Managed Instance pipelines due to environment-specific instability. + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), + nameof(DataTestUtility.IsNotAzureServer), nameof(DataTestUtility.IsNotManagedInstance), + nameof(DataTestUtility.IsNotKerberosTest))] [InlineData(true)] [InlineData(false)] public async Task CancelAsyncConnections(bool useMars) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncTimeoutTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncTimeoutTest.cs index 834157aeec..bc514dce92 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncTimeoutTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncTimeoutTest.cs @@ -28,6 +28,7 @@ public enum AsyncAPI // Synapse: WAITFOR DELAY not supported [Parse error at line: 1, column: 1: Incorrect syntax near 'WAITFOR'.] [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] [ClassData(typeof(AsyncTimeoutTestVariations))] + [Trait("Category", "flaky")] public static void TestDelayedAsyncTimeout(AsyncAPI api, string commonObj, int delayPeriod, bool marsEnabled) => RunTest(api, commonObj, delayPeriod, marsEnabled); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolTest.cs index 478f78a5b9..10c3939774 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolTest.cs @@ -36,6 +36,7 @@ public static class ConnectionPoolTest /// [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] [ClassData(typeof(ConnectionPoolConnectionStringProvider))] + [Trait("Category", "flaky")] public static void BasicConnectionPoolingTest(string connectionString) { SqlConnection.ClearAllPools(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTest.cs index fbc7cd222a..32780dcfe6 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTest.cs @@ -32,7 +32,7 @@ public class CertificateTest : IDisposable // InstanceName will get replaced with an instance name in the connection string private static string InstanceName = "MSSQLSERVER"; - + // s_instanceNamePrefix will get replaced with MSSQL$ is there is an instance name in connection string private static string InstanceNamePrefix = ""; @@ -51,10 +51,14 @@ private static string ForceEncryptionRegistryPath { return $@"SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL15.{InstanceName}\MSSQLSERVER\SuperSocketNetLib"; } - if (DataTestUtility.IsSQL2016()) + if (DataTestUtility.IsSQL2017()) { return $@"SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL14.{InstanceName}\MSSQLSERVER\SuperSocketNetLib"; } + if (DataTestUtility.IsSQL2016()) + { + return $@"SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL13.{InstanceName}\MSSQLSERVER\SuperSocketNetLib"; + } return string.Empty; } } @@ -196,7 +200,9 @@ private static void CreateValidCertificate(string script) RedirectStandardError = true, RedirectStandardOutput = true, UseShellExecute = false, - Arguments = $"{script} -Prefix {InstanceNamePrefix} -Instance {InstanceName}", + Arguments = string.IsNullOrEmpty(InstanceNamePrefix) + ? $"{script} -Instance \"{InstanceName}\"" + : $"{script} -Prefix \"{InstanceNamePrefix}\" -Instance \"{InstanceName}\"", CreateNoWindow = false, Verb = "runas" } @@ -224,7 +230,12 @@ private static void CreateValidCertificate(string script) proc.Kill(); // allow async output to process proc.WaitForExit(2000); - throw new Exception($"Could not generate certificate.Error out put: {output}"); + throw new Exception($"Could not generate certificate. Error output: {output}"); + } + + if (proc.ExitCode != 0) + { + throw new Exception($"Certificate generation script failed with exit code {proc.ExitCode}. Output: {output}"); } } else @@ -252,6 +263,11 @@ private static string GetLocalIpAddress() private void RemoveCertificate() { + if (string.IsNullOrEmpty(_thumbprint)) + { + return; + } + using X509Store certStore = new(StoreName.Root, StoreLocation.LocalMachine); certStore.Open(OpenFlags.ReadWrite); X509Certificate2Collection certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, _thumbprint, false); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTestWithTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTestWithTdsServer.cs index 98ce8efafa..bb1bb8b5f2 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTestWithTdsServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTestWithTdsServer.cs @@ -89,7 +89,7 @@ private static string ForceEncryptionRegistryPath { return $@"SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL15.{s_instanceName}\MSSQLSERVER\SuperSocketNetLib"; } - if (DataTestUtility.IsSQL2016()) + if (DataTestUtility.IsSQL2017()) { return $@"SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL14.{s_instanceName}\MSSQLSERVER\SuperSocketNetLib"; } 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 040119616e..7a11401531 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs @@ -702,6 +702,22 @@ DROP TABLE IF EXISTS [{tableName}] } } + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static async Task GetCharsSequentialAccess_NullBufferNegativeBufferIndex_ThrowsArgumentOutOfRange() + { + using var connection = new SqlConnection(DataTestUtility.TCPConnectionString); + await connection.OpenAsync(); + + using var command = connection.CreateCommand(); + command.CommandText = "SELECT CONVERT(NVARCHAR(MAX), 'test')"; + + using var reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess); + Assert.True(await reader.ReadAsync()); + + var ex = Assert.Throws(() => reader.GetChars(0, 0, null, -1, 0)); + Assert.Equal("bufferIndex", ex.ParamName); + } + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] public static async Task CanGetCharsSequentially() { @@ -905,6 +921,7 @@ public _Row(int id, Guid documentIdentificationId, string name, string value) } } + [Trait("Category", "flaky")] [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] public static async Task CanReadAwkwardDataLengths() { diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs index 1c6d851ecc..118f709d89 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataStreamTest/DataStreamTest.cs @@ -180,6 +180,7 @@ public static void GetXmlReader_ReturnsXmlReaderCorrectly(string connectionStrin [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] [MemberData(nameof(ConnectionStrings))] + [Trait("Category", "flaky")] public static void ReadStream_ReadsStreamDataCorrectly(string connectionString) { ReadStream(connectionString); @@ -276,7 +277,7 @@ static async Task LocalCopyTo(Stream source, Stream destination, int bufferSize, { await destination.WriteAsync(new ReadOnlyMemory(buffer, 0, bytesRead), cancellationToken).ConfigureAwait(false); } -#endif +#endif } finally { @@ -320,7 +321,7 @@ private static byte[] CreateBinaryTable(SqlConnection connection, string tableNa { cmd.CommandText = $@" IF OBJECT_ID('dbo.{tableName}', 'U') IS NOT NULL -DROP TABLE {tableName}; +DROP TABLE {tableName}; CREATE TABLE {tableName} (id INT, foo VARBINARY(MAX)) "; cmd.ExecuteNonQuery(); @@ -377,7 +378,7 @@ private static void MultipleResults(string connectionString) { Assert.True(numBatches < expectedResults.Length, "ERROR: Received more batches than were expected."); object[] values = new object[r1.FieldCount]; - // Current "column" in expected row is (valuesChecked MOD FieldCount), since + // Current "column" in expected row is (valuesChecked MOD FieldCount), since // expected rows for current batch are appended together for easy formatting int valuesChecked = 0; while (r1.Read()) @@ -410,7 +411,7 @@ private static void InvalidRead(string connectionString) using (SqlDataReader reader = cmd.ExecuteReader()) { string errorMessage = SystemDataResourceManager.Instance.SQL_InvalidRead; - DataTestUtility.AssertThrowsWrapper(() => reader.GetInt32(0), errorMessage); + DataTestUtility.AssertThrows(() => reader.GetInt32(0), errorMessage); } } } @@ -497,7 +498,7 @@ private static void TypeRead(string connectionString) s = rdr.GetString(10); //ShipCity; // should get an exception here string errorMessage = SystemDataResourceManager.Instance.SqlMisc_NullValueMessage; - DataTestUtility.AssertThrowsWrapper(() => rdr.GetString(11), errorMessage); + DataTestUtility.AssertThrows(() => rdr.GetString(11), errorMessage); s = rdr.GetString(12); //ShipPostalCode; s = rdr.GetString(13); //ShipCountry; @@ -533,7 +534,7 @@ private static void GetValueOfTRead(string connectionString) rdr.IsDBNull(10); rdr.GetFieldValue(10); //ShipCity; // should get an exception here - DataTestUtility.AssertThrowsWrapper(() => rdr.GetFieldValue(11), errorMessage); + DataTestUtility.AssertThrows(() => rdr.GetFieldValue(11), errorMessage); rdr.IsDBNull(11); rdr.GetFieldValue(11); rdr.IsDBNull(11); @@ -542,7 +543,7 @@ private static void GetValueOfTRead(string connectionString) rdr.IsDBNull(12); rdr.GetFieldValue(13);//ShipCountry; rdr.GetFieldValue(14); - DataTestUtility.AssertThrowsWrapper(() => rdr.GetFieldValue(15), errorMessage); + DataTestUtility.AssertThrows(() => rdr.GetFieldValue(15), errorMessage); rdr.Read(); // read data out of buffer @@ -559,7 +560,7 @@ private static void GetValueOfTRead(string connectionString) Assert.False(rdr.IsDBNullAsync(10).Result, "FAILED: IsDBNull was true for a non-null value"); rdr.GetFieldValueAsync(10).Wait(); //ShipCity; // should get an exception here - DataTestUtility.AssertThrowsWrapper(() => rdr.GetFieldValueAsync(11).Wait(), innerExceptionMessage: errorMessage); + DataTestUtility.AssertThrowsInner(() => rdr.GetFieldValueAsync(11).Wait(), innerExceptionMessage: errorMessage); Assert.True(rdr.IsDBNullAsync(11).Result, "FAILED: IsDBNull was false for a null value"); rdr.IsDBNullAsync(11).Wait(); @@ -787,7 +788,7 @@ private static void OrphanReader(string connectionString) conn.Close(); errorMessage = string.Format(SystemDataResourceManager.Instance.ADP_DataReaderClosed, "CheckDataIsReady"); - DataTestUtility.AssertThrowsWrapper(() => value = reader[0], errorMessage); + DataTestUtility.AssertThrows(() => value = reader[0], errorMessage); Assert.True(reader.IsClosed, "FAILED: Stream was not closed by connection close (Scenario: Read)"); conn.Open(); } @@ -798,7 +799,7 @@ private static void OrphanReader(string connectionString) value = reader[0]; conn.Close(); - DataTestUtility.AssertThrowsWrapper(() => value = reader[0], errorMessage); + DataTestUtility.AssertThrows(() => value = reader[0], errorMessage); Assert.True(reader.IsClosed, "FAILED: Stream was not closed by connection close (Scenario: Read Partial Data)"); conn.Open(); } @@ -817,7 +818,7 @@ private static void OrphanReader(string connectionString) } while (reader.NextResult()); conn.Close(); - DataTestUtility.AssertThrowsWrapper(() => value = reader[0], errorMessage); + DataTestUtility.AssertThrows(() => value = reader[0], errorMessage); Assert.True(reader.IsClosed, "FAILED: Stream was not closed by connection close (Scenario: Read All Data)"); } } @@ -866,7 +867,7 @@ private static void ExecuteXmlReaderTest(string connectionString) // make sure we get an exception if we try to get another reader errorMessage = SystemDataResourceManager.Instance.ADP_OpenReaderExists("Connection"); - DataTestUtility.AssertThrowsWrapper(() => xr = cmd.ExecuteXmlReader(), errorMessage); + DataTestUtility.AssertThrows(() => xr = cmd.ExecuteXmlReader(), errorMessage); } // use a big result to fill up the pipe and do a partial read @@ -943,12 +944,12 @@ private static void ExecuteXmlReaderTest(string connectionString) // multiple columns cmd.CommandText = "select * from customers"; errorMessage = SystemDataResourceManager.Instance.SQL_NonXmlResult; - DataTestUtility.AssertThrowsWrapper(() => xr = cmd.ExecuteXmlReader(), errorMessage); + DataTestUtility.AssertThrows(() => xr = cmd.ExecuteXmlReader(), errorMessage); // non-ntext column cmd.CommandText = "select employeeID from employees"; errorMessage = SystemDataResourceManager.Instance.SQL_NonXmlResult; - DataTestUtility.AssertThrowsWrapper(() => xr = cmd.ExecuteXmlReader(), errorMessage); + DataTestUtility.AssertThrows(() => xr = cmd.ExecuteXmlReader(), errorMessage); } } } @@ -1114,31 +1115,31 @@ private static void SequentialAccess(string connectionString) i = reader.GetOrdinal("notes"); reader.GetChars(i, 14, chars, 0, 14); string errorMessage = string.Format(SystemDataResourceManager.Instance.ADP_NonSequentialColumnAccess, i, i + 1); - DataTestUtility.AssertThrowsWrapper(() => reader.GetString(i), errorMessage); + DataTestUtility.AssertThrows(() => reader.GetString(i), errorMessage); // Tests GetValue before GetBytes\Chars reader.Read(); i = reader.GetOrdinal("photo"); reader.GetSqlBinary(i); errorMessage = string.Format(SystemDataResourceManager.Instance.ADP_NonSequentialColumnAccess, i, i + 1); - DataTestUtility.AssertThrowsWrapper(() => reader.GetBytes(i, 0, data, 0, 13), errorMessage); + DataTestUtility.AssertThrows(() => reader.GetBytes(i, 0, data, 0, 13), errorMessage); i = reader.GetOrdinal("notes"); reader.GetString(i); errorMessage = string.Format(SystemDataResourceManager.Instance.ADP_NonSequentialColumnAccess, i, i + 1); - DataTestUtility.AssertThrowsWrapper(() => reader.GetChars(i, 0, chars, 0, 14), errorMessage); + DataTestUtility.AssertThrows(() => reader.GetChars(i, 0, chars, 0, 14), errorMessage); // Tests GetBytes\GetChars re-reading same characters reader.Read(); i = reader.GetOrdinal("photo"); reader.GetBytes(i, 0, data, 0, 13); errorMessage = string.Format(SystemDataResourceManager.Instance.ADP_NonSeqByteAccess, 0, 13, "GetBytes"); - DataTestUtility.AssertThrowsWrapper(() => reader.GetBytes(i, 0, data, 0, 13), errorMessage); + DataTestUtility.AssertThrows(() => reader.GetBytes(i, 0, data, 0, 13), errorMessage); i = reader.GetOrdinal("notes"); reader.GetChars(i, 0, chars, 0, 14); errorMessage = string.Format(SystemDataResourceManager.Instance.ADP_NonSeqByteAccess, 0, 14, "GetChars"); - DataTestUtility.AssertThrowsWrapper(() => reader.GetChars(i, 0, chars, 0, 14), errorMessage); + DataTestUtility.AssertThrows(() => reader.GetChars(i, 0, chars, 0, 14), errorMessage); } using (reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) @@ -1150,12 +1151,12 @@ private static void SequentialAccess(string connectionString) int columnToTry = 0; string errorMessage = string.Format(SystemDataResourceManager.Instance.ADP_NonSequentialColumnAccess, columnToTry, sqldata.Length); - DataTestUtility.AssertThrowsWrapper(() => reader.GetInt32(columnToTry), errorMessage); - DataTestUtility.AssertThrowsWrapper(() => reader.GetValue(columnToTry), errorMessage); - DataTestUtility.AssertThrowsWrapper(() => reader.GetFieldValue(columnToTry), errorMessage); - DataTestUtility.AssertThrowsWrapper(() => reader.GetFieldValue(columnToTry), errorMessage); - DataTestUtility.AssertThrowsWrapper(() => reader.GetFieldValueAsync(columnToTry).Wait(), innerExceptionMessage: errorMessage); - DataTestUtility.AssertThrowsWrapper(() => reader.GetFieldValueAsync(columnToTry).Wait(), innerExceptionMessage: errorMessage); + DataTestUtility.AssertThrows(() => reader.GetInt32(columnToTry), errorMessage); + DataTestUtility.AssertThrows(() => reader.GetValue(columnToTry), errorMessage); + DataTestUtility.AssertThrows(() => reader.GetFieldValue(columnToTry), errorMessage); + DataTestUtility.AssertThrows(() => reader.GetFieldValue(columnToTry), errorMessage); + DataTestUtility.AssertThrowsInner(() => reader.GetFieldValueAsync(columnToTry).Wait(), innerExceptionMessage: errorMessage); + DataTestUtility.AssertThrowsInner(() => reader.GetFieldValueAsync(columnToTry).Wait(), innerExceptionMessage: errorMessage); reader.Read(); columnToTry = 17; @@ -1163,12 +1164,12 @@ private static void SequentialAccess(string connectionString) s = reader.GetString(columnToTry); DataTestUtility.AssertEqualsWithDescription("http://accweb/emmployees/fuller.bmp", s, "FAILED: Did not receive expected string."); - DataTestUtility.AssertThrowsWrapper(() => reader.GetInt32(columnToTry), errorMessage); - DataTestUtility.AssertThrowsWrapper(() => reader.GetValue(columnToTry), errorMessage); - DataTestUtility.AssertThrowsWrapper(() => reader.GetFieldValue(columnToTry), errorMessage); - DataTestUtility.AssertThrowsWrapper(() => reader.GetFieldValue(columnToTry), errorMessage); - DataTestUtility.AssertThrowsWrapper(() => reader.GetFieldValueAsync(columnToTry).Wait(), innerExceptionMessage: errorMessage); - DataTestUtility.AssertThrowsWrapper(() => reader.GetFieldValueAsync(columnToTry).Wait(), innerExceptionMessage: errorMessage); + DataTestUtility.AssertThrows(() => reader.GetInt32(columnToTry), errorMessage); + DataTestUtility.AssertThrows(() => reader.GetValue(columnToTry), errorMessage); + DataTestUtility.AssertThrows(() => reader.GetFieldValue(columnToTry), errorMessage); + DataTestUtility.AssertThrows(() => reader.GetFieldValue(columnToTry), errorMessage); + DataTestUtility.AssertThrowsInner(() => reader.GetFieldValueAsync(columnToTry).Wait(), innerExceptionMessage: errorMessage); + DataTestUtility.AssertThrowsInner(() => reader.GetFieldValueAsync(columnToTry).Wait(), innerExceptionMessage: errorMessage); reader.Read(); // skip all columns up to photo, and read from it partially @@ -1189,14 +1190,14 @@ private static void SequentialAccess(string connectionString) // now try to read one more byte string errorMessage = string.Format(SystemDataResourceManager.Instance.ADP_DataReaderClosed, "GetBytes"); - DataTestUtility.AssertThrowsWrapper(() => cb = reader.GetBytes(i, 51, data, 0, 1), errorMessage); + DataTestUtility.AssertThrows(() => cb = reader.GetBytes(i, 51, data, 0, 1), errorMessage); errorMessage = string.Format(SystemDataResourceManager.Instance.ADP_DataReaderClosed, "CheckDataIsReady"); - DataTestUtility.AssertThrowsWrapper(() => reader.GetValue(i), errorMessage); - DataTestUtility.AssertThrowsWrapper(() => reader.GetFieldValue(i), errorMessage); - DataTestUtility.AssertThrowsWrapper(() => reader.GetFieldValue(i), errorMessage); + DataTestUtility.AssertThrows(() => reader.GetValue(i), errorMessage); + DataTestUtility.AssertThrows(() => reader.GetFieldValue(i), errorMessage); + DataTestUtility.AssertThrows(() => reader.GetFieldValue(i), errorMessage); errorMessage = string.Format(SystemDataResourceManager.Instance.ADP_DataReaderClosed, "GetFieldValueAsync"); - DataTestUtility.AssertThrowsWrapper(() => reader.GetFieldValueAsync(i).Wait(), innerExceptionMessage: errorMessage); - DataTestUtility.AssertThrowsWrapper(() => reader.GetFieldValueAsync(i).Wait(), innerExceptionMessage: errorMessage); + DataTestUtility.AssertThrowsInnerWithAlternate(() => reader.GetFieldValueAsync(i).Wait(), innerExceptionMessage: errorMessage); + DataTestUtility.AssertThrowsInnerWithAlternate(() => reader.GetFieldValueAsync(i).Wait(), innerExceptionMessage: errorMessage); } } } @@ -1236,10 +1237,10 @@ private static void NumericRead(string connectionString) // Em object value; string errorMessage = SystemDataResourceManager.Instance.SqlMisc_ConversionOverflowMessage; - DataTestUtility.AssertThrowsWrapper(() => value = reader[0], errorMessage); - DataTestUtility.AssertThrowsWrapper(() => value = reader[1], errorMessage); - DataTestUtility.AssertThrowsWrapper(() => value = reader.GetDecimal(0), errorMessage); - DataTestUtility.AssertThrowsWrapper(() => value = reader.GetDecimal(1), errorMessage); + DataTestUtility.AssertThrows(() => value = reader[0], errorMessage); + DataTestUtility.AssertThrows(() => value = reader[1], errorMessage); + DataTestUtility.AssertThrows(() => value = reader.GetDecimal(0), errorMessage); + DataTestUtility.AssertThrows(() => value = reader.GetDecimal(1), errorMessage); } } finally @@ -1297,7 +1298,7 @@ private static void HasRowsTest(string connectionString) bool result; string errorMessage = string.Format(SystemDataResourceManager.Instance.ADP_DataReaderClosed, "HasRows"); - DataTestUtility.AssertThrowsWrapper(() => result = reader.HasRows, errorMessage); + DataTestUtility.AssertThrows(() => result = reader.HasRows, errorMessage); } } } @@ -1382,7 +1383,7 @@ private static void SeqAccessFailureWrapper(Action action, CommandBe { if (behavior == CommandBehavior.SequentialAccess) { - DataTestUtility.AssertThrowsWrapper(action); + DataTestUtility.AssertThrows(action); } else { @@ -1390,6 +1391,24 @@ private static void SeqAccessFailureWrapper(Action action, CommandBe } } + /// + /// Waits for a task that may or may not throw due to a race condition, and if they do + /// throw, they may throw one of a few acceptable exceptions. + /// + /// See: https://github.com/dotnet/SqlClient/issues/4088 + /// + private static void WaitIgnoringFlakyException(Task task) + { + try { task.Wait(); } + catch (AggregateException) + { + // A faulted Task stores its exception permanently. Calling .Wait() again is + // guaranteed to re-throw the same AggregateException, so we can safely pass it to + // the assert helper for inner-exception validation. + DataTestUtility.AssertThrowsInnerWithAlternate(() => task.Wait()); + } + } + private static void GetStream(string connectionString) { using (SqlConnection connection = new SqlConnection(connectionString)) @@ -1410,7 +1429,7 @@ private static void GetStream(string connectionString) reader.GetStream(1); // Bad values - DataTestUtility.AssertThrowsWrapper(() => reader.GetStream(2)); + DataTestUtility.AssertThrows(() => reader.GetStream(2)); // Null stream Stream stream = reader.GetStream(3); Assert.False(stream.Read(buffer, 0, buffer.Length) > 0, "FAILED: Read more than 0 bytes from a null stream"); @@ -1446,12 +1465,12 @@ private static void GetStream(string connectionString) { t = reader.ReadAsync(); Assert.False(t.Wait(1), "FAILED: Read completed immediately"); - DataTestUtility.AssertThrowsWrapper(() => reader.GetStream(8)); + DataTestUtility.AssertThrows(() => reader.GetStream(8)); } - DataTestUtility.AssertThrowsWrapper(() => t.Wait()); + WaitIgnoringFlakyException(t); - // GetStream after Read - DataTestUtility.AssertThrowsWrapper(() => reader.GetStream(0)); + // GetStream after Read + DataTestUtility.AssertThrows(() => reader.GetStream(0)); #endif } @@ -1486,8 +1505,7 @@ private static void GetStream(string connectionString) Assert.True(t.IsCompleted, "FAILED: Failed to get stream within 1 second"); t = reader.ReadAsync(); } - // TODO(GH-3604): Fix this failing assertion. - // DataTestUtility.AssertThrowsWrapper(() => t.Wait()); + WaitIgnoringFlakyException(t); } #endif } @@ -1523,7 +1541,7 @@ private static void GetTextReader(string connectionString) reader.GetTextReader(1); // Bad values - DataTestUtility.AssertThrowsWrapper(() => reader.GetTextReader(2)); + DataTestUtility.AssertThrows(() => reader.GetTextReader(2)); // Null stream TextReader textReader = reader.GetTextReader(3); Assert.False(textReader.Read(buffer, 0, buffer.Length) > 0, "FAILED: Read more than 0 chars from a null TextReader"); @@ -1559,13 +1577,12 @@ private static void GetTextReader(string connectionString) { t = reader.ReadAsync(); Assert.False(t.IsCompleted, "FAILED: Read completed immediately"); - DataTestUtility.AssertThrowsWrapper(() => reader.GetTextReader(8)); + DataTestUtility.AssertThrows(() => reader.GetTextReader(8)); } - // TODO(GH-3604): Fix this failing assertion. - // DataTestUtility.AssertThrowsWrapper(() => t.Wait()); + WaitIgnoringFlakyException(t); - // GetTextReader after Read - DataTestUtility.AssertThrowsWrapper(() => reader.GetTextReader(0)); + // GetTextReader after Read + DataTestUtility.AssertThrows(() => reader.GetTextReader(0)); #endif } @@ -1601,8 +1618,7 @@ private static void GetTextReader(string connectionString) Assert.True(t.IsCompleted, "FAILED: Failed to get TextReader within 1 second"); t = reader.ReadAsync(); } - // TODO(GH-3604): Fix this failing assertion. - // DataTestUtility.AssertThrowsWrapper(() => t.Wait()); + WaitIgnoringFlakyException(t); } #endif } @@ -1631,7 +1647,7 @@ private static void GetXmlReader(string connectionString) reader.GetXmlReader(1); // Bad values - DataTestUtility.AssertThrowsWrapper(() => reader.GetXmlReader(2)); + DataTestUtility.AssertThrows(() => reader.GetXmlReader(2)); // Null stream XmlReader xmlReader = reader.GetXmlReader(3); Assert.False(xmlReader.Read(), "FAILED: Successfully read on a null XmlReader"); @@ -1651,13 +1667,12 @@ private static void GetXmlReader(string connectionString) { t = reader.ReadAsync(); Assert.False(t.IsCompleted, "FAILED: Read completed immediately"); - DataTestUtility.AssertThrowsWrapper(() => reader.GetXmlReader(6)); + DataTestUtility.AssertThrows(() => reader.GetXmlReader(6)); } - // TODO(GH-3604): Fix this failing assertion. - // DataTestUtility.AssertThrowsWrapper(() => t.Wait()); + WaitIgnoringFlakyException(t); - // GetXmlReader after Read - DataTestUtility.AssertThrowsWrapper(() => reader.GetXmlReader(0)); + // GetXmlReader after Read + DataTestUtility.AssertThrows(() => reader.GetXmlReader(0)); #endif } } @@ -1691,16 +1706,16 @@ private static void ReadStream(string connectionString) // Testing stream properties stream.Flush(); - DataTestUtility.AssertThrowsWrapper(() => stream.SetLength(1)); + DataTestUtility.AssertThrows(() => stream.SetLength(1)); Action performOnStream = ((s) => { int i = s.WriteTimeout; }); - DataTestUtility.AssertThrowsWrapper(() => performOnStream(stream)); + DataTestUtility.AssertThrows(() => performOnStream(stream)); if (behavior == CommandBehavior.SequentialAccess) { - DataTestUtility.AssertThrowsWrapper(() => stream.Seek(0, SeekOrigin.Begin)); + DataTestUtility.AssertThrows(() => stream.Seek(0, SeekOrigin.Begin)); performOnStream = ((s) => { long i = s.Position; }); - DataTestUtility.AssertThrowsWrapper(() => performOnStream(stream)); + DataTestUtility.AssertThrows(() => performOnStream(stream)); performOnStream = ((s) => { long i = s.Length; }); - DataTestUtility.AssertThrowsWrapper(() => performOnStream(stream)); + DataTestUtility.AssertThrows(() => performOnStream(stream)); } else { @@ -1711,7 +1726,7 @@ private static void ReadStream(string connectionString) } // Once Stream is closed - DataTestUtility.AssertThrowsWrapper(() => { _ = stream.Read(buffer, 0, buffer.Length); }); + DataTestUtility.AssertThrows(() => { _ = stream.Read(buffer, 0, buffer.Length); }); } using (SqlDataReader reader = cmd.ExecuteReader(behavior)) @@ -1723,9 +1738,9 @@ private static void ReadStream(string connectionString) _ = stream.Read(buffer, 0, buffer.Length); // Argument exceptions - DataTestUtility.AssertThrowsWrapper(() => { _ = stream.Read(null, 0, 1); }); - DataTestUtility.AssertThrowsWrapper(() => { _ = stream.Read(buffer, -1, 2); }); - DataTestUtility.AssertThrowsWrapper(() => { _ = stream.Read(buffer, 2, -1); }); + DataTestUtility.AssertThrows(() => { _ = stream.Read(null, 0, 1); }); + DataTestUtility.AssertThrows(() => { _ = stream.Read(buffer, -1, 2); }); + DataTestUtility.AssertThrows(() => { _ = stream.Read(buffer, 2, -1); }); // Prior to net6 comment:ArgumentException is thrown in net5 and earlier. ArgumentOutOfRangeException in net6 and later ArgumentException ex = Assert.ThrowsAny(() => { _ = stream.Read(buffer, buffer.Length, buffer.Length); }); @@ -1777,10 +1792,10 @@ private static void ReadStream(string connectionString) { // Read during async t = stream.ReadAsync(largeBuffer, 0, largeBuffer.Length); - DataTestUtility.AssertThrowsWrapper(() => { _ = stream.Read(largeBuffer, 0, largeBuffer.Length); }); - DataTestUtility.AssertThrowsWrapper(() => reader.Read()); + DataTestUtility.AssertThrows(() => { _ = stream.Read(largeBuffer, 0, largeBuffer.Length); }); + DataTestUtility.AssertThrows(() => reader.Read()); } - DataTestUtility.AssertThrowsWrapper(() => t.Wait()); + DataTestUtility.AssertThrowsInnerWithAlternate(() => t.Wait()); } using (SqlDataReader reader = cmd.ExecuteReader(behavior)) { @@ -1796,7 +1811,7 @@ private static void ReadStream(string connectionString) // Guarantee that timeout occurs: Thread.Sleep(stream.ReadTimeout * 4); } - DataTestUtility.AssertThrowsWrapper(() => t.Wait()); + DataTestUtility.AssertThrowsInnerWithAlternate(() => t.Wait()); } using (SqlDataReader reader = cmd.ExecuteReader(behavior)) @@ -1811,7 +1826,10 @@ private static void ReadStream(string connectionString) t = stream.ReadAsync(largeBuffer, 0, largeBuffer.Length, tokenSource.Token); tokenSource.Cancel(); } - DataTestUtility.AssertThrowsWrapper(() => t.Wait()); + // Normally the cancellation wins (TaskCanceledException), but if the + // PendAsyncReadsScope disposal completes the read first, the inner + // exception may be InvalidOperationException instead (GH-4088). + DataTestUtility.AssertThrowsInnerWithAlternate(() => t.Wait()); } using (SqlDataReader reader = cmd.ExecuteReader(behavior)) @@ -1824,7 +1842,18 @@ private static void ReadStream(string connectionString) // Error during read t = stream.ReadAsync(largeBuffer, 0, largeBuffer.Length); } - DataTestUtility.AssertThrowsWrapper(() => t.Wait()); + // PendAsyncReadsScope(errorCode: 11) injects a network error, normally producing + // AggregateException -> IOException -> SqlException. In rare race conditions + // the inner exception may be ObjectDisposedException instead (GH-4088). + AggregateException aex = Assert.Throws(() => t.Wait()); + if (aex.InnerException is IOException ioEx) + { + Assert.IsAssignableFrom(ioEx.InnerException); + } + else + { + Assert.IsAssignableFrom(aex.InnerException); + } } #endif } @@ -1876,7 +1905,7 @@ private static void ReadTextReader(string connectionString) } // Once Reader is closed - DataTestUtility.AssertThrowsWrapper(() => textReader.Read(buffer, 0, buffer.Length)); + DataTestUtility.AssertThrows(() => textReader.Read(buffer, 0, buffer.Length)); } using (SqlDataReader reader = cmd.ExecuteReader(behavior)) @@ -1890,11 +1919,11 @@ private static void ReadTextReader(string connectionString) textReader.Peek(); // Argument exceptions - DataTestUtility.AssertThrowsWrapper(() => textReader.Read(null, 0, 1)); - DataTestUtility.AssertThrowsWrapper(() => textReader.Read(buffer, -1, 2)); - DataTestUtility.AssertThrowsWrapper(() => textReader.Read(buffer, 2, -1)); - DataTestUtility.AssertThrowsWrapper(() => textReader.Read(buffer, buffer.Length, buffer.Length)); - DataTestUtility.AssertThrowsWrapper(() => textReader.Read(buffer, int.MaxValue, int.MaxValue)); + DataTestUtility.AssertThrows(() => textReader.Read(null, 0, 1)); + DataTestUtility.AssertThrows(() => textReader.Read(buffer, -1, 2)); + DataTestUtility.AssertThrows(() => textReader.Read(buffer, 2, -1)); + DataTestUtility.AssertThrows(() => textReader.Read(buffer, buffer.Length, buffer.Length)); + DataTestUtility.AssertThrows(() => textReader.Read(buffer, int.MaxValue, int.MaxValue)); } // Once Reader is closed @@ -1936,11 +1965,10 @@ private static void ReadTextReader(string connectionString) { // Read during async t = textReader.ReadAsync(largeBuffer, 0, largeBuffer.Length); - DataTestUtility.AssertThrowsWrapper(() => textReader.Read(largeBuffer, 0, largeBuffer.Length)); - DataTestUtility.AssertThrowsWrapper(() => reader.Read()); + DataTestUtility.AssertThrows(() => textReader.Read(largeBuffer, 0, largeBuffer.Length)); + DataTestUtility.AssertThrows(() => reader.Read()); } - // TODO(GH-3604): Fix this failing assertion. - // DataTestUtility.AssertThrowsWrapper(() => t.Wait()); + WaitIgnoringFlakyException(t); } using (SqlDataReader reader = cmd.ExecuteReader(behavior)) @@ -1953,7 +1981,18 @@ private static void ReadTextReader(string connectionString) // Error during read t = textReader.ReadAsync(largeBuffer, 0, largeBuffer.Length); } - DataTestUtility.AssertThrowsWrapper(() => t.Wait()); + // PendAsyncReadsScope(errorCode: 11) injects a network error, normally producing + // AggregateException -> IOException -> SqlException. In rare race conditions + // the inner exception may be ObjectDisposedException instead (GH-4088). + AggregateException aex = Assert.Throws(() => t.Wait()); + if (aex.InnerException is IOException ioEx) + { + Assert.IsAssignableFrom(ioEx.InnerException); + } + else + { + Assert.IsAssignableFrom(aex.InnerException); + } } #endif } @@ -2118,7 +2157,7 @@ private void TestXEventsStreaming(string connectionString) byte[] bytes = new byte[cb]; long read = reader.GetBytes(1, 0, bytes, 0, cb); - // Don't send data on the first read because there is already data in the buffer. + // Don't send data on the first read because there is already data in the buffer. // Don't send data on the last iteration. We will not be reading that data. if (i == 0 || i == streamXeventCount - 1) { @@ -2164,7 +2203,7 @@ private static void TimeoutDuringReadAsyncWithClosedReaderTest(string connection // Wait for the task to see the timeout string errorMessage = SystemDataResourceManager.Instance.SQL_Timeout_Execution; - DataTestUtility.AssertThrowsWrapper(() => task.Wait(), innerExceptionMessage: errorMessage); + DataTestUtility.AssertThrowsInnerWithAlternate(() => task.Wait(), innerExceptionMessage: errorMessage); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/LocalDBTest/LocalDBTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/LocalDBTest/LocalDBTest.cs index ad84ac7881..c3aed471c0 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/LocalDBTest/LocalDBTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/LocalDBTest/LocalDBTest.cs @@ -54,7 +54,7 @@ public static void LocalDBMarsTest() public static void InvalidLocalDBTest() { using var connection = new SqlConnection(s_badConnectionString); - DataTestUtility.AssertThrowsWrapper(() => connection.Open()); + DataTestUtility.AssertThrows(() => connection.Open()); } #endregion diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSTest/MARSTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSTest/MARSTest.cs index 69265c4e75..1213c5bfa4 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSTest/MARSTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSTest/MARSTest.cs @@ -591,7 +591,7 @@ public static void MARSMultiDataReaderErrTest() { using (SqlDataReader reader1 = command.ExecuteReader()) { - DataTestUtility.AssertThrowsWrapper(() => + DataTestUtility.AssertThrows(() => { SqlDataReader reader2 = command.ExecuteReader(); }, openReaderExistsMessage); @@ -611,7 +611,7 @@ public static void MARSMultiDataReaderErrTest() { using (SqlDataReader reader1 = command1.ExecuteReader()) { - DataTestUtility.AssertThrowsWrapper(() => + DataTestUtility.AssertThrows(() => { SqlDataReader reader2 = command2.ExecuteReader(); }, openReaderExistsMessage); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParallelTransactionsTest/ParallelTransactionsTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParallelTransactionsTest/ParallelTransactionsTest.cs index efc99ea757..73fbd8bdc6 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParallelTransactionsTest/ParallelTransactionsTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParallelTransactionsTest/ParallelTransactionsTest.cs @@ -20,7 +20,7 @@ public static void BasicParallelTest_shouldThrowsUnsupported() try { tempTableName = CreateTempTable(connectionString); - DataTestUtility.AssertThrowsWrapper( + DataTestUtility.AssertThrows( actionThatFails: () => { BasicParallelTest(connectionString, tempTableName); }, exceptionMessage: expectedErrorMessage); } @@ -77,7 +77,7 @@ public static void MultipleExecutesInSameTransactionTest_shouldThrowsUnsupported try { tempTableName = CreateTempTable(connectionString); - DataTestUtility.AssertThrowsWrapper( + DataTestUtility.AssertThrows( actionThatFails: () => { MultipleExecutesInSameTransactionTest(connectionString, tempTableName); }, exceptionMessage: expectedErrorMessage); } @@ -157,5 +157,3 @@ private static void DropTempTable(string connectionString, string tempTableName) } } } - - 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 a4cd63b0c1..d53209e912 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs @@ -42,14 +42,14 @@ public static void CodeCoverageSqlClient() { string failValue; - DataTestUtility.AssertThrowsWrapper(() => failValue = opc[0].ParameterName, "Invalid index 0 for this SqlParameterCollection with Count=0."); + DataTestUtility.AssertThrows(() => failValue = opc[0].ParameterName, "Invalid index 0 for this SqlParameterCollection with Count=0."); - DataTestUtility.AssertThrowsWrapper(() => failValue = opc["@p1"].ParameterName, "A SqlParameter with ParameterName '@p1' is not contained by this SqlParameterCollection."); + DataTestUtility.AssertThrows(() => failValue = opc["@p1"].ParameterName, "A SqlParameter with ParameterName '@p1' is not contained by this SqlParameterCollection."); - DataTestUtility.AssertThrowsWrapper(() => opc["@p1"] = null, "A SqlParameter with ParameterName '@p1' is not contained by this SqlParameterCollection."); + DataTestUtility.AssertThrows(() => opc["@p1"] = null, "A SqlParameter with ParameterName '@p1' is not contained by this SqlParameterCollection."); } - DataTestUtility.AssertThrowsWrapper(() => opc.Add(null), "The SqlParameterCollection only accepts non-null SqlParameter type objects."); + DataTestUtility.AssertThrows(() => opc.Add(null), "The SqlParameterCollection only accepts non-null SqlParameter type objects."); opc.Add((object)new SqlParameter()); IEnumerator enm = opc.GetEnumerator(); @@ -70,11 +70,11 @@ public static void CodeCoverageSqlClient() SqlParameter p = opc[0]; - DataTestUtility.AssertThrowsWrapper(() => opc.Add((object)p), "The SqlParameter is already contained by another SqlParameterCollection."); + DataTestUtility.AssertThrows(() => opc.Add((object)p), "The SqlParameter is already contained by another SqlParameterCollection."); - DataTestUtility.AssertThrowsWrapper(() => new SqlCommand().Parameters.Add(p), "The SqlParameter is already contained by another SqlParameterCollection."); + DataTestUtility.AssertThrows(() => new SqlCommand().Parameters.Add(p), "The SqlParameter is already contained by another SqlParameterCollection."); - DataTestUtility.AssertThrowsWrapper(() => opc.Remove(null), "The SqlParameterCollection only accepts non-null SqlParameter type objects."); + DataTestUtility.AssertThrows(() => opc.Remove(null), "The SqlParameterCollection only accepts non-null SqlParameter type objects."); string pname = p.ParameterName; p.ParameterName = pname; @@ -106,13 +106,13 @@ public static void CodeCoverageSqlClient() new SqlCommand().Parameters.CopyTo(new object[0], 0); Assert.False(new SqlCommand().Parameters.GetEnumerator().MoveNext(), "FAILED: Expected MoveNext to be false"); - DataTestUtility.AssertThrowsWrapper(() => new SqlCommand().Parameters.Add(0), "The SqlParameterCollection only accepts non-null Microsoft.Data.SqlClient.SqlParameter type objects, not System.Int32 objects."); + DataTestUtility.AssertThrows(() => new SqlCommand().Parameters.Add(0), "The SqlParameterCollection only accepts non-null Microsoft.Data.SqlClient.SqlParameter type objects, not System.Int32 objects."); - DataTestUtility.AssertThrowsWrapper(() => new SqlCommand().Parameters.Insert(0, 0), "The SqlParameterCollection only accepts non-null Microsoft.Data.SqlClient.SqlParameter type objects, not System.Int32 objects."); + DataTestUtility.AssertThrows(() => new SqlCommand().Parameters.Insert(0, 0), "The SqlParameterCollection only accepts non-null Microsoft.Data.SqlClient.SqlParameter type objects, not System.Int32 objects."); - DataTestUtility.AssertThrowsWrapper(() => new SqlCommand().Parameters.Remove(0), "The SqlParameterCollection only accepts non-null Microsoft.Data.SqlClient.SqlParameter type objects, not System.Int32 objects."); + DataTestUtility.AssertThrows(() => new SqlCommand().Parameters.Remove(0), "The SqlParameterCollection only accepts non-null Microsoft.Data.SqlClient.SqlParameter type objects, not System.Int32 objects."); - DataTestUtility.AssertThrowsWrapper(() => new SqlCommand().Parameters.Remove(new SqlParameter()), "Attempted to remove an SqlParameter that is not contained by this SqlParameterCollection."); + DataTestUtility.AssertThrows(() => new SqlCommand().Parameters.Remove(new SqlParameter()), "Attempted to remove an SqlParameter that is not contained by this SqlParameterCollection."); } // TODO Synapse: Parse error at line: 1, column: 12: Incorrect syntax near 'IF'. diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs index 5ba727be5d..a0ccf69301 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs @@ -61,9 +61,9 @@ public static void Test(string srcConstr, string dstConstr, string dstTable) DataTestUtility.AssertEqualsWithDescription((long)3, stats["BuffersReceived"], "Unexpected BuffersReceived value."); DataTestUtility.AssertEqualsWithDescription((long)3, stats["BuffersSent"], "Unexpected BuffersSent value."); DataTestUtility.AssertEqualsWithDescription((long)0, stats["IduCount"], "Unexpected IduCount value."); - DataTestUtility.AssertEqualsWithDescription((long)6, stats["SelectCount"], "Unexpected SelectCount value."); + DataTestUtility.AssertEqualsWithDescription((long)11, stats["SelectCount"], "Unexpected SelectCount value."); DataTestUtility.AssertEqualsWithDescription((long)3, stats["ServerRoundtrips"], "Unexpected ServerRoundtrips value."); - DataTestUtility.AssertEqualsWithDescription((long)9, stats["SelectRows"], "Unexpected SelectRows value."); + DataTestUtility.AssertEqualsWithDescription((long)14, stats["SelectRows"], "Unexpected SelectRows value."); DataTestUtility.AssertEqualsWithDescription((long)2, stats["SumResultSets"], "Unexpected SumResultSets value."); DataTestUtility.AssertEqualsWithDescription((long)0, stats["Transactions"], "Unexpected Transactions value."); } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReaderCancelAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReaderCancelAsync.cs index 3ae79dbe31..3fb3d051a1 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReaderCancelAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReaderCancelAsync.cs @@ -19,7 +19,7 @@ public static void Test(string srcConstr, string dstConstr, string dstTable) cts = new CancellationTokenSource(); cts.Cancel(); Task t = TestAsync(srcConstr, dstConstr, dstTable, cts.Token); - DataTestUtility.AssertThrowsWrapper(() => t.Wait()); + DataTestUtility.AssertThrowsInner(() => t.Wait()); Assert.True(t.IsCompleted, "Task did not complete! Status: " + t.Status); } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReaderConnectionCloseAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReaderConnectionCloseAsync.cs index fd6062659a..ede16437d3 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReaderConnectionCloseAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReaderConnectionCloseAsync.cs @@ -14,7 +14,7 @@ public class CopyAllFromReaderConnectionClosedAsync public static void Test(string srcConstr, string dstConstr, string dstTable) { Task t = TestAsync(srcConstr, dstConstr, dstTable); - DataTestUtility.AssertThrowsWrapper(() => t.Wait()); + DataTestUtility.AssertThrowsInner(() => t.Wait()); Assert.True(t.IsCompleted, "Task did not complete! Status: " + t.Status); } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReaderConnectionCloseOnEventAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReaderConnectionCloseOnEventAsync.cs index 1afd2d93d0..3d832925a6 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReaderConnectionCloseOnEventAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReaderConnectionCloseOnEventAsync.cs @@ -4,6 +4,7 @@ using System; using System.Data.Common; +using System.IO; namespace Microsoft.Data.SqlClient.ManualTesting.Tests { @@ -49,7 +50,7 @@ public static void Test(string srcConstr, string dstConstr, string dstTable) // Check that the copying fails string message = string.Format(SystemDataResourceManager.Instance.ADP_OpenConnectionRequired, "WriteToServer", SystemDataResourceManager.Instance.ADP_ConnectionStateMsg_Closed); - DataTestUtility.AssertThrowsWrapper(() => bulkcopy.WriteToServerAsync(reader).Wait(5000), innerExceptionMessage: message); + DataTestUtility.AssertThrowsInnerWithAlternate(() => bulkcopy.WriteToServerAsync(reader).Wait(5000), innerExceptionMessage: message); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyWithEvent1.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyWithEvent1.cs index e89f2f83a3..8bc72435d0 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyWithEvent1.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyWithEvent1.cs @@ -66,7 +66,7 @@ public static void Test(string srcConstr, string dstConstr, string dstTable) ColumnMappings.Add("ShipName", "shipname"); bulkcopy.NotifyAfter = 3; - DataTestUtility.AssertThrowsWrapper(() => bulkcopy.WriteToServer(reader)); + DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader)); bulkcopy.SqlRowsCopied -= new SqlRowsCopiedEventHandler(OnRowCopied); bulkcopy.Close(); } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/MissingTargetColumn.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/MissingTargetColumn.cs index d8fe8f66da..e4df366137 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/MissingTargetColumn.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/MissingTargetColumn.cs @@ -38,7 +38,7 @@ public static void Test(string srcConstr, string dstConstr, string dstTable) string errorMsg = SystemDataResourceManager.Instance.SQL_BulkLoadNonMatchingColumnName; errorMsg = string.Format(errorMsg, "col2"); - DataTestUtility.AssertThrowsWrapper(() => bulkcopy.WriteToServer(reader), exceptionMessage: errorMsg); + DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader), exceptionMessage: errorMsg); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/MissingTargetColumns.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/MissingTargetColumns.cs index 2696770f69..5fb47f44c1 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/MissingTargetColumns.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/MissingTargetColumns.cs @@ -38,7 +38,7 @@ public static void Test(string srcConstr, string dstConstr, string dstTable) string errorMsg = SystemDataResourceManager.Instance.SQL_BulkLoadNonMatchingColumnName; errorMsg = string.Format(errorMsg, "col3,col4"); - DataTestUtility.AssertThrowsWrapper(() => bulkcopy.WriteToServer(reader), exceptionMessage: errorMsg); + DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader), exceptionMessage: errorMsg); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintDuplicateColumn.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintDuplicateColumn.cs index 9e535ccae1..ebed5e9918 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintDuplicateColumn.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintDuplicateColumn.cs @@ -45,7 +45,7 @@ public static void Test(string connStr, string dstTable) string expectedErrorMsg = string.Format( SystemDataResourceManager.Instance.SQL_BulkLoadOrderHintDuplicateColumn, destColumn); - DataTestUtility.AssertThrowsWrapper( + DataTestUtility.AssertThrows( () => bulkcopy.ColumnOrderHints.Add(destColumn, SortOrder.Ascending), exceptionMessage: expectedErrorMsg); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintMissingTargetColumn.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintMissingTargetColumn.cs index 46a7f39eee..cc486e9e4e 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintMissingTargetColumn.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintMissingTargetColumn.cs @@ -44,7 +44,7 @@ public static void Test(string connStr, string dstTable) string expectedErrorMsg = string.Format( SystemDataResourceManager.Instance.SQL_BulkLoadOrderHintInvalidColumn, nonexistentColumn); - DataTestUtility.AssertThrowsWrapper( + DataTestUtility.AssertThrows( () => bulkcopy.WriteToServer(reader), exceptionMessage: expectedErrorMsg); @@ -56,7 +56,7 @@ public static void Test(string connStr, string dstTable) expectedErrorMsg = string.Format( SystemDataResourceManager.Instance.SQL_BulkLoadOrderHintInvalidColumn, sourceColumn); - DataTestUtility.AssertThrowsWrapper( + DataTestUtility.AssertThrows( () => bulkcopy.WriteToServer(reader), exceptionMessage: expectedErrorMsg); } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/SqlGraphTables.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/SqlGraphTables.cs index d83693080f..837ce96e76 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/SqlGraphTables.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/SqlGraphTables.cs @@ -11,7 +11,7 @@ namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SqlBulkCopyTests { public class SqlGraphTables { - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsAtLeastSQL2017))] public void WriteToServer_CopyToSqlGraphNodeTable_Succeeds() { string connectionString = DataTestUtility.TCPConnectionString; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/TestBulkCopyWithUTF8.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/TestBulkCopyWithUTF8.cs index 5b7112476d..da5287e831 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/TestBulkCopyWithUTF8.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/TestBulkCopyWithUTF8.cs @@ -26,6 +26,13 @@ public sealed class TestBulkCopyWithUtf8 : IDisposable /// public TestBulkCopyWithUtf8() { + // xUnit instantiates the class even when ConditionalTheory conditions cause the + // test to be skipped, so we must guard setup that requires UTF-8 collations. + if (!DataTestUtility.IsAtLeastSQL2019()) + { + return; + } + using SqlConnection sourceConnection = new SqlConnection(GetConnectionString(true)); sourceConnection.Open(); SetupTables(sourceConnection, s_sourceTable, s_destinationTable, s_insertQuery); @@ -36,6 +43,12 @@ public TestBulkCopyWithUtf8() /// public void Dispose() { + // Guard matches the constructor: no tables were created on older SQL versions. + if (!DataTestUtility.IsAtLeastSQL2019()) + { + return; + } + using SqlConnection connection = new SqlConnection(GetConnectionString(true)); connection.Open(); DataTestUtility.DropTable(connection, s_sourceTable); @@ -56,7 +69,7 @@ private string GetConnectionString(bool enableMars) /// /// Creates source and destination tables with a varchar(max) column with a collation setting - /// that stores the data in UTF8 encoding and inserts the data in the source table. + /// that stores the data in UTF8 encoding and inserts the data in the source table. /// private void SetupTables(SqlConnection connection, string sourceTable, string destinationTable, string insertQuery) { @@ -75,7 +88,8 @@ private void SetupTables(SqlConnection connection, string sourceTable, string de [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer), - nameof(DataTestUtility.IsNotAzureSynapse))] + nameof(DataTestUtility.IsNotAzureSynapse), + nameof(DataTestUtility.IsAtLeastSQL2019))] [InlineData(true, true)] [InlineData(false, true)] [InlineData(true, false)] @@ -139,7 +153,8 @@ public void BulkCopy_Utf8Data_ShouldMatchSource(bool isMarsEnabled, bool enableS [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer), - nameof(DataTestUtility.IsNotAzureSynapse))] + nameof(DataTestUtility.IsNotAzureSynapse), + nameof(DataTestUtility.IsAtLeastSQL2019))] [InlineData(true, true)] [InlineData(false, true)] [InlineData(true, false)] diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/Transaction.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/Transaction.cs index e7632044b0..0738f32010 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/Transaction.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/Transaction.cs @@ -34,7 +34,7 @@ public static void Test(string srcConstr, string dstConstr, string dstTable) SqlTransaction myTrans = dstConn.BeginTransaction(); try { - DataTestUtility.AssertThrowsWrapper(() => bulkcopy.WriteToServer(reader)); + DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader)); } finally { diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/Transaction1.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/Transaction1.cs index 1f6ba5e823..930e1ac6f5 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/Transaction1.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/Transaction1.cs @@ -37,7 +37,7 @@ public static void Test(string srcConstr, string dstConstr, string dstTable) try { - DataTestUtility.AssertThrowsWrapper(() => bulkcopy.WriteToServer(reader)); + DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader)); } finally { diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/Transaction3.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/Transaction3.cs index 8e53989ed5..4b65f2a5d2 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/Transaction3.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/Transaction3.cs @@ -37,13 +37,13 @@ public static void Test(string srcConstr, string dstConstr, string dstTable) bulkcopy.DestinationTableName = dstTable; string exceptionMsg = SystemDataResourceManager.Instance.ADP_TransactionConnectionMismatch; - DataTestUtility.AssertThrowsWrapper(() => bulkcopy.WriteToServer(reader), exceptionMessage: exceptionMsg); + DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader), exceptionMessage: exceptionMsg); SqlCommand myCmd = dstConn.CreateCommand(); myCmd.CommandText = "select * from " + dstTable; myCmd.Transaction = myTrans; - DataTestUtility.AssertThrowsWrapper(() => myCmd.ExecuteReader(), exceptionMessage: exceptionMsg); + DataTestUtility.AssertThrows(() => myCmd.ExecuteReader(), exceptionMessage: exceptionMsg); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/Transaction4.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/Transaction4.cs index c8028ca5c5..40b2d2c251 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/Transaction4.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/Transaction4.cs @@ -31,7 +31,7 @@ public static void Test(string srcConstr, string dstConstr, string dstTable) // Start a local transaction on the wrong connection. SqlTransaction myTrans = conn3.BeginTransaction(); string errorMsg = SystemDataResourceManager.Instance.SQL_BulkLoadConflictingTransactionOption; - DataTestUtility.AssertThrowsWrapper(() => new SqlBulkCopy(dstConn, SqlBulkCopyOptions.UseInternalTransaction, myTrans), exceptionMessage: errorMsg); + DataTestUtility.AssertThrows(() => new SqlBulkCopy(dstConn, SqlBulkCopyOptions.UseInternalTransaction, myTrans), exceptionMessage: errorMsg); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/TransactionTestAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/TransactionTestAsync.cs index ac28a9d883..729dac74a9 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/TransactionTestAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/TransactionTestAsync.cs @@ -14,7 +14,7 @@ public class TransactionTestAsync public static void Test(string srcConstr, string dstConstr, string dstTable) { Task t = TestAsync(srcConstr, dstConstr, dstTable); - DataTestUtility.AssertThrowsWrapper(() => t.Wait()); + DataTestUtility.AssertThrowsInner(() => t.Wait()); Assert.True(t.IsCompleted, "Task did not complete! Status: " + t.Status); } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs index 97c225c9ca..552fd4cea5 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs @@ -65,7 +65,7 @@ private static void PlainCancel(string connString) using (SqlDataReader reader = cmd.ExecuteReader()) { cmd.Cancel(); - DataTestUtility.AssertThrowsWrapper( + DataTestUtility.AssertThrows( () => { do @@ -88,7 +88,7 @@ private static void PlainCancelAsync(string connString) { conn.Open(); Task readerTask = cmd.ExecuteReaderAsync(); - DataTestUtility.AssertThrowsWrapper( + DataTestUtility.AssertThrows( () => { readerTask.Wait(2000); @@ -214,7 +214,7 @@ private static void CancelFollowedByTransaction(string constr) private static void CancelFollowedByAlert(string constr) { var alertName = "myAlert" + Guid.NewGuid().ToString(); - // Since Alert conditions are randomly generated, + // Since Alert conditions are randomly generated, // we will retry on unexpected error messages to avoid collision in pipelines. var n = new Random().Next(1, 100); bool retry = true; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlNotificationTest/SqlNotificationTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlNotificationTest/SqlNotificationTest.cs index 98fa8997de..e4bd1133ac 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlNotificationTest/SqlNotificationTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlNotificationTest/SqlNotificationTest.cs @@ -71,7 +71,7 @@ public void Test_DoubleStart_DifferentConnStr() try { - DataTestUtility.AssertThrowsWrapper(() => SqlDependency.Start(cb.ToString())); + DataTestUtility.AssertThrows(() => SqlDependency.Start(cb.ToString())); } finally { @@ -116,7 +116,7 @@ public void Test_SingleDependency_NoStart() Console.WriteLine("4 Notification callback. Type={0}, Info={1}, Source={2}", args.Type, args.Info, args.Source); }; - DataTestUtility.AssertThrowsWrapper(() => cmd.ExecuteReader()); + DataTestUtility.AssertThrows(() => cmd.ExecuteReader()); } } @@ -138,7 +138,7 @@ public void Test_SingleDependency_Stopped() Console.WriteLine("5 Notification callback. Type={0}, Info={1}, Source={2}", args.Type, args.Info, args.Source); }; - DataTestUtility.AssertThrowsWrapper(() => cmd.ExecuteReader()); + DataTestUtility.AssertThrows(() => cmd.ExecuteReader()); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/TransactionTest/TransactionTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/TransactionTest/TransactionTest.cs index 1a97d6df31..0adff2c8ae 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/TransactionTest/TransactionTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/TransactionTest/TransactionTest.cs @@ -339,13 +339,13 @@ private void ExceptionTest() string executeCommandWithoutTransactionMessage = SystemDataResourceManager.Instance.ADP_TransactionRequired("ExecuteNonQuery"); string transactionConflictErrorMessage = SystemDataResourceManager.Instance.ADP_TransactionConnectionMismatch; string parallelTransactionErrorMessage = SystemDataResourceManager.Instance.ADP_ParallelTransactionsNotSupported("SqlConnection"); - DataTestUtility.AssertThrowsWrapper(() => + DataTestUtility.AssertThrows(() => { SqlCommand command = new SqlCommand("sql", connection); command.ExecuteNonQuery(); }, executeCommandWithoutTransactionMessage); - DataTestUtility.AssertThrowsWrapper(() => + DataTestUtility.AssertThrows(() => { using (SqlConnection con1 = new SqlConnection(_connectionString)) { @@ -357,32 +357,32 @@ private void ExceptionTest() } }, transactionConflictErrorMessage); - DataTestUtility.AssertThrowsWrapper(() => + DataTestUtility.AssertThrows(() => { connection.BeginTransaction(null); }, parallelTransactionErrorMessage); - DataTestUtility.AssertThrowsWrapper(() => + DataTestUtility.AssertThrows(() => { connection.BeginTransaction(""); }, parallelTransactionErrorMessage); - DataTestUtility.AssertThrowsWrapper(() => + DataTestUtility.AssertThrows(() => { tx.Rollback(null); }, invalidSaveStateMessage); - DataTestUtility.AssertThrowsWrapper(() => + DataTestUtility.AssertThrows(() => { tx.Rollback(""); }, invalidSaveStateMessage); - DataTestUtility.AssertThrowsWrapper(() => + DataTestUtility.AssertThrows(() => { tx.Save(null); }, invalidSaveStateMessage); - DataTestUtility.AssertThrowsWrapper(() => + DataTestUtility.AssertThrows(() => { tx.Save(""); }, invalidSaveStateMessage); @@ -456,7 +456,7 @@ private void ReadCommitedIsolationLevel_ShouldReceiveTimeoutExceptionBecauseItWa SqlTransaction tx2 = connection2.BeginTransaction(IsolationLevel.ReadCommitted); command2.Transaction = tx2; - DataTestUtility.AssertThrowsWrapper(() => command2.ExecuteReader(), SystemDataResourceManager.Instance.SQL_Timeout_Execution as string); + DataTestUtility.AssertThrows(() => command2.ExecuteReader(), SystemDataResourceManager.Instance.SQL_Timeout_Execution as string); tx2.Rollback(); connection2.Close(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/UdtTest2.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/UdtTest2.cs index 79f38262cc..1dae314730 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/UdtTest2.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/UdtTest/UdtTest2.cs @@ -75,7 +75,7 @@ public void UDTParams_Binary() value[7] = 0; p.Value = new SqlBinary(value); - DataTestUtility.AssertThrowsWrapper( + DataTestUtility.AssertThrows( () => cmd.ExecuteReader(), "Error converting data type varbinary to Point."); } @@ -110,7 +110,7 @@ public void UDTParams_Invalid2() p.Value = addr; pName.Value = addr; - DataTestUtility.AssertThrowsWrapper( + DataTestUtility.AssertThrows( () => cmd.ExecuteReader(), "Failed to convert parameter value from a Address to a String."); } @@ -134,7 +134,7 @@ public void UDTParams_Invalid() p.UdtTypeName = "UdtTestDb.dbo.Point"; p.Value = 32; - DataTestUtility.AssertThrowsWrapper( + DataTestUtility.AssertThrows( () => cmd.ExecuteReader(), "Specified type is not registered on the target server. System.Int32"); } @@ -221,7 +221,7 @@ public void UDTParams_NullInput() string errorMsg = "Procedure or function '" + spInsertCustomerNoBrackets + "' expects parameter '@addr', which was not supplied."; - DataTestUtility.AssertThrowsWrapper( + DataTestUtility.AssertThrows( () => cmd.ExecuteNonQuery(), errorMsg); } @@ -317,7 +317,7 @@ public void UDTFields_WrongType() reader.Read(); // retrieve the UDT as a string - DataTestUtility.AssertThrowsWrapper( + DataTestUtility.AssertThrows( () => reader.GetString(1), "Unable to cast object of type 'System.Byte[]' to type 'System.String'."); } @@ -584,12 +584,12 @@ Func create string udtError = SystemDataResourceManager.Instance.SQLUDT_MaxByteSizeValue; string errorMessage = (new ArgumentOutOfRangeException("MaxByteSize", 8001, udtError)).Message; - DataTestUtility.AssertThrowsWrapper( + DataTestUtility.AssertThrows( () => create(SqlUserDefinedAggregateAttribute.MaxByteSizeValue + 1), errorMessage); errorMessage = (new ArgumentOutOfRangeException("MaxByteSize", -2, udtError)).Message; - DataTestUtility.AssertThrowsWrapper( + DataTestUtility.AssertThrows( () => create(-2), errorMessage); } @@ -702,4 +702,3 @@ public void UDTParams_DeriveParameters_CheckAutoFixOverride() } } } - diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat32Tests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat32Tests.cs index 2ff72bba06..0a86534112 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat32Tests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat32Tests.cs @@ -29,19 +29,19 @@ public static IEnumerable GetVectorFloat32TestData() yield return new object[] { 3, new SqlVector(testData), testData, vectorColumnLength }; yield return new object[] { 4, new SqlVector(testData), testData, vectorColumnLength }; - // Pattern 1-4 with SqlVector(n) + // Pattern 1-4 with SqlVector(n) yield return new object[] { 1, SqlVector.CreateNull(vectorColumnLength), Array.Empty(), vectorColumnLength }; yield return new object[] { 2, SqlVector.CreateNull(vectorColumnLength), Array.Empty(), vectorColumnLength }; yield return new object[] { 3, SqlVector.CreateNull(vectorColumnLength), Array.Empty(), vectorColumnLength }; yield return new object[] { 4, SqlVector.CreateNull(vectorColumnLength), Array.Empty(), vectorColumnLength }; - // Pattern 1-4 with DBNull + // Pattern 1-4 with DBNull yield return new object[] { 1, DBNull.Value, Array.Empty(), vectorColumnLength }; yield return new object[] { 2, DBNull.Value, Array.Empty(), vectorColumnLength }; yield return new object[] { 3, DBNull.Value, Array.Empty(), vectorColumnLength }; yield return new object[] { 4, DBNull.Value, Array.Empty(), vectorColumnLength }; - // Pattern 1-4 with SqlVector.Null + // Pattern 1-4 with SqlVector.Null yield return new object[] { 1, SqlVector.Null, Array.Empty(), vectorColumnLength }; // Following scenario is not supported in SqlClient. @@ -561,6 +561,41 @@ public async Task TestBulkCopyFromSqlTableAsync(int bulkCopySourceMode) Assert.Equal(VectorFloat32TestData.testData.Length, vector.Length); } + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsSqlVectorSupported))] + public void TestGetFieldTypeReturnsSqlVectorForVectorColumn() + { + using var connection = new SqlConnection(s_connectionString); + connection.Open(); + + // Insert a row so we can query it + using (var insertCmd = new SqlCommand(s_insertCmdString, connection)) + { + var param = insertCmd.Parameters.Add(s_vectorParamName, SqlDbTypeExtensions.Vector); + param.Value = new SqlVector(VectorFloat32TestData.testData); + insertCmd.ExecuteNonQuery(); + } + + using var selectCmd = new SqlCommand(s_selectCmdString, connection); + using var reader = selectCmd.ExecuteReader(); + + // Verify GetFieldType returns SqlVector for the vector column + Assert.Equal(typeof(SqlVector), reader.GetFieldType(0)); + + // Verify GetProviderSpecificFieldType also returns SqlVector + Assert.Equal(typeof(SqlVector), reader.GetProviderSpecificFieldType(0)); + + // Verify that GetValue returns an instance consistent with GetFieldType + Assert.True(reader.Read(), "No data found in the table."); + object value = reader.GetValue(0); + Assert.IsType>(value); + Assert.Equal(VectorFloat32TestData.testData, ((SqlVector)value).Memory.ToArray()); + + // Verify GetFieldValue> returns the correct typed value + SqlVector typedValue = reader.GetFieldValue>(0); + Assert.IsType>(typedValue); + Assert.Equal(VectorFloat32TestData.testData, typedValue.Memory.ToArray()); + } + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsSqlVectorSupported))] public void TestInsertVectorsFloat32WithPrepare() { diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/MetricsTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/MetricsTest.cs index fbcb27bb8c..af10a5824e 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/MetricsTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/MetricsTest.cs @@ -278,6 +278,7 @@ public void ReclaimedConnectionsCounter_Functional() } [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + [Trait("Category", "flaky")] public void ConnectionPoolGroupsCounter_Functional() { SqlConnection.ClearAllPools(); 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 59445a26b6..2435eb2f4a 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,7 +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 Microsoft.Data.SqlClient.Tests.Common; using Xunit; namespace Microsoft.Data.SqlClient.UnitTests; @@ -18,6 +18,36 @@ public class LocalAppContextSwitchesTest [Fact] public void TestDefaultAppContextSwitchValues() { + // LocalAppContextSwitches caches each switch value on first access for + // the lifetime of the process. Other tests running in parallel may + // already have triggered caching, or may use LocalAppContextSwitchesHelper + // to mutate the cached fields via reflection. To make this test + // deterministic, acquire the helper (which serializes against every + // other helper user via a process-wide semaphore) and reset each + // cached field to None so the properties re-read from AppContext. + using LocalAppContextSwitchesHelper switchesHelper = new(); + + switchesHelper.EnableMultiSubnetFailoverByDefault = null; + switchesHelper.IgnoreServerProvidedFailoverPartner = null; + switchesHelper.LegacyRowVersionNullBehavior = null; + switchesHelper.LegacyVarTimeZeroScaleBehaviour = null; + switchesHelper.MakeReadAsyncBlocking = null; + switchesHelper.SuppressInsecureTlsWarning = null; + switchesHelper.TruncateScaledDecimal = null; + switchesHelper.UseCompatibilityAsyncBehaviour = null; + switchesHelper.UseCompatibilityProcessSni = null; + switchesHelper.UseConnectionPoolV2 = null; + switchesHelper.UseMinimumLoginTimeout = null; + #if NET + switchesHelper.GlobalizationInvariantMode = null; + #endif + #if NET && _WINDOWS + switchesHelper.UseManagedNetworking = null; + #endif + #if NETFRAMEWORK + switchesHelper.DisableTnirByDefault = null; + #endif + Assert.False(LocalAppContextSwitches.LegacyRowVersionNullBehavior); Assert.False(LocalAppContextSwitches.SuppressInsecureTlsWarning); Assert.False(LocalAppContextSwitches.MakeReadAsyncBlocking); @@ -28,7 +58,6 @@ public void TestDefaultAppContextSwitchValues() Assert.False(LocalAppContextSwitches.UseConnectionPoolV2); Assert.False(LocalAppContextSwitches.TruncateScaledDecimal); Assert.False(LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner); - Assert.False(LocalAppContextSwitches.EnableUserAgent); Assert.False(LocalAppContextSwitches.EnableMultiSubnetFailoverByDefault); #if NET Assert.False(LocalAppContextSwitches.GlobalizationInvariantMode); diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SignatureVerificationCacheTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SignatureVerificationCacheTests.cs new file mode 100644 index 0000000000..a03dd8910e --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SignatureVerificationCacheTests.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 Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests +{ + public class SignatureVerificationCacheTests + { + [Fact] + public void GetSignatureVerificationResult_ReturnsFalseForCachedFailure() + { + ColumnMasterKeyMetadataSignatureVerificationCache cache = ColumnMasterKeyMetadataSignatureVerificationCache.Instance; + string keyStoreName = $"TEST_PROVIDER_{Guid.NewGuid():N}"; + string masterKeyPath = $"https://unit-test/{Guid.NewGuid():N}"; + byte[] signature = [1, 2, 3, 4]; + + cache.AddSignatureVerificationResult(keyStoreName, masterKeyPath, allowEnclaveComputations: true, signature, result: false); + + Assert.Equal(SignatureVerificationResult.False, cache.GetSignatureVerificationResult(keyStoreName, masterKeyPath, allowEnclaveComputations: true, signature)); + } + + [Fact] + public void GetSignatureVerificationResult_ReturnsTrueForCachedSuccess() + { + ColumnMasterKeyMetadataSignatureVerificationCache cache = ColumnMasterKeyMetadataSignatureVerificationCache.Instance; + string keyStoreName = $"TEST_PROVIDER_{Guid.NewGuid():N}"; + string masterKeyPath = $"https://unit-test/{Guid.NewGuid():N}"; + byte[] signature = [4, 3, 2, 1]; + + cache.AddSignatureVerificationResult(keyStoreName, masterKeyPath, allowEnclaveComputations: true, signature, result: true); + + Assert.Equal(SignatureVerificationResult.True, cache.GetSignatureVerificationResult(keyStoreName, masterKeyPath, allowEnclaveComputations: true, signature)); + } + + [Fact] + public void GetSignatureVerificationResult_ReturnsNotFoundForCacheMiss() + { + ColumnMasterKeyMetadataSignatureVerificationCache cache = ColumnMasterKeyMetadataSignatureVerificationCache.Instance; + string keyStoreName = $"TEST_PROVIDER_{Guid.NewGuid():N}"; + string masterKeyPath = $"https://unit-test/{Guid.NewGuid():N}"; + byte[] signature = [9, 9, 9, 9]; + + Assert.Equal(SignatureVerificationResult.NotFound, cache.GetSignatureVerificationResult(keyStoreName, masterKeyPath, allowEnclaveComputations: true, signature)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlAuthenticationProviderManagerTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlAuthenticationProviderManagerTests.cs index 778d69991b..08089abe98 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlAuthenticationProviderManagerTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlAuthenticationProviderManagerTests.cs @@ -73,4 +73,200 @@ public void Abstractions_And_Manager_GetSetProvider_Equivalent() SqlAuthenticationProvider.GetProvider( SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); } + + // Regression: the manager's static initializer reflectively constructs the Azure extension's + // ActiveDirectoryAuthenticationProvider. That class has overlapping 1-arg constructors + // ((string) and (ProviderOptions)), so calling Activator.CreateInstance(type, [null]) used + // to throw AmbiguousMatchException -- which surfaced as TypeInitializationException from + // GetProvider and broke every AD-authenticated connection. Calling GetProvider for an AD + // method must succeed (returning either the registered provider or null) and must not throw. + [Fact] + public void GetProvider_ForActiveDirectoryMethod_DoesNotThrow() + { + foreach (SqlAuthenticationMethod method in new[] + { + SqlAuthenticationMethod.ActiveDirectoryIntegrated, + #pragma warning disable CS0618 // ActiveDirectoryPassword is obsolete. + SqlAuthenticationMethod.ActiveDirectoryPassword, + #pragma warning restore CS0618 + SqlAuthenticationMethod.ActiveDirectoryInteractive, + SqlAuthenticationMethod.ActiveDirectoryServicePrincipal, + SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, + SqlAuthenticationMethod.ActiveDirectoryManagedIdentity, + SqlAuthenticationMethod.ActiveDirectoryMSI, + SqlAuthenticationMethod.ActiveDirectoryDefault, + SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity, + }) + { + // No assertion on the value -- the provider may or may not be installed depending on + // whether the Azure extension is on disk. We only assert no throw (which is what a + // TypeInitializationException from the static initializer would do). + _ = SqlAuthenticationProviderManager.GetProvider(method); + } + } + + // CreateAzureAuthenticationProvider tests ---------------------------------------------- + // + // Each Stub* container mimics one shape the real Azure extension might expose: + // * StubModern - both a (string) ctor and an (Options) ctor. + // * StubLegacy - only the (string) ctor; no Options type at all. + // * StubMinimal - only a parameterless ctor. + // + // The helper takes a Type directly, so these stubs do not need any particular full name. + + public class StubProviderBase : SqlAuthenticationProvider + { + public string? CapturedApplicationClientId; + public bool? CapturedUseWamBroker; + public bool ParameterlessCtorUsed; + public bool StringCtorUsed; + public bool OptionsCtorUsed; + + public override Task AcquireTokenAsync(SqlAuthenticationParameters parameters) + => Task.FromResult(new SqlAuthenticationToken("stub", DateTimeOffset.UtcNow.AddMinutes(5))); + + public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) => true; + } + + public static class StubModern + { + public sealed class ActiveDirectoryAuthenticationProviderOptions + { + public string? ApplicationClientId { get; set; } + public bool UseWamBroker { get; set; } + } + + public sealed class ActiveDirectoryAuthenticationProvider : StubProviderBase + { + public ActiveDirectoryAuthenticationProvider() { ParameterlessCtorUsed = true; } + + public ActiveDirectoryAuthenticationProvider(string applicationClientId) + { + StringCtorUsed = true; + CapturedApplicationClientId = applicationClientId; + } + + public ActiveDirectoryAuthenticationProvider(ActiveDirectoryAuthenticationProviderOptions options) + { + OptionsCtorUsed = true; + CapturedApplicationClientId = options.ApplicationClientId; + CapturedUseWamBroker = options.UseWamBroker; + } + } + } + + public static class StubLegacy + { + // No Options type defined -- mimics older Azure extension versions. + public sealed class ActiveDirectoryAuthenticationProvider : StubProviderBase + { + public ActiveDirectoryAuthenticationProvider() { ParameterlessCtorUsed = true; } + + public ActiveDirectoryAuthenticationProvider(string applicationClientId) + { + StringCtorUsed = true; + CapturedApplicationClientId = applicationClientId; + } + } + } + + public static class StubMinimal + { + // Parameterless only -- mimics a hypothetical extension with no 1-arg ctors at all. + public sealed class ActiveDirectoryAuthenticationProvider : StubProviderBase + { + public ActiveDirectoryAuthenticationProvider() { ParameterlessCtorUsed = true; } + } + } + + [Fact] + public void CreateAzureAuthenticationProvider_NeitherConfigured_UsesParameterlessCtor() + { + var instance = SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( + typeof(StubModern.ActiveDirectoryAuthenticationProvider), + typeof(StubModern.ActiveDirectoryAuthenticationProviderOptions), + applicationClientId: null, + useWamBroker: null); + + var stub = Assert.IsType(instance); + Assert.True(stub.ParameterlessCtorUsed); + Assert.False(stub.StringCtorUsed); + Assert.False(stub.OptionsCtorUsed); + Assert.Null(stub.CapturedApplicationClientId); + Assert.Null(stub.CapturedUseWamBroker); + } + + [Fact] + public void CreateAzureAuthenticationProvider_AppIdOnly_OptionsAvailable_UsesOptionsCtor() + { + var instance = SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( + typeof(StubModern.ActiveDirectoryAuthenticationProvider), + typeof(StubModern.ActiveDirectoryAuthenticationProviderOptions), + applicationClientId: "app-123", + useWamBroker: null); + + var stub = Assert.IsType(instance); + Assert.True(stub.OptionsCtorUsed); + Assert.False(stub.StringCtorUsed); + Assert.Equal("app-123", stub.CapturedApplicationClientId); + Assert.Equal(false, stub.CapturedUseWamBroker); + } + + [Fact] + public void CreateAzureAuthenticationProvider_AppIdOnly_OptionsMissing_FallsBackToStringCtor() + { + var instance = SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( + typeof(StubLegacy.ActiveDirectoryAuthenticationProvider), + optionsType: null, + applicationClientId: "legacy-456", + useWamBroker: null); + + var stub = Assert.IsType(instance); + Assert.True(stub.StringCtorUsed); + Assert.False(stub.OptionsCtorUsed); + Assert.False(stub.ParameterlessCtorUsed); + Assert.Equal("legacy-456", stub.CapturedApplicationClientId); + Assert.Null(stub.CapturedUseWamBroker); + } + + [Fact] + public void CreateAzureAuthenticationProvider_AppIdOnly_NoCompatibleCtor_ReturnsNull() + { + var instance = SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( + typeof(StubMinimal.ActiveDirectoryAuthenticationProvider), + optionsType: null, + applicationClientId: "no-ctor", + useWamBroker: null); + + Assert.Null(instance); + } + + [Fact] + public void CreateAzureAuthenticationProvider_UseWamBroker_OptionsMissing_Throws() + { + InvalidOperationException ex = Assert.Throws(() => + SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( + typeof(StubLegacy.ActiveDirectoryAuthenticationProvider), + optionsType: null, + applicationClientId: null, + useWamBroker: true)); + + Assert.Contains("ActiveDirectoryAuthenticationProviderOptions", ex.Message); + Assert.Contains("Microsoft.Data.SqlClient.Extensions.Azure", ex.Message); + } + + [Fact] + public void CreateAzureAuthenticationProvider_UseWamBroker_OptionsAvailable_UsesOptionsCtor() + { + var instance = SqlAuthenticationProviderManager.CreateAzureAuthenticationProvider( + typeof(StubModern.ActiveDirectoryAuthenticationProvider), + typeof(StubModern.ActiveDirectoryAuthenticationProviderOptions), + applicationClientId: "app-789", + useWamBroker: true); + + var stub = Assert.IsType(instance); + Assert.True(stub.OptionsCtorUsed); + Assert.Equal("app-789", stub.CapturedApplicationClientId); + Assert.Equal(true, stub.CapturedUseWamBroker); + } } diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionEnhancedRoutingTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionEnhancedRoutingTests.cs index 7d8941a07d..c85365d98e 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionEnhancedRoutingTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionEnhancedRoutingTests.cs @@ -14,6 +14,8 @@ namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests; /// /// Tests connection routing using the enhanced routing feature extension and envchange token /// +// TODO: Do we need this collection? It serializes all tests within it, which we probably don't +// need since each test uses its own TDS Server with ephemeral listen port. [Collection("SimulatedServerTests")] public class ConnectionEnhancedRoutingTests { diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs index d23fc9c532..8a41477f26 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs @@ -12,10 +12,13 @@ namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests { [Trait("Category", "flaky")] + // TODO: Do we need this collection? It serializes all tests within it, which we probably don't + // need since each test uses its own TDS Server with ephemeral listen port. [Collection("SimulatedServerTests")] public class ConnectionFailoverTests { //TODO parameterize for transient errors + [Trait("Category", "flaky")] [Theory] [InlineData(40613)] [InlineData(42108)] @@ -75,6 +78,7 @@ public void TransientFault_NoFailover_DoesNotClearPool(uint errorCode) Assert.Equal(0, failoverServer.PreLoginCount); } + [Trait("Category", "flaky")] [Fact] public void NetworkError_TriggersFailover_ClearsPool() { @@ -171,7 +175,7 @@ public void NetworkTimeout_ShouldFail() InitialCatalog = "master",// Required for failover partner to work ConnectTimeout = 1, ConnectRetryInterval = 1, - ConnectRetryCount = 0, // Disable retry + ConnectRetryCount = 0, // Disable retry Encrypt = false, MultiSubnetFailover = false, #if NETFRAMEWORK @@ -239,6 +243,7 @@ public void NetworkDelay_ShouldConnectToPrimary() } [Fact] + [Trait("Category", "flaky")] public void NetworkError_WithUserProvidedPartner_RetryDisabled_ShouldConnectToFailoverPartner() { using TdsServer failoverServer = new( @@ -285,6 +290,7 @@ public void NetworkError_WithUserProvidedPartner_RetryDisabled_ShouldConnectToFa } [Fact] + [Trait("Category", "flaky")] public void NetworkError_WithUserProvidedPartner_RetryEnabled_ShouldConnectToFailoverPartner() { using TdsServer failoverServer = new( @@ -376,6 +382,7 @@ public void TransientFault_ShouldConnectToPrimary(uint errorCode) [InlineData(40613)] [InlineData(42108)] [InlineData(42109)] + [Trait("Category", "flaky")] public void TransientFault_RetryDisabled_ShouldFail(uint errorCode) { // Arrange @@ -424,6 +431,7 @@ public void TransientFault_RetryDisabled_ShouldFail(uint errorCode) [InlineData(40613)] [InlineData(42108)] [InlineData(42109)] + [Trait("Category", "flaky")] public void TransientFault_WithUserProvidedPartner_ShouldConnectToPrimary(uint errorCode) { // Arrange @@ -454,7 +462,7 @@ public void TransientFault_WithUserProvidedPartner_ShouldConnectToPrimary(uint e FailoverPartner = $"localhost:{failoverServer.EndPoint.Port}", // User provided failover partner }; using SqlConnection connection = new(builder.ConnectionString); - + // Act connection.Open(); @@ -470,6 +478,7 @@ public void TransientFault_WithUserProvidedPartner_ShouldConnectToPrimary(uint e [InlineData(40613)] [InlineData(42108)] [InlineData(42109)] + [Trait("Category", "flaky")] public void TransientFault_WithUserProvidedPartner_RetryDisabled_ShouldFail(uint errorCode) { // Arrange diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionReadOnlyRoutingTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionReadOnlyRoutingTests.cs index f0618ac269..8220871f22 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionReadOnlyRoutingTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionReadOnlyRoutingTests.cs @@ -11,6 +11,8 @@ namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests { + // TODO: Do we need this collection? It serializes all tests within it, which we probably don't + // need since each test uses its own TDS Server with ephemeral listen port. [Collection("SimulatedServerTests")] public class ConnectionReadOnlyRoutingTests { @@ -71,7 +73,7 @@ public void RecursivelyRoutedConnection(int layers) router.Start(); routingLayers.Push(router); lastEndpoint = router.EndPoint; - lastConnectionString = (new SqlConnectionStringBuilder() { + lastConnectionString = (new SqlConnectionStringBuilder() { DataSource = $"localhost,{lastEndpoint.Port}", ApplicationIntent = ApplicationIntent.ReadOnly, Encrypt = false @@ -114,8 +116,8 @@ public async Task RecursivelyRoutedAsyncConnection(int layers) router.Start(); routingLayers.Push(router); lastEndpoint = router.EndPoint; - lastConnectionString = (new SqlConnectionStringBuilder() { - DataSource = $"localhost,{lastEndpoint.Port}", + lastConnectionString = (new SqlConnectionStringBuilder() { + DataSource = $"localhost,{lastEndpoint.Port}", ApplicationIntent = ApplicationIntent.ReadOnly, Encrypt = false }).ConnectionString; diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionRoutingTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionRoutingTests.cs index 108118dda7..4f45e49782 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionRoutingTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionRoutingTests.cs @@ -11,6 +11,8 @@ namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests { [Trait("Category", "flaky")] + // TODO: Do we need this collection? It serializes all tests within it, which we probably don't + // need since each test uses its own TDS Server with ephemeral listen port. [Collection("SimulatedServerTests")] public class ConnectionRoutingTests { @@ -157,6 +159,7 @@ public void NetworkDelayAtRoutedLocation_RetryDisabled_ShouldSucceed(bool multiS } [Fact] + [Trait("Category", "flaky")] public void NetworkTimeoutAtRoutedLocation_RetryDisabled_ShouldFail() { // Arrange @@ -194,7 +197,7 @@ public void NetworkTimeoutAtRoutedLocation_RetryDisabled_ShouldFail() // Act var e = Assert.Throws(connection.Open); - // Assert + // Assert Assert.Equal(ConnectionState.Closed, connection.State); Assert.Contains("Connection Timeout Expired", e.Message); } diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionRoutingTestsAzure.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionRoutingTestsAzure.cs index dd945e37f3..74190e847b 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionRoutingTestsAzure.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionRoutingTestsAzure.cs @@ -11,6 +11,8 @@ namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests { [Trait("Category", "flaky")] + // TODO: Do we need this collection? It serializes all tests within it, which we probably don't + // need since each test uses its own TDS Server with ephemeral listen port. [Collection("SimulatedServerTests")] public class ConnectionRoutingTestsAzure : IDisposable { @@ -22,8 +24,8 @@ public ConnectionRoutingTestsAzure() adpHelper.AddAzureSqlServerEndpoint("localhost"); } - public void Dispose() - { + public void Dispose() + { adpHelper.Dispose(); } @@ -159,6 +161,7 @@ public void NetworkDelayAtRoutedLocation_RetryDisabled_ShouldSucceed() } [Fact] + [Trait("Category", "flaky")] public void NetworkTimeoutAtRoutedLocation_RetryDisabled_ShouldFail() { // Arrange @@ -192,7 +195,7 @@ public void NetworkTimeoutAtRoutedLocation_RetryDisabled_ShouldFail() // Act var e = Assert.Throws(connection.Open); - // Assert + // Assert Assert.Equal(ConnectionState.Closed, connection.State); Assert.Contains("Connection Timeout Expired", e.Message); } diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs index a729e6afe2..06553b11f5 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs @@ -840,10 +840,6 @@ public void TestConnWithVectorFeatExtVersionNegotiation(bool expectedConnectionR [InlineData(false)] public void TestConnWithUserAgentFeatureExtension(bool sendAck) { - // Enable sending the UserAgent if desired. - using LocalAppContextSwitchesHelper switchesHelper = new(); - switchesHelper.EnableUserAgent = true; - // Start the test server. using TdsServer server = new(); server.Start(); @@ -904,55 +900,5 @@ public void TestConnWithUserAgentFeatureExtension(bool sendAck) // TODO: Confirm the server sent an Ack by reading log message from SqlInternalConnectionTds } - - /// - /// Test to verify no UserAgent relevant information is sent when EnableUserAgentField switch is disabled. - /// - [Fact] - public void TestConnWithoutUserAgentFeatureExtension() - { - // Disable the client-side UserAgent field entirely - using LocalAppContextSwitchesHelper switchesHelper = new(); - switchesHelper.EnableUserAgent = false; - - using var server = new TdsServer(); - server.Start(); - - // Do not advertise or force the UserAgent feature on the server - server.ServerSupportedUserAgentFeatureExtVersion = 0x00; // no support - server.EnableUserAgentFeatureExt = false; // no forced ACK - - bool loginValidated = false; - bool userAgentFeatureSeen = false; - - // Inspect the LOGIN7 packet captured by the test server - server.OnLogin7Validated = loginToken => - { - var featureExtTokens = loginToken.FeatureExt - .OfType() - .ToArray(); - - // Ensure there is no UserAgentSupport token at all - var uaToken = featureExtTokens.FirstOrDefault(t => t.FeatureID == TDSFeatureID.UserAgentSupport); - userAgentFeatureSeen = uaToken is not null; - - loginValidated = true; - }; - - // Connect to the test TDS server with a basic connection string - var connStr = new SqlConnectionStringBuilder - { - DataSource = $"localhost,{server.EndPoint.Port}", - Encrypt = SqlConnectionEncryptOption.Optional, - }.ConnectionString; - - using var connection = new SqlConnection(connStr); - connection.Open(); - - // Verify that the connection succeeded and no UserAgent data was sent - Assert.Equal(ConnectionState.Open, connection.State); - Assert.True(loginValidated, "Expected LOGIN7 to be validated by the test server"); - Assert.False(userAgentFeatureSeen, "Did not expect a UserAgentSupport feature token in LOGIN7"); - } } } diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/FeatureExtAckBoundsTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/FeatureExtAckBoundsTests.cs new file mode 100644 index 0000000000..20c94c0361 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/FeatureExtAckBoundsTests.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.IO; +using System.Linq; +using Microsoft.SqlServer.TDS; +using Microsoft.SqlServer.TDS.FeatureExtAck; +using Microsoft.SqlServer.TDS.Servers; +using TDSDoneToken = global::Microsoft.SqlServer.TDS.Done.TDSDoneToken; +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests; + +/// +/// Tests that the TDS parser rejects feature extension acknowledgment tokens +/// with data lengths exceeding protocol-reasonable bounds. This prevents a +/// malicious server from causing unbounded memory allocation on the client. +/// +// TODO: Do we need this collection? It serializes all tests within it, which we probably don't +// need since each test uses its own TDS Server with ephemeral listen port. +[Collection("SimulatedServerTests")] +public class FeatureExtAckBoundsTests : IDisposable +{ + private readonly TdsServerFixture _fixture; + private readonly TdsServer _server; + private readonly string _connectionString; + + public FeatureExtAckBoundsTests() + { + _fixture = new TdsServerFixture(); + _server = _fixture.TdsServer; + SqlConnectionStringBuilder builder = new() + { + DataSource = $"localhost,{_server.EndPoint.Port}", + Encrypt = SqlConnectionEncryptOption.Optional, + Pooling = false + }; + _connectionString = builder.ConnectionString; + } + + public void Dispose() => _fixture.Dispose(); + + /// + /// Verifies that the TDS parser rejects a FeatureExtAck token whose data length + /// field exceeds (1 MB), throwing a + /// parsing error instead of attempting an unbounded heap allocation. + /// This guards against CVE denial-of-service via pre-auth memory exhaustion. + /// + [Fact] + public void FeatureExtAck_OversizedDataLength_ThrowsParsingError() + { + // Arrange: inject a malicious FeatureExtAck token with an absurdly large data length + _server.OnAuthenticationResponseCompleted = responseMessage => + { + // Remove any existing FeatureExtAck token + var existing = responseMessage.OfType().FirstOrDefault(); + if (existing != null) + { + responseMessage.Remove(existing); + } + + // Insert a malicious token with oversized data length before the DONE token + int doneIndex = responseMessage.FindIndex(t => t is TDSDoneToken); + if (doneIndex < 0) + { + doneIndex = responseMessage.Count; + } + + responseMessage.Insert(doneIndex, new MaliciousFeatureExtAckToken( + featureId: (TDSFeatureID)TdsEnums.FEATUREEXT_GLOBALTRANSACTIONS, + claimedDataLen: (uint)(TdsEnums.MaxTokenDataLength + 1))); + }; + + // Act & Assert: connection should fail with a parsing error, NOT an OutOfMemoryException + using SqlConnection connection = new(_connectionString); + Exception ex = Assert.ThrowsAny(connection.Open); + + // The exception message should indicate a corrupted TDS stream + // with the oversized length value, not an OOM from attempting the allocation. + Assert.Contains("Error state: 18", ex.Message); // ParsingErrorState.CorruptedTdsStream = 18 + Assert.Contains($"Length: {TdsEnums.MaxTokenDataLength + 1}", ex.Message); + } + + /// + /// Verifies that a FeatureExtAck token with a data length at exactly the + /// allowed maximum is accepted without error, confirming there is no + /// off-by-one in the bounds check. + /// + [Fact] + public void FeatureExtAck_MaxAllowedDataLength_DoesNotThrow() + { + // Arrange: inject a FeatureExtAck token whose declared data length equals + // MaxTokenDataLength exactly. The bounds check should NOT fire for this + // value. The connection will fail for other reasons (not enough data on + // the wire), but the error must NOT be state 18 (CorruptedTdsStream). + _server.OnAuthenticationResponseCompleted = responseMessage => + { + var existing = responseMessage.OfType().FirstOrDefault(); + if (existing != null) + { + responseMessage.Remove(existing); + } + + int doneIndex = responseMessage.FindIndex(t => t is TDSDoneToken); + if (doneIndex < 0) + { + doneIndex = responseMessage.Count; + } + + // Insert token with dataLen = MaxTokenDataLength (at boundary, not over) + responseMessage.Insert(doneIndex, new MaliciousFeatureExtAckToken( + featureId: (TDSFeatureID)TdsEnums.FEATUREEXT_GLOBALTRANSACTIONS, + claimedDataLen: (uint)TdsEnums.MaxTokenDataLength)); + }; + + using SqlConnection connection = new(_connectionString); + // The connection will fail (insufficient data for the claimed length), + // but the failure must NOT be the bounds-check error. + Exception ex = Assert.ThrowsAny(connection.Open); + Assert.DoesNotContain("Error state: 18", ex.Message); + } + + /// + /// A custom TDS packet token that writes a FEATUREEXTACK token with a fraudulently + /// large data length field. This simulates a malicious server attempting to cause + /// the client to allocate an unbounded byte array. + /// + private sealed class MaliciousFeatureExtAckToken : TDSPacketToken + { + private readonly TDSFeatureID _featureId; + private readonly uint _claimedDataLen; + + public MaliciousFeatureExtAckToken(TDSFeatureID featureId, uint claimedDataLen) + { + _featureId = featureId; + _claimedDataLen = claimedDataLen; + } + + public override bool Inflate(Stream source) => throw new NotSupportedException(); + + public override void Deflate(Stream destination) + { + // Write the FEATUREEXTACK token type (0xAE) + destination.WriteByte((byte)TDSTokenType.FeatureExtAck); + + // Write the feature ID byte + destination.WriteByte((byte)_featureId); + + // Write the claimed data length (uint32, little-endian) — this is the lie + byte[] lenBytes = BitConverter.GetBytes(_claimedDataLen); + destination.Write(lenBytes, 0, 4); + + // Write only 1 byte of actual data (the client will try to read _claimedDataLen bytes + // but we only provide 1 — the bounds check should fire before the read attempt) + destination.WriteByte(0x01); + + // Write terminator + destination.WriteByte((byte)TDSFeatureID.Terminator); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/FeatureExtensionNegotiationTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/FeatureExtensionNegotiationTests.cs index e6342c4fa8..0141d44000 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/FeatureExtensionNegotiationTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/FeatureExtensionNegotiationTests.cs @@ -13,6 +13,8 @@ namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests; +// Serializes execution with other SimulatedServerTests classes to avoid port/resource conflicts. +// Required here because IClassFixture shares a single TdsServer instance across all tests in this class. [Collection("SimulatedServerTests")] public class FeatureExtensionNegotiationTests : IClassFixture { diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/TdsTokenBoundsTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/TdsTokenBoundsTests.cs new file mode 100644 index 0000000000..ef975dccb3 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/TdsTokenBoundsTests.cs @@ -0,0 +1,1051 @@ +// 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.Linq; +using Microsoft.SqlServer.TDS; +using Microsoft.SqlServer.TDS.ColMetadata; +using Microsoft.SqlServer.TDS.Done; +using Microsoft.SqlServer.TDS.FeatureExtAck; +using Microsoft.SqlServer.TDS.Servers; +using Microsoft.SqlServer.TDS.SessionState; +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests; + +/// +/// Tests that the TDS parser rejects various token types with data lengths +/// exceeding protocol-reasonable bounds, preventing unbounded memory allocation +/// from a malicious server. +/// +// Serializes execution with other SimulatedServerTests classes. Required here because +// DebugAssertSuppressor mutates the global Trace.Listeners collection, which is not +// safe to do concurrently with other tests that may trigger Debug.Assert. +[Collection("SimulatedServerTests")] +public class TdsTokenBoundsTests : IDisposable +{ + private readonly TdsServerFixture _fixture; + private readonly TdsServer _server; + private readonly string _connectionString; + + public TdsTokenBoundsTests() + { + _fixture = new TdsServerFixture(); + _server = _fixture.TdsServer; + SqlConnectionStringBuilder builder = new() + { + DataSource = $"localhost,{_server.EndPoint.Port}", + Encrypt = SqlConnectionEncryptOption.Optional, + Pooling = false + }; + _connectionString = builder.ConnectionString; + } + + public void Dispose() => _fixture.Dispose(); + + // ────────────────────────────────────────────────────────────────────────── + // Test 1: SessionState token with oversized total length + // ────────────────────────────────────────────────────────────────────────── + + /// + /// Verifies that TryProcessSessionState rejects a SessionState token (0xE4) + /// whose total length field exceeds (1 MB), + /// preventing unbounded memory allocation from a spoofed token length. + /// + [Fact] + public void SessionState_OversizedTotalLength_ThrowsParsingError() + { + _server.OnAuthenticationResponseCompleted = responseMessage => + { + int doneIndex = responseMessage.FindIndex(t => t is TDSDoneToken); + if (doneIndex < 0) + { + doneIndex = responseMessage.Count; + } + + // Inject a SessionState token claiming a total length exceeding MaxTokenDataLength + responseMessage.Insert(doneIndex, new MaliciousSessionStateToken( + claimedTotalLength: (uint)(TdsEnums.MaxTokenDataLength + 1))); + }; + + using SqlConnection connection = new(_connectionString); + Exception ex = Assert.ThrowsAny(connection.Open); + Assert.Contains("Error state: 18", ex.Message); // CorruptedTdsStream + Assert.Contains($"Length: {TdsEnums.MaxTokenDataLength + 1}", ex.Message); + } + + // ────────────────────────────────────────────────────────────────────────── + // Test 2: SessionState token with oversized inner option length + // ────────────────────────────────────────────────────────────────────────── + + /// + /// Verifies that TryProcessSessionState rejects an individual session state + /// option whose inner data length (encoded via the 0xFF + DWORD path) exceeds + /// , even when the outer token length is valid. + /// + [Fact] + public void SessionState_OversizedInnerOptionLength_ThrowsParsingError() + { + _server.OnAuthenticationResponseCompleted = responseMessage => + { + int doneIndex = responseMessage.FindIndex(t => t is TDSDoneToken); + if (doneIndex < 0) + { + doneIndex = responseMessage.Count; + } + + // Inject a SessionState token with a valid outer length but an inner + // state option claiming a huge data length + responseMessage.Insert(doneIndex, new MaliciousSessionStateInnerLenToken( + innerClaimedLength: TdsEnums.MaxTokenDataLength + 1)); + }; + + using SqlConnection connection = new(_connectionString); + Exception ex = Assert.ThrowsAny(connection.Open); + Assert.Contains("Error state: 18", ex.Message); // CorruptedTdsStream + Assert.Contains($"Length: {TdsEnums.MaxTokenDataLength + 1}", ex.Message); + } + + // ────────────────────────────────────────────────────────────────────────── + // Test 2b: SessionState token with negative inner option length + // ────────────────────────────────────────────────────────────────────────── + + /// + /// Verifies that TryProcessSessionState rejects an individual session state + /// option whose inner data length (encoded via the 0xFF + DWORD path) is negative, + /// which would be interpreted as a huge unsigned value if not bounds-checked. + /// + [Fact] + public void SessionState_NegativeInnerOptionLength_ThrowsParsingError() + { + _server.OnAuthenticationResponseCompleted = responseMessage => + { + int doneIndex = responseMessage.FindIndex(t => t is TDSDoneToken); + if (doneIndex < 0) + { + doneIndex = responseMessage.Count; + } + + // Inject a SessionState token with an inner state option claiming + // a negative data length (-1 = 0xFFFFFFFF as uint32) + responseMessage.Insert(doneIndex, new MaliciousSessionStateInnerLenToken( + innerClaimedLength: -1)); + }; + + using SqlConnection connection = new(_connectionString); + Exception ex = Assert.ThrowsAny(connection.Open); + Assert.Contains("Error state: 18", ex.Message); // CorruptedTdsStream + Assert.Contains("Length: -1", ex.Message); + } + + // ────────────────────────────────────────────────────────────────────────── + // Test 3: SRECOVERY feature ack with malformed inner state data + // ────────────────────────────────────────────────────────────────────────── + + /// + /// Verifies that the secondary parse of FEATUREEXT_SRECOVERY data in + /// SqlConnectionInternal.OnFeatureExtAck rejects inner state options + /// whose claimed length exceeds the remaining buffer, preventing an + /// out-of-bounds read or over-allocation. + /// + [Fact] + public void SRecovery_MalformedInnerStateLength_ThrowsParsingError() + { + _server.OnAuthenticationResponseCompleted = responseMessage => + { + // Remove existing FeatureExtAck if present + var existing = responseMessage.OfType().FirstOrDefault(); + if (existing != null) + { + responseMessage.Remove(existing); + } + + int doneIndex = responseMessage.FindIndex(t => t is TDSDoneToken); + if (doneIndex < 0) + { + doneIndex = responseMessage.Count; + } + + // Inject a FeatureExtAck with SessionRecovery feature containing + // inner state data where a state option claims a length exceeding the buffer + responseMessage.Insert(doneIndex, new MaliciousSRecoveryFeatureExtAckToken()); + }; + + using SqlConnection connection = new(_connectionString); + Exception ex = Assert.ThrowsAny(connection.Open); + Assert.Contains("Error state: 18", ex.Message); // CorruptedTdsStream + Assert.Contains("Length: 999", ex.Message); + } + + // ────────────────────────────────────────────────────────────────────────── + // Test 4: FedAuthInfo token with oversized total length + // ────────────────────────────────────────────────────────────────────────── + + /// + /// Verifies that TryProcessFedAuthInfo rejects a FedAuthInfo token (0xEE) + /// whose total length exceeds (1 MB). + /// The token type is dispatched unconditionally by TryRun, so this check + /// fires regardless of whether federated authentication was negotiated. + /// + [Fact] + public void FedAuthInfo_OversizedTokenLength_ThrowsParsingError() + { + _server.OnAuthenticationResponseCompleted = responseMessage => + { + int doneIndex = responseMessage.FindIndex(t => t is TDSDoneToken); + if (doneIndex < 0) + { + doneIndex = responseMessage.Count; + } + + // Inject a FedAuthInfo token with a total length exceeding MaxTokenDataLength. + // The parser dispatches on token type regardless of whether FedAuth was negotiated. + responseMessage.Insert(doneIndex, new MaliciousFedAuthInfoToken( + claimedLength: TdsEnums.MaxTokenDataLength + 1)); + }; + + using SqlConnection connection = new(_connectionString); + Exception ex = Assert.ThrowsAny(connection.Open); + Assert.Contains("Error state: 18", ex.Message); // CorruptedTdsStream + Assert.Contains($"Length: {TdsEnums.MaxTokenDataLength + 1}", ex.Message); + } + + // ────────────────────────────────────────────────────────────────────────── + // Test 5: ENV_PROMOTETRANSACTION with oversized newLength + // ────────────────────────────────────────────────────────────────────────── + + /// + /// Verifies that TryProcessEnvChange rejects a PromoteTransaction + /// environment change token (type 15) whose inner newLength field exceeds + /// (64 KB). A malicious server + /// can set the outer uint16 token length to a small value while writing an + /// int32 inner length claiming gigabytes, causing unbounded allocation. + /// + [Fact] + public void EnvChange_PromoteTransaction_OversizedLength_ThrowsParsingError() + { + _server.OnAuthenticationResponseCompleted = responseMessage => + { + int doneIndex = responseMessage.FindIndex(t => t is TDSDoneToken); + if (doneIndex < 0) + { + doneIndex = responseMessage.Count; + } + + // Inject a PromoteTransaction env change with a fraudulently large newLength + responseMessage.Insert(doneIndex, new MaliciousPromoteTransactionEnvChangeToken( + claimedNewLength: TdsEnums.MaxPromoteTransactionLength + 1)); + }; + + using SqlConnection connection = new(_connectionString); + Exception ex = Assert.ThrowsAny(connection.Open); + Assert.Contains("Error state: 18", ex.Message); // CorruptedTdsStream + Assert.Contains($"Length: {TdsEnums.MaxPromoteTransactionLength + 1}", ex.Message); + } + + // ══════════════════════════════════════════════════════════════════════════ + // Malicious token helpers + // ══════════════════════════════════════════════════════════════════════════ + + /// + /// Writes a SessionState token (0xE4) with a fraudulently large total length. + /// Wire format: [0xE4][uint32 totalLen][uint32 seqNum][byte status][...] + /// The bounds check fires on the totalLen value before any data is read. + /// + private sealed class MaliciousSessionStateToken : TDSPacketToken + { + private readonly uint _claimedTotalLength; + + public MaliciousSessionStateToken(uint claimedTotalLength) + { + _claimedTotalLength = claimedTotalLength; + } + + public override bool Inflate(Stream source) => throw new NotSupportedException(); + + public override void Deflate(Stream destination) + { + // Token type + destination.WriteByte((byte)TDSTokenType.SessionState); // 0xE4 + + // Total token length (uint32) — this is the fraudulent value + byte[] lenBytes = BitConverter.GetBytes(_claimedTotalLength); + destination.Write(lenBytes, 0, 4); + + // Write minimal valid-looking data: seqNum (4 bytes) + status (1 byte) + // The bounds check should fire before trying to process option data. + destination.Write(new byte[] { 0x01, 0x00, 0x00, 0x00 }, 0, 4); // seqNum = 1 + destination.WriteByte(0x01); // status = recoverable + } + } + + /// + /// Writes a SessionState token (0xE4) with a valid outer length but an inner + /// state option that claims a huge data length (using the 0xFF + DWORD encoding). + /// Wire format: [0xE4][uint32 totalLen][uint32 seqNum][byte status] + /// [byte stateId][0xFF][int32 innerLen][...data...] + /// The inner bounds check fires on the innerLen value. + /// + private sealed class MaliciousSessionStateInnerLenToken : TDSPacketToken + { + private readonly int _innerClaimedLength; + + public MaliciousSessionStateInnerLenToken(int innerClaimedLength) + { + _innerClaimedLength = innerClaimedLength; + } + + public override bool Inflate(Stream source) => throw new NotSupportedException(); + + public override void Deflate(Stream destination) + { + // Token type + destination.WriteByte((byte)TDSTokenType.SessionState); // 0xE4 + + // Calculate total token length: + // seqNum(4) + status(1) + stateId(1) + lenMarker(1) + innerLen(4) + minimal data(1) + uint totalLength = 4 + 1 + 1 + 1 + 4 + 1; + + byte[] lenBytes = BitConverter.GetBytes(totalLength); + destination.Write(lenBytes, 0, 4); + + // Sequence number + destination.Write(new byte[] { 0x01, 0x00, 0x00, 0x00 }, 0, 4); + + // Status (recoverable) + destination.WriteByte(0x01); + + // State option: stateId + destination.WriteByte(0x00); // UserOptions state ID + + // Length marker: 0xFF means next 4 bytes are the DWORD length + destination.WriteByte(0xFF); + + // Inner claimed length — fraudulently large + byte[] innerLenBytes = BitConverter.GetBytes(_innerClaimedLength); + destination.Write(innerLenBytes, 0, 4); + + // Write only 1 byte of actual data + destination.WriteByte(0x42); + } + } + + /// + /// Writes a FeatureExtAck token (0xAE) with a SessionRecovery feature (ID=1) + /// that carries inner state data where a state option claims a length exceeding + /// the remaining buffer. This exercises the bounds check in + /// SqlConnectionInternal.OnFeatureExtAck for FEATUREEXT_SRECOVERY. + /// + private sealed class MaliciousSRecoveryFeatureExtAckToken : TDSPacketToken + { + public override bool Inflate(Stream source) => throw new NotSupportedException(); + + public override void Deflate(Stream destination) + { + // Token type: FEATUREEXTACK + destination.WriteByte((byte)TDSTokenType.FeatureExtAck); // 0xAE + + // Feature ID: SessionRecovery = 0x01 + destination.WriteByte(0x01); + + // The feature data for SRECOVERY is parsed by SqlConnectionInternal.OnFeatureExtAck: + // It reads pairs of [stateId(1)][lenByte(1)][data(len)] or [stateId(1)][0xFF][int32 len][data(len)] + // We'll craft inner data where one option claims length > remaining buffer. + + // Inner data layout: + // stateId(1) + 0xFF marker(1) + int32 len(4) + 1 byte actual data + byte[] innerData; + using (var ms = new MemoryStream()) + { + ms.WriteByte(0x00); // stateId = 0 + + // Use 0xFF marker to indicate DWORD length + ms.WriteByte(0xFF); + + // Claim a length that exceeds the remaining buffer + // The remaining buffer after reading stateId + 0xFF + 4-byte-len will be 1 byte, + // but we claim 999 bytes + byte[] claimedLen = BitConverter.GetBytes(999); + ms.Write(claimedLen, 0, 4); + + // Only provide 1 byte of actual data + ms.WriteByte(0x42); + + innerData = ms.ToArray(); + } + + // Feature data length (uint32) + byte[] featureDataLen = BitConverter.GetBytes((uint)innerData.Length); + destination.Write(featureDataLen, 0, 4); + + // Feature data + destination.Write(innerData, 0, innerData.Length); + + // Terminator + destination.WriteByte((byte)TDSFeatureID.Terminator); // 0xFF + } + } + + /// + /// Writes a FedAuthInfo token (0xEE) with a fraudulently large total length. + /// Wire format: [0xEE][int32 tokenLen][...data...] + /// The bounds check fires on tokenLen before any data is read. + /// + private sealed class MaliciousFedAuthInfoToken : TDSPacketToken + { + private readonly int _claimedLength; + + public MaliciousFedAuthInfoToken(int claimedLength) + { + _claimedLength = claimedLength; + } + + public override bool Inflate(Stream source) => throw new NotSupportedException(); + + public override void Deflate(Stream destination) + { + // Token type + destination.WriteByte(0xEE); // SQLFEDAUTHINFO + + // Token length (int32) — fraudulently large + byte[] lenBytes = BitConverter.GetBytes(_claimedLength); + destination.Write(lenBytes, 0, 4); + + // Write minimal data (at least sizeof(uint) to pass the lower bound check, + // but the upper bound check should fire first) + destination.Write(new byte[] { 0x01, 0x00, 0x00, 0x00 }, 0, 4); // optionsCount = 1 + } + } + + /// + /// Writes an EnvChange token (0xE3) with type PromoteTransaction (15) whose + /// inner int32 newLength exceeds . + /// Wire format: [0xE3][uint16 tokenLen][byte type=15][int32 newLen][data...][byte oldLen=0] + /// The outer uint16 tokenLen is set to accommodate the header but the int32 + /// newLength claims far more data than actually follows, triggering the bounds check. + /// + private sealed class MaliciousPromoteTransactionEnvChangeToken : TDSPacketToken + { + private readonly int _claimedNewLength; + + public MaliciousPromoteTransactionEnvChangeToken(int claimedNewLength) + { + _claimedNewLength = claimedNewLength; + } + + public override bool Inflate(Stream source) => throw new NotSupportedException(); + + public override void Deflate(Stream destination) + { + // Token type: ENVCHANGE + destination.WriteByte((byte)TDSTokenType.EnvironmentChange); // 0xE3 + + // Outer token length (uint16): type(1) + newLength(4) + 1 byte data + oldLength(1) + // We write just enough to contain the header fields the parser reads before + // hitting the bounds check. The parser reads: type(1) + int32 newLen(4) = 5 bytes min. + ushort outerLength = 1 + 4 + 1 + 1; // type + newLen + 1 fake byte + oldLen + byte[] outerLenBytes = BitConverter.GetBytes(outerLength); + destination.Write(outerLenBytes, 0, 2); + + // Env change type: PromoteTransaction = 15 + destination.WriteByte(15); + + // newLength (int32) — fraudulently large + byte[] newLenBytes = BitConverter.GetBytes(_claimedNewLength); + destination.Write(newLenBytes, 0, 4); + + // Write 1 byte of fake data (the bounds check fires before attempting to read this much) + destination.WriteByte(0x00); + + // Old value length (byte) = 0 + destination.WriteByte(0x00); + } + } + + // ────────────────────────────────────────────────────────────────────────── + // Test 6: Post-login batch response injection — PromoteTransaction + // ────────────────────────────────────────────────────────────────────────── + + /// + /// Verifies that bounds checks fire during command execution (post-login) + /// by injecting a malicious PromoteTransaction env change token into the + /// SQL batch response. This exercises the OnSQLBatchCompleted hook + /// on the simulated server and proves the same bounds check fires regardless + /// of whether the token arrives during login or command execution. + /// + [Fact] + public void BatchResponse_PromoteTransaction_OversizedLength_ThrowsParsingError() + { + _server.OnSQLBatchCompleted = responseMessage => + { + int doneIndex = responseMessage.FindIndex(t => t is TDSDoneToken); + if (doneIndex < 0) + { + doneIndex = responseMessage.Count; + } + + responseMessage.Insert(doneIndex, new MaliciousPromoteTransactionEnvChangeToken( + claimedNewLength: TdsEnums.MaxPromoteTransactionLength + 1)); + }; + + using SqlConnection connection = new(_connectionString); + connection.Open(); + + using SqlCommand command = connection.CreateCommand(); + command.CommandText = "SELECT 1"; + + Exception ex = Assert.ThrowsAny( + () => command.ExecuteNonQuery()); + Assert.Contains("Error state: 18", ex.Message); // CorruptedTdsStream + Assert.Contains($"Length: {TdsEnums.MaxPromoteTransactionLength + 1}", ex.Message); + } + + // ────────────────────────────────────────────────────────────────────────── + // Test 7: Post-login batch response — DateTime oversized length + // ────────────────────────────────────────────────────────────────────────── + + /// + /// Verifies that TryReadSqlDateTime rejects a TIME column value whose + /// data length byte exceeds the maximum datetime wire size (10 bytes). A + /// malicious server could set this length to a large value, causing the + /// parser to attempt an unbounded heap allocation. + /// + [Fact] + public void BatchResponse_DateTime_OversizedLength_ThrowsParsingError() + { + _server.OnSQLBatchCompleted = responseMessage => + { + // Replace the entire response with a crafted result set containing + // a TIME column whose ROW data length exceeds the maximum. + responseMessage.Clear(); + + // Use proper library tokens for COLMETADATA so framing is correct + var metadata = new TDSColMetadataToken(); + var col = new TDSColumnData(); + col.DataType = TDSDataType.TimeN; + col.DataTypeSpecific = (byte)7; // scale = 7 + col.Flags.IsNullable = true; + col.Name = string.Empty; + metadata.Columns.Add(col); + responseMessage.Add(metadata); + + // Add a malicious ROW token with oversized data length + responseMessage.Add(new MaliciousTimeRowToken()); + + // Add DONE token + responseMessage.Add(new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Count, TDSDoneTokenCommandType.Select, 1)); + }; + + using SqlConnection connection = new(_connectionString); + connection.Open(); + + using SqlCommand command = connection.CreateCommand(); + command.CommandText = "MALICIOUS_QUERY_NOT_RECOGNIZED"; + + SqlDataReader reader = command.ExecuteReader(); + try + { + // Read() returns true (data is ready) but doesn't actually parse the + // column values — that happens on GetValue/GetTimeSpan. Force the read. + Assert.True(reader.Read()); + Exception ex = Assert.ThrowsAny( + () => reader.GetValue(0)); + Assert.Contains("Error state: 18", ex.Message); // CorruptedTdsStream + Assert.Contains("Length: 11", ex.Message); + } + finally + { + // Disposing the reader after a corrupted stream causes the driver to + // attempt further TDS parsing during teardown, which can trip unrelated + // Debug.Assert calls in TdsParser. + using (new DebugAssertSuppressor()) + { + try { reader.Dispose(); } catch { } + } + } + } + + /// + /// Writes a ROW token (0xD1) with a single TimeN column whose data length + /// byte is set to 11 (exceeding the maximum datetime wire size of 10 bytes). + /// + private sealed class MaliciousTimeRowToken : TDSPacketToken + { + public override bool Inflate(Stream source) => throw new NotSupportedException(); + + public override void Deflate(Stream destination) + { + // ROW token type + destination.WriteByte(0xD1); + + // TimeN data: length prefix (1 byte) = 11 (INVALID — max for time is 5, max datetime overall is 10) + destination.WriteByte(11); + + // Write 11 bytes of dummy data + destination.Write(new byte[11], 0, 11); + } + } + + // ────────────────────────────────────────────────────────────────────────── + // Test 8: Post-login batch response — ReturnValue with oversized data length + // ────────────────────────────────────────────────────────────────────────── + + /// + /// Verifies that TryProcessReturnValue rejects a RETURNVALUE token (0xAC) + /// whose inner data length (for a non-PLP IMAGE type) exceeds int.MaxValue. + /// A malicious server can craft a TEXT/IMAGE return value with a spoofed int32 + /// data length that becomes a huge value when cast to ulong, triggering unbounded + /// allocation. + /// + [Fact] + public void BatchResponse_ReturnValue_OversizedLength_ThrowsParsingError() + { + _server.OnSQLBatchCompleted = responseMessage => + { + int doneIndex = responseMessage.FindIndex(t => t is TDSDoneToken); + if (doneIndex < 0) + { + doneIndex = responseMessage.Count; + } + + responseMessage.Insert(doneIndex, new MaliciousReturnValueToken()); + }; + + using SqlConnection connection = new(_connectionString); + connection.Open(); + + using SqlCommand command = connection.CreateCommand(); + command.CommandText = "SELECT 1"; + + // The malicious RETURNVALUE token corrupts parser state before the + // exception propagates. Suppress Debug.Assert calls that fire in + // TdsParser during error handling and connection teardown. + using (new DebugAssertSuppressor()) + { + Exception ex = Assert.ThrowsAny( + () => command.ExecuteNonQuery()); + Assert.Contains("Error state: 18", ex.Message); // CorruptedTdsStream + Assert.Contains("Length: -1", ex.Message); + } + } + + /// + /// Writes a RETURNVALUE token (0xAC) with an IMAGE (0x22) type whose data + /// length field is set to -1 (0xFFFFFFFF). When cast to ulong this exceeds + /// int.MaxValue, triggering the bounds check in TryProcessReturnValue. + /// Wire layout: + /// [0xAC] token + /// [uint16] ordinal + /// [byte] param name length (0) + /// [byte] status + /// [uint32] user type + /// [byte] flags1 + /// [byte] flags2 + /// [byte] tds type = 0x22 (IMAGE) + /// [int32] max length + /// [byte] textPtrLen = 16 + /// [16 bytes] textPtr + /// [8 bytes] timestamp + /// [int32] data length = -1 (INVALID) + /// + private sealed class MaliciousReturnValueToken : TDSPacketToken + { + public override bool Inflate(Stream source) => throw new NotSupportedException(); + + public override void Deflate(Stream destination) + { + // RETURNVALUE token + destination.WriteByte(0xAC); + + // Ordinal (uint16 LE) + destination.WriteByte(0x00); + destination.WriteByte(0x00); + + // Param name length (byte) = 0 + destination.WriteByte(0x00); + + // Status (byte) = 0x01 (output parameter) + destination.WriteByte(0x01); + + // UserType (uint32 LE) = 0 + destination.Write(new byte[4], 0, 4); + + // Flags byte 1 (ignored) + destination.WriteByte(0x00); + + // Flags byte 2 + destination.WriteByte(0x00); + + // TDS type = SQLIMAGE (0x22) + destination.WriteByte(0x22); + + // MaxLen (int32 LE) — for IMAGE this is read via TryGetTokenLength + // which for 0x22 reads int32. Value doesn't matter much, just needs + // to be valid for MetaType lookup. + destination.Write(new byte[] { 0x10, 0x00, 0x00, 0x00 }, 0, 4); // 16 + + // -- TryProcessColumnHeaderNoNBC: IsLong && !IsPlp path -- + // TextPtr length (byte) = 16 + destination.WriteByte(0x10); + + // TextPtr data (16 bytes) + destination.Write(new byte[16], 0, 16); + + // Timestamp (8 bytes) + destination.Write(new byte[8], 0, 8); + + // Data length (int32 LE) = -1 (0xFFFFFFFF) + // (ulong)(-1) = 0xFFFFFFFFFFFFFFFF > int.MaxValue → triggers bounds check + destination.WriteByte(0xFF); + destination.WriteByte(0xFF); + destination.WriteByte(0xFF); + destination.WriteByte(0xFF); + } + } + + // ────────────────────────────────────────────────────────────────────────── + // Test 9: Post-login batch response — PLP ReturnValue (regression guard) + // ────────────────────────────────────────────────────────────────────────── + + /// + /// Verifies that TryProcessReturnValue correctly handles PLP + /// (Partially Length-Prefixed) return values without triggering the bounds + /// check. PLP types use the unknown-length sentinel (0xFFFFFFFFFFFFFFFE) + /// which must NOT be rejected by the non-PLP bounds check. + /// + [Fact] + public void BatchResponse_ReturnValue_PlpUnknownLength_Succeeds() + { + _server.OnSQLBatchCompleted = responseMessage => + { + int doneIndex = responseMessage.FindIndex(t => t is TDSDoneToken); + if (doneIndex < 0) + { + doneIndex = responseMessage.Count; + } + + responseMessage.Insert(doneIndex, new ValidPlpReturnValueToken()); + }; + + using SqlConnection connection = new(_connectionString); + connection.Open(); + + using SqlCommand command = connection.CreateCommand(); + command.CommandText = "SELECT 1"; + + // Should NOT throw — PLP unknown-length sentinel is valid + command.ExecuteNonQuery(); + } + + /// + /// Writes a valid RETURNVALUE token (0xAC) with a PLP VARBINARY(MAX) type + /// using the unknown-length sentinel (0xFFFFFFFFFFFFFFFE) followed by an + /// immediate PLP terminator (chunk length = 0). This exercises the IsPlp + /// branch in TryProcessReturnValue and must NOT trigger the bounds check. + /// Wire layout: + /// [0xAC] token + /// [uint16] ordinal + /// [byte] param name length (0) + /// [byte] status + /// [uint32] user type + /// [byte] flags1 + /// [byte] flags2 + /// [byte] tds type = 0xA5 (BIGVARBINARY) + /// [uint16] max length = 0xFFFF (PLP marker) + /// [uint64] PLP length = 0xFFFFFFFFFFFFFFFE (unknown) + /// [uint32] chunk length = 0 (terminator) + /// + private sealed class ValidPlpReturnValueToken : TDSPacketToken + { + public override bool Inflate(Stream source) => throw new NotSupportedException(); + + public override void Deflate(Stream destination) + { + // RETURNVALUE token + destination.WriteByte(0xAC); + + // Ordinal (uint16 LE) + destination.WriteByte(0x00); + destination.WriteByte(0x00); + + // Param name length (byte) = 0 + destination.WriteByte(0x00); + + // Status (byte) = 0x01 (output parameter) + destination.WriteByte(0x01); + + // UserType (uint32 LE) = 0 + destination.Write(new byte[4], 0, 4); + + // Flags byte 1 (ignored) + destination.WriteByte(0x00); + + // Flags byte 2 + destination.WriteByte(0x00); + + // TDS type = SQLBIGVARBINARY (0xA5) + destination.WriteByte(0xA5); + + // MaxLen (uint16 LE) = 0xFFFF — PLP marker + destination.WriteByte(0xFF); + destination.WriteByte(0xFF); + + // -- TryProcessColumnHeaderNoNBC: non-IsLong path -- + // TryGetDataLength → TryReadPlpLength: + // reads uint64 = 0xFFFFFFFFFFFFFFFE (unknown length sentinel) + destination.WriteByte(0xFE); + destination.WriteByte(0xFF); + destination.WriteByte(0xFF); + destination.WriteByte(0xFF); + destination.WriteByte(0xFF); + destination.WriteByte(0xFF); + destination.WriteByte(0xFF); + destination.WriteByte(0xFF); + + // PLP chunk terminator (uint32 = 0) — empty data + destination.Write(new byte[4], 0, 4); + } + } + + // ────────────────────────────────────────────────────────────────────────── + // Test 10: Post-login batch response — sql_variant with oversized binary + // ────────────────────────────────────────────────────────────────────────── + + /// + /// Verifies that TryReadSqlValueInternal rejects a binary value inside + /// a sql_variant column whose inner data length exceeds + /// (8000 bytes). The bounds check in the + /// sql_variant deserialization path prevents unbounded heap allocation. + /// + [Fact] + public void BatchResponse_SqlVariantBinary_OversizedLength_ThrowsParsingError() + { + _server.OnSQLBatchCompleted = responseMessage => + { + responseMessage.Clear(); + + // COLMETADATA: one SSVariant column + var metadata = new TDSColMetadataToken(); + var col = new TDSColumnData(); + col.DataType = TDSDataType.SSVariant; + col.DataTypeSpecific = (uint)8009; // max length for SSVariant + col.Flags.IsNullable = true; + col.Name = string.Empty; + metadata.Columns.Add(col); + responseMessage.Add(metadata); + + // ROW with a sql_variant containing oversized binary data + responseMessage.Add(new MaliciousSqlVariantBinaryRowToken()); + + // DONE + responseMessage.Add(new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Count, TDSDoneTokenCommandType.Select, 1)); + }; + + using SqlConnection connection = new(_connectionString); + connection.Open(); + + using SqlCommand command = connection.CreateCommand(); + command.CommandText = "MALICIOUS_QUERY_NOT_RECOGNIZED"; + + SqlDataReader reader = command.ExecuteReader(); + try + { + Assert.True(reader.Read()); + Exception ex = Assert.ThrowsAny( + () => reader.GetValue(0)); + Assert.Contains("Error state: 18", ex.Message); // CorruptedTdsStream + Assert.Contains("Length: 8001", ex.Message); + } + finally + { + // Disposing the reader after a corrupted stream causes the driver to + // attempt further TDS parsing during teardown, which can trip unrelated + // Debug.Assert calls in TdsParser. + using (new DebugAssertSuppressor()) + { + try { reader.Dispose(); } catch { } + } + } + } + + // ────────────────────────────────────────────────────────────────────────── + // Test 11: Post-login batch response — sql_variant with negative inner length + // ────────────────────────────────────────────────────────────────────────── + + /// + /// Verifies that TryReadSqlValueInternal rejects a sql_variant binary + /// value whose declared total length is too small for the type overhead, + /// causing the computed inner data length to be negative. The bounds check + /// if (length < 0 || length > TdsEnums.MAXSIZE) catches this. + /// + [Fact] + public void BatchResponse_SqlVariantBinary_NegativeLength_ThrowsParsingError() + { + _server.OnSQLBatchCompleted = responseMessage => + { + responseMessage.Clear(); + + // COLMETADATA: one SSVariant column + var metadata = new TDSColMetadataToken(); + var col = new TDSColumnData(); + col.DataType = TDSDataType.SSVariant; + col.DataTypeSpecific = (uint)8009; // max length for SSVariant + col.Flags.IsNullable = true; + col.Name = string.Empty; + metadata.Columns.Add(col); + responseMessage.Add(metadata); + + // ROW with a sql_variant claiming a total length too small for its overhead + responseMessage.Add(new NegativeLenSqlVariantBinaryRowToken()); + + // DONE + responseMessage.Add(new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Count, TDSDoneTokenCommandType.Select, 1)); + }; + + using SqlConnection connection = new(_connectionString); + connection.Open(); + + using SqlCommand command = connection.CreateCommand(); + command.CommandText = "MALICIOUS_QUERY_NOT_RECOGNIZED"; + + SqlDataReader reader = command.ExecuteReader(); + try + { + Assert.True(reader.Read()); + Exception ex = Assert.ThrowsAny( + () => reader.GetValue(0)); + Assert.Contains("Error state: 18", ex.Message); // CorruptedTdsStream + Assert.Contains("Length: -1", ex.Message); + } + finally + { + // Disposing the reader after a corrupted stream causes the driver to + // attempt further TDS parsing during teardown, which can trip unrelated + // Debug.Assert calls in TdsParser. + using (new DebugAssertSuppressor()) + { + try { reader.Dispose(); } catch { } + } + } + } + + /// + /// Writes a ROW token (0xD1) with a single SSVariant column containing a + /// BigVarBinary variant whose inner data length exceeds MAXSIZE (8000). + /// Wire layout for the variant: + /// [int32] total variant length = 8005 + /// [byte] inner type = 0xA5 (BigVarBinary) + /// [byte] cbPropBytes = 2 + /// [ushort] maxLen (property) = 8001 + /// [8001 bytes would be data, but we only write 4 to trigger the check] + /// lenData = 8005 - 2(SQLVARIANT_SIZE) - 2(cbProps) = 8001 > MAXSIZE → throws + /// + private sealed class MaliciousSqlVariantBinaryRowToken : TDSPacketToken + { + public override bool Inflate(Stream source) => throw new NotSupportedException(); + + public override void Deflate(Stream destination) + { + // ROW token type + destination.WriteByte(0xD1); + + // SSVariant column data: total length (int32 LE) + // lenData = totalLength - SQLVARIANT_SIZE(2) - cbPropBytes(2) = totalLength - 4 + // We want lenData = 8001, so totalLength = 8005 + int totalLength = 8005; + byte[] lenBytes = BitConverter.GetBytes(totalLength); + destination.Write(lenBytes, 0, 4); + + // Inner type: BigVarBinary = 0xA5 + destination.WriteByte(0xA5); + + // cbPropBytes = 2 + destination.WriteByte(0x02); + + // Properties: maxLen (ushort) = 8001 + destination.WriteByte(0x41); // 8001 & 0xFF = 0x41 + destination.WriteByte(0x1F); // 8001 >> 8 = 0x1F + + // Write 4 bytes of dummy data (bounds check fires before trying to read 8001) + destination.Write(new byte[4], 0, 4); + } + } + + /// + /// Writes a ROW token (0xD1) with a single SSVariant column containing a + /// BigVarBinary variant whose total length (3) is too small to cover the + /// type overhead (SQLVARIANT_SIZE=2 + cbPropBytes=2 = 4), producing a + /// negative computed inner data length: lenData = 3 - 4 = -1. + /// + private sealed class NegativeLenSqlVariantBinaryRowToken : TDSPacketToken + { + public override bool Inflate(Stream source) => throw new NotSupportedException(); + + public override void Deflate(Stream destination) + { + // ROW token type + destination.WriteByte(0xD1); + + // SSVariant column data: total length (int32 LE) + // lenConsumed = SQLVARIANT_SIZE(2) + cbPropBytes(2) = 4 + // lenData = totalLength - lenConsumed = 3 - 4 = -1 → triggers < 0 check + int totalLength = 3; + byte[] lenBytes = BitConverter.GetBytes(totalLength); + destination.Write(lenBytes, 0, 4); + + // Inner type: BigVarBinary = 0xA5 + destination.WriteByte(0xA5); + + // cbPropBytes = 2 + destination.WriteByte(0x02); + + // Properties: maxLen (ushort) = 100 (arbitrary, just need 2 bytes) + destination.WriteByte(0x64); // 100 & 0xFF + destination.WriteByte(0x00); // 100 >> 8 + + // No data bytes — the bounds check fires before attempting to read + } + } + + /// + /// Temporarily suppresses Debug.Assert failures by clearing trace listeners. Used when + /// disposing resources after intentionally corrupting a TDS stream. A static lock serializes + /// access for the lifetime of the instance because Trace.Listeners is a global collection. + /// + private sealed class DebugAssertSuppressor : IDisposable + { + private static readonly object s_listenerLock = new(); + private readonly System.Diagnostics.TraceListener[] _listeners; + + public DebugAssertSuppressor() + { + System.Threading.Monitor.Enter(s_listenerLock); + try + { + _listeners = new System.Diagnostics.TraceListener[System.Diagnostics.Trace.Listeners.Count]; + System.Diagnostics.Trace.Listeners.CopyTo(_listeners, 0); + System.Diagnostics.Trace.Listeners.Clear(); + } + catch + { + System.Threading.Monitor.Exit(s_listenerLock); + throw; + } + } + + public void Dispose() + { + try + { + System.Diagnostics.Trace.Listeners.Clear(); + System.Diagnostics.Trace.Listeners.AddRange(_listeners); + } + finally + { + System.Threading.Monitor.Exit(s_listenerLock); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/TypeForwardTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/TypeForwardTests.cs new file mode 100644 index 0000000000..8cb9811933 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/TypeForwardTests.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.Reflection; +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests +{ + public class TypeForwardTests + { + private static readonly Assembly s_sqlClientAssembly = typeof(SqlConnection).Assembly; + + [Theory] + [InlineData("Microsoft.Data.SqlClient.SqlAuthenticationMethod", true)] + [InlineData("Microsoft.Data.SqlClient.SqlAuthenticationParameters", false)] + [InlineData("Microsoft.Data.SqlClient.SqlAuthenticationProvider", false)] + [InlineData("Microsoft.Data.SqlClient.SqlAuthenticationProviderException", false)] + [InlineData("Microsoft.Data.SqlClient.SqlAuthenticationToken", false)] + public void AbstractionsType_CanBeLoadedFromSqlClientAssembly(string typeName, bool isEnum) + { + // Types moved to the Abstractions assembly must remain loadable via the + // Microsoft.Data.SqlClient assembly for backward compatibility. + Type? type = s_sqlClientAssembly.GetType(typeName, throwOnError: true); + + Assert.NotNull(type); + Assert.Equal(isEnum, type.IsEnum); + + // Assert that the assembly containing the type is the Abstractions assembly. + Assert.Equal("Microsoft.Data.SqlClient.Extensions.Abstractions", type.Assembly.GetName().Name); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs index d2c81e6ca1..3ec8f6efaf 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs @@ -139,6 +139,13 @@ public GenericTdsServer(T arguments, QueryEngine queryEngine) public OnAuthenticationCompletedDelegate OnAuthenticationResponseCompleted { private get; set; } + /// + /// Delegate invoked after a SQL batch response is prepared but before it is + /// sent to the client. Tests can use this to inject or replace tokens in the + /// response message. + /// + public Action OnSQLBatchCompleted { get; set; } + public OnLogin7ValidatedDelegate OnLogin7Validated { private get; set; } @@ -451,6 +458,9 @@ public virtual TDSMessageCollection OnSQLBatchRequest(ITDSServerSession session, session.PacketSize = (uint)Arguments.PacketSize; } + // Allow tests to modify or inject tokens into the response + OnSQLBatchCompleted?.Invoke(responseMessage[0]); + return responseMessage; } diff --git a/tools/props/Versions.props b/tools/props/Versions.props index 739055dbf0..16ce7defda 100644 --- a/tools/props/Versions.props +++ b/tools/props/Versions.props @@ -31,7 +31,7 @@ - 7.0.0 + 7.0.2 $(MdsVersionDefault).$(BuildNumber)-dev @@ -67,4 +67,29 @@ + + + + 6.0.2 + [$(SniVersion), $([MSBuild]::Add($(SniVersion.Split('.')[0]), 1)).0.0) + [1.0.0, 2.0.0) + [$(AbstractionsPackageVersion), $([MSBuild]::Add($(AbstractionsPackageVersion.Trim().Split('.')[0]), 1)).0.0) + [$(LoggingPackageVersion), $([MSBuild]::Add($(LoggingPackageVersion.Trim().Split('.')[0]), 1)).0.0) + [$(MdsPackageVersion), $([MSBuild]::Add($(MdsPackageVersion.Trim().Split('.')[0]), 1)).0.0) + [$(AzurePackageVersion), $([MSBuild]::Add($(AzurePackageVersion.Trim().Split('.')[0]), 1)).0.0) + [$(AkvPackageVersion), $([MSBuild]::Add($(AkvPackageVersion.Trim().Split('.')[0]), 1)).0.0) + + diff --git a/tools/scripts/downloadLatestNuget.ps1 b/tools/scripts/downloadLatestNuget.ps1 deleted file mode 100644 index faddf3bb09..0000000000 --- a/tools/scripts/downloadLatestNuget.ps1 +++ /dev/null @@ -1,27 +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. -# Script: downloadLatestNuget.ps1 -# Author: Keerat Singh -# Date: 07-Dec-2018 -# Comments: This script downloads the latest NuGet Binary. -# -param( - [string]$nugetDestPath, - [string]$nugetSrcPath="https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -) -Function DownloadLatestNuget() -{ - if(!$nugetDestPath) - { - $nugetDestPath = (Get-location).ToString() +'\.nuget\' - } - if (!(Test-Path $nugetDestPath)) - { - New-Item -ItemType Directory -Path $nugetDestPath - } - Write-Output "Source: $nugetSrcPath" - Write-Output "Destination: $nugetDestPath" - Start-BitsTransfer -Source $nugetSrcPath -Destination $nugetDestPath\nuget.exe -} -DownloadLatestNuget \ No newline at end of file diff --git a/tools/specs/Microsoft.Data.SqlClient.nuspec b/tools/specs/Microsoft.Data.SqlClient.nuspec index cbea2140c7..426548984b 100644 --- a/tools/specs/Microsoft.Data.SqlClient.nuspec +++ b/tools/specs/Microsoft.Data.SqlClient.nuspec @@ -29,21 +29,28 @@ sqlclient microsoft.data.sqlclient - - - + + + + + @@ -54,37 +61,37 @@ - - - + + + - + - - - + + + - + - - - + + + - + diff --git a/tools/targets/GenerateMdsPackage.targets b/tools/targets/GenerateMdsPackage.targets index b4bf4a0d5c..d77b175fec 100644 --- a/tools/targets/GenerateMdsPackage.targets +++ b/tools/targets/GenerateMdsPackage.targets @@ -5,10 +5,23 @@ - + - - + + + + + + + +