From 6d4e9e9282ff6f5893d475f5b7643e51c80ff872 Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:32:28 -0300 Subject: [PATCH 1/3] Enforce exact loaded authentication family versions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/AuthenticationFeatureSwitches.cs | 8 +++ .../Abstractions/src/PackageReadme.md | 19 +++++- .../src/SqlAuthenticationProviderRegistry.cs | 52 +++++++++++++++ .../test/RuntimeFamilyVersionTest.cs | 63 +++++++++++++++++++ .../src/PackageReadme.md | 8 +++ .../RuntimeVersionTest.csproj | 9 +++ 6 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/RuntimeFamilyVersionTest.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/AuthenticationTests/RuntimeVersionTest/RuntimeVersionTest.csproj diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationFeatureSwitches.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationFeatureSwitches.cs index a9a3ecc426..dc993b9b26 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationFeatureSwitches.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationFeatureSwitches.cs @@ -55,6 +55,14 @@ internal static class AuthenticationFeatureSwitches #region Trimming Guards + /// Indicates whether bootstrap may inspect loaded SqlClient family assembly versions. + // Fixed trimmed/AOT images cannot replace assemblies; their package graph is checked at build time. + [FeatureGuard(typeof(RequiresUnreferencedCodeAttribute))] + [FeatureGuard(typeof(RequiresDynamicCodeAttribute))] + [UnconditionalSuppressMessage("Trimming", "IL4000", + Justification = "Runtime assembly replacement is unavailable in fixed trimmed/AOT images; the package graph is checked at build time.")] + internal static bool IsRuntimeVersionValidationSupported => true; + /// /// Guards the reflection-based authentication configuration and initializer loading path. /// diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/PackageReadme.md b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/PackageReadme.md index 26c5241deb..908a641c9d 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/PackageReadme.md +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/PackageReadme.md @@ -22,7 +22,7 @@ This package supports: ## Authentication registration The provider registry and bootstrap live in this package. `GetProvider` and `SetProvider` -work directly, including under NativeAOT, without loading SqlClient through reflection. +work directly, including under NativeAOT, without a reflective registration bridge to SqlClient. Configuration providers and configured initializers take precedence over application registrations; application registrations take precedence over the discovered Azure default. Registry initialization failures return `null` and `false`, respectively. Null providers, @@ -48,6 +48,23 @@ each required authentication method before opening connections. The Azure provid by `Microsoft.Data.SqlClient.Extensions.Azure`. Publish-time overrides use `RuntimeHostConfigurationOption` with `Trim="true"`; runtime changes cannot affect trimming. +## Package version policy + +Upgrade all installed SqlClient family packages to the same exact version, including +prerelease labels: SqlClient, Abstractions, Azure, Logging and AKV. SqlServer.Server +and native SNI are independently versioned. + +Logging supplies the consumer build/transitive checks (`SQLCLIENT001`). Untrimmed +registry bootstrap also compares the loaded SqlClient and discovered Azure assemblies' +informational versions against Abstractions, excluding source-control build metadata. +Mismatches are cached and traced with upgrade/restart guidance; `GetProvider` and +`SetProvider` return `null` and `false`. Disabling configuration or Azure discovery +does not disable the SqlClient runtime check. + +Trimmed/NativeAOT fixed images remove reflective runtime validation and rely on +build-time package validation. The emergency `SqlClientEnforceFamilyVersions=false` +build opt-out does not disable runtime checks or make mixed versions supported. + ## Authentication configuration The public `SqlAuthenticationProviderConfigurationSection` (legacy diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderRegistry.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderRegistry.cs index 771d839e38..ebb10fd66a 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderRegistry.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderRegistry.cs @@ -62,6 +62,10 @@ private void EnsureInitialized() _initializing = true; try { + if (AuthenticationFeatureSwitches.IsRuntimeVersionValidationSupported) + { + ValidateSqlClientVersion(); + } if (AuthenticationFeatureSwitches.IsAppConfigSupported) { LoadConfiguration(); @@ -205,6 +209,7 @@ private void LoadAzureExtension() return; } + ValidateFamilyVersion(assembly); SqlClientEventSource.Log.TryTraceEvent( nameof(SqlAuthenticationProviderRegistry) + ": Azure extension assembly={0} found; " + @@ -693,6 +698,53 @@ private bool Register(SqlAuthenticationMethod authenticationMethod, return default; } + /// Loads SqlClient and validates its exact family version against Abstractions. + /// A missing SqlClient assembly is logged and does not prevent registration. + /// The family versions differ or cannot be determined. + [RequiresUnreferencedCode("The SqlClient assembly is loaded by name for version validation.")] + [RequiresDynamicCode("The SqlClient assembly is loaded dynamically.")] + private static void ValidateSqlClientVersion() + { + Assembly assembly; + try + { + assembly = Assembly.Load("Microsoft.Data.SqlClient"); + } + catch (FileNotFoundException e) + { + SqlClientEventSource.Log.TryTraceEvent( + "SqlClient version validation skipped: assembly not installed. {0}", e.Message); + return; + } + ValidateFamilyVersion(assembly); + } + + /// Enforces exact family-version agreement between an assembly and Abstractions. + /// The loaded SqlClient family assembly to validate. + /// The family versions differ or cannot be determined. + internal static void ValidateFamilyVersion(Assembly assembly) + { + string expected = GetFamilyVersion(typeof(SqlAuthenticationProvider).Assembly); + string actual = GetFamilyVersion(assembly); + if (!string.Equals(expected, actual, StringComparison.Ordinal)) + { + throw new InvalidOperationException( + $"SqlClient family version mismatch: {assembly.GetName().Name} is {actual}, " + + $"but Microsoft.Data.SqlClient.Extensions.Abstractions is {expected}. " + + "Upgrade all SqlClient family packages together."); + } + } + + /// Reads an assembly's informational version without build metadata. + /// The assembly whose family version is required. + /// The version including any prerelease label, but excluding the build metadata suffix. + /// The assembly has no informational version attribute. + private static string GetFamilyVersion(Assembly assembly) => + assembly.GetCustomAttribute()? + .InformationalVersion.Split('+')[0] + ?? throw new InvalidOperationException( + $"Cannot determine the exact SqlClient family version of {assembly.GetName().Name}."); + /// Maps a configured authentication method name to its enum value, ignoring case. /// The authentication method name from a configured provider entry. /// The corresponding supported Active Directory authentication method. diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/RuntimeFamilyVersionTest.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/RuntimeFamilyVersionTest.cs new file mode 100644 index 0000000000..fcd183d525 --- /dev/null +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/test/RuntimeFamilyVersionTest.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Reflection; +using System.Reflection.Emit; + +namespace Microsoft.Data.SqlClient.Extensions.Abstractions.Test; + +/// Checks exact loaded-family versions independently of assembly binding versions. +public class RuntimeFamilyVersionTest +{ + /// The owning assembly is always accepted as the expected family version. + [Fact] + public void MatchingRuntimeVersionSucceeds() => + SqlAuthenticationProviderRegistry.ValidateFamilyVersion(typeof(SqlAuthenticationProvider).Assembly); + + /// Source-control build metadata does not change an otherwise identical package version. + [Fact] + public void MatchingVersionWithDifferentBuildMetadataSucceeds() + { + string expected = typeof(SqlAuthenticationProvider).Assembly + .GetCustomAttribute()!.InformationalVersion.Split('+')[0]; + SqlAuthenticationProviderRegistry.ValidateFamilyVersion(CreateAssembly(expected + "+different.commit")); + } + + /// Different stable or prerelease versions fail with exact versions and upgrade guidance. + [Theory] + [InlineData("7.0.1")] + [InlineData("1.0.0")] + [InlineData("8.0.0-different-preview")] + public void MixedRuntimeVersionThrows(string actual) + { + InvalidOperationException error = Assert.Throws( + () => SqlAuthenticationProviderRegistry.ValidateFamilyVersion(CreateAssembly(actual))); + Assert.Contains("version mismatch", error.Message); + Assert.Contains(actual, error.Message); + Assert.Contains("Microsoft.Data.SqlClient.Extensions.Abstractions", error.Message); + Assert.Contains("Upgrade all SqlClient family packages together", error.Message); + } + + /// Assemblies lacking version evidence are rejected rather than silently accepted. + [Fact] + public void MissingInformationalVersionThrows() + { + InvalidOperationException error = Assert.Throws( + () => SqlAuthenticationProviderRegistry.ValidateFamilyVersion(CreateAssembly(null))); + Assert.Contains("Cannot determine the exact SqlClient family version", error.Message); + } + + /// Creates an in-memory assembly with controlled version metadata and no filesystem side effects. + private static Assembly CreateAssembly(string? version) + { + var assembly = AssemblyBuilder.DefineDynamicAssembly( + new AssemblyName("RuntimeVersionFixture"), AssemblyBuilderAccess.Run); + if (version is not null) + { + ConstructorInfo constructor = typeof(AssemblyInformationalVersionAttribute).GetConstructor([typeof(string)])!; + assembly.SetCustomAttribute(new CustomAttributeBuilder(constructor, [version])); + } + return assembly; + } +} diff --git a/src/Microsoft.Data.SqlClient/src/PackageReadme.md b/src/Microsoft.Data.SqlClient/src/PackageReadme.md index d299d594a9..be0371d0be 100644 --- a/src/Microsoft.Data.SqlClient/src/PackageReadme.md +++ b/src/Microsoft.Data.SqlClient/src/PackageReadme.md @@ -78,6 +78,14 @@ on first access. Failed initialization or registration returns `null`/`false` an diagnostic guidance through `Microsoft.Data.SqlClient.EventSource` (Informational level, Trace keyword `0x2`). Bootstrap failures are cached; correct the cause and restart the application. +Upgrade SqlClient, Abstractions, Azure, Logging and AKV together to the same exact package +version, including prerelease labels. Logging supplies the `SQLCLIENT001` consumer build +check. Untrimmed bootstrap also validates loaded SqlClient and discovered Azure informational +versions against Abstractions; source-control build metadata is ignored. Disabling app.config +or discovery does not disable the SqlClient runtime check. Fixed trimmed/AOT images remove +reflective runtime validation and rely on build-time package checks. SqlServer.Server and +native SNI remain independently versioned. + With this package reference, you can continue to use Entra ID authentication modes directly in your connection string: ```csharp diff --git a/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/RuntimeVersionTest/RuntimeVersionTest.csproj b/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/RuntimeVersionTest/RuntimeVersionTest.csproj new file mode 100644 index 0000000000..9ae02a6a17 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/AuthenticationTests/RuntimeVersionTest/RuntimeVersionTest.csproj @@ -0,0 +1,9 @@ + + + Microsoft.Data.SqlClient + net462;net10.0 + 7.0.1 + $(SqlClientAssemblyVersion) + false + + From 1dc5bc0da2c77d9c669ed66df27a968414ffef06 Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:42:39 -0300 Subject: [PATCH 2/3] Describe runtime validation in authentication bootstrap documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Abstractions/src/AuthenticationFeatureSwitches.cs | 2 +- .../Abstractions/src/SqlAuthenticationProviderRegistry.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationFeatureSwitches.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationFeatureSwitches.cs index dc993b9b26..de9992850c 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationFeatureSwitches.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationFeatureSwitches.cs @@ -8,7 +8,7 @@ namespace Microsoft.Data.SqlClient; /// /// Defines authentication bootstrap switches and capability guards so configuration loading, Azure -/// extension discovery can be excluded from trimmed or AOT builds. +/// extension discovery, and runtime version checks can be excluded from trimmed or AOT builds. /// /// /// Configuration and discovery default to enabled to preserve existing runtime behavior. Each diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderRegistry.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderRegistry.cs index ebb10fd66a..6ec312f8ad 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderRegistry.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProviderRegistry.cs @@ -43,7 +43,7 @@ internal sealed class SqlAuthenticationProviderRegistry private ExceptionDispatchInfo? _bootstrapFailure; /// - /// Runs the enabled configuration and Azure discovery steps on first access. + /// Runs the enabled version validation, configuration and Azure discovery steps on first access. /// /// /// Reentrant initializer registrations can use the registry while bootstrap is in progress. @@ -129,7 +129,7 @@ private void LoadConfiguration() /// Discovers the optional Azure extension and registers its provider as the authentication default. /// /// - /// Validates the extension's public key token in signed builds. + /// Validates the extension's family version and, in signed builds, its public key token. /// Defaults do not replace existing registrations. Handled loading or construction failures /// are logged and leave the application responsible for supplying a provider. /// From 78c9f155c18b3d37b080e3e468523e393061d243 Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Wed, 14 Oct 2026 08:20:58 -0300 Subject: [PATCH 3/3] Decouple authentication guidance from consumer version enforcement Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Abstractions/src/AuthenticationFeatureSwitches.cs | 4 ++-- .../Abstractions/src/PackageReadme.md | 9 ++++----- src/Microsoft.Data.SqlClient/src/PackageReadme.md | 11 ++++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationFeatureSwitches.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationFeatureSwitches.cs index de9992850c..be278994e8 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationFeatureSwitches.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationFeatureSwitches.cs @@ -56,11 +56,11 @@ internal static class AuthenticationFeatureSwitches #region Trimming Guards /// Indicates whether bootstrap may inspect loaded SqlClient family assembly versions. - // Fixed trimmed/AOT images cannot replace assemblies; their package graph is checked at build time. + // Fixed trimmed/AOT images cannot replace assemblies after publishing. [FeatureGuard(typeof(RequiresUnreferencedCodeAttribute))] [FeatureGuard(typeof(RequiresDynamicCodeAttribute))] [UnconditionalSuppressMessage("Trimming", "IL4000", - Justification = "Runtime assembly replacement is unavailable in fixed trimmed/AOT images; the package graph is checked at build time.")] + Justification = "Runtime assembly replacement is unavailable in fixed trimmed/AOT images.")] internal static bool IsRuntimeVersionValidationSupported => true; /// diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/PackageReadme.md b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/PackageReadme.md index 908a641c9d..09d0ec8a02 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/PackageReadme.md +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/PackageReadme.md @@ -54,16 +54,15 @@ Upgrade all installed SqlClient family packages to the same exact version, inclu prerelease labels: SqlClient, Abstractions, Azure, Logging and AKV. SqlServer.Server and native SNI are independently versioned. -Logging supplies the consumer build/transitive checks (`SQLCLIENT001`). Untrimmed -registry bootstrap also compares the loaded SqlClient and discovered Azure assemblies' +Untrimmed registry bootstrap compares the loaded SqlClient and discovered Azure assemblies' informational versions against Abstractions, excluding source-control build metadata. Mismatches are cached and traced with upgrade/restart guidance; `GetProvider` and `SetProvider` return `null` and `false`. Disabling configuration or Azure discovery does not disable the SqlClient runtime check. -Trimmed/NativeAOT fixed images remove reflective runtime validation and rely on -build-time package validation. The emergency `SqlClientEnforceFamilyVersions=false` -build opt-out does not disable runtime checks or make mixed versions supported. +Trimmed/NativeAOT fixed images remove reflective runtime validation. Ensure the +resolved package versions match before publishing; consumer build-time enforcement +is maintained separately from authentication bootstrap. Mixed versions are not supported. ## Authentication configuration diff --git a/src/Microsoft.Data.SqlClient/src/PackageReadme.md b/src/Microsoft.Data.SqlClient/src/PackageReadme.md index be0371d0be..736aa69cd1 100644 --- a/src/Microsoft.Data.SqlClient/src/PackageReadme.md +++ b/src/Microsoft.Data.SqlClient/src/PackageReadme.md @@ -79,12 +79,13 @@ diagnostic guidance through `Microsoft.Data.SqlClient.EventSource` (Informationa Trace keyword `0x2`). Bootstrap failures are cached; correct the cause and restart the application. Upgrade SqlClient, Abstractions, Azure, Logging and AKV together to the same exact package -version, including prerelease labels. Logging supplies the `SQLCLIENT001` consumer build -check. Untrimmed bootstrap also validates loaded SqlClient and discovered Azure informational -versions against Abstractions; source-control build metadata is ignored. Disabling app.config +version, including prerelease labels. Untrimmed bootstrap validates loaded SqlClient and +discovered Azure informational versions against Abstractions; source-control build metadata +is ignored. Disabling app.config or discovery does not disable the SqlClient runtime check. Fixed trimmed/AOT images remove -reflective runtime validation and rely on build-time package checks. SqlServer.Server and -native SNI remain independently versioned. +reflective runtime validation, so ensure the resolved package versions match before +publishing. Consumer build-time enforcement is maintained separately from authentication +bootstrap. SqlServer.Server and native SNI remain independently versioned. With this package reference, you can continue to use Entra ID authentication modes directly in your connection string: