Problem with HttpContext and Interactive rendering

Hello, I'm moving from ASP.net Blazor standard Identity UI to a Radzen-based set of pages. (Using ASP.net 8).
I have a problem with the Login page.
My code is as follows:

App.razor

<!DOCTYPE html>
<html lang="en" data-bs-theme="light">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
    <link rel="stylesheet" href="app.css" />
    <link rel="stylesheet" href="BlazId.styles.css" />
    <link rel="icon" type="image/png" href="favicon.png" />
    <link rel="stylesheet" href="_content/Radzen.Blazor/css/material-base.css">
    <HeadOutlet @rendermode="@InteractiveServer" />
</head>

<body>
    <Routes @rendermode="@InteractiveServer" />
    <script src="_framework/blazor.web.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
    <script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script>
</body>

</html>

In the file SideMenu.razor the following line calls the login page:

<RadzenPanelMenu>
    ...
    <AuthorizeView>
        <Authorized>
            ...
        </Authorized>
        <NotAuthorized>
            <RadzenPanelMenuItem  Text="Login" Icon="login" Path="Account/Login2"></RadzenPanelMenuItem>
        </NotAuthorized>
    </AuthorizeView>
</RadzenPanelMenu>

The Login2.razor page is the following, but it's totally irrelevant for the issue I have. For sake of simplicity, I'll bypass it in my example here

@page "/Account/Login2"

@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Identity
@using BlazId.Data
@using Account

@inject SignInManager<MyUser> SignInManager
@inject ILogger<Login2> Logger
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager

<RadzenCard class="rz-my-12 rz-mx-auto rz-p-4 rz-p-md-12" style="max-width: 600px;">
    <RadzenTemplateForm  Data=@Input Method="post">
        @* <AntiforgeryToken /> *@
        <RadzenLogin AllowRegister="true" AllowResetPassword="true" Username=@userName Password=@password
                     AllowRememberMe="true" RememberMe="@rememberMe" 
                     Login=@(args => OnLogin(args, "Login with default values"))
                     ResetPassword=@(args => OnResetPassword(args, "Login with default values"))
                     Register=@(args => OnRegister("Login with default values")) />
    </RadzenTemplateForm>
</RadzenCard>


@code {
    string userName = "user@domain.com";
    string password = "Welcome1";
    bool rememberMe = true;

    async void Submit(InputModel args)
    {
        await LoginUser(args);
    }

    protected override async Task OnInitializedAsync()
    {
        if (HttpMethods.IsGet(HttpContext.Request.Method))
        {
            // Clear the existing external cookie to ensure a clean login process
            await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
        }
    }

    public async Task OnLogin(LoginArgs args, string name)
    {
        // console.Log($"{name} -> Username: {args.Username}, password: {args.Password}, remember me: {args.RememberMe}");
        var result = await SignInManager.PasswordSignInAsync(args.Username, args.Password, args.RememberMe, lockoutOnFailure: false);

        if (result.Succeeded)
        {
            Logger.LogInformation("User logged in.");
            // RedirectManager.RedirectTo(ReturnUrl);
        }
    }

    void OnRegister(string name)
    {
        // console.Log($"{name} -> Register");
    }

    void OnResetPassword(string value, string name)
    {
        // console.Log($"{name} -> ResetPassword for user: {value}");
    }

    [SupplyParameterFromForm]
    private InputModel Input { get; set; } = new();

    [SupplyParameterFromQuery]
    private string? ReturnUrl { get; set; }

    [SupplyParameterFromQuery]
    private string? RedirectUrl { get; set; }

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


    public async Task LoginUser(InputModel model)
    {
        // This doesn't count login failures towards account lockout
        // To enable password failures to trigger account lockout, set lockoutOnFailure: true
        var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
        if (result.Succeeded)
        {
            Logger.LogInformation("User logged in.");
            // RedirectManager.RedirectTo(ReturnUrl);
        }
        else if (result.RequiresTwoFactor)
        {
            // RedirectManager.RedirectTo(
            //     "Account/LoginWith2fa",
            //     new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe });
        }
        else if (result.IsLockedOut)
        {
            Logger.LogWarning("User account locked out.");
            // RedirectManager.RedirectTo("Account/Lockout");
        }
        else
        {
            // errorMessage = "Error: Invalid login attempt.";
        }
    }

    public sealed class InputModel
    {
        [Required]
        [EmailAddress]
        public string Email { get; set; } = "";

        [Required]
        [DataType(DataType.Password)]
        public string Password { get; set; } = "";

        [Display(Name = "Remember me?")]
        public bool RememberMe { get; set; }
    }

}

The Login2.razor page refers to the follwing _imports.razor file

@using BlazId.Components.Account.Shared
@layout AccountLayout

The big issue comes in the AccountLayout.razor file:

@inherits LayoutComponentBase
@layout BlazId.Components.Layout.MainLayout
@inject NavigationManager NavigationManager

