dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.57k stars 10.05k forks source link

Blazor AsyncLocal is null on first re-render following hot reload #45741

Open Bouke opened 1 year ago

Bouke commented 1 year ago

Is there an existing issue for this?

Describe the bug

When a hot reload is performed, any value stored with AsyncLocal is not available on the first render. Any subsequent renders will have the original AsyncLocal values available again. AsyncLocal is a common design approach for storing contextual information, e.g. Thread.CurrentPrincipal or CultureInfo.CurrentCulture as set by app.UseRequestLocalization().

Expected Behavior

AsyncLocal should flow and also be available on the rerender triggered by hot reload.

Steps To Reproduce

  1. https://github.com/Bouke/BlazorAsyncLocal/commit/640aeb50a77e26e5bef38556d8d4e6147de5ac7b Or:

    diff --git a/Pages/Index.razor b/Pages/Index.razor
    index b1a9fbd..b94d274 100644
    --- a/Pages/Index.razor
    +++ b/Pages/Index.razor
    @@ -1,9 +1,12 @@
     @page "/"
    +@using System.Globalization
    
     <PageTitle>Index</PageTitle>
    
     <h1>Hello, world!</h1>
    
    -Welcome to your new app.
    +<p>Welcome to your new app. You're on instance @AsyncLocalMiddleware.Instance.Value. Your current culture is @CultureInfo.CurrentCulture.Name.</p>
    +
    +<p><button class="btn btn-primary" @onclick="() => StateHasChanged()">Invoke StateHasChanged()</button></p>
    
     <SurveyPrompt Title="How is Blazor working for you?" />
    diff --git a/Program.cs b/Program.cs
    index 0a103e7..a47fbaf 100644
    --- a/Program.cs
    +++ b/Program.cs
    @@ -19,6 +19,9 @@ if (!app.Environment.IsDevelopment())
         app.UseHsts();
     }
    
    +app.UseMiddleware<AsyncLocalMiddleware>();
    +app.UseRequestLocalization("nl");
    +
     app.UseHttpsRedirection();
    
     app.UseStaticFiles();
    @@ -29,3 +32,22 @@ app.MapBlazorHub();
     app.MapFallbackToPage("/_Host");
    
     app.Run();
    +
    +public class AsyncLocalMiddleware
    +{
    +    private static int instances = 0;
    +    public static readonly AsyncLocal<int?> Instance = new();
    +    private readonly RequestDelegate next;
    +
    +    public AsyncLocalMiddleware(RequestDelegate next)
    +    {
    +        this.next = next ?? throw new ArgumentNullException(nameof(next));
    +    }
    +
    +    public async Task InvokeAsync(HttpContext context, ILogger<AsyncLocalMiddleware> logger)
    +    {
    +        Instance.Value = instances++;
    +        logger.LogInformation($"Set instance to {Instance.Value}");
    +        await next(context);
    +    }
    +}
  2. Run dotnet watch and open in browser. Notice that the page says:

    Welcome to your new app. You're on instance 13. Your current culture is nl.

  3. Change Pages/Index.razor in a way that triggers hot reload. E.g. change the heading to <h1>Hello, you!</h1>

  4. Notice that the page now says:

    Welcome to your new app. You're on instance . Your current culture is en-US.

  5. Click the button labeled "Invoke StateHasChanged()". Notice that the page now updates to the text from step 2:

    Welcome to your new app. You're on instance 13. Your current culture is nl.

Exceptions (if any)

No response

.NET Version

6.0.403

Anything else?

.NET SDK (reflecting any global.json): Version: 6.0.403 Commit: 2bc18bf292

Runtime Environment: OS Name: Mac OS X OS Version: 13.1 OS Platform: Darwin RID: osx-x64 Base Path: /usr/local/share/dotnet/sdk/6.0.403/

global.json file: Not found

Host: Version: 6.0.11 Architecture: x64 Commit: 943474ca16

.NET SDKs installed: 6.0.403 [/usr/local/share/dotnet/sdk]

.NET runtimes installed: Microsoft.AspNetCore.App 6.0.11 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.NETCore.App 6.0.11 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]

davidfowl commented 1 year ago

I'm not sure how that would work. The request isn't being re-run when StateHashChanged fires. This isn't about async locals as much as its about where the async locals are being set. In the above case, the middleware isn't being re-run so it's by design that the value doesn't change.

Bouke commented 1 year ago

The problem is that the value of AsyncLocal is expected to stay the same for the duration of the circuit, once set from the middleware. The render triggered by hot reload is rendered with different AsyncLocal (null values), however on subsequent renders the original AsyncLocal values are back. So I'm thinking that the render triggered by hot reload is executed from the wrong ExecutionContext.

davidfowl commented 1 year ago

I've updated the title to reflect the issue. This does sound like a bug. I assume it's coming from somewhere here

https://github.com/dotnet/aspnetcore/blob/95c74fef646a262909be462c9edda8d8ed4d5b24/src/Components/Components/src/RenderTree/Renderer.cs#L132

javiercn commented 1 year ago

The execution context needs to be captured at the location where the delegate is being registered here and the callback needs to be dispatched to that context.

javiercn commented 1 year ago

With that in mind, I would avoid relying on AsyncLocals set inside middleware, as they won't survive re-connection. The right approach to work with async locals in Blazor is covered https://github.com/dotnet/AspNetCore.Docs/pull/27909/files?short_path=e2a9b3d#diff-e2a9b3d2f3c263585acc721fd569aa0a0666144e4e9bbbf5673e8e53c007749e (can't find the link to the actual docs).

ghost commented 1 year ago

Thanks for contacting us.

We're moving this issue to the .NET 8 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

Bouke commented 1 year ago

With that in mind, I would avoid relying on AsyncLocals set inside middleware, as they won't survive re-connection.

This issue is not about AsyncLocals in middleware. It is about AsyncLocal in Blazor. The middleware sets the AsyncLocal (the current culture), and the Blazor components gets the AsyncLocal.

The right approach to work with async locals in Blazor is covered https://github.com/dotnet/AspNetCore.Docs/pull/27909/files?short_path=e2a9b3d#diff-e2a9b3d2f3c263585acc721fd569aa0a0666144e4e9bbbf5673e8e53c007749e (can't find the link to the actual docs).

Link to docs: https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-7.0#access-blazor-services-from-a-different-di-scope. I fail to see how this applies to this issue though? Getting/setting the current culture is just an example of AsyncLocal in combination with hot reload: it only fails for hot reload. Having to rewrite core runtime functionality using AsyncLocal such as CultureInfo.CurrentCulture in an ad-hoc manner to be blazor-compatible (and consequently all my blazor un-aware services) seems the wrong approach to me.

ghost commented 1 year ago

Thanks for contacting us.

We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

bxjg1987 commented 4 months ago

blazor web app .net8

public class AbpExceptionInterceptor1
{ 
    public static readonly AsyncLocal<IServiceProvider> Services = new AsyncLocal<IServiceProvider>();
}

in routers component

[Inject]
public IServiceProvider ServiceProvider { get; set; }
protected override void OnInitialized()
{
     AbpExceptionInterceptor1.Services.Value = ServiceProvider;
     //...

why the AbpExceptionInterceptor1.Services always null.I am using Fody based AOP and particularly need such functionality.

Thank you again for the asp.net core blazer!