aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

[Blazor] Add ability to filter persistent component state callbacks based on persistence reason

Open Copilot opened this issue 5 months ago • 6 comments

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 IPersistenceReason interface with PersistByDefault property
  • Added concrete persistence reason implementations:
    • PersistOnPrerendering (default: true)
    • PersistOnEnhancedNavigation (default: false)
    • PersistOnCircuitPause (default: true)
  • Added IPersistenceReasonFilter interface for filtering logic
  • Added generic PersistReasonFilter<TReason> base class

Filter Classes in Components.Web

  • PersistOnPrerenderingFilter - Controls persistence during prerendering
  • PersistOnEnhancedNavigationFilter - Controls persistence during enhanced navigation
  • PersistOnCircuitPauseFilter - Controls persistence during circuit pause

Infrastructure Updates

  • Enhanced PersistComponentStateRegistration to include reason filters
  • Added new RegisterOnPersisting overload accepting IReadOnlyList<IPersistenceReasonFilter>
  • Added new PersistStateAsync overload accepting IPersistenceReason
  • Updated calling code to pass appropriate persistence reasons:
    • RazorComponentEndpointInvoker uses PersistOnPrerendering
    • CircuitPersistenceManager uses PersistOnCircuitPause

Filtering Logic

The persistence manager now evaluates filters in order:

  1. If any filter returns a definitive result (true/false), that decision is used
  2. If no filters match the persistence reason, the reason's PersistByDefault value is used
  3. 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.

Copilot avatar Jun 18 '25 13:06 Copilot

Create concrete persistence reason implementations: PersistOnPrerendering, PersistOnEnhancedNavigation, PersistOnCircuitPause must live on the Components.Web assembly.

javiercn avatar Jun 18 '25 13:06 javiercn

RazorComponentEndpointInvoker needs to pass in Prerendering as the reason during the initial rendering and EnhancedNavigation when the request comes from an enhanced navigation

javiercn avatar Jun 18 '25 14:06 javiercn

Create concrete persistence reason implementations: PersistOnPrerendering, PersistOnEnhancedNavigation, PersistOnCircuitPause must live on the Components.Web assembly.

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)

RazorComponentEndpointInvoker needs to pass in Prerendering as the reason during the initial rendering and EnhancedNavigation when 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)

Copilot avatar Jun 18 '25 14:06 Copilot

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

javiercn avatar Jun 18 '25 15:06 javiercn

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:

  1. Basic filtering behavior - Components can selectively persist state based on persistence reasons (prerendering, enhanced navigation, circuit pause)
  2. Enhanced navigation filtering - State persistence can be controlled differently for enhanced navigation scenarios
  3. Prerendering filter disabling - Ability to disable persistence during prerendering when needed

Test Components Created:

  • FilteredPersistentStateComponent.razor - Demonstrates different filter combinations and validates selective persistence
  • FilteringTestPage.razor - Test page for E2E scenarios with parameterized filter configurations

Test Methods Added:

  • CanFilterPersistentStateCallbacks - Tests filtering across server/wasm/auto modes with enhanced navigation
  • CanFilterPersistentStateForEnhancedNavigation - Validates enhanced navigation-specific filtering behavior
  • CanDisablePersistenceForPrerendering - 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)

Copilot avatar Jun 18 '25 15:06 Copilot