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.28k stars 9.96k forks source link

Blazor Identity and LocalStorage with InteractiveRenderMode #55799

Open gndev-vn opened 4 months ago

gndev-vn commented 4 months ago

Is there an existing issue for this?

Describe the bug

Ok, I am working on a Blazor Web App, including individual identity and local storage to build the cart. There is a CartStateProvider which provide realtime updates of the local storage for the whole app.

What I have learned from MS Document is the local storage required the render mode to be InteractiveServerRenderMode(prerender: false) which leads to another problem with the identity is that any page uses AccountLayout will stuck in reload loop because HttpContext is null while in InteractiveServerRenderMode.

What should we do in this scenario?

App.razor

<HeadOutlet @rendermode="new InteractiveServerRenderMode(false)" />
<Routes @rendermode="new InteractiveServerRenderMode(false)" />

CartStateProvider

@if (_isLoaded)
{
    <CascadingValue Value="this">
        @ChildContent
    </CascadingValue>
}
else
{
    <GdLoader></GdLoader>
}

@code {
    private bool _isLoaded;

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public int TotalItems { get; set; }

    protected override async Task OnInitializedAsync()
    {
        CartService.OnChange += Update;
        TotalItems = await CartService.GetTotalItemsAsync();
        _isLoaded = true;
    }

    private async void Update()
    {
        TotalItems = await CartService.GetTotalItemsAsync();
        StateHasChanged();
    }

    public void Dispose()
    {
        CartService.OnChange -= StateHasChanged;
    }
}

CartService.cs

public class CartService(ProtectedLocalStorage localStorage) : ICartService
{
    private const string StorageKey = "cart";

    public event Action? OnChange;

    public async Task<int> GetTotalItemsAsync()
    {
        var cartResult = await localStorage.GetAsync<CartModel>(StorageKey);
        if (!cartResult.Success) return 0;
        var cart = cartResult.Value;
        return cart == null ? 0 : cart.Items.Sum(x => x.Quantity);
    }
}

Routes.razor

<CartStateProvider>
    <Router AppAssembly="typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)">
                <NotAuthorized>
                    <RedirectToLogin />
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
    </Router>
</CartStateProvider>

There is a recommendation to add the dynamic render mode variable on App.razor but that won't work.

Expected Behavior

No response

Steps To Reproduce

No response

Exceptions (if any)

No response

.NET Version

8.0.205

Anything else?

No response

mkArtakMSFT commented 4 months ago

Thanks for contacting us. The problem you're facing is because the Account pages currently don't support interactive rendering. We recommend you try to redesign your app, so you don't need to rely on the interactivity support of account pages, and instead expose your cart information only on parts of the app, where you do have interactivity support.

mkArtakMSFT commented 4 months ago

Parking this on our backlog to think more about this in the future and see if we can come up with a better solution for this scenario.

gndev-vn commented 4 months ago

I think the problem is with the incompatible render mode between ProtectedBrowserStorage and the current implementation of Identity. If I use ProtectedBrowserStorage, I must set the render mode to InteractiveServerRenderMode(false) in the App.razor and thus there is no chance the identity can be used because this is global mode. If I remove the render mode, identity work but the app will throw error on startup because JS interop. So, how do I use ProtectedBrowserStorage beside Identity should be a question.

halter73 commented 4 months ago

Do you need the CartStateProvider for the identity pages in particular? You could leave <Routes @rendermode="@RenderModeForPage" /> where RenderModeForPage is defined as follows:

    private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account")
        ? null
        : new InteractiveServerRenderMode(prerender: false);

Then you could add [CascadingParameter] private HttpContext? HttpContext { get; set; } to CartStateProvider. When the HttpContext is not null, you know that you must be rendering a non-interactive /Account page and skip the call to await CartService.GetTotalItemsAsync() since you know the account pages won't need the CartStateProvider cascading value anyway.

If you do need the cascading value on the account pages because it's part of the navigation menu, you could include <NavMenu @rendermode="new InteractiveServerRenderMode(prerender: false)" /> inside your MainLayout. This will make the NavMenu always interactive even on the otherwise static account pages. Then you could wrap the contents of NavMenu with <CartStateProvider> where it will always be interactive rather than wrapping the entirety of Routes.razor. Or you could do some combination of both.

gndev-vn commented 4 months ago

Do you need the CartStateProvider for the identity pages in particular? You could leave <Routes @rendermode="@RenderModeForPage" /> where RenderModeForPage is defined as follows:

    private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account")
        ? null
        : new InteractiveServerRenderMode(prerender: false);

Then you could add [CascadingParameter] private HttpContext? HttpContext { get; set; } to CartStateProvider. When the HttpContext is not null, you know that you must be rendering a non-interactive /Account page and skip the call to await CartService.GetTotalItemsAsync() since you know the account pages won't need the CartStateProvider cascading value anyway.

If you do need the cascading value on the account pages because it's part of the navigation menu, you could include <NavMenu @rendermode="new InteractiveServerRenderMode(prerender: false)" /> inside your MainLayout. This will make the NavMenu always interactive even on the otherwise static account pages. Then you could wrap the contents of NavMenu with <CartStateProvider> where it will always be interactive rather than wrapping the entirety of Routes.razor. Or you could do some combination of both.

yeah, I tried with that as I said in last line of the original issue. Wrapping CartStateProvider outside component won't work, the CascadeParameter will be null.

john-kuber commented 5 days ago

@gndev-vn, I also have the same issue on a Blazor Server WebApp that I've added ASp.Net Core Identity (.net 8.0) to it from the Blazor Server WebApp template. Was the issue resolved now? If yes, could you please provide the solution? Regards

gndev-vn commented 3 days ago

@gndev-vn, I also have the same issue on a Blazor Server WebApp that I've added ASp.Net Core Identity (.net 8.0) to it from the Blazor Server WebApp template. Was the issue resolved now? If yes, could you please provide the solution? Regards

Try this

<HeadOutlet @rendermode="RenderModeForPage" /> <Routes @rendermode="RenderModeForPage" />

@code {
    [CascadingParameter] private HttpContext HttpContext { get; set; } = default!;

    private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/account")
        ? null
        : new InteractiveServerRenderMode(false);

}

I still have some problem with the local storage service though, but I quit using it until MS devs figure out how to make them compatible.