diff --git a/doc/Directory.Packages.props b/doc/Directory.Packages.props index 784c37e9c3..66b295ed74 100644 --- a/doc/Directory.Packages.props +++ b/doc/Directory.Packages.props @@ -3,7 +3,7 @@ - + diff --git a/doc/apps/AzureAuthentication/App.cs b/doc/apps/AzureAuthentication/App.cs new file mode 100644 index 0000000000..3447e5489f --- /dev/null +++ b/doc/apps/AzureAuthentication/App.cs @@ -0,0 +1,203 @@ +using System.Diagnostics; + +using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider; + +namespace Microsoft.Data.SqlClient.Samples.AzureAuthentication; + +/// +/// Console application that validates SqlClient connectivity using Entra ID (formerly Azure Active +/// Directory) authentication. +/// +public class App : IDisposable +{ + // ────────────────────────────────────────────────────────────────── + #region Construction / Disposal + + /// + public void Dispose() + { + _eventListener?.Dispose(); + } + + #endregion + + // ────────────────────────────────────────────────────────────────── + #region Internal Methods + + /// + /// Options for . + /// + internal class RunOptions + { + /// + /// The ADO.NET connection string to use. + /// + public string ConnectionString { get; set; } = string.Empty; + + /// + /// When , SqlClient events are emitted to the console. + /// + public bool LogEvents { get; set; } = false; + + /// + /// When , execution pauses to allow dotnet-trace attachment. + /// + public bool Trace { get; set; } = false; + + /// + /// When , detailed error information is displayed. + /// + public bool Verbose { get; set; } = false; + } + + /// + /// Runs the connectivity test against SQL Server using the specified options. + /// + /// The options controlling the connectivity test. + /// 0 on success; non-zero on failure. + internal int Run(RunOptions options) + { + Out($""" + {AppName} + --------------------------- + + Packages used: + SqlClient: {PackageVersions.MicrosoftDataSqlClient} + AKV Provider: {PackageVersions.MicrosoftDataSqlClientAlwaysEncryptedAzureKeyVaultProvider} + Azure: {PackageVersions.AzureExtensionsVersion} + + """); + + try + { + // Canonicalize the connection string for emission. + SqlConnectionStringBuilder builder = new(options.ConnectionString); + + Out($""" + Connection details: + Data Source: {builder.DataSource} + Initial Catalog: {builder.InitialCatalog} + Authentication: {builder.Authentication} + + """); + + if (options.Verbose) + { + Out($""" + Full connection string: + {builder} + + """); + } + } + catch (Exception ex) + { + Err($""" + Failed to parse connection string: + {ex.Message} + """); + + if (options.Verbose) + { + Err($" {ex}"); + } + return 1; + } + + // Enable SqlClient event logging if requested. + if (options.LogEvents) + { + string prefix = "[EVENT]"; + Out($"SqlClient event logging enabled; events will be prefixed with {prefix}"); + + _eventListener = new SqlClientEventListener(Out, prefix); + } + + // Pause for trace attachment if requested. + if (options.Trace) + { + Out($""" + Execution paused; attach dotnet-trace and press Enter to resume: + + dotnet-trace collect -p {Process.GetCurrentProcess().Id} --providers Microsoft.Data.SqlClient.EventSource:1FFF:5 + + """); + Console.ReadLine(); + } + + // Touch the AKV Provider type to ensure its assembly is present and loadable. + _ = typeof(SqlColumnEncryptionAzureKeyVaultProvider); + + try + { + Out("Testing connectivity..."); + + using SqlConnection connection = new(options.ConnectionString); + connection.Open(); + + Console.ForegroundColor = ConsoleColor.Green; + Out("Connected successfully!"); + Console.ResetColor(); + Out($" Server version: {connection.ServerVersion}"); + + return 0; + } + catch (Exception ex) + { + Err($""" + Connection failed: + {ex.Message} + """); + + if (options.Verbose) + { + Err($" {ex}"); + } + + return 1; + } + } + + #endregion + + // ────────────────────────────────────────────────────────────────── + #region Private Helpers + + /// + /// Writes an informational message to standard output. + /// + /// The message to write. + internal static void Out(string message) + { + Console.Out.WriteLine(message); + } + + /// + /// Writes an error message to standard error in red. + /// + /// The message to write. + internal static void Err(string message) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.Error.WriteLine(message); + Console.ResetColor(); + } + + #endregion + + // ────────────────────────────────────────────────────────────────── + #region Private Fields + + /// + /// The display name of the application. + /// + internal const string AppName = "Azure Authentication Tester"; + + /// + /// The optional event listener used to capture SqlClient diagnostic events. + /// + private SqlClientEventListener? _eventListener; + + #endregion +} diff --git a/doc/apps/AzureAuthentication/AzureAuthentication.csproj b/doc/apps/AzureAuthentication/AzureAuthentication.csproj new file mode 100644 index 0000000000..8b7c6f5ee9 --- /dev/null +++ b/doc/apps/AzureAuthentication/AzureAuthentication.csproj @@ -0,0 +1,45 @@ + + + + Exe + net481;net10.0 + Microsoft.Data.SqlClient.Samples.AzureAuthentication + enable + enable + latest + + + + + + + + + + + + + + + $(DefineConstants);AZURE_EXTENSIONS + + + + + + + + + + + diff --git a/doc/apps/AzureAuthentication/Directory.Build.props b/doc/apps/AzureAuthentication/Directory.Build.props new file mode 100644 index 0000000000..425707ecad --- /dev/null +++ b/doc/apps/AzureAuthentication/Directory.Build.props @@ -0,0 +1,10 @@ + + + + + + + true + + + diff --git a/doc/apps/AzureAuthentication/Directory.Packages.props b/doc/apps/AzureAuthentication/Directory.Packages.props new file mode 100644 index 0000000000..b512683765 --- /dev/null +++ b/doc/apps/AzureAuthentication/Directory.Packages.props @@ -0,0 +1,32 @@ + + + + + + + + 7.0.0-preview4.26064.3 + + + 7.0.0-preview1.26064.3 + + + + + + + + + + + + + + + + + + diff --git a/doc/apps/AzureAuthentication/EntryPoint.cs b/doc/apps/AzureAuthentication/EntryPoint.cs new file mode 100644 index 0000000000..847224e289 --- /dev/null +++ b/doc/apps/AzureAuthentication/EntryPoint.cs @@ -0,0 +1,85 @@ +using System.CommandLine; + +namespace Microsoft.Data.SqlClient.Samples.AzureAuthentication; + +/// +/// Contains the application entry point responsible for parsing command-line arguments and +/// delegating execution to . +/// +public static class EntryPoint +{ + /// + /// Application entry point. Parses command-line arguments and executes the connectivity test. + /// + /// Command-line arguments. + /// 0 on success; non-zero on failure. + public static int Main(string[] args) + { + Option connectionStringOption = new("--connection-string", "-c") + { + Description = + "The ADO.NET connection string used to connect to SQL Server. " + + "Supports SQL, Azure AD, and integrated authentication modes.", + Required = true + }; + + Option logOption = new("--log-events", "-l") + { + Description = "Enable SqlClient event emission to the console." + }; + + Option traceOption = new("--trace", "-t") + { + Description = "Pauses execution to allow dotnet-trace to be attached." + }; + + Option verboseOption = new("--verbose", "-v") + { + Description = "Enable verbose output with detailed error information." + }; + + RootCommand rootCommand = new( + $""" + {App.AppName} + ----------------------------------------- + + Validates SqlClient connectivity using EntraID (formerly Azure Active Directory) + authentication. Connects to SQL Server using the supplied connection string, + which must specify the authentication method. + + Supply specific package versions when building to test different versions of the + SqlClient suite, for example: + + -p:SqlClientVersion=7.0.0-preview4 + -p:AkvProviderVersion=7.0.1-preview2 + -p:AzureVersion=1.0.0-preview1 + + Current package versions: + SqlClient: {PackageVersions.MicrosoftDataSqlClient} + AKV Provider: {PackageVersions.MicrosoftDataSqlClientAlwaysEncryptedAzureKeyVaultProvider} + Azure: {PackageVersions.AzureExtensionsVersion} + """) + { + connectionStringOption, + logOption, + traceOption, + verboseOption + }; + + rootCommand.SetAction(parseResult => + { + App.RunOptions options = new() + { + ConnectionString = parseResult.GetValue(connectionStringOption)!, + LogEvents = parseResult.GetValue(logOption), + Trace = parseResult.GetValue(traceOption), + Verbose = parseResult.GetValue(verboseOption) + }; + + using App app = new(); + return app.Run(options); + }); + + return rootCommand.Parse(args).Invoke(); + } +} diff --git a/doc/apps/AzureAuthentication/GeneratePackageVersions.targets b/doc/apps/AzureAuthentication/GeneratePackageVersions.targets new file mode 100644 index 0000000000..191860c3fe --- /dev/null +++ b/doc/apps/AzureAuthentication/GeneratePackageVersions.targets @@ -0,0 +1,96 @@ + + + + + + + + + + + + +( + System.StringComparer.OrdinalIgnoreCase); +foreach (var v in Versions) + versionMap[v.ItemSpec] = v.GetMetadata("Version"); + +var sb = new System.Text.StringBuilder(); +sb.AppendLine("// "); +sb.AppendLine(); +sb.AppendLine($"namespace {Namespace};"); +sb.AppendLine(); +sb.AppendLine("internal static partial class PackageVersions"); +sb.AppendLine("{"); +foreach (var pkg in Packages) +{ + var version = pkg.GetMetadata("Version"); + if (string.IsNullOrEmpty(version)) + versionMap.TryGetValue(pkg.ItemSpec, out version); + if (string.IsNullOrEmpty(version)) + continue; + var name = pkg.ItemSpec.Replace(".", ""); + sb.AppendLine($" public const string {name} = \"{version}\";"); +} +sb.AppendLine("}"); + +var dir = System.IO.Path.GetDirectoryName(OutputFile); +if (!string.IsNullOrEmpty(dir) && !System.IO.Directory.Exists(dir)) + System.IO.Directory.CreateDirectory(dir); + +var content = sb.ToString(); +if (!System.IO.File.Exists(OutputFile) || System.IO.File.ReadAllText(OutputFile) != content) + System.IO.File.WriteAllText(OutputFile, content); +]]> + + + + + + + + + + + + + + diff --git a/doc/apps/AzureAuthentication/NuGet.config b/doc/apps/AzureAuthentication/NuGet.config new file mode 100644 index 0000000000..1c814bbc3c --- /dev/null +++ b/doc/apps/AzureAuthentication/NuGet.config @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/doc/apps/AzureAuthentication/PackageVersions.Partial.cs b/doc/apps/AzureAuthentication/PackageVersions.Partial.cs new file mode 100644 index 0000000000..3c88781daa --- /dev/null +++ b/doc/apps/AzureAuthentication/PackageVersions.Partial.cs @@ -0,0 +1,23 @@ +namespace Microsoft.Data.SqlClient.Samples.AzureAuthentication; + +/// +/// Hand-written companion to the auto-generated PackageVersions.g.cs file +/// (produced by GeneratePackageVersions.targets at build time). +/// +/// The generated half contains a string constant for every NuGet PackageReference +/// in the project. This partial class adds members that require conditional +/// compilation (e.g. the AZURE_EXTENSIONS define) and therefore cannot be +/// expressed by the purely data-driven code generator. +/// +internal static partial class PackageVersions +{ + /// + /// Version of the Azure extensions package, or "N/A" when not referenced. + /// + public const string AzureExtensionsVersion = + #if AZURE_EXTENSIONS + MicrosoftDataSqlClientExtensionsAzure; + #else + "N/A"; + #endif +} diff --git a/doc/apps/AzureAuthentication/README.md b/doc/apps/AzureAuthentication/README.md new file mode 100644 index 0000000000..20a3c628ba --- /dev/null +++ b/doc/apps/AzureAuthentication/README.md @@ -0,0 +1,165 @@ +# AzureAuthentication Sample App + +A minimal console application that verifies **SqlClient** can connect to a SQL Server using Entra ID +authentication (formerly Azure Active Directory authentication) via the **Azure** package. It also +references the **Azure Key Vault Provider** package to confirm there are no transitive dependency +conflicts between the packages. + +The following SqlClient packages are used, either directly or transitively: + +- `Microsoft.Data.SqlClient` +- `Microsoft.SqlServer.Server` +- `Microsoft.Data.SqlClient.Extensions.Logging` +- `Microsoft.Data.SqlClient.Extensions.Abstractions` +- `Microsoft.Data.SqlClient.Extensions.Azure` +- `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` + +## Purpose + +This app serves as a smoke test for package compatibility. It: + +1. Instantiates `SqlColumnEncryptionAzureKeyVaultProvider` to ensure the AKV provider assembly loads + without conflicts. +2. Opens a `SqlConnection` using a connection string you provide, validating that authentication and + connectivity work end-to-end. + +The app is designed to run against both **published NuGet packages** and **locally-built packages** +(via the `packages/` directory configured in `NuGet.config`). + +## Build Parameters + +Package versions are controlled through MSBuild properties. Pass them on the command line with `-p:` +(or `/p:`) to override the defaults defined in `Directory.Packages.props`. + +| Property | Default | Description | +| --- | --- | --- | +| `SqlClientVersion` | `7.0.0-preview4.26064.3` | Version of `Microsoft.Data.SqlClient` to reference. | +| `AkvProviderVersion` | `7.0.0-preview1.26064.3` | Version of `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` to reference. | +| `AzureVersion` | None | Version of `Microsoft.Data.SqlClient.Extensions.Azure` to reference. When omitted, the `Azure` package will not be referenced. | + +## Local Package Source + +The `NuGet.config` adds a `packages/` directory as a local package source. To test against packages +that haven't been published to NuGet yet, copy the `.nupkg` files into this folder and specify the +matching version via the build properties above. + +NuGet will cache copies of the packages it finds in `packages/` after a successful restore. If you +update the `.nupkg` files in `packages/` without incrementing their version numbers (and referencing +those new version numbers) you will have to clear the NuGet caches in order for the next restore +operation to pick them up: + +```bash +dotnet nuget locals all --clear +``` + +## Running the App + +The app has built-in help: + +```bash +dotnet run -- --help + +Description: + Azure Authentication Tester + --------------------------- + + Validates SqlClient connectivity using EntraID (formerly Azure Active Directory) authentication. + Connects to SQL Server using the supplied connection string, which must specify the authentication method. + + Supply specific package versions when building to test different versions of the SqlClient suite, for example: + + -p:SqlClientVersion=7.0.0.preview4 + -p:AkvProviderVersion=7.0.1-preview2 + -p:AzureVersion=1.0.0-preview1 + +Usage: + AzureAuthentication [options] + +Options: + -c, --connection-string (REQUIRED) The ADO.NET connection string used to connect to SQL Server. + Supports SQL, Azure AD, and integrated authentication modes. + -l, --log-events Enable SqlClient event emission to the console. + -t, --trace Pauses execution to allow dotnet-trace to be attached. + -v, --verbose Enable verbose output with detailed error information. + -?, -h, --help Show help and usage information + --version Show version information +``` + +The app expects a single argument: a full connection string. + +```bash +dotnet run -- -c "" +``` + +For Azure AD authentication, use an `Authentication` keyword in the connection string. For example: + +```bash +dotnet run -- -c "Server=myserver.database.windows.net;Database=mydb;Authentication=ActiveDirectoryDefault" +``` + +On success the app emits to standard out: + +```bash +Azure Authentication Tester +--------------------------- + +Packages used: + SqlClient: 7.0.0-preview4.26055.1 + AKV Provider: 6.1.2 + Azure: 1.0.0-preview1.26055.1 + +Connection details: + Data Source: adotest.database.windows.net + Initial Catalog: Northwind + Authentication: ActiveDirectoryPassword + +Testing connectivity... +Connected successfully! + Server version: 12.00.1017 +``` + +Errors will be emitted to standard error: + +```bash +Testing connectivity... +Connection failed: + Cannot find an authentication provider for 'ActiveDirectoryPassword'. +``` + +### Examples + +Run with the default (published) package versions, and no `Azure` package: + +```bash +dotnet run -- -c "" +``` + +If the connection string specifies one of the Azure Active Directory authentication methods, +`SqlClient` will fail with an error indicating that no authentication provider has been registered. +This is because the `Azure` package was not referenced, and the app did not provide its own custom +authentication provider. + +Run against locally-built packages (drop `.nupkg` files into the `packages/` folder first): + +```bash +dotnet run -p:SqlClientVersion=7.0.0-preview4 -- -c "" +``` + +Run including the `Azure` extensions package: + +```bash +dotnet run -p:AzureVersion=1.0.0-preview1 -- -c "" +``` + +Override all three versions at once: + +```bash +dotnet run -p:SqlClientVersion=7.0.0-preview1 -p:AkvProviderVersion=7.0.0-preview1 -p:AzureVersion=1.0.0-preview1 -- -c "" +``` + +## Prerequisites + +- [.NET 10.0 SDK](https://dotnet.microsoft.com/download) and .NET Framework 4.8.1 or later. +- A SQL Server or Azure SQL instance accessible with Azure AD credentials. +- Azure credentials available to `DefaultAzureCredential` (e.g. Azure CLI login, environment + variables, or managed identity). diff --git a/doc/apps/AzureAuthentication/SqlClientEventListener.cs b/doc/apps/AzureAuthentication/SqlClientEventListener.cs new file mode 100644 index 0000000000..5e861f4796 --- /dev/null +++ b/doc/apps/AzureAuthentication/SqlClientEventListener.cs @@ -0,0 +1,66 @@ +using System.Diagnostics.Tracing; + +namespace Microsoft.Data.SqlClient.Samples.AzureAuthentication; + +/// +/// Listens for events from Microsoft.Data.SqlClient.EventSource and emits them via the +/// supplied output function. +/// +internal sealed class SqlClientEventListener : EventListener +{ + // ────────────────────────────────────────────────────────────────── + #region Construction / Disposal + + /// + /// Initializes a new instance of the class. + /// + /// The delegate invoked for each event message. + /// The prefix prepended to each emitted event message. + internal SqlClientEventListener(Action output, string prefix) + { + _out = output; + _prefix = prefix; + } + + #endregion + + // ────────────────────────────────────────────────────────────────── + #region Derived Methods + + /// + protected override void OnEventSourceCreated(EventSource eventSource) + { + if (eventSource.Name.Equals( + "Microsoft.Data.SqlClient.EventSource", StringComparison.Ordinal)) + { + // Enable all keywords at all levels. + EnableEvents(eventSource, EventLevel.LogAlways, EventKeywords.All); + } + } + + /// + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + _out($"{_prefix} {eventData.EventName}: " + + (eventData.Payload != null && eventData.Payload.Count > 0 + ? eventData.Payload[0] + : string.Empty)); + } + + #endregion + + // ────────────────────────────────────────────────────────────────── + #region Private Fields + + /// + /// The delegate used to emit event messages. + /// + private readonly Action _out; + + /// + /// The prefix prepended to each emitted event message. + /// + private readonly string _prefix; + + #endregion +} diff --git a/doc/apps/AzureAuthentication/packages/.gitkeep b/doc/apps/AzureAuthentication/packages/.gitkeep new file mode 100644 index 0000000000..eb0f557d02 --- /dev/null +++ b/doc/apps/AzureAuthentication/packages/.gitkeep @@ -0,0 +1,2 @@ +# The presence of this file ensures that git creates the packages/ directory, which must exist +# because it is declared in our NuGet.config. diff --git a/src/Microsoft.Data.SqlClient.sln b/src/Microsoft.Data.SqlClient.sln index 636064e61a..a82d9f616c 100644 --- a/src/Microsoft.Data.SqlClient.sln +++ b/src/Microsoft.Data.SqlClient.sln @@ -379,6 +379,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Data.SqlClient", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Data.SqlClient", "Microsoft.Data.SqlClient\src\Microsoft.Data.SqlClient.csproj", "{AA77C107-9A78-4A99-98BB-21FF7A1E0B01}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureAuthentication", "..\doc\apps\AzureAuthentication\AzureAuthentication.csproj", "{C3FE67C1-D288-45ED-A35C-08107396F8BB}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "notsupported", "notsupported", "{351BE847-A0BF-450C-A5BC-8337AFA49EAA}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Data.SqlClient", "Microsoft.Data.SqlClient\notsupported\Microsoft.Data.SqlClient.csproj", "{1DB299CE-95EA-4566-84DD-171768758291}" @@ -799,6 +801,18 @@ Global {AA77C107-9A78-4A99-98BB-21FF7A1E0B01}.Release|x64.Build.0 = Release|Any CPU {AA77C107-9A78-4A99-98BB-21FF7A1E0B01}.Release|x86.ActiveCfg = Release|Any CPU {AA77C107-9A78-4A99-98BB-21FF7A1E0B01}.Release|x86.Build.0 = Release|Any CPU + {C3FE67C1-D288-45ED-A35C-08107396F8BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3FE67C1-D288-45ED-A35C-08107396F8BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3FE67C1-D288-45ED-A35C-08107396F8BB}.Debug|x64.ActiveCfg = Debug|Any CPU + {C3FE67C1-D288-45ED-A35C-08107396F8BB}.Debug|x64.Build.0 = Debug|Any CPU + {C3FE67C1-D288-45ED-A35C-08107396F8BB}.Debug|x86.ActiveCfg = Debug|Any CPU + {C3FE67C1-D288-45ED-A35C-08107396F8BB}.Debug|x86.Build.0 = Debug|Any CPU + {C3FE67C1-D288-45ED-A35C-08107396F8BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3FE67C1-D288-45ED-A35C-08107396F8BB}.Release|Any CPU.Build.0 = Release|Any CPU + {C3FE67C1-D288-45ED-A35C-08107396F8BB}.Release|x64.ActiveCfg = Release|Any CPU + {C3FE67C1-D288-45ED-A35C-08107396F8BB}.Release|x64.Build.0 = Release|Any CPU + {C3FE67C1-D288-45ED-A35C-08107396F8BB}.Release|x86.ActiveCfg = Release|Any CPU + {C3FE67C1-D288-45ED-A35C-08107396F8BB}.Release|x86.Build.0 = Release|Any CPU {1DB299CE-95EA-4566-84DD-171768758291}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1DB299CE-95EA-4566-84DD-171768758291}.Debug|x64.ActiveCfg = Debug|Any CPU {1DB299CE-95EA-4566-84DD-171768758291}.Debug|x86.ActiveCfg = Debug|Any CPU