dotnet / dotnet/SqlClient

Entra ID authentication broken under NativeAOT in v7.0 — Extensions.Azure reflection-based discovery is AOT-incompatible

Open
#4,193 5 comments 1 reaction 1 assignee Claimed by @paulmedynski View on GitHub
Regression :boom:
Dominant language
C#
Stars
989
Forks
340
Avg merge
4d 19h
Merged PRs (30d)
72

Description

### Describe the bug

SqlClient 7.0 moved `ActiveDirectoryAuthenticationProvider` from the core `Microsoft.Data.SqlClient` assembly into the separate `Microsoft.Data.SqlClient.Extensions.Azure` package. The provider is now discovered at runtime via reflection (`Assembly.Load` + `Activator.CreateInstance`), which breaks under NativeAOT because the linker has no static reference to follow and trims the assembly.

In v6.x, the provider was part of the core assembly and registered via a direct `new ActiveDirectoryAuthenticationProvider(...)` call in `SqlAuthenticationProviderManager.SetDefaultAuthProviders()`. This worked correctly under NativeAOT because the AOT compiler could trace the entire type hierarchy statically.

In v7.0, `SqlAuthenticationProviderManager`'s static constructor ([source](https://github.com/dotnet/SqlClient/blob/main/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs)) uses:
```csharp
var assembly = Assembly.Load("Microsoft.Data.SqlClient.Extensions.Azure");
var type = assembly.GetType("Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProvider");
var instance = Activator.CreateInstance(type, ...) as SqlAuthenticationProvider;
```

Under NativeAOT, this fails silently (caught by the exception filter), leaving the `_providers` dictionary empty. When a connection with `Authentication=Active Directory Managed Identity` (or any other Entra ID method) is opened, `GetFedAuthToken` calls `SqlAuthenticationProviderManager.GetProvider()`, gets `null`, and throws `SqlException: Cannot find an authentication provider for 'ActiveDirectoryManagedIdentity'`.

**Calling `SqlAuthenticationProvider.SetProvider()` from application code also does not work**, because the public `SetProvider` API in `Microsoft.Data.SqlClient.Extensions.Abstractions` communicates with the internal `SqlAuthenticationProviderManager` via reflection as well ([source](https://github.com/dotnet/SqlClient/blob/main/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.Internal.cs)):
```csharp
var assembly = Assembly.Load("Microsoft.Data.SqlClient");
var manager = assembly.GetType("Microsoft.Data.SqlClient.SqlAuthenticationProviderManager");
_setProvider = manager.GetMethod("SetProvider", BindingFlags.NonPublic | BindingFlags.Static);
// ...
_setProvider.Invoke(null, [authenticationMethod, provider]);
```

This means there is **no AOT-safe way to register an authentication provider** in v7.0.

None of these code paths have `[RequiresUnreferencedCode]`, `[RequiresDynamicCode]`, or `[DynamicallyAccessedMembers]` annotations, so the AOT compiler emits no warnings — the failure is entirely silent until runtime.

```
Exception message:
Microsoft.Data.SqlClient.SqlException (0x80131904):
Cannot find an authentication provider for 'ActiveDirectoryManagedIdentity'.

Stack trace:
at Microsoft.Data.SqlClient.Connection.SqlConnectionInternal.GetFedAuthToken(SqlFedAuthInfo fedAuthInfo)
at Microsoft.Data.SqlClient.Connection.SqlConnectionInternal.OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo)
at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, ...)
at Microsoft.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, ...)
at Microsoft.Data.SqlClient.Connection.SqlConnectionInternal.CompleteLogin(Boolean enlistOK)
at Microsoft.Data.SqlClient.Connection.SqlConnectionInternal.AttemptOneLogin(...)
at Microsoft.Data.SqlClient.Connection.SqlConnectionInternal.LoginNoFailover(...)
at Microsoft.Data.SqlClient.Connection.SqlConnectionInternal.OpenLoginEnlist(...)
at Microsoft.Data.SqlClient.Connection.SqlConnectionInternal..ctor(...)
```

### To reproduce

Minimal reproduction — a NativeAOT-published app connecting to Azure SQL with Entra ID Managed Identity:

```csharp
// Program.cs
using Microsoft.Data.SqlClient;

var connStr = "Server=myserver.database.windows.net;Database=mydb;Authentication=Active Directory Managed Identity;";

using var connection = new SqlConnection(connStr);
await connection.OpenAsync(); // throws: Cannot find an authentication provider for 'ActiveDirectoryManagedIdentity'
```

Project file:
```xml


Exe
net10.0
true




```

Publish and run:
```bash
dotnet publish -r linux-x64 -c Release
./bin/Release/net10.0/linux-x64/publish/MyApp
```

This works correctly with `Microsoft.Data.SqlClient` 6.1.4 (no Extensions.Azure package needed), because `ActiveDirectoryAuthenticationProvider` was in the core assembly with a direct `new` instantiation.

### Expected behavior

Entra ID authentication methods (Managed Identity, Workload Identity, Default, etc.) should work under NativeAOT, as they did in v6.x.

At minimum, the reflection-based code paths should be annotated with `[RequiresUnreferencedCode]` / `[RequiresDynamicCode]` so that the AOT compiler warns users that these code paths are unsafe.

Ideally, an AOT-compatible registration path should be provided — for example:
- A direct (non-reflection) `SetProvider` overload on `SqlAuthenticationProviderManager` accessible from application code
- Or a builder/options pattern that lets users register the provider at compile time

### Workaround

The `AccessTokenCallback` property on `SqlConnection` bypasses the `SqlAuthenticationProvider` registry entirely and works under NativeAOT. Combined with `Azure.Identity` (which is AOT-compatible), this is currently the only reliable approach:

```csharp
var builder = new SqlConnectionStringBuilder(connectionString);
var credential = new ManagedIdentityCredential(); // from Azure.Identity

// Strip the Authentication keyword so SqlClient doesn't try the provider registry
builder.Authentication = SqlAuthenticationMethod.NotSpecified;

using var connection = new SqlConnection(builder.ConnectionString);
connection.AccessTokenCallback = async (parameters, cancellationToken) =>
{
var token = await credential.GetTokenAsync(
new Azure.Core.TokenRequestContext(["https://database.windows.net/.default"]),
cancellationToken);
return new SqlAuthenticationToken(token.Token, token.ExpiresOn);
};
await connection.OpenAsync(); // works under NativeAOT
```

### Further technical details

Microsoft.Data.SqlClient version: 7.0.0
.NET target: .NET 9.0 / .NET 10.0 (both affected)
SQL Server version: Azure SQL Database
Operating system: Linux (Ubuntu 24.04 Noble, Docker container with `mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled`)

**Additional context**

- Related: #1947 (NativeAOT support: get to zero warnings)
- Related: #2742 (native aot deployment error using Microsoft Entra authentication — different root cause in v5.x, `MemoryCache` static constructor failure)
- The v6 → v7 change: in v6.1.4, `SqlAuthenticationProviderManager.SetDefaultAuthProviders()` called `new ActiveDirectoryAuthenticationProvider(...)` directly. In v7.0, this was replaced with `Assembly.Load("Microsoft.Data.SqlClient.Extensions.Azure")` + `Activator.CreateInstance`.
- `SqlAuthenticationProvider.SetProvider()` (the public API intended for manual provider registration) also fails under NativeAOT because `SqlAuthenticationProvider.Internal` class uses `MethodInfo.Invoke` with `BindingFlags.NonPublic` to reach the internal `SqlAuthenticationProviderManager`.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.