Skip to content

Declarative state persistence base case failure #61456

@guardrex

Description

@guardrex
Contributor

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Javier ...

Working on validating/updating the code demos in articles for the new persistent component state model and hit a gremlin 😈.

Expected Behavior

No exception and retain the generated value in a counter component that uses the new model.

Steps To Reproduce

Akin to what you show in the OP of the PR at #60634 (but I made the property public), BUT this demo should set an initial value on initialization to demo how a value created on the server can be held for final rendering.

This issue is really just asking how to approach this scenario more so than a bug report. I don't think it's a bug, of course! 😄

Vanilla BWA 10.0 Preview 3 with global Interactive Server rendering.

Counter2.razor:

@page "/counter-2"
@inject ILogger<Counter2> Logger

<PageTitle>Prerendered Counter 2</PageTitle>

<h1>Prerendered Counter 2</h1>

<p role="status">Current count: @CurrentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    [SupplyParameterFromPersistentComponentState]
    public int? CurrentCount { get; set; }

    protected override void OnInitialized()
    {
        CurrentCount ??= Random.Shared.Next(100);
        Logger.LogInformation("CurrentCount set to {Count}", CurrentCount);
    }

    private void IncrementCount() => CurrentCount++;
}

Exceptions (if any)

Output ...

info: BlazorSample.Components.Pages.Counter2[0]
    CurrentCount set to 77
fail: Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager[1000]
      There was an error executing a callback while pausing the application.
      System.ArgumentException: Cannot bind to the target method because its signature is not compatible with that of the delegate type.
         at System.Reflection.RuntimeMethodInfo.CreateDelegateInternal(Type delegateType, Object firstArgument, DelegateBindingFlags bindingFlags)
         at Microsoft.AspNetCore.Components.Reflection.PropertyGetter..ctor(Type targetType, PropertyInfo property)
         at Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateValueProvider.PropertyGetterFactory(ValueTuple`2 key)
         at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
         at Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateValueProvider.ResolvePropertyGetter(Type type, String propertyName)
         at Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateValueProvider.<>c__DisplayClass11_0.<Subscribe>b__0()
         at Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.<TryPauseAsync>g__TryExecuteCallback|18_0(Func`1 callback, ILogger`1 logger)
info: BlazorSample.Components.Pages.Counter2[0]
    CurrentCount set to 43

.NET Version

10.0.100-preview.3.25201.16

Anything else?

No response

Sub-issues

Sub-issues

1 of 1 Issues completed

Activity

ghost added
area-blazorIncludes: Blazor, Razor Components
on Apr 11, 2025
soundaranbu

soundaranbu commented on Apr 11, 2025

@soundaranbu

You beat me to it. I was about to click submit on the same issue that I encountered after trying Preview 3. I also commented about similar issue I faced on the other day after trying the daily bits #26794 (comment)

guardrex

guardrex commented on Apr 11, 2025

@guardrex
ContributorAuthor

Indeed, @soundaranbu! Actually, the docs that I put up yesterday for the use of this are currently wrong 😢, but I only had the PR to go on. We (the whole docs team) even thought that release was next Tuesday, so we thought we had more time to iron bugs out of things. I'll have the guidance fixed by EOD.

However, a Weather component approach akin to the counter example that I showed does work ...

[SupplyParameterFromPersistentComponentState]
public WeatherForecast[]? Forecasts { get; set; }

protected override async Task OnInitializedAsync()
{
    // Simulate asynchronous loading to demonstrate a loading indicator
    await Task.Delay(500);

    var startDate = DateOnly.FromDateTime(DateTime.Now);
    var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };

    Forecasts ??= Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = startDate.AddDays(index),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = summaries[Random.Shared.Next(summaries.Length)]
    }).ToArray();
}
      XXXXXXXXXXX Logger: Executing OnInitializedAsync! 4/11/2025 1:12:44 PM
info: BWA10P3TestDeclarativePersistStateDELETEME.Components.Pages.Weather[0]
      4/12/2025 - Bracing - 7 - 44
info: BWA10P3TestDeclarativePersistStateDELETEME.Components.Pages.Weather[0]
      4/13/2025 - Balmy - 5 - 40
info: BWA10P3TestDeclarativePersistStateDELETEME.Components.Pages.Weather[0]
      4/14/2025 - Chilly - -13 - 9
info: BWA10P3TestDeclarativePersistStateDELETEME.Components.Pages.Weather[0]
      4/15/2025 - Cool - 39 - 102
info: BWA10P3TestDeclarativePersistStateDELETEME.Components.Pages.Weather[0]
      4/16/2025 - Bracing - 44 - 111
info: BWA10P3TestDeclarativePersistStateDELETEME.Components.Pages.Weather[0]
      XXXXXXXXXXX Logger: Executing OnInitializedAsync! 4/11/2025 1:12:45 PM
info: BWA10P3TestDeclarativePersistStateDELETEME.Components.Pages.Weather[0]
      4/12/2025 - Bracing - 7 - 44
info: BWA10P3TestDeclarativePersistStateDELETEME.Components.Pages.Weather[0]
      4/13/2025 - Balmy - 5 - 40
info: BWA10P3TestDeclarativePersistStateDELETEME.Components.Pages.Weather[0]
      4/14/2025 - Chilly - -13 - 9
info: BWA10P3TestDeclarativePersistStateDELETEME.Components.Pages.Weather[0]
      4/15/2025 - Cool - 39 - 102
info: BWA10P3TestDeclarativePersistStateDELETEME.Components.Pages.Weather[0]
      4/16/2025 - Bracing - 44 - 111
javiercn

javiercn commented on Apr 11, 2025

@javiercn
Member

BTW ... I'm also not sure if we're supposed to account for the restoration of a persisted value during initialization ...

if (CurrentCount == 0)
{
CurrentCount = Random.Shared.Next(100);
}
I'm getting the impression that if the dev wants to set an initial value that they must adopt the direct use of the > PersistentComponentState service and not use the [SupplyParameterFromPersistentComponentState] attribute.

The way to set an initial value is through null coalescing within OnInitialized(Async)

Property ??= default
javiercn

javiercn commented on Apr 11, 2025

@javiercn
Member

@guardrex can you push the repro to a GH repo so that I can read through the details?

guardrex

guardrex commented on Apr 11, 2025

@guardrex
Author
guardrex

guardrex commented on Apr 11, 2025

@guardrex
ContributorAuthor

Placed it here ...

https://github.com/guardrex/DeclarativeStateTesting

The Counter2 component is the one throwing and not restoring state 😈.

UPDATE (5/12): I modified the Counter2 component to make ...

  • The counter property a nullable int.
  • The OnInitialized method use null-coalescing.
guardrex

guardrex commented on Apr 11, 2025

@guardrex
ContributorAuthor

Good news BTW ...

I checked all of the guidance that I placed yesterday; and except for a few minor NITs here and there, all of the other examples of the approach are correct.

This counter component problem/exception is the only actually broken 😈 example.

guardrex

guardrex commented on Apr 11, 2025

@guardrex
ContributorAuthor

One more note ...

For demo purposes in the article, I need to show the assignment and the restoration, if possible. I was hoping something like this would work ...

if (CurrentCount is null)
{
    CurrentCount = Random.Shared.Next(100);
    Logger.LogInformation("CurrentCount set to {Count}", CurrentCount);
}
else
{
    Logger.LogInformation("CurrentCount restored to {Count}", CurrentCount);
}

... but since the approach is breaking, I just get the first block of the conditional called twice ...

info: BWA10P3TestDeclarativePersistStateDELETEME.Components.Pages.Counter2[0]
      CurrentCount set to 69
fail: Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager[1000]
      There was an error executing a callback while pausing the application.
      System.ArgumentException: Cannot bind to the target method because its signature is not compatible with that of the delegate type.
         at System.Reflection.RuntimeMethodInfo.CreateDelegateInternal(Type delegateType, Object firstArgument, DelegateBindingFlags bindingFlags)
         at Microsoft.AspNetCore.Components.Reflection.PropertyGetter..ctor(Type targetType, PropertyInfo property)
         at Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateValueProvider.PropertyGetterFactory(ValueTuple`2 key)
         at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
         at Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateValueProvider.ResolvePropertyGetter(Type type, String propertyName)
         at Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateValueProvider.<>c__DisplayClass11_0.<Subscribe>b__0()
         at Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.<TryPauseAsync>g__TryExecuteCallback|18_0(Func`1 callback, ILogger`1 logger)
info: BWA10P3TestDeclarativePersistStateDELETEME.Components.Pages.Counter2[0]
      CurrentCount set to 97

decimal and double do the same thing.

Works well with a class, so I'll use the following in the article so that we don't have a broken example published ...

@page "/counter-3"
@inject ILogger<Counter3> Logger

<PageTitle>Prerendered Counter 3</PageTitle>

<h1>Prerendered Counter 3</h1>

<p role="status">Current count: @State?.CurrentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    [SupplyParameterFromPersistentComponentState]
    public CounterState? State { get; set; }

    protected override void OnInitialized()
    {
        if (State is null)
        {
            State = new() { CurrentCount = Random.Shared.Next(100) };
            Logger.LogInformation("CurrentCount set to {Count}", State.CurrentCount);
        }
        else
        {
            Logger.LogInformation("CurrentCount restored to {Count}", State.CurrentCount);
        }
    }

    private void IncrementCount()
    {
        if (State is not null)
        {
            State.CurrentCount++;
        }
    }

    public class CounterState
    {
        public int CurrentCount { get; set; }
    }
}
info: BWA10P3TestDeclarativePersistStateDELETEME.Components.Pages.Counter3[0]
      CurrentCount set to 60
info: BWA10P3TestDeclarativePersistStateDELETEME.Components.Pages.Counter3[0]
      CurrentCount restored to 60
added this to the 10.0-preview5 milestone on Apr 15, 2025
changed the title [-]Declarative state persistence base case failure[/-] [+][Pre5] Declarative state persistence base case failure[/+] on May 12, 2025
changed the title [-][Pre5] Declarative state persistence base case failure[/-] [+]Declarative state persistence base case failure[/+] on May 12, 2025
self-assigned this
on Jun 12, 2025
javiercn

javiercn commented on Jul 16, 2025

@javiercn
Member

This was fixed in #62368

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area-blazorIncludes: Blazor, Razor Components

Type

No type

Projects

No projects

Relationships

None yet

    Development

    No branches or pull requests

      Participants

      @guardrex@javiercn@soundaranbu

      Issue actions

        Declarative state persistence base case failure · Issue #61456 · dotnet/aspnetcore