Blazor Web App partly run code as WASM

iKingNinja 140 Reputation points
2025-11-07T17:49:25.3533333+00:00

I'm trying to create a register account page in Blazor Web App. I need the form submission callback to run on the client as it needs to set the authentication cookie, but I can't manage to achieve this.

This is my current component code:

@using ClassLibrary.Models.Auth
@using ClassLibrary.Models.Database
@using ClassLibrary.Localization.Shared.Forms
@using IDFW.Models.HttpClients

@rendermode InteractiveWebAssembly
@page "/auth/register"

@inject IStringLocalizer<Register> localizer
@inject ApiHttpClient api

<HeadContent>
    <link rel="stylesheet" href="/css/auth/login.css" />
    <link rel="stylesheet" href="/css/components/loader.css" />
</HeadContent>
<div class="content-container">
    <EditForm FormName="LoginForm" EditContext="editContext" class="data-form" OnValidSubmit="HandleOnValidSubmit">
        <DataAnnotationsValidator />
        <h1>@localizer["PageTitle"]</h1>
        <fieldset disabled="@isProcessing">
            <label>
                <p>@FormLocalization.FirstName<span class="danger">*</span></p>
                <InputText DisplayName="@FormLocalization.FirstName" @bind-Value="RegisterModel.FirstName" />
                <ValidationMessage For="() => RegisterModel.FirstName" class="danger" />
            </label>
            <label>
                <p>@FormLocalization.LastName<span class="danger">*</span></p>
                <InputText DisplayName="@FormLocalization.LastName" @bind-Value="RegisterModel.LastName" />
                <ValidationMessage For="() => RegisterModel.LastName" class="danger" />
            </label>
            <label>
                <p>Email<span class="danger">*</span></p>
                <InputText DisplayName="Email" @bind-Value="RegisterModel.Email" type="email" />
                <ValidationMessage For="() => RegisterModel.Email" class="danger" />
            </label>
            <label>
                <p>Password<span class="danger">*</span></p>
                <div class="form-password">
                    <InputText DisplayName="Password" @bind-Value="RegisterModel.Password" type="password" autocomplete="new-password" />
                    <span class="fluent-icon icon-ic_fluent_eye_16_filled password-toggle"></span>
                </div>
                <ValidationMessage For="() => RegisterModel.Password" class="danger" />
            </label>
            <label>
                <p>@AuthFormLocalization.ConfirmPassword<span class="danger">*</span></p>
                <div class="form-password">
                    <InputText DisplayName="@AuthFormLocalization.ConfirmPassword" @bind-Value="RegisterModel.ConfirmPassword" type="password" autocomplete="new-password" />
                    <span class="fluent-icon icon-ic_fluent_eye_16_filled password-toggle"></span>
                </div>
                <ValidationMessage For="() => RegisterModel.ConfirmPassword" class="danger" />
            </label>
        </fieldset>
        <fieldset class="form-checkbox-area" disabled="@isProcessing">
            <label>
                <InputCheckbox @bind-Value="RegisterModel.KeepLogin" />
                @AuthFormLocalization.KeepLogin
            </label>
            <label>
                <InputCheckbox @bind-Value="RegisterModel.AgreeLegal" />
                @((MarkupString)FormLocalization.LegalNote)
                <ValidationMessage For="() => RegisterModel.AgreeLegal" class="danger" />
            </label>
        </fieldset>
        <button type="submit" class="cta-btn primary-cta-btn" disabled="@isProcessing">
            @if (!isProcessing)
            {
                @localizer["Register"]
            }
            else
            {
                <span class="loader"></span>
            }
        </button>
        <p class="form-hint">@((MarkupString)localizer["FormHint"].Value)</p>
    </EditForm>
</div>
<script src="/js/form.js"></script>
@code {
    private RegisterModel RegisterModel { get; set; } = null!;
    private EditContext editContext = null!;
    private ValidationMessageStore validationMessageStore = null!;
    private bool isProcessing = false;

    [Inject]
    private NavigationManager navigation { get; set; } = null!;

    protected override void OnInitialized()
    {
        RegisterModel ??= new();
        editContext = new(RegisterModel);
        validationMessageStore = new(editContext);
        editContext.OnFieldChanged += (_, args) =>
        {
            validationMessageStore.Clear(args.FieldIdentifier);
            editContext.NotifyValidationStateChanged();
        };
    }

    private async Task HandleOnValidSubmit()
    {
        isProcessing = true;
        validationMessageStore.Clear();

        // Try register account
        HttpResponseMessage res = await api.HttpClient.PostAsJsonAsync("auth/register", RegisterModel);

        if (res.IsSuccessStatusCode)
        {
            navigation.NavigateTo("/");
        }
        else if (res.StatusCode == System.Net.HttpStatusCode.BadRequest)
        {
            HttpValidationProblemDetails problemDetails = (await res.Content.ReadFromJsonAsync<HttpValidationProblemDetails>())!;

            if (problemDetails.Errors != null)
            {
                foreach (KeyValuePair<string, string[]> message in problemDetails.Errors)
                {
                    FieldIdentifier fieldIdentifier = new(RegisterModel, message.Key);
                    validationMessageStore.Add(fieldIdentifier, message.Value);
                }

                editContext.NotifyValidationStateChanged();
            }
        }

        isProcessing = false;
    }
}

I want everything in the page to be rendered on the server as well as realtime data validation, except HandleOnValidSubmit() to run on the client. I tried using the InteractiveWebAssembly rendermode, but apparently it's not the right approach or I'm doing something wrong.

Developer technologies | .NET | Blazor
0 comments No comments
{count} votes

3 answers

Sort by: Most helpful
  1. Bruce (SqlWork.com) 81,971 Reputation points Volunteer Moderator
    2025-11-08T01:09:30.81+00:00

    Client code can not set an authentication cookie. If using WASM typically you would use jwt tickets rather than a cookie. Also to run server code from Blazor WASM ( client interactive) you use HttpClient to call a server api, you can’t call Blazor code. A Blazor either runs on the server or on the client.

    Blazor client typically use msal (oauth authentication). Cookie authentication is user with Blazor Server. In both cases the Blazor app redirects to a login page that unloads the Blazor app. After authentication the Blazor is reloaded and passed the token when authentication state is initialized.


  2. iKingNinja 140 Reputation points
    2025-11-19T23:18:32.6033333+00:00

    What I ended up doing is:

    • Creating a WASM project
    • Adding it with AddInteractiveWebAssemblyRenderMode() and AddAdditionalAssemblies() in the Web App (WA) project's Program.cs
    • Created a wrapper component in WA project with static render mode
    • Created the register page component in the WASM project and added it as child content of the wrapper with InteractiveWebAssembly render mode.

    This way I can have my HttpClient make requests from the browser (WASM) thus setting the authentication cookie.


  3. Danny Nguyen (WICLOUD CORPORATION) 4,985 Reputation points Microsoft External Staff Moderator
    2025-11-10T08:54:11.75+00:00

    Hi,

    The core issue is a mismatch between what you want (Identity cookie auth) and the render mode you chose. Cookie-based ASP.NET Core Identity only works naturally when the component runs on the server (InteractiveServer or InteractiveAuto). You can’t make just HandleOnValidSubmit run on WebAssembly; render mode applies to the whole component. You also don’t need client-side execution to receive a Set-Cookie; the server issues it and the browser stores it automatically. So drop InteractiveWebAssembly for this page and let the server handle registration + sign-in.

    To keep Identity, Cookie Auth and Use Server Interactivity:

    Change the rendermode component to run all events (including submit) on the server so Identity’s cookie is recognized immediately.

    
    @page "/auth/register"
    
    @rendermode InteractiveServer
    
    

    For submit handler, force a round trip so the new cookie is on the next request

    
    if (res.IsSuccessStatusCode)
    
    {
    
        navigation.NavigateTo("/", forceLoad: true);
    
        return;
    
    }
    
    

    In Program.cs, ensure the app can issue/read auth cookies during server-side events

    
    // Identity & Cookie Auth
    
    builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
    
        .AddEntityFrameworkStores<AppDbContext>()
    
        .AddDefaultTokenProviders();  // Tokens for reset/email if needed
    
     
    
    builder.Services.AddAuthentication()
    
        .AddCookie(IdentityConstants.ApplicationScheme); // HttpOnly auth cookie
    
     
    
    // Razor Components with server interactivity
    
    builder.Services.AddRazorComponents()
    
        .AddInteractiveServerComponents();
    
     
    
    // Middleware pipeline
    
    app.UseAuthentication();
    
    app.UseAuthorization();
    
     
    
    // Map the root components (server render mode only needed here)
    
    app.MapRazorComponents<App>()
    
       .AddInteractiveServerRenderMode();
    
    

    Registration endpoint (create + sign in):

    
    // Issues the auth cookie on success
    
    app.MapPost("/auth/register", async (
    
        RegisterModel model,
    
        UserManager<ApplicationUser> userManager,
    
        SignInManager<ApplicationUser> signInManager) =>
    
    {
    
        var user = new ApplicationUser
    
        {
    
            UserName = model.Email,
    
            Email = model.Email,
    
            FirstName = model.FirstName,
    
            LastName = model.LastName
    
        };
    
     
    
        var result = await userManager.CreateAsync(user, model.Password);
    
        if (!result.Succeeded)
    
        {
    
            var errors = result.Errors
    
                .GroupBy(e => e.Code)
    
                .ToDictionary(g => g.Key, g => g.Select(e => e.Description).ToArray());
    
     
    
            return Results.ValidationProblem(errors); // Matches your ValidationMessageStore usage
    
        }
    
     
    
        await signInManager.SignInAsync(user, isPersistent: model.KeepLogin); // Writes Set-Cookie
    
        return Results.Ok();
    
    });
    
    

    Hope this helps. Please reach out if you need any help.


Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.