Skip to content

Update ResourceCollectionProvider to take advantage of Declarative persistent component state #61748

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

ilonatommy
Copy link
Member

@ilonatommy ilonatommy commented Apr 30, 2025

Follow-up for #60634.

Framework changes

ResourceCollectionUrl saving and restoring responsibility is moved to PersistentServicesRegistry.

I order to support [SupplyParameterFromPersistentComponentState] attribute, the field holding resources url had to be transformed from private field to a public property (ResourceCollectionUrl). Now it exposes a setter that replaced SetResourceCollectionUrl method used internally on initialization.

Key computation for some internal assemblies that are referenced by both: server and client blazor code was not deterministic. We produced the key hashing a string that contained assembly name. Assembly name changes between prerendering and WASM rendering in case of internal shared assemblies. As a workaround, we decided to detect types with internal modifier and skip assembly name in the key computation.

Test changes

AddRazorComponentsTwice_DoesNotDuplicateServices test was checking if double call of AddRazorComponents does not duplicate the services that were supposed to be instantiated once. Before this PR IPersistentServiceRegistration was registered only once, so it was listed as 'single-registration' service. In this PR we added a new registration of IPersistentServiceRegistration in the same AddRazorComponents. We had to change test's expectations and list IPersistentServiceRegistration as a 'multi-registration' service.

@ilonatommy ilonatommy added the area-blazor Includes: Blazor, Razor Components label Apr 30, 2025
@ilonatommy ilonatommy self-assigned this Apr 30, 2025
@ilonatommy ilonatommy requested a review from a team as a code owner April 30, 2025 12:37
@ilonatommy
Copy link
Member Author

@javiercn, is it possible that we are not restoring the state for webassembly interactive at all? We have it done for server when the circuit is set up:

await appLifetime.RestoreStateAsync(store);

but I cannot find any RestoreStateAsync calls in wasm render mode context?

is the only one but it's not triggered on "rehydration" after prerendering.

@javiercn
Copy link
Member

javiercn commented May 6, 2025

@ilonatommy not sure what's going on here, but we have tests covering this.

You could try debugging the sample project to figure out what is going on

@ilonatommy
Copy link
Member Author

ilonatommy commented May 7, 2025

Summary what's going on:

Key in prerendering for Microsoft.AspNetCore.Components.ResourceCollectionProvider.ResourceCollectionUrl is different than the key for WASM rendering.

There are 2 reasons I suspect:

  • ResourceCollectionProvider is defined in "shared", so in prerendering it belongs to Microsoft.AspNetCore.Components.Endpoints assembly and in WASM rendering it belongs to Microsoft.AspNetCore.Components.WebAssembly.
  • version string differs between prerendering and WASM, Version=10.0.0.0 vs Version=42.42.42.42 (dummy version for WASM assemblies) and PublicKeyToken.

Even if we skip the version by using simplified name keyType.Assembly.GetName().Name instead of keyType.Assembly.FullName, the problem with changing assembly name on the rendering boundary persists.

Logs for using the simplified assembly name:

[ComputeKey] Using assembly: Microsoft.AspNetCore.Components.Endpoints, type: Microsoft.AspNetCore.Components.ResourceCollectionProvider, propertyName: ResourceCollectionUrl, inputString: Microsoft.AspNetCore.Components.Endpoints.Microsoft.AspNetCore.Components.ResourceCollectionProvider.ResourceCollectionUrl
[ComputeKey] computed key: reRoeSH0zUNg1lV5T25mU63ifKBQDmwekOTpUjGhOzw=
[Persist] Persisting Key: reRoeSH0zUNg1lV5T25mU63ifKBQDmwekOTpUjGhOzw= for Declared type: Microsoft.AspNetCore.Components.ResourceCollectionProvider, Runtime type: Microsoft.AspNetCore.Components.ResourceCollectionProvider Value: ./_framework/resource-collection.7sl7m.js

