Repository navigation
Description
Is there an existing issue for this?
- I have searched the existing issues
Describe the bug
In .NET 11, KeyRingBasedDataProtectionProvider.CreateProtector(string) now eagerly resolves the current key ring instead of deferring it to the first Protect/Unprotect. Creating a data protector now reads the key store immediately rather than on first use.
This matters at startup: apps create a protector during startup (e.g. app.UseAntiforgery() resolves IAntiforgery, cookie-auth setup, etc.). For apps that persist keys to an external store (PersistKeysToDbContext<T>(), Redis, Azure Blob), the app now performs a key-store round-trip at boot and fails to start if the store is unavailable — whereas on .NET 10 it started and only accessed the store on the first request that used data protection.
Cause: introduced by #64262 ("Spanify DataProtector with IBufferWriter<byte>", commit 249b29c2, on main only — not backported to release/10.0, which is why .NET 10 is unaffected). To return the faster span-based protector when the default encryptor supports it, CreateProtector now resolves the key ring up front to inspect the encryptor — and that is what pulls the key-store read into protector creation:
public IDataProtector CreateProtector(string purpose)
{
ArgumentNullThrowHelper.ThrowIfNull(purpose);
var currentKeyRing = _keyRingProvider.GetCurrentKeyRing(); // added in #64262 — reads the key store
var encryptor = currentKeyRing.DefaultAuthenticatedEncryptor; // needs the key ring
#if NET
if (encryptor is ISpanAuthenticatedEncryptor)
{
return new KeyRingBasedSpanDataProtector(...); // faster span path
}
#endif
return new KeyRingBasedDataProtector(...); // .NET 10 returned here without reading
}This looks like an unintended side effect of a performance optimization rather than a deliberate decision to read keys at protector creation. Either way it's a behavioral change from .NET 10 that isn't currently documented.
Expected Behavior
Either:
- Key-store access stays deferred to the first
Protect/Unprotect(no read duringCreateProtector), matching .NET 10; or - The change is intentional and is documented as a .NET 11 breaking change, with a supported opt-out. (The existing
Microsoft.AspNetCore.DataProtection.KeyManagement.DisableAsyncKeyRingUpdateswitch controls async refresh, not eager initialization, so it does not address this.)
Steps To Reproduce
Public minimal repro: https://github.com/KennethHoff/Repros/tree/master/DataProtectionEagerStartupNet11
git clone https://github.com/KennethHoff/Repros && cd Repros/DataProtectionEagerStartupNet11
nix run .#repro # installs .NET 10 + 11 and runs both
# or, with the SDKs already installed:
dotnet run -f net11.0 # throws from CreateProtector (eager)
dotnet run -f net10.0 # CreateProtector returns; throws from Protect (lazy)A self-contained console app — no web host, no antiforgery — with an IXmlRepository that throws when read, so where the read happens is visible in the stack trace. The same project runs on both runtimes; only the target framework differs:
using System.Xml.Linq;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddDataProtection()
.AddKeyManagementOptions(options => options.XmlRepository = new TripwireXmlRepository());
using var provider = services.BuildServiceProvider();
var dataProtection = provider.GetRequiredService<IDataProtectionProvider>();
// Step 1 — .NET 11 reads the key store HERE (eager); .NET 10 returns without reading.
var protector = dataProtection.CreateProtector("repro");
Console.WriteLine("CreateProtector returned without reading the key store [.NET 10 path]");
// Step 2 — .NET 10 reads the key store HERE (lazy, on first use).
protector.Protect("hello");
internal sealed class TripwireXmlRepository : IXmlRepository
{
IReadOnlyCollection<XElement> IXmlRepository.GetAllElements() =>
throw new InvalidOperationException("IXmlRepository.GetAllElements() called — the key store was read");
void IXmlRepository.StoreElement(XElement element, string friendlyName) { }
}.csproj: SDK Microsoft.NET.Sdk, <TargetFrameworks>net10.0;net11.0</TargetFrameworks>, and <FrameworkReference Include="Microsoft.AspNetCore.App" /> (so Data Protection comes from the shared framework and the behavior is driven by the runtime version, not a pinned NuGet package).
Exceptions (if any)
.NET 11 — the read happens in CreateProtector:
System.InvalidOperationException: IXmlRepository.GetAllElements() called — the key store was read
at TripwireXmlRepository.Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository.GetAllElements()
at Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager.GetAllKeys()
at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingProvider.GetCurrentKeyRingCoreNew(DateTime utcNow, Boolean forceRefresh)
at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtectionProvider.CreateProtector(String purpose)
at Program.<Main>$(String[] args)
.NET 10 — CreateProtector returns; the read happens in Protect:
System.Security.Cryptography.CryptographicException: An error occurred while trying to encrypt the provided data.
---> System.InvalidOperationException: IXmlRepository.GetAllElements() called — the key store was read
at TripwireXmlRepository.Microsoft.AspNetCore.DataProtection.Repositories.IXmlRepository.GetAllElements()
at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.Protect(Byte[] plaintext)
at Microsoft.AspNetCore.DataProtection.DataProtectionCommonExtensions.Protect(IDataProtector protector, String plaintext)
at Program.<Main>$(String[] args)
.NET Version
11.0.100-preview.5.26302.115
Anything else?
- ASP.NET Core / runtime version:
Microsoft.AspNetCore.App 11.0.0-preview.5.26302.115. Does not reproduce on .NET 10 (verified on runtime10.0.9— lazy, as before). - Cleanly isolated: same machine/SDK install, only the project's
TargetFrameworkchanged —net10.0reads on firstProtect,net11.0reads inCreateProtector. The shared-framework version is the sole difference. - Impact: any app that resolves a data protector during startup (antiforgery via
UseAntiforgery(), cookie auth, etc.) and persists keys to an external store now depends on that store being reachable at boot. - Not documented: absent from Breaking changes in .NET 11, Breaking changes in ASP.NET Core 11, and What's new in ASP.NET Core in .NET 11 (checked 2026-06-26).
- Cause: #64262 (commit
249b29c2) added the eagerGetCurrentKeyRing()call inCreateProtector. Possibly related: Change how the key ring cache is updated #54675 (async key-ring refresh), KeyRingProvider causes thread pool starvation on cold start in .NET 10 #66380 (cold-start thread-pool starvation, .NET 10), Async APIs for Data Protection key ring access #66684 (async key-ring APIs). - Request: please confirm whether the eager
CreateProtectoris intentional; if so, document it as a .NET 11 breaking change (and consider an opt-out switch).
Activity
Link upstream issue dotnet/aspnetcore#67447 in DataProtection repro r…
Youssef1313 commentedon Jun 29, 2026
@DeagleGross
DeagleGross commentedon Jun 29, 2026
Thanks for report, fix is attached; we'll see if we want to introduce a new
ISpanDataProtectionProviderwhich will distinguish usage patterns. If you have suggestions or use-cases - please share!