aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

`PersistentStateAttribute` not working in Blazor when server and client have different trimming settings

Open StefanOverHaevgRZ opened this issue 2 weeks ago • 3 comments

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:

Image

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:

Image

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:

  1. Prepare SQL server (either by the docker-compose.yml or manually).
    1. Create Docker network sqlserver for the container and the SQL server.
    2. Start SQL Server in a separate Docker container called sqlserver (or adjust connection string), attached to the sqlserver network.
  2. Start the app with Container (.NET SDK) profile.
  3. Register a new account and confirm the registration.
  4. Go to login page and login with the new account.
  5. 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

StefanOverHaevgRZ avatar Dec 08 '25 10:12 StefanOverHaevgRZ

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 avatar Dec 08 '25 12:12 ilonatommy

@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.

StefanOverHaevgRZ avatar Dec 08 '25 13:12 StefanOverHaevgRZ

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.FullName can change.

Debug mode hosted locally works properly.

How does the Type name change with trimming? Isn't that a trimming bug?

javiercn avatar Dec 10 '25 11:12 javiercn

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.

mreisz7 avatar Dec 13 '25 17:12 mreisz7

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.

ilonatommy avatar Dec 16 '25 13:12 ilonatommy

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>

jirikanda avatar Dec 16 '25 14:12 jirikanda

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 avatar Dec 17 '25 15:12 ilonatommy

@ilonatommy What about EndpointAntiforgeryStateProvider? Would that need to be included as well?

StefanOverHaevgRZ avatar Dec 18 '25 07:12 StefanOverHaevgRZ

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 avatar Dec 19 '25 08:12 ilonatommy

@ilonatommy No, no specific scenario. Just wanted to make sure if that one could be part of the problem as well or not 👍

StefanOverHaevgRZ avatar Dec 19 '25 09:12 StefanOverHaevgRZ