MonoPlatform.ts:234 [ComputeKey] Using assembly: Microsoft.AspNetCore.Components.WebAssembly, type: Microsoft.AspNetCore.Components.ResourceCollectionProvider, propertyName: ResourceCollectionUrl, inputString: Microsoft.AspNetCore.Components.WebAssembly.Microsoft.AspNetCore.Components.ResourceCollectionProvider.ResourceCollectionUrl
MonoPlatform.ts:234 [ComputeKey] computed key: OJ5IulA9AwVvEZZuXoXvRFiqSKythSxVh7f6TDMswl4=
MonoPlatform.ts:234 [Restore] Failed restoring Key OJ5IulA9AwVvEZZuXoXvRFiqSKythSxVh7f6TDMswl4= for Declared type: Microsoft.AspNetCore.Components.ResourceCollectionProvider, Runtime type: Microsoft.AspNetCore.Components.ResourceCollectionProvider.

@javiercn
Copy link
Member

javiercn commented May 7, 2025

@ilonatommy good catch.

This is something that we solved for other types (AntiforgeryStateProvider) relying on the public abstraction, but I don't think we want to add a public abstraction for this type, as it's completely internal.

We should figure out what we want to do here, but there are some options like explicitly providing a key in these cases where stuff is "shared".

We want to support passing information across boundaries without having to require any public API, we just need to tweak the way we do this.

I think we still want to use the full name to compute the key by default as that's the common case and guarantee's us that we don't squat over types that have the same name.

@ilonatommy ilonatommy requested a review from Copilot May 12, 2025 08:20
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR updates the handling of persistent component state for ResourceCollection by moving its saving and restoring responsibility to PersistentServicesRegistry.

  • Update ResourceCollectionProvider to use a persistent state attribute and remove internal state persistence logic.
  • Register ResourceCollectionProvider for persistent state registration in both WebAssemblyHostBuilder and RazorComponentsServiceCollectionExtensions.
  • Refactor key computation in PersistentServicesRegistry to ensure canonical assembly names are used.

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs Registers ResourceCollectionProvider as a singleton with an added persistent service registration.
src/Components/Shared/src/ResourceCollectionProvider.cs Removes internal persistent state logic and switches to a property marked with SupplyParameterFromPersistentComponentState.
src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs Updates the code to use the new property setter instead of a method call for setting the URL.
src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs Registers ResourceCollectionProvider as a scoped service with persistent service registration.
src/Components/Components/src/PersistentState/PersistentServicesRegistry.cs Refactors the ComputeKey method to use a canonical mapping based on assembly names.
Comments suppressed due to low confidence (2)

src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs:310

  • ResourceCollectionProvider is registered as a singleton in WebAssemblyHostBuilder, while in RazorComponentsServiceCollectionExtensions it is registered as scoped. Consider aligning the DI registration lifetimes across the project to avoid inconsistent behavior.
Services.AddSingleton<ResourceCollectionProvider>();

src/Components/Shared/src/ResourceCollectionProvider.cs:21

  • [nitpick] The previous registration of a persisting callback on the ResourceCollectionUrl setter has been removed. Please ensure that the external registration via RegisterPersistentComponentStateServiceCollectionExtensions fully covers the intended persistence behavior to avoid potential state loss.
get => _url; set { ... }

@ilonatommy ilonatommy requested a review from javiercn May 16, 2025 11:11
Comment on lines 209 to 215
// Mapping for internal classes bundled in different assemblies during prerendering and WASM rendering.
private static readonly Dictionary<string, string> _canonicalMap = new(StringComparer.OrdinalIgnoreCase)
{
{ "Microsoft.AspNetCore.Components.Endpoints", "Microsoft.AspNetCore.Components" },
{ "Microsoft.AspNetCore.Components.WebAssembly", "Microsoft.AspNetCore.Components" }
};

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works, but ideally, we don't want to do it this way. Microsoft.AspNetCore.Components shouldn't really depend on Endpoints or WebAssembly (by depend on I mean make a reference to those or include some built-in knowledge about them).

This makes it problematic the next time we add one of these things. It's "interaction at a distance" where someone will ask themselves why something doesn't work and it's because they are missing an update to this mapping.

Instead, maybe we can consider some heuristic, but I don't know exactly which one it is. We could do something like "if the type is internal, we could consider the type name only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants