`PersistentStateAttribute` not working in Blazor when server and client have different trimming settings
Is there an existing issue for this?
- [x] I have searched the existing issues
Describe the bug
Observations
The AntiforgeryToken in the logout form does not always render:
The antiforgery token was correctly "rendered" with .NET 9. The issue only appeared after migrating to .NET 10. After investigating for several hours, we noticed the issue only appears when the app is running in Docker. We created a minimalistic project based on the offical Blazor project template and could reproduce the issue there.
After further investigation, we assume that the issue is caused by the new declarative model for persisting state from components and services.
With .NET 9, the antiforgery token was retrieved using PersistentComponentState. With .NET 10 it's using the PersistentState attribute on the DefaultAntiforgeryStateProvider.CurrentToken.
✅ The antiforgery token gets correctly generated and persisted to the component state during the prerendering, both for normal debugging (without Docker) and Docker debugging.
✅ When inspecting the persistent state in WASM, the antiforgery token is part of the payload, both for normal and Docker debugging.
❌ The CurrentToken property does not get populated with the payload during Docker debugging (nor hosting on a server).
Assumption
During docker compilation, something gets trimmed/modfied, causing the key generation to be different.
Workaround
We created a custom implementation of the AntiforgeryStateProvider and registered it in the client with a constant key, circumventing the key generation:
// Program.cs
builder.Services.AddSingleton<AntiforgeryStateProvider, WorkaroundAntiforgeryStateProvider>();
// WorkaroundAntiforgeryStateProvider
public class WorkaroundAntiforgeryStateProvider : AntiforgeryStateProvider
{
private readonly PersistentComponentState _persistentComponentState;
public WorkaroundAntiforgeryStateProvider(PersistentComponentState persistentComponentState)
{
_persistentComponentState = persistentComponentState;
}
public override AntiforgeryRequestToken? GetAntiforgeryToken()
{
_persistentComponentState.TryTakeFromJson<AntiforgeryRequestToken>(
"KSztT84gAiV0IO/UBRRpOm8K4jqEf\u002B1JzvVOCMBlsiU=",
out var token);
return token;
}
}
Correlations
Issue #63928 might have the same underlying issue.
Expected Behavior
The hidden antiforgery token input field should be rendered when running in Docker:
Steps To Reproduce
Repository to reproduce the issue: https://github.com/StefanOverHaevgRZ/dotnet-antiforgerytoken-issue The repo is basically the standard .NET 10 Blazor Web App template, with individual accounts + .NET SDK Container support.
Steps to reproduce:
- Prepare SQL server (either by the
docker-compose.ymlor manually).- Create Docker network
sqlserverfor the container and the SQL server. - Start SQL Server in a separate Docker container called
sqlserver(or adjust connection string), attached to thesqlservernetwork.
- Create Docker network
- Start the app with
Container (.NET SDK)profile. - Register a new account and confirm the registration.
- Go to login page and login with the new account.
- Inspect logout button --> No antiforgery token rendered.
Exceptions (if any)
No response
.NET Version
10.0.100
Anything else?
- Visual Studio 2026 (18.0.2)
- Docker Desktop 4.53.0
The issue is in PersistentStateValueProviderKeyResolver.cs, the key is computed using:
- Parent component type's FullName
- Component type's FullName
- Property name
https://github.com/dotnet/aspnetcore/blob/344dc33bb839a72e781a5e57beeb67659297c6df/src/Components/Components/src/PersistentState/PersistentStateValueProviderKeyResolver.cs#L53
When code is trimmed (in Docker/Release publish), Type.FullName can change.
Running without publish, hosted locally works properly.
@ilonatommy I guess this would affect every property marked with PersistentStateAttribute, when server and client are trimmed differently. Antiforgery token is just the first thing I noticed the issue with.
The issue is in
PersistentStateValueProviderKeyResolver.cs, the key is computed using:
- Parent component type's FullName
- Component type's FullName
- Property name
aspnetcore/src/Components/Components/src/PersistentState/PersistentStateValueProviderKeyResolver.cs
Line 53 in 344dc33
var preKey = _keyCache.GetOrAdd((parentComponentType, componentType, propertyName), KeyFactory); When code is trimmed (in Docker/Release builds),
Type.FullNamecan change.Debug mode hosted locally works properly.
How does the Type name change with trimming? Isn't that a trimming bug?
I believe that this is likely affecting me too. Using AntiforgeryStateProvider.GetAntiforgeryToken() was working perfectly with Blazor WebAssembly in .NET 9 and appeared to continue working as expected after upgrading to .NET 10 until we tried deploying to a Linux App Service. When I run locally on Windows it is returning the token, but when deployed it is not returning a value at all.
Reproduction in a template app:
It happens only for dotnet new blazor -int WebAssembly -au Individual -ai, so when we have Router.razor in the client project.
The minimal repro is:
dotnet new blazor -int WebAssembly -au Individual -ai -o AuthTemplateAllInteractive
cd AuthTemplateAllInteractive
dotnet publish -c Release -o ./publish
cd publish
dotnet AuthTemplateAllInteractive.dll
Register a new account and confirm the registration. Go to login page and login with the new account. Inspect logout button --> No antiforgery token rendered.
Workaround
Use this hint for linker.xml:
<linker>
<!-- AntiforgeryRequestToken deserialization issues -->
<assembly fullname="Microsoft.AspNetCore.Components.Web">
<type fullname="Microsoft.AspNetCore.Components.Forms.AntiforgeryRequestToken" preserve="all"/>
</assembly>
<assembly fullname="Microsoft.AspNetCore.Components.WebAssembly">
<type fullname="Microsoft.AspNetCore.Components.Forms.DefaultAntiforgeryStateProvider" preserve="all"/>
</assembly>
</linker>
How does the Type name change with trimming? Isn't that a trimming bug?
Not a bug.
PropertyAccessor has a flag LinkerFlags.Component that in theory preserves all, see:
https://github.com/dotnet/aspnetcore/blob/38e7d0c66527ca40832b54b9665c6fdcdfe03c7c/src/Components/Components/src/PersistentState/PersistentServicesRegistry.cs#L165
https://github.com/dotnet/aspnetcore/blob/38e7d0c66527ca40832b54b9665c6fdcdfe03c7c/src/Shared/LinkerFlags.cs#L18
So the adnotations that we are using, e.g. on GetCandidateBindableProperties, should be enough:
https://github.com/dotnet/aspnetcore/blob/63b69a275309dd2d54c7b2e19294d83d86a459a6/src/Components/Components/src/PersistentState/PersistentServicesRegistry.cs#L236
But we are not relying on compile-type types in case of AntiforgeryToken. We are using runtime type, which differs. The trimmer preserves properties on AntiforgeryStateProvider (the base), but CurrentToken is defined on DefaultAntiforgeryStateProvider (the subclass) - so it gets trimmed, see the registration:
https://github.com/dotnet/aspnetcore/blob/38e7d0c66527ca40832b54b9665c6fdcdfe03c7c/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs#L343
A targeted fix for that class would be:
WebAssemblyHostBuilder.cs:
-internal void InitializeDefaultServices()
+ [DynamicDependency(JsonSerialized, typeof(DefaultAntiforgeryStateProvider))]
+ [DynamicDependency(JsonSerialized, typeof(AntiforgeryRequestToken))]
+ internal void InitializeDefaultServices()
It would be good to have a more general fix, in PropertyAccessor logic.
@ilonatommy What about EndpointAntiforgeryStateProvider? Would that need to be included as well?
EndpointAntiforgeryStateProvider is used for serialization on the server. Publish does not trim server code. Do you have a specific scenario where serialization would be broken?
@ilonatommy No, no specific scenario. Just wanted to make sure if that one could be part of the problem as well or not 👍