@if (HttpContext is null)
{
    <p>Loading...</p>
}
else
{
    <p>LOADED @HttpContext.ToString();</p>
    @* @Body *@
}

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

    protected override void OnParametersSet()
    {
        if (HttpContext is null)
        {
            // If this code runs, we're currently rendering in interactive mode, so there is no HttpContext.
            // The identity pages need to set cookies, so they require an HttpContext. To achieve this we
            // must transition back from interactive mode to a server-rendered page.
            NavigationManager.Refresh(forceReload: true);
        }
    }
}

Here I intentionally commented out the @Body just to avoid any overhead from the Layout2.razor content, because the bad behavior is the same:
It continues to reload the page, once with a HttpContext set to null and once with HttpContext set to the proper value. It continues to flash between the two situations indefinitely. If I stop the page using the browser's 'X' (stop) button, sometimes it stops on "Loading...", sometimes it stops on the correct page. When it stops in the correct side, I can properly login and the cookies are properly set.
If in the App.razor page, I change

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

with

<HeadOutlet  />
<Routes  />

this part work properly, but (as you could imagine) the modal DialogService like DialogService.Alert(...) don't work any more.

How can I have both modal Dialogs AND proper Login page loaded correctly ?

Try to disable prerendering

Another things you can try:

  1. Replace RadzenTemplateForm with EditForm and see if this makes a difference.
  2. Avoid setting @rendermode at Routes. Set it to <RadzenComponent /> which is in your layout instead as per our getting started instructions. I am not 100% sure that accessing HttpContext works in interactive rendering modes.

In any case I would start from a working configuration and make changes step by step to see what breaks the implementation.

Hi @mpolazzo

It may be right about HttpContext not working in InteractiveServer mode.

My project created from scratch in Visual Studio 2022 has the following code in App.razor -

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

    private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account")
        ? null
        : InteractiveServer;
}

All my "Login" routes begin "/Account".

As already mentioned above, I would strongly recommend creating a fresh project and using the generated code as a learning curve / starting point.

Many regards

Paul

Thanks for your answer.
I tried this, the effect is that now it's only flashing (very quickly) "Loading...". The other Login page is not shown alternatively. (:frowning:).
I'll try further... thanks.

@korchev Thanks for your answer.
Mine was a clean new application and I isolated the cause of breakage in

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

instead of the standard

<HeadOutlet  />
<Routes  />

I changed it back (without InteractiveServer) and added it to <RadzenComponent /> in MainLayout.razor as follows:

...
@inject ContextMenuService ContextMenuService
@inject NotificationService NotificationService

<RadzenComponents @rendermode="@InteractiveServer" />

<RadzenLayout style="grid-template-areas: 'rz-sidebar rz-header' 'rz-sidebar rz-body';">
    <RadzenHeader>
...

Now, Login works properly (no flashing with "Loading..."), but the Dialog boxes don't work any more (original reason why I had to set InteractiveServer in App.razor).

Thanks @Paul_Ruston, I had the same in a fresh project, but it didn't have any effect. I tried with and without. It seems it is completely useless.

Hi @Paul_Ruston finally it worked!!!
I used your @code snippet (that I also had in my App.razor), but instead of using InteractiveServer I had to use RenderModeForPage in the declarations.

Finally, App.razor is as follows:

<html lang="en" data-bs-theme="light">
<head>
    ...
    <HeadOutlet @rendermode="@RenderModeForPage" />
</head>

<body>
    <Routes @rendermode="@RenderModeForPage" />
    ...
</body>
</html>

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

    private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account")
        ? null
        : InteractiveServer;
}

Thank you all!!

This would work if used from pages that have @rendermode InteractiveServer. The workaround that you have applied in the end does something similar by changing the "global" render mode depending on the route.

This is case number X where rendering modes make things harder than easier. And there is no end in sight.

1 Like

I had exactly the same problems but I already solved them.

  1. In App.razor use this in the head:
    <HeadOutlet @rendermode="RenderModeForPage" />
    then use this in the body:
    <Routes @rendermode="RenderModeForPage" />

then in the code:
@code {
** [CascadingParameter]**
** private HttpContext HttpContext { get; set; } = default!;**

** private IComponentRenderMode RenderModeForPage => (HttpContext.Request.Path.StartsWithSegments("/Account") &&**
** !HttpContext.Request.Path.StartsWithSegments("/Account/Manage"))**
** ? null**
** : InteractiveServer;**
}

  1. in the ManageLayout.razor use @layout MainLayout instead of @layout AccountLayout
    then @inject IHttpContextAccessor ctxAccessor
    and use:

    ** @Body**

  2. stop using these 3 classes in all InteractiveServer Mode components:
    IdentityUserAccessor
    IdentityRedirectManager
    IdentityRevalidatingAuthenticationStateProvider

Good luck

2 Likes