Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Microsoft.Data.SqlClient;

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Configuration and discovery default to enabled to preserve existing runtime behavior. Each
Expand Down Expand Up @@ -55,6 +55,14 @@ internal static class AuthenticationFeatureSwitches

#region Trimming Guards

/// <summary>Indicates whether bootstrap may inspect loaded SqlClient family assembly versions.</summary>
// 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;
Comment on lines +60 to +64

/// <summary>
/// Guards the reflection-based authentication configuration and initializer loading path.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ internal sealed class SqlAuthenticationProviderRegistry
private ExceptionDispatchInfo? _bootstrapFailure;

/// <summary>
/// Runs the enabled configuration and Azure discovery steps on first access.
/// Runs the enabled version validation, configuration and Azure discovery steps on first access.
/// </summary>
/// <remarks>
/// Reentrant initializer registrations can use the registry while bootstrap is in progress.
Expand All @@ -62,6 +62,10 @@ private void EnsureInitialized()
_initializing = true;
try
{
if (AuthenticationFeatureSwitches.IsRuntimeVersionValidationSupported)
{
ValidateSqlClientVersion();
}
if (AuthenticationFeatureSwitches.IsAppConfigSupported)
{
LoadConfiguration();
Expand Down Expand Up @@ -125,7 +129,7 @@ private void LoadConfiguration()
/// Discovers the optional Azure extension and registers its provider as the authentication default.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
Expand Down Expand Up @@ -205,6 +209,7 @@ private void LoadAzureExtension()
return;
}

ValidateFamilyVersion(assembly);
SqlClientEventSource.Log.TryTraceEvent(
nameof(SqlAuthenticationProviderRegistry) +
": Azure extension assembly={0} found; " +
Expand Down Expand Up @@ -693,6 +698,53 @@ private bool Register(SqlAuthenticationMethod authenticationMethod,
return default;
}

/// <summary>Loads SqlClient and validates its exact family version against Abstractions.</summary>
/// <remarks>A missing SqlClient assembly is logged and does not prevent registration.</remarks>
/// <exception cref="InvalidOperationException">The family versions differ or cannot be determined.</exception>
[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);
}

/// <summary>Enforces exact family-version agreement between an assembly and Abstractions.</summary>
/// <param name="assembly">The loaded SqlClient family assembly to validate.</param>
/// <exception cref="InvalidOperationException">The family versions differ or cannot be determined.</exception>
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.");
}
}

/// <summary>Reads an assembly's informational version without build metadata.</summary>
/// <param name="assembly">The assembly whose family version is required.</param>
/// <returns>The version including any prerelease label, but excluding the build metadata suffix.</returns>
/// <exception cref="InvalidOperationException">The assembly has no informational version attribute.</exception>
private static string GetFamilyVersion(Assembly assembly) =>
assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.InformationalVersion.Split('+')[0]
?? throw new InvalidOperationException(
$"Cannot determine the exact SqlClient family version of {assembly.GetName().Name}.");

/// <summary>Maps a configured authentication method name to its enum value, ignoring case.</summary>
/// <param name="authentication">The authentication method name from a configured provider entry.</param>
/// <returns>The corresponding supported Active Directory authentication method.</returns>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>Checks exact loaded-family versions independently of assembly binding versions.</summary>
public class RuntimeFamilyVersionTest
{
/// <summary>The owning assembly is always accepted as the expected family version.</summary>
[Fact]
public void MatchingRuntimeVersionSucceeds() =>
SqlAuthenticationProviderRegistry.ValidateFamilyVersion(typeof(SqlAuthenticationProvider).Assembly);

/// <summary>Source-control build metadata does not change an otherwise identical package version.</summary>
[Fact]
public void MatchingVersionWithDifferentBuildMetadataSucceeds()
{
string expected = typeof(SqlAuthenticationProvider).Assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion.Split('+')[0];
SqlAuthenticationProviderRegistry.ValidateFamilyVersion(CreateAssembly(expected + "+different.commit"));
}

/// <summary>Different stable or prerelease versions fail with exact versions and upgrade guidance.</summary>
[Theory]
[InlineData("7.0.1")]
[InlineData("1.0.0")]
[InlineData("8.0.0-different-preview")]
public void MixedRuntimeVersionThrows(string actual)
{
InvalidOperationException error = Assert.Throws<InvalidOperationException>(
() => 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);
}

/// <summary>Assemblies lacking version evidence are rejected rather than silently accepted.</summary>
[Fact]
public void MissingInformationalVersionThrows()
{
InvalidOperationException error = Assert.Throws<InvalidOperationException>(
() => SqlAuthenticationProviderRegistry.ValidateFamilyVersion(CreateAssembly(null)));
Assert.Contains("Cannot determine the exact SqlClient family version", error.Message);
}

/// <summary>Creates an in-memory assembly with controlled version metadata and no filesystem side effects.</summary>
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;
}
}
9 changes: 9 additions & 0 deletions src/Microsoft.Data.SqlClient/src/PackageReadme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>Microsoft.Data.SqlClient</AssemblyName>
<TargetFrameworks>net462;net10.0</TargetFrameworks>
<Version>7.0.1</Version>
<AssemblyVersion>$(SqlClientAssemblyVersion)</AssemblyVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>
</Project>
Loading