-
Notifications
You must be signed in to change notification settings - Fork 10.4k
[Blazor] Add ability to filter persistent component state callbacks based on persistence reason #62394
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
base: main
Are you sure you want to change the base?
Conversation
Greetings human! You've submitted a PR that modifies code that is shared with https://github.com/dotnet/runtime . Please make sure you synchronize this code with the changes in that repo! |
|
|
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
…RazorComponentEndpointInvoker to detect enhanced navigation Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Moved the concrete persistence reason implementations (PersistOnPrerendering, PersistOnEnhancedNavigation, PersistOnCircuitPause) to the Components.Web assembly as requested. Updated all references and PublicAPI files accordingly. Created a DefaultPersistenceReason for internal fallback use in the Components assembly to avoid dependency issues. (f938ea4)
Updated RazorComponentEndpointInvoker to detect enhanced navigation requests using the IsProgressivelyEnhancedNavigation method. Now passes PersistOnPrerendering for initial rendering and PersistOnEnhancedNavigation when the request comes from enhanced navigation, as determined by the "text/html; blazor-enhanced-nav=on" Accept header. (f938ea4) |
@@ -7,6 +7,7 @@ | |||
|
|||
<ItemGroup> | |||
<Reference Include="Microsoft.AspNetCore.Components" /> | |||
<Reference Include="Microsoft.AspNetCore.Components.Web" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not add this reference here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed the Microsoft.AspNetCore.Components.Web reference from the Components.Tests project. Created test-specific implementations of persistence reasons within the test file to maintain test functionality without the dependency. (4439a8c)
internal Task<bool> TryPauseAsync(IPersistentComponentStateStore store) | ||
=> TryPauseAsync(store, DefaultPersistenceReason.Instance); | ||
|
||
internal Task<bool> TryPauseAsync(IPersistentComponentStateStore store, IPersistenceReason persistenceReason) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not create and overload. Simply change this method and adjust all the places it's used
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed the overload from ComponentStatePersistenceManager.PersistStateAsync and modified the existing method to accept an optional nullable IPersistenceReason parameter. Updated all calling sites to work with the new signature. (a2c9f1c)
internal sealed class DefaultPersistenceReason : IPersistenceReason | ||
{ | ||
public static readonly DefaultPersistenceReason Instance = new(); | ||
|
||
private DefaultPersistenceReason() { } | ||
|
||
/// <inheritdoc /> | ||
public bool PersistByDefault => true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use null
as a value in legacy overloads (and handle that within PauseAsync) instead of a "default" reason
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replaced DefaultPersistenceReason with null handling in legacy overloads. The ComponentStatePersistenceManager now handles null persistence reasons with default behavior (persist by default). Removed the DefaultPersistenceReason.cs file entirely. (a2c9f1c)
/// <param name="renderMode"></param> | ||
/// <param name="reasonFilters">Filters to control when the callback should be invoked based on the persistence reason.</param> | ||
/// <returns>A subscription that can be used to unregister the callback when disposed.</returns> | ||
public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> callback, IComponentRenderMode? renderMode, IReadOnlyList<IPersistenceReasonFilter>? reasonFilters) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IReadOnlyList? reasonFilters <= Make this mandatory
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made the reasonFilters parameter mandatory in the RegisterOnPersisting overload that accepts filters. Updated the legacy overloads to pass Array.Empty<IPersistenceReasonFilter>() when no filters are provided. (a2c9f1c)
{ | ||
public Func<Task> Callback { get; } = callback; | ||
|
||
public IComponentRenderMode? RenderMode { get; } = renderMode; | ||
|
||
public IReadOnlyList<IPersistenceReasonFilter>? ReasonFilters { get; } = reasonFilters; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public IReadOnlyList<IPersistenceReasonFilter> ReasonFilters { get; } = reasonFilters ?? Array.Empty<IPersistenceReasonFilter>();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated PersistComponentStateRegistration.ReasonFilters property initialization to use the suggested pattern: reasonFilters ?? Array.Empty<IPersistenceReasonFilter>()
. The property is now non-nullable and always returns a valid collection. (a2c9f1c)
public class PersistOnPrerendering : IPersistenceReason | ||
{ | ||
/// <inheritdoc /> | ||
public bool PersistByDefault { get; } = true; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the singleton pattern here and provide a static Instance
value that gets reused everywhere
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented the singleton pattern for all persistence reason classes. Each class (PersistOnPrerendering, PersistOnEnhancedNavigation, PersistOnCircuitPause) now has a static Instance property and private constructor. Updated all usage sites to use the singleton instances. (a2c9f1c)
public class PersistOnPrerendering : IPersistenceReason | ||
{ | ||
/// <inheritdoc /> | ||
public bool PersistByDefault { get; } = true; | ||
} | ||
|
||
/// <summary> | ||
/// Represents persistence during enhanced navigation. | ||
/// </summary> | ||
public class PersistOnEnhancedNavigation : IPersistenceReason | ||
{ | ||
/// <inheritdoc /> | ||
public bool PersistByDefault { get; } | ||
} | ||
|
||
/// <summary> | ||
/// Represents persistence when a circuit is paused. | ||
/// </summary> | ||
public class PersistOnCircuitPause : IPersistenceReason | ||
{ | ||
/// <inheritdoc /> | ||
public bool PersistByDefault { get; } = true; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put each class in its own file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Separated the persistence reason classes into individual files: PersistOnPrerendering.cs, PersistOnEnhancedNavigation.cs, and PersistOnCircuitPause.cs. Removed the original PersistenceReasons.cs file. (a2c9f1c)
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Here are instructions on how to add an E2E test to validate the changes, separated by -------------------------- Instructions for Adding E2E Tests for a Feature1. Create Test Class FilesCreate test classes that inherit from appropriate base classes and follow naming conventions: using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETests.Tests;
public class YourFeatureTest : ServerTestBase<BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>>>
{
static int _nextTestIdContext;
public YourFeatureTest(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}
// Separate contexts to ensure that caches and other state don't interfere across tests
public override Task InitializeAsync()
=> InitializeAsync(BrowserFixture.StreamingContext + _nextTestIdContext++);
} 2. Add Test Methods with Theory/Fact AttributesUse [Theory]
[InlineData(true, typeof(InteractiveServerRenderMode), (string)null)]
[InlineData(true, typeof(InteractiveWebAssemblyRenderMode), (string)null)]
[InlineData(true, typeof(InteractiveAutoRenderMode), (string)null)]
[InlineData(false, typeof(InteractiveServerRenderMode), (string)null)]
public void CanUseYourFeature(bool parameter1, Type renderMode, string parameter2)
{
var mode = renderMode switch
{
var t when t == typeof(InteractiveServerRenderMode) => "server",
var t when t == typeof(InteractiveWebAssemblyRenderMode) => "wasm",
var t when t == typeof(InteractiveAutoRenderMode) => "auto",
_ => throw new ArgumentException($"Unknown render mode: {renderMode.Name}")
};
// Test implementation
Navigate($"your-feature-page?mode={mode}");
// Assertions using Browser.Equal
Browser.Equal("Expected Value", () => Browser.FindElement(By.Id("test-element")).Text);
}
[Fact]
public void YourFeature_SpecificScenario()
{
Navigate($"{ServerPathBase}/your-feature-specific-page");
Browser.Equal("Expected Result", () => Browser.FindElement(By.Id("result")).Text);
Browser.Click(By.Id("action-button"));
Browser.Equal("Updated Result", () => Browser.FindElement(By.Id("result")).Text);
} 3. Create Test Pages and ComponentsAdd Razor pages for testing in the test assets: @page "/your-feature-page"
@using Microsoft.AspNetCore.Components.Web
<h1>Your Feature Test Page</h1>
@if (Mode == "server") {
<YourFeatureComponent @rendermode="@RenderMode.InteractiveServer" />
}
@if (Mode == "wasm") {
<YourFeatureComponent @rendermode="@RenderMode.InteractiveWebAssembly" />
}
@if (Mode == "auto") {
<YourFeatureComponent @rendermode="@RenderMode.InteractiveAuto" />
}
@code {
[Parameter, SupplyParameterFromQuery(Name = "mode")]
public string Mode { get; set; }
} 4. Create Test ComponentsAdd test components in the TestContentPackage: @implements IDisposable
<p id="test-element">@_value</p>
<p id="render-mode">Render mode: <span>@_renderMode</span></p>
<button id="action-button" @onclick="PerformAction">Test Action</button>
@code {
private string _value = "Initial Value";
private string _renderMode = "SSR";
protected override void OnInitialized()
{
_renderMode = OperatingSystem.IsBrowser() ? "WebAssembly" : "Server";
// Initialize your feature
}
private void PerformAction()
{
_value = "Updated Value";
}
public void Dispose()
{
// Cleanup if needed
}
} 5. Add Helper Methods for Complex ScenariosCreate helper methods for reusable test logic: private void ValidateFeatureState(
string expectedValue,
string renderMode,
bool isInteractive = true)
{
Browser.Equal($"Render mode: {renderMode}", () => Browser.FindElement(By.Id("render-mode")).Text);
Browser.Equal($"Interactive: {isInteractive}", () => Browser.FindElement(By.Id("interactive")).Text);
Browser.Equal(expectedValue, () => Browser.FindElement(By.Id("test-element")).Text);
}
private void BlockWebAssemblyResourceLoad()
{
((IJavaScriptExecutor)Browser).ExecuteScript("sessionStorage.setItem('block-load-boot-resource', 'true')");
((IJavaScriptExecutor)Browser).ExecuteScript("caches.keys().then(keys => keys.forEach(key => caches.delete(key)))");
} 6. Add Services for Test Support (if needed)Create services that support your testing scenarios: namespace TestContentPackage.Services;
public class YourFeatureTestService
{
private readonly Dictionary<string, object> _state = new();
public void SetState(string key, object value)
{
_state[key] = value;
}
public T GetState<T>(string key)
{
return _state.TryGetValue(key, out var value) ? (T)value : default(T);
}
} 7. Register Services in Test ApplicationsUpdate Program.cs files to register test services: // ...existing code...
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddSingleton<YourFeatureTestService>();
// ...existing code... 8. Use Proper AssertionsUse // Wait for element to have expected value
Browser.Equal("Expected Text", () => Browser.FindElement(By.Id("element-id")).Text);
// Click elements
Browser.Click(By.Id("button-id"));
// Check element existence
Browser.Exists(By.Id("element-id"));
// Navigate to pages
Navigate($"{ServerPathBase}/test-page?param=value"); 9. Add Comments for Test OrganizationInclude clear comments explaining test scenarios: // These tests validate that your feature works correctly across different render modes
// and scenarios including enhanced navigation, streaming rendering, and state persistence
[Theory]
[InlineData("server")]
[InlineData("wasm")]
[InlineData("auto")]
public void YourFeature_WorksAcrossRenderModes(string renderMode)
{
// Arrange: Navigate to test page
Navigate($"your-feature-page?mode={renderMode}");
// Act: Perform feature action
Browser.Click(By.Id("trigger-feature"));
// Assert: Verify expected behavior
Browser.Equal("Feature Activated", () => Browser.FindElement(By.Id("status")).Text);
} Use these instructions to add an E2E test for filtering |
…fault reason, implement singleton pattern Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
... Added comprehensive E2E tests for the persistent component state filtering functionality. The tests validate:
Test Components Created:
Test Methods Added:
The tests cover all render modes (server, WebAssembly, auto) and validate that the filtering logic works correctly for different persistence scenarios. (52e8488) |
This PR implements the ability to filter persistent component state callbacks based on the reason for persistence, addressing scenarios where components need different persistence behavior for prerendering, enhanced navigation, and circuit pause events.
Changes
Core Interfaces and Classes
IPersistenceReason
interface withPersistByDefault
propertyPersistOnPrerendering
(default: true)PersistOnEnhancedNavigation
(default: false)PersistOnCircuitPause
(default: true)IPersistenceReasonFilter
interface for filtering logicPersistReasonFilter<TReason>
base classFilter Classes in Components.Web
PersistOnPrerenderingFilter
- Controls persistence during prerenderingPersistOnEnhancedNavigationFilter
- Controls persistence during enhanced navigationPersistOnCircuitPauseFilter
- Controls persistence during circuit pauseInfrastructure Updates
PersistComponentStateRegistration
to include reason filtersRegisterOnPersisting
overload acceptingIReadOnlyList<IPersistenceReasonFilter>
PersistStateAsync
overload acceptingIPersistenceReason
RazorComponentEndpointInvoker
usesPersistOnPrerendering
CircuitPersistenceManager
usesPersistOnCircuitPause
Filtering Logic
The persistence manager now evaluates filters in order:
PersistByDefault
value is usedUsage Example
This allows fine-grained control over when component state should be persisted based on the specific persistence scenario.
Fixes #62393.
💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click here to start the survey.