webforms-migration

star 449

Migrate ASP.NET Web Forms applications (.aspx/.ascx/.master) to Blazor Server using BlazorWebFormsComponents (BWFC). Use this skill when converting Web Forms markup, code-behind, Master Pages, User Controls, or data-binding patterns to Blazor equivalents.

FritzAndFriends By FritzAndFriends schedule Updated 3/4/2026

name: webforms-migration description: "Migrate ASP.NET Web Forms applications (.aspx/.ascx/.master) to Blazor Server using BlazorWebFormsComponents (BWFC). Use this skill when converting Web Forms markup, code-behind, Master Pages, User Controls, or data-binding patterns to Blazor equivalents."

Web Forms → Blazor Migration Skill

This skill provides complete transformation rules for migrating ASP.NET Web Forms applications to Blazor Server using the BlazorWebFormsComponents (BWFC) NuGet package. The BWFC library provides Blazor components with identical names, attributes, and HTML output to ASP.NET Web Forms controls — enabling migration with minimal markup changes.

Core Principle

Strip asp: and runat="server", keep everything else, and it just works.

BWFC components match Web Forms control names, property names, and rendered HTML. A well-structured Web Forms page can often be migrated by removing the asp: prefix, removing runat="server", and making a small set of structural adjustments.


Migration Recipe (Step-by-Step)

Step 1: Create Blazor Server Project

dotnet new blazor -n MyBlazorApp --interactivity Server
cd MyBlazorApp
dotnet add package Fritz.BlazorWebFormsComponents

Step 2: Configure _Imports.razor

Add these to the project-level _Imports.razor:

@using BlazorWebFormsComponents
@using BlazorWebFormsComponents.Enums

Step 3: Register BWFC Services

In Program.cs:

builder.Services.AddBlazorWebFormsComponents();

Step 3b: Configure Static Asset Middleware

In Program.cs, use MapStaticAssets() (not UseStaticFiles()) to serve both wwwroot/ content AND _framework/blazor.web.js:

app.MapStaticAssets();   // Required for _framework/blazor.web.js in .NET 9+
app.UseAntiforgery();

⚠️ CRITICAL: Using app.UseStaticFiles() alone will NOT serve _framework/blazor.web.js in .NET 9+, which means the Blazor runtime never loads and NO interactivity works on any page.

Step 3c: Add launchSettings.json

Create Properties/launchSettings.json so dotnet run uses Development mode (required for MapStaticAssets() to resolve framework files during development):

{
  "profiles": {
    "MyBlazorApp": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Without this file, dotnet run defaults to Production mode, and MapStaticAssets() expects a published manifest that doesn't exist during development — resulting in blazor.web.js 404.

Step 4: Add BWFC JavaScript

In App.razor or the host page <head>:

<script src="_content/Fritz.BlazorWebFormsComponents/js/Basepage.js"></script>

Step 5: Migrate Master Page → Blazor Layout

See Master Page Migration Rules.

Step 6: Migrate Pages

For each .aspx page, apply the Page Migration Rules and Control Translation Table.

Step 7: Migrate Code-Behind

See Code-Behind Migration Rules.

Step 8: Wire Data Access

Replace DataSource controls and DataBind() calls with service injection. See Data Binding Migration.


Page Migration Rules

File Conversion

Web Forms Blazor
MyPage.aspx MyPage.razor
MyPage.aspx.cs MyPage.razor.cs (partial class) or @code { } block
MyControl.ascx MyControl.razor
MyControl.ascx.cs MyControl.razor.cs
Site.Master MainLayout.razor
Site.Master.cs MainLayout.razor.cs

Directive Conversion

Web Forms Directive Blazor Equivalent
<%@ Page Title="X" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Y.aspx.cs" Inherits="NS.Y" %> @page "/route"
<%@ Master Language="C#" ... %> (remove — layouts don't need directives)
<%@ Control Language="C#" ... %> (remove — components don't need directives)
<%@ Register TagPrefix="uc" TagName="X" Src="~/Controls/X.ascx" %> @using MyApp.Components (if needed)
<%@ Import Namespace="X" %> @using X

Drop these attributes entirely (no Blazor equivalent):

  • AutoEventWireup
  • CodeBehind / CodeFile
  • Inherits (unless using @inherits for a base class)
  • EnableViewState / ViewStateMode
  • MasterPageFile (layouts are set differently — see Layout section)
  • ValidateRequest
  • MaintainScrollPositionOnPostBack

Expression Conversion

Web Forms Expression Blazor Equivalent Notes
<%: expression %> @(expression) HTML-encoded output
<%= expression %> @(expression) Blazor always HTML-encodes
<%# Item.Property %> @context.Property Inside data-bound templates
<%#: Item.Property %> @context.Property Same — Blazor always encodes
<%# Eval("Property") %> @context.Property Direct property access
<%# Bind("Property") %> @bind-Value="context.Property" Two-way binding
<%# string.Format("{0:C}", Item.Price) %> @context.Price.ToString("C") Format in code
<%$ RouteValue:id %> @Id (with [Parameter]) Route parameters
<%-- comment --%> @* comment *@ Razor comments
<% if (condition) { %> @if (condition) { Control flow
<% foreach (var x in items) { %> @foreach (var x in items) { Loops

Route URL Conversion

Web Forms Blazor
href="~/Products" href="/Products"
NavigateUrl="~/Products/<%: Item.ID %>" NavigateUrl="@($"/Products/{context.ID}")"
<%: GetRouteUrl("ProductRoute", new { id = Item.ID }) %> @($"/Products/{context.ID}")
Response.Redirect("~/Products") NavigationManager.NavigateTo("/Products")
Response.RedirectToRoute("ProductRoute", new { id }) NavigationManager.NavigateTo($"/Products/{id}")

Content/Layout Conversion

Web Forms Blazor
<asp:Content ContentPlaceHolderID="MainContent" runat="server"> (remove — page body IS the content)
</asp:Content> (remove)
<asp:Content ContentPlaceHolderID="HeadContent" runat="server"> <HeadContent> ... </HeadContent>
<asp:ContentPlaceHolder ID="MainContent" runat="server" /> @Body (in layout)

Form Wrapper

Web Forms wraps everything in <form runat="server">. In Blazor:

  • Remove the <form runat="server"> wrapper entirely
  • For forms that need validation, wrap with <EditForm Model="@model"> instead
  • Individual BWFC validation controls work inside <EditForm> the same way

Control Translation Table

Simple Controls (Trivial Migration)

These controls require only removing asp: and runat="server":

Web Forms BWFC Changes
<asp:Label ID="x" runat="server" Text="Hello" CssClass="title" /> <Label @ref="x" Text="Hello" CssClass="title" /> Remove asp:, runat; ID@ref (if referenced)
<asp:Literal ID="x" runat="server" Text="Hello" /> <Literal Text="Hello" /> Remove asp:, runat
<asp:HyperLink NavigateUrl="~/About" Text="About" runat="server" /> <HyperLink NavigateUrl="/About" Text="About" /> Remove asp:, runat; ~//
<asp:Image ImageUrl="~/images/logo.png" runat="server" /> <Image ImageUrl="/images/logo.png" /> Remove asp:, runat; ~//
<asp:Panel CssClass="container" runat="server"> <Panel CssClass="container"> Remove asp:, runat
<asp:PlaceHolder runat="server"> <PlaceHolder> Remove asp:, runat
<asp:HiddenField Value="x" runat="server" /> <HiddenField Value="x" /> Remove asp:, runat

Form Controls (Easy Migration)

Web Forms BWFC Notes
<asp:TextBox ID="Name" runat="server" /> <TextBox @bind-Text="model.Name" /> Add @bind-Text for data binding
<asp:TextBox TextMode="Password" runat="server" /> <TextBox TextMode="Password" @bind-Text="model.Password" /> TextMode preserved
<asp:TextBox TextMode="MultiLine" Rows="5" runat="server" /> <TextBox TextMode="Multiline" Rows="5" @bind-Text="model.Notes" /> Note: Multiline not MultiLine
<asp:DropDownList ID="Category" runat="server" /> <DropDownList @bind-SelectedValue="model.Category" Items="categories" /> Bind items + selected value
<asp:CheckBox ID="Active" runat="server" Checked="true" /> <CheckBox @bind-Checked="model.Active" /> @bind-Checked
<asp:RadioButton GroupName="G" runat="server" /> <RadioButton GroupName="G" /> Same attributes
<asp:FileUpload ID="Upload" runat="server" /> <FileUpload /> Uses InputFile internally
<asp:Button Text="Submit" OnClick="Submit_Click" runat="server" /> <Button Text="Submit" OnClick="Submit_Click" /> OnClick is now EventCallback
<asp:LinkButton Text="Edit" CommandName="Edit" runat="server" /> <LinkButton Text="Edit" CommandName="Edit" /> Same attributes
<asp:ImageButton ImageUrl="~/btn.png" OnClick="Btn_Click" runat="server" /> <ImageButton ImageUrl="/btn.png" OnClick="Btn_Click" /> ~//

Validation Controls (Easy Migration)

Validation controls are nearly 1:1 — same names, same attributes:

Web Forms BWFC Notes
<asp:RequiredFieldValidator ControlToValidate="Name" ErrorMessage="Required" runat="server" /> <RequiredFieldValidator ControlToValidate="Name" ErrorMessage="Required" /> Remove asp:, runat
<asp:CompareValidator ControlToCompare="Password" ControlToValidate="Confirm" runat="server" /> <CompareValidator ControlToCompare="Password" ControlToValidate="Confirm" /> Same
<asp:RangeValidator MinimumValue="1" MaximumValue="100" Type="Integer" runat="server" /> <RangeValidator MinimumValue="1" MaximumValue="100" Type="Integer" /> Same
<asp:RegularExpressionValidator ValidationExpression="\d+" runat="server" /> <RegularExpressionValidator ValidationExpression="\d+" /> Same
<asp:CustomValidator OnServerValidate="Validate" runat="server" /> <CustomValidator OnServerValidate="Validate" /> Same
<asp:ValidationSummary DisplayMode="BulletList" runat="server" /> <ValidationSummary DisplayMode="BulletList" /> Same
<asp:ModelErrorMessage ModelStateKey="key" runat="server" /> <ModelErrorMessage ModelStateKey="key" /> Same

Data Controls (Medium Migration)

Data controls require additional changes for data binding:

GridView

<!-- Web Forms -->
<asp:GridView ID="ProductGrid" runat="server"
    ItemType="WingtipToys.Models.Product"
    SelectMethod="GetProducts"
    AutoGenerateColumns="false"
    AllowPaging="true" PageSize="10">
    <Columns>
        <asp:BoundField DataField="Name" HeaderText="Product" />
        <asp:TemplateField HeaderText="Price">
            <ItemTemplate><%#: Item.UnitPrice.ToString("C") %></ItemTemplate>
        </asp:TemplateField>
    </Columns>
</asp:GridView>
<!-- Blazor with BWFC -->
<GridView Items="products" TItem="Product"
    AutoGenerateColumns="false"
    AllowPaging="true" PageSize="10">
    <Columns>
        <BoundField DataField="Name" HeaderText="Product" />
        <TemplateField HeaderText="Price">
            <ItemTemplate Context="Item">@Item.UnitPrice.ToString("C")</ItemTemplate>
        </TemplateField>
    </Columns>
</GridView>

Key changes:

  • ItemTypeTItem (generic type parameter)
  • SelectMethod="GetProducts"Items="products" (bind to a property loaded in OnInitializedAsync)
  • <%#: Item.X %>@Item.X inside templates
  • Add Context="Item" to <ItemTemplate> for naming the loop variable

ListView

<!-- Web Forms -->
<asp:ListView ID="ProductList" runat="server"
    ItemType="WingtipToys.Models.Product"
    SelectMethod="GetProducts">
    <ItemTemplate>
        <div class="product">
            <h3><%#: Item.ProductName %></h3>
            <asp:Image ImageUrl="<%#: Item.ImagePath %>" runat="server" />
            <p><%#: Item.UnitPrice.ToString("C") %></p>
        </div>
    </ItemTemplate>
</asp:ListView>
<!-- Blazor with BWFC -->
<ListView Items="products" TItem="Product">
    <ItemTemplate Context="Item">
        <div class="product">
            <h3>@Item.ProductName</h3>
            <Image ImageUrl="@Item.ImagePath" />
            <p>@Item.UnitPrice.ToString("C")</p>
        </div>
    </ItemTemplate>
</ListView>

FormView

<!-- Web Forms -->
<asp:FormView ID="ProductDetail" runat="server"
    ItemType="WingtipToys.Models.Product"
    SelectMethod="GetProduct"
    RenderOuterTable="false">
    <ItemTemplate>
        <h2><%#: Item.ProductName %></h2>
        <p><%#: Item.Description %></p>
        <p>Price: <%#: Item.UnitPrice.ToString("C") %></p>
    </ItemTemplate>
</asp:FormView>
<!-- Blazor with BWFC -->
<FormView DataItem="product" TItem="Product" RenderOuterTable="false">
    <ItemTemplate Context="Item">
        <h2>@Item.ProductName</h2>
        <p>@Item.Description</p>
        <p>Price: @Item.UnitPrice.ToString("C")</p>
    </ItemTemplate>
</FormView>

Key changes:

  • SelectMethodDataItem (single object, loaded in OnInitializedAsync)
  • Items for collection-bound controls, DataItem for single-record controls

Repeater

<!-- Blazor with BWFC -->
<Repeater Items="items" TItem="MyItem">
    <ItemTemplate Context="Item">
        <div>@Item.Name — @Item.Value</div>
    </ItemTemplate>
    <SeparatorTemplate><hr /></SeparatorTemplate>
</Repeater>

Navigation Controls

Web Forms BWFC Notes
<asp:Menu> <Menu> MenuItem structure preserved
<asp:SiteMapPath> <SiteMapPath> Provide SiteMapNode data
<asp:ScriptManager runat="server" /> <ScriptManager /> Renders nothing — correct for Blazor

Login Controls

Web Forms BWFC Notes
<asp:Login> <Login> Wire auth provider via service
<asp:LoginView> <LoginView> Uses AuthenticationState
<asp:LoginStatus> <LoginStatus> Uses AuthenticationState
<asp:LoginName> <LoginName> Uses AuthenticationState

Code-Behind Migration

Lifecycle Methods

Web Forms Blazor Notes
Page_Load(object sender, EventArgs e) protected override async Task OnInitializedAsync() First load
Page_PreRender(...) protected override async Task OnParametersSetAsync() Before each render
Page_Init(...) protected override void OnInitialized() Sync initialization
IsPostBack check (not needed) Blazor doesn't have postback

Pattern:

// Web Forms
protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        products = GetProducts();
        GridView1.DataBind();
    }
}

// Blazor
protected override async Task OnInitializedAsync()
{
    products = await ProductService.GetProductsAsync();
}

Event Handlers

// Web Forms
protected void SubmitBtn_Click(object sender, EventArgs e)
{
    // handle click
    Response.Redirect("~/Confirmation");
}

// Blazor
private void SubmitBtn_Click()
{
    // handle click
    NavigationManager.NavigateTo("/Confirmation");
}

Navigation

Web Forms Blazor
Response.Redirect("~/path") NavigationManager.NavigateTo("/path")
Response.RedirectToRoute(...) NavigationManager.NavigateTo($"/path/{param}")
Server.Transfer("~/page.aspx") NavigationManager.NavigateTo("/page")

Session State

Web Forms Blazor
Session["key"] = value; Inject a scoped service; use ProtectedSessionStorage
Application["key"] Use a singleton service
Cache["key"] Use IMemoryCache or IDistributedCache
ViewState["key"] Use component fields (state is per-component)

Query String / Route Parameters

// Web Forms (Model Binding)
public IQueryable<Product> GetProducts([QueryString] int? categoryId) { ... }

// Blazor
[SupplyParameterFromQuery] public int? CategoryId { get; set; }

protected override async Task OnInitializedAsync()
{
    products = await ProductService.GetProductsAsync(CategoryId);
}
// Web Forms (RouteData)
public void GetProduct([RouteData] int productId) { ... }

// Blazor
@page "/Products/{ProductId:int}"
[Parameter] public int ProductId { get; set; }

Data Access

// Web Forms (SelectMethod pattern)
public IQueryable<Product> GetProducts()
{
    var db = new ProductContext();
    return db.Products;
}

// Blazor (Service injection)
@inject IProductService ProductService

private List<Product> products = new();

protected override async Task OnInitializedAsync()
{
    products = await ProductService.GetProductsAsync();
}

Key change: Replace inline DbContext usage with injected services. Register in Program.cs:

builder.Services.AddDbContext<ProductContext>();
builder.Services.AddScoped<IProductService, ProductService>();

Master Page Migration

Web Forms Master Page Structure

<%@ Master Language="C#" CodeBehind="Site.master.cs" Inherits="MyApp.SiteMaster" %>
<!DOCTYPE html>
<html>
<head runat="server">
    <title><%: Page.Title %></title>
    <asp:ContentPlaceHolder ID="HeadContent" runat="server" />
</head>
<body>
    <form runat="server">
        <asp:ScriptManager runat="server" />
        <header>
            <nav>
                <asp:Menu ID="MainMenu" runat="server" ... />
            </nav>
        </header>
        <main>
            <asp:ContentPlaceHolder ID="MainContent" runat="server" />
        </main>
        <footer>© <%: DateTime.Now.Year %></footer>
    </form>
</body>
</html>

Blazor Layout Equivalent

@inherits LayoutComponentBase

<PageTitle>@PageTitle</PageTitle>
<HeadContent>
    @HeadContent
</HeadContent>

<header>
    <nav>
        <Menu ... />
    </nav>
</header>
<main>
    @Body
</main>
<footer>© @DateTime.Now.Year</footer>

Key changes:

  • <%@ Master %> directive → @inherits LayoutComponentBase
  • <form runat="server"> → removed entirely
  • <asp:ContentPlaceHolder ID="MainContent">@Body
  • <asp:ContentPlaceHolder ID="HeadContent"> → Handled by <HeadContent> in child pages
  • <asp:ScriptManager><ScriptManager /> (renders nothing — correct)
  • <%: expression %>@expression

Common Gotchas

blazor.web.js 404 (No Interactivity)

If pages render as static HTML but nothing is interactive (buttons don't click, forms don't submit), check:

  1. _framework/blazor.web.js is returning 404 (check browser DevTools console)
  2. Fix: Use app.MapStaticAssets() in Program.cs instead of app.UseStaticFiles()
  3. Fix: Ensure Properties/launchSettings.json sets ASPNETCORE_ENVIRONMENT=Development

No ViewState

Blazor components maintain their own state in fields/properties. There is no ViewState dictionary. If code reads/writes ViewState["key"], replace with a component field.

No PostBack

There is no IsPostBack. Code in Page_Load that checks if (!IsPostBack) should move to OnInitializedAsync() (which runs once on first load).

No DataSource Controls

<asp:SqlDataSource>, <asp:ObjectDataSource>, <asp:EntityDataSource> have no BWFC equivalents. Replace with injected services that load data in OnInitializedAsync().

ID Rendering

Web Forms generates client IDs like ctl00_MainContent_GridView1. Blazor doesn't render component IDs into HTML. If CSS or JavaScript targets these IDs, use CssClass instead or add explicit id attributes.

Template Context Variable

In Web Forms, Item is implicitly available in templates. In BWFC, use Context="Item" on template elements:

<ItemTemplate Context="Item">
    @Item.PropertyName
</ItemTemplate>

runat="server" on HTML Elements

Some Web Forms pages add runat="server" to plain HTML elements (e.g., <div runat="server">). Remove runat="server" — use @ref if the element needs programmatic access.

String Format in Binding

<!-- Web Forms -->
<%#: string.Format("{0:C}", Item.Price) %>

<!-- Blazor -->
@Item.Price.ToString("C")

Visibility Pattern

<!-- Web Forms -->
<asp:Panel Visible="false" runat="server">...</asp:Panel>

<!-- Blazor option 1: BWFC Visible parameter -->
<Panel Visible="false">...</Panel>

<!-- Blazor option 2: Razor conditional (preferred for dynamic) -->
@if (showPanel)
{
    <Panel>...</Panel>
}

Nested Master Pages

Web Forms supports nested Master Pages. In Blazor, use nested layouts:

@* ChildLayout.razor *@
@inherits LayoutComponentBase
@layout MainLayout

<div class="child-wrapper">
    @Body
</div>

Attributes Removed During Migration

These Web Forms attributes have no Blazor equivalent and should be silently removed:

  • runat="server" — always remove
  • AutoEventWireup="true" — no equivalent
  • CodeBehind="X.aspx.cs" — no equivalent (use .razor.cs convention)
  • CodeFile="X.aspx.cs" — same as CodeBehind
  • Inherits="Namespace.Class" — use @inherits only if needed
  • EnableViewState="false" — no ViewState in Blazor
  • ViewStateMode="Disabled" — no ViewState in Blazor
  • ValidateRequest="false" — no request validation in Blazor
  • MaintainScrollPositionOnPostBack="true" — no postback in Blazor
  • ClientIDMode="Static" — no client ID munging in Blazor
  • EnableTheming="false" — not applicable
  • SkinID="X" — use BWFC theming system if needed

BWFC Component Coverage

52 components across 7 categories:

Category Components
Editor Controls AdRotator, BulletedList, Button, Calendar, CheckBox, CheckBoxList, DropDownList, FileUpload, HiddenField, HyperLink, Image, ImageButton, Label, LinkButton, ListBox, Literal, Localize, MultiView, Panel, PlaceHolder, RadioButton, RadioButtonList, Table, TextBox, View
Data Controls DataGrid, DataList, DataPager, DetailsView, FormView, GridView, ListView, Repeater
Validation Controls CompareValidator, CustomValidator, ModelErrorMessage, RangeValidator, RegularExpressionValidator, RequiredFieldValidator, ValidationSummary
Navigation Controls Menu, SiteMapPath, TreeView
Login Controls ChangePassword, CreateUserWizard, Login, LoginName, LoginStatus, LoginView, PasswordRecovery
AJAX Controls ScriptManager, ScriptManagerProxy, Timer, UpdatePanel, UpdateProgress

WingtipToys-Specific Patterns

The WingtipToys canonical demo (2013) uses these specific patterns that BWFC fully supports:

WingtipToys Pattern BWFC Migration
ItemType="WingtipToys.Models.Product" TItem="Product"
SelectMethod="GetProducts" Items="products" (load in OnInitializedAsync)
<asp:ListView> with LayoutTemplate/GroupTemplate <ListView> — templates preserved
<asp:FormView> with RenderOuterTable="false" <FormView RenderOuterTable="false">
<asp:GridView> with BoundField + TemplateField Same column types in BWFC
ASP.NET Identity login/register Migrate to Blazor Identity (separate from BWFC)
Shopping cart with Session state Replace with scoped service + ProtectedSessionStorage
~/ route prefix Replace with /
<asp:ModelErrorMessage> <ModelErrorMessage>

Files to Create During Migration

For a typical Web Forms → Blazor migration, create these files:

  1. Program.cs — Service registration, middleware pipeline
  2. Properties/launchSettings.json — Environment config for dotnet run
  3. App.razor — Root component with Router
  4. _Imports.razor — Global usings including BWFC namespaces
  5. Components/Layout/MainLayout.razor — From Master Page
  6. Components/Pages/*.razor — One per .aspx page
  7. Services/*.cs — Replace DataSource controls and code-behind data methods
  8. Models/*.cs — Copy/migrate from Web Forms project (often .NET Standard already)

Identity & Authentication Migration

ASP.NET Web Forms Identity uses SignInManager and forms authentication cookies, which require an active HTTP response. In Blazor Server (SignalR), there is no HTTP response after the initial page load. This requires a different pattern.

Authentication Endpoints

Authentication operations (login, logout, register) must be HTTP endpoints, NOT Blazor component handlers:

// Program.cs — HTTP endpoints for auth operations

// Login: SignInManager needs HTTP context to set auth cookies
app.MapGet("/Account/PerformLogin", async (
    string email, string password,
    SignInManager<IdentityUser> signInManager) =>
{
    var result = await signInManager.PasswordSignInAsync(email, password,
        isPersistent: false, lockoutOnFailure: false);
    if (result.Succeeded)
        return Results.Redirect("/");
    return Results.Redirect("/Account/Login?error=" +
        Uri.EscapeDataString("Invalid login attempt."));
});

// Logout: must be POST for CSRF protection
app.MapPost("/Account/PerformLogout", async (
    SignInManager<IdentityUser> signInManager) =>
{
    await signInManager.SignOutAsync();
    return Results.Redirect("/");
});

Login Page Pattern

The Login page is an InteractiveServer component that collects credentials, then navigates to the HTTP endpoint with forceLoad: true:

private void HandleLogin(MouseEventArgs args)
{
    var loginUrl = $"/Account/PerformLogin?email={Uri.EscapeDataString(email)}" +
                   $"&password={Uri.EscapeDataString(password)}";
    NavigationManager.NavigateTo(loginUrl, forceLoad: true);
}

Logout Form Pattern

The logout button must be a plain HTML form (not a Blazor form) that posts to the HTTP endpoint. Use data-enhance="false" to prevent Blazor enhanced navigation from intercepting:

<form method="post" action="/Account/PerformLogout" data-enhance="false">
    <AntiforgeryToken />
    <button type="submit" class="btn btn-link">Log off</button>
</form>

⚠️ CRITICAL: Without data-enhance="false", Blazor intercepts the form POST and tries to handle it as a Blazor form submission, which fails because there's no matching @formname handler.

AuthorizeView in Layout

Replace Web Forms <asp:LoginView> with <AuthorizeView>:

<AuthorizeView>
    <Authorized>
        <li><a href="/Account/Manage">Hello, @context.User.Identity?.Name!</a></li>
        <li>
            <form method="post" action="/Account/PerformLogout" data-enhance="false">
                <AntiforgeryToken />
                <button type="submit" class="btn btn-link">Log off</button>
            </form>
        </li>
    </Authorized>
    <NotAuthorized>
        <li><a href="/Account/Register">Register</a></li>
        <li><a href="/Account/Login">Log in</a></li>
    </NotAuthorized>
</AuthorizeView>
Install via CLI
npx skills add https://github.com/FritzAndFriends/BlazorWebFormsComponents --skill webforms-migration
Repository Details
star Stars 449
call_split Forks 78
navigation Branch main
article Path SKILL.md
More from Creator
FritzAndFriends
FritzAndFriends Explore all skills →