diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationFeatureSwitches.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AuthenticationFeatureSwitches.cs index a9a3ecc426..be278994e8 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 @@ -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 after publishing. + [FeatureGuard(typeof(RequiresUnreferencedCodeAttribute))] + [FeatureGuard(typeof(RequiresDynamicCodeAttribute))] + [UnconditionalSuppressMessage("Trimming", "IL4000", + Justification = "Runtime assembly replacement is unavailable in fixed trimmed/AOT images.")] + 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..09d0ec8a02 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,22 @@ 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. + +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. 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 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..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. @@ -62,6 +62,10 @@ private void EnsureInitialized() _initializing = true; try { + if (AuthenticationFeatureSwitches.IsRuntimeVersionValidationSupported) + { + ValidateSqlClientVersion(); + } if (AuthenticationFeatureSwitches.IsAppConfigSupported) { LoadConfiguration(); @@ -125,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. /// @@ -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..736aa69cd1 100644 --- a/src/Microsoft.Data.SqlClient/src/PackageReadme.md +++ b/src/Microsoft.Data.SqlClient/src/PackageReadme.md @@ -78,6 +78,15 @@ 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. 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, 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: ```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 + +