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/Directory.Packages.props b/Directory.Packages.props
index b1a858bf72..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,14 +97,15 @@
+
-
-
+
+
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/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/steps/configure-sql-server-macos-step.yml b/eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml
index a989bf19bf..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
@@ -34,8 +34,19 @@ steps:
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
+ # 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
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/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/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-sqlclient-package-job.yml b/eng/pipelines/onebranch/jobs/build-signed-sqlclient-package-job.yml
index 98f5c811aa..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,17 +93,29 @@ 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:
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/variables/common-variables.yml b/eng/pipelines/onebranch/variables/common-variables.yml
index 35236f7ff6..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,7 +84,7 @@ variables:
# The NuGet package version for GA releases (non-preview).
- name: mdsPackageVersion
- value: '7.0.1'
+ value: '7.0.2'
# The NuGet package version for preview releases.
#
@@ -97,7 +97,7 @@ variables:
# The AssemblyFileVersion for all assemblies in the MDS package.
- name: mdsAssemblyFileVersion
- value: 7.0.1.$(assemblyBuildNumber)
+ value: 7.0.2.$(assemblyBuildNumber)
# The path to the NuGet packaging specification file used to generate the MDS NuGet package.
- name: nuspecPath
@@ -111,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
@@ -129,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
@@ -165,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 35ec3a15cd..a6646d12af 100644
--- a/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml
+++ b/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml
@@ -48,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 c12fcee659..5069bbea5e 100644
--- a/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml
+++ b/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml
@@ -48,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.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 48cbbf1a80..3c51141e58 100644
--- a/global.json
+++ b/global.json
@@ -4,13 +4,14 @@
// We currently require the .NET 10 SDK to build and test the project.
//
// 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 specify the most
- // recent 10.0.1xx series release which is compatible with MSBuild 17.x.
+ // .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.
//
- "version": "10.0.107",
+ "version": "10.0.109",
- // We cannot allow any roll forward due to the above MSBuild compatibility issues.
- "rollForward": "disable",
+ // 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/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