From 33131a4fe61e61cf556bbb7b22aa273acc014470 Mon Sep 17 00:00:00 2001
From: Cheena Malhotra <13396919+cheenamalhotra@users.noreply.github.com>
Date: Thu, 18 Jun 2026 23:56:52 -0700
Subject: [PATCH 1/6] [7/0] Enable WAM Broker support for Entra ID Auth modes
(#4288)
---
Directory.Packages.props | 3 +-
.../AzureSqlConnector.csproj | 34 +
doc/apps/AzureSqlConnector/IdentityQuery.cs | 25 +
.../AzureSqlConnector/MainForm.Designer.cs | 394 ++++++++++++
doc/apps/AzureSqlConnector/MainForm.cs | 590 +++++++++++++++++
.../MainFormWorker.Designer.cs | 383 +++++++++++
doc/apps/AzureSqlConnector/MainFormWorker.cs | 598 ++++++++++++++++++
.../AzureSqlConnector/ModeSelectorForm.cs | 116 ++++
doc/apps/AzureSqlConnector/Program.cs | 39 ++
doc/apps/AzureSqlConnector/README.md | 137 ++++
.../ActiveDirectoryAuthenticationProvider.xml | 14 +-
.../src/SqlAuthenticationProvider.Internal.cs | 2 +-
.../ActiveDirectoryAuthenticationProvider.xml | 49 +-
...DirectoryAuthenticationProviderOptions.xml | 34 +
...DirectoryAuthenticationProvider.Windows.cs | 108 ++++
.../ActiveDirectoryAuthenticationProvider.cs | 184 +++++-
...eDirectoryAuthenticationProviderOptions.cs | 21 +
.../Azure/src/Azure.csproj | 1 +
.../Azure/src/Interop/Interop.GetAncestor.cs | 42 ++
.../src/Interop/Interop.GetConsoleWindow.cs | 27 +
.../Azure/test/AADAuthenticationTests.cs | 1 +
.../Azure/test/AADConnectionTest.cs | 114 ----
.../Azure/test/Config.cs | 1 +
.../Azure/test/DefaultAuthProviderTests.cs | 1 +
.../SqlAuthenticationProviderCollection.cs | 14 +
.../Azure/test/WamBrokerTests.cs | 316 +++++++++
src/Microsoft.Data.SqlClient.sln | 25 +-
.../Connection/SqlConnectionInternal.cs | 9 +-
.../SqlAuthenticationProviderManager.cs | 145 ++++-
.../src/Microsoft/Data/SqlClient/SqlUtil.cs | 5 +
.../src/Resources/Strings.Designer.cs | 11 +-
.../src/Resources/Strings.resx | 3 +
.../SqlAuthenticationProviderManagerTests.cs | 196 ++++++
33 files changed, 3469 insertions(+), 173 deletions(-)
create mode 100644 doc/apps/AzureSqlConnector/AzureSqlConnector.csproj
create mode 100644 doc/apps/AzureSqlConnector/IdentityQuery.cs
create mode 100644 doc/apps/AzureSqlConnector/MainForm.Designer.cs
create mode 100644 doc/apps/AzureSqlConnector/MainForm.cs
create mode 100644 doc/apps/AzureSqlConnector/MainFormWorker.Designer.cs
create mode 100644 doc/apps/AzureSqlConnector/MainFormWorker.cs
create mode 100644 doc/apps/AzureSqlConnector/ModeSelectorForm.cs
create mode 100644 doc/apps/AzureSqlConnector/Program.cs
create mode 100644 doc/apps/AzureSqlConnector/README.md
create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/doc/ActiveDirectoryAuthenticationProviderOptions.xml
create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/src/ActiveDirectoryAuthenticationProvider.Windows.cs
create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/src/ActiveDirectoryAuthenticationProviderOptions.cs
create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/src/Interop/Interop.GetAncestor.cs
create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/src/Interop/Interop.GetConsoleWindow.cs
create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/test/SqlAuthenticationProviderCollection.cs
create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Azure/test/WamBrokerTests.cs
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 4baf8cbfec..2d150a29a4 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -65,7 +65,7 @@
-
+
@@ -97,6 +97,7 @@
+
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/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 @@
-
+