Entra ID authentication broken under NativeAOT in v7.0 — Extensions.Azure reflection-based discovery is AOT-incompatible
- 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
Assessment
This issue has not been assessed yet.