aspnetcore
aspnetcore copied to clipboard
[Blazor] Add ability to filter persistent component state callbacks based on persistence reason
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
- Added
IPersistenceReasoninterface withPersistByDefaultproperty - Added concrete persistence reason implementations:
PersistOnPrerendering(default: true)PersistOnEnhancedNavigation(default: false)PersistOnCircuitPause(default: true)
- Added
IPersistenceReasonFilterinterface for filtering logic - Added generic
PersistReasonFilter<TReason>base class
Filter Classes in Components.Web
PersistOnPrerenderingFilter- Controls persistence during prerenderingPersistOnEnhancedNavigationFilter- Controls persistence during enhanced navigationPersistOnCircuitPauseFilter- Controls persistence during circuit pause
Infrastructure Updates
- Enhanced
PersistComponentStateRegistrationto include reason filters - Added new
RegisterOnPersistingoverload acceptingIReadOnlyList<IPersistenceReasonFilter> - Added new
PersistStateAsyncoverload acceptingIPersistenceReason - Updated calling code to pass appropriate persistence reasons:
RazorComponentEndpointInvokerusesPersistOnPrerenderingCircuitPersistenceManagerusesPersistOnCircuitPause
Filtering Logic
The persistence manager now evaluates filters in order:
- If any filter returns a definitive result (true/false), that decision is used
- If no filters match the persistence reason, the reason's
PersistByDefaultvalue is used - Maintains full backward compatibility - existing code works unchanged
Usage Example
// Register callback with filtering
var filters = new List<IPersistenceReasonFilter>
{
new PersistOnPrerenderingFilter(persist: true),
new PersistOnEnhancedNavigationFilter(persist: false),
new PersistOnCircuitPauseFilter(persist: true)
};
PersistentState.RegisterOnPersisting(PersistData, renderMode, filters);
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.
Create concrete persistence reason implementations: PersistOnPrerendering, PersistOnEnhancedNavigation, PersistOnCircuitPause must live on the
Components.Webassembly.
RazorComponentEndpointInvoker needs to pass in Prerendering as the reason during the initial rendering and EnhancedNavigation when the request comes from an enhanced navigation
Create concrete persistence reason implementations: PersistOnPrerendering, PersistOnEnhancedNavigation, PersistOnCircuitPause must live on the
Components.Webassembly.
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. (f938ea48)
RazorComponentEndpointInvokerneeds to pass inPrerenderingas the reason during the initial rendering andEnhancedNavigationwhen the request comes from an enhanced navigation
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. (f938ea48)
Here are instructions on how to add an E2E test to validate the changes, separated by --------------------------
Instructions for Adding E2E Tests for a Feature
1. Create Test Class Files
Create 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 Attributes
Use [Theory] for parameterized tests and [Fact] for single-scenario tests:
[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 Components
Add 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 Components
Add 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 Scenarios
Create 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 Applications
Update Program.cs files to register test services:
// ...existing code...
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddSingleton<YourFeatureTestService>();
// ...existing code...
8. Use Proper Assertions
Use Browser.Equal for element text comparisons and other Browser methods for interactions:
// 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 Organization
Include 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
Here are instructions on how to add an E2E test to validate the changes, separated by --------------------------
...
Added comprehensive E2E tests for the persistent component state filtering functionality. The tests validate:
- Basic filtering behavior - Components can selectively persist state based on persistence reasons (prerendering, enhanced navigation, circuit pause)
- Enhanced navigation filtering - State persistence can be controlled differently for enhanced navigation scenarios
- Prerendering filter disabling - Ability to disable persistence during prerendering when needed
Test Components Created:
FilteredPersistentStateComponent.razor- Demonstrates different filter combinations and validates selective persistenceFilteringTestPage.razor- Test page for E2E scenarios with parameterized filter configurations
Test Methods Added:
CanFilterPersistentStateCallbacks- Tests filtering across server/wasm/auto modes with enhanced navigationCanFilterPersistentStateForEnhancedNavigation- Validates enhanced navigation-specific filtering behaviorCanDisablePersistenceForPrerendering- Tests disabling persistence during prerendering
The tests cover all render modes (server, WebAssembly, auto) and validate that the filtering logic works correctly for different persistence scenarios. (52e84883)