question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Calling InvokeAsync(StateHasChanged) causes page to fallback to default culture

See original GitHub issue

Describe the bug

In a Blazor Server application, I am updating page data from a background service. This update from the service ultimately ends up calling InvokeAsync(StateHasChanged) in the page. The page is also using IStringLocalizer<T> to provide translations for table headers. When the application runs, the user can select the current culture (e.g. German (de-DE)), and the table is displayed with translated headers. As soon as the background updates begin, the German culture is ignored, and the page is rendered using English (en-US) resources.

My expectation is that calling StateHasChanged will use the proper resources for rendering the page.

Localization_Debugging

To Reproduce

I have created a small application that reproduces this scenario:

https://github.com/DaveNay/Service_Localization_Debugging

In the sample application, you can switch to the FetchData page, and then change the culture to German. You will see that the page is rendered with the de-DE resources. 10 seconds after the application starts, the background updates will begin. As soon as the updates begin, the page is rendered using en-US resources.

Exceptions (if any)

None

Further technical details

  • .Net 5.0 (also .Net Core 3.1)
  • Visual Studio Pro 16.8.2
  • Windows 10 Enterprise 1809
  • dotnet_info.txt

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:3
  • Comments:10 (6 by maintainers)

github_iconTop GitHub Comments

1reaction
beppemarazzicommented, Apr 29, 2022

@javiercn: Your explanation was very clear an i understood it.

Probably we agree that any kind of “global state” smells, but this is how AddLocalization() paired with UseRequestLocalization() works (ultimately the ResourceManagerStringLocalizer uses the global CurrentCulture and CurrentUICulture stored into circuit’s execution context by the Localization Middleware).

Given this, i’ve to disagree with you when you say:

We aren’t going to provide any functionality to capture and restore the current culture or any other ambient value (backed by an async local) because it is not the framework responsibility to do so.

In fact IMHO there is a very particular case in the server rendered blazor’s app model: we have one single server where we can centralize for all users some low level “active” logic (i.e. raising some events/notifications when something happens), mixed with “per user” render pipeline (i.e. components in the circuit). This is very powerful and you can write “live” apps where the data are “pushed” from server to clients very quickly.

So i think that some sort of framework provided utility capturing at the component’s initialization the circuit’s Culture informations (or if you prefer all the execution context), and restores it back when component’s state related actions are dispatched to renderer sync context may be very useful (IMHO it may be the default behavior). A such framework utility could minimize the technical complex bolierplate “bridging” code in the app’s codebase…

That said, i’m already happy with my solution (the ComponentBaseWithCulture used as base class for my blazor pages)… probably other users are not aware of this design and may fall in this pitfall!

Many thanks for your patience: i will not push further for this 😄

Edit: i’m not lone with the expectation of some framework defined utility to manage this: https://github.com/dotnet/aspnetcore/pull/36259#discussion_r707172784

1reaction
javiercncommented, Apr 28, 2022

This is not a bug, but an issue with “from where” the code is calling InvokeAsync.

The problem is that the code triggers a global event from outside the scope of the circuit and a registered handler calls InvokeAsync to update the state.

The problem is that the call originates from a thread that doesn’t have the correct ExecutionContext because it is external to the circuit. InvokeAsync ensures that code that updates the UI runs single threaded and captures the ExecutionContext when it is invoked as part of responding to a UI event or rendering a component.

However, when done in the fashion described here, there is no way for InvokeAsync to have access to the ExecutionContext normally associated with the circuit (because the source of the event that calls InvokeAsync is a separate thread).

The issue that we are observing here is because the culture on the External thread that originates the call (and that comes ultimately from the ExecutionContext of that thread) is not the one associated with the circuit and for that reason, the culture changes when the event is triggered.

This can be corrected by manually capturing the context at the time the event is being registered and restoring it manually when the event is raised, like this:

        var context = ExecutionContext.Capture();
        Cache.ForecastHasChanged += (forecasts) =>
        {
            var current = ExecutionContext.Capture();
            try
            {
                ExecutionContext.Restore(context);
                OnForecastChanged(forecasts);
            }
            finally
            {
                ExecutionContext.Restore(current);
            }
        };

At this point, when we register the event, we capture the ExecutionContext so that when the event is triggered (no matter where from) we can restore the original ExecutionContext, run the callback and restore the caller ExecutionContext afterwards.

It is key to understand that the issue here is with the callstack by the time InvokeAsync is called.

When that callstack originates from handling an event or rendering a component in the context of a circuit everything works as expected and automatically because the call to InvokeAsync happens within the circuit, where the ExecutionContext is correct and flows as part of the async chain.

When the callstack originates from outside of the circuit, the execution context is not going to be correct and InvokeAsync doesn’t have a way determine the execution context since the logical chain broke.

That’s why using the snippet above bridges the two pieces. During registration the correct context is captured. During execution the context is restored, the callback runs and then the caller restores their context back.

Read more comments on GitHub >

github_iconTop Results From Across the Web

StateHasChanged() vs InvokeAsync ...
I know that calling the StateHasChanged() method notifies the component that the state has changed and so it should re-render.
Read more >
ASP.NET Core Blazor synchronization context
For more information, see Calling InvokeAsync(StateHasChanged) causes page to fallback to default culture (dotnet/aspnetcore #28521).
Read more >
WebAssembly
To get a browser's culture in Blazor WebAssembly, call the (navigator.language) property in JavaScript using the JavaScript interop function in Blazor. It will ......
Read more >
Blazor for ASP.NET Web Forms Developers
With Blazor you can write your client-side logic and UI components in C#, compile them into normal .NET assemblies, and then run them...
Read more >
Blazor PDF | PDF | Dynamic Web Page | Web Application
Blazor Server apps. Recall from the Blazor architecture discussion that Blazor components render their output to an intermediate abstraction called a RenderTree ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found