Migrate ASP.NET Web Forms .aspx/.ascx/.master markup to Blazor Server using BlazorWebFormsComponents (BWFC). Covers control translation, expression conversion, data binding, code-behind lifecycle, and Master Page to Layout conversion. WHEN: "migrate aspx", "convert web forms markup", "master page to layout", "asp prefix removal", "data binding expressions". FOR SINGLE OPERATIONS: use /bwfc-identity-migration for auth, /bwfc-data-migration for EF/architecture.
88
85%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Risky
Do not use without reviewing
This skill provides transformation rules for migrating ASP.NET Web Forms markup to Blazor Server using the BlazorWebFormsComponents (BWFC) NuGet package.
Related skills:
/bwfc-identity-migration — ASP.NET Identity/Membership → Blazor Identity/bwfc-data-migration — EF6 → EF Core, DataSource → services, architecture decisionsBlazorWebFormsComponents is an open-source library that provides drop-in Blazor replacements for ASP.NET Web Forms server controls. It preserves the same component names, attribute names, and rendered HTML output — enabling migration with minimal markup changes.
Core Principle: Strip
asp:andrunat="server", keep everything else, and it just works.
dotnet new blazor -n MyBlazorApp --interactivity Server
cd MyBlazorApp
dotnet add package Fritz.BlazorWebFormsComponents_Imports.razor@using BlazorWebFormsComponents
@using BlazorWebFormsComponents.Enums
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@inherits BlazorWebFormsComponents.WebFormsPageBaseThe @inherits line makes every page inherit from WebFormsPageBase, which provides Page.Title, Page.MetaDescription, Page.MetaKeywords, and IsPostBack — so Web Forms code-behind patterns compile unchanged. No per-page @inject IPageService is needed for page-level usage. Individual pages can override with their own @inherits if needed.
Note: The
@using staticimport lets you writeInteractiveServeras shorthand inApp.razor. Do not add@rendermode InteractiveServeras a line in_Imports.razor—@rendermodeis a directive attribute that belongs on component instances, not a standalone directive.
Note:
@inject IPageServiceis still valid for non-page components (e.g., a shared header component) that need access to page metadata.WebFormsPageBaseonly applies to routable pages.
App.razorThe dotnet new blazor --interactivity Server template generates App.razor with render mode already set. Verify it contains:
<HeadOutlet @rendermode="InteractiveServer" />
...
<Routes @rendermode="InteractiveServer" />This enables global server interactivity for all pages. See ASP.NET Core Blazor render modes for per-page alternatives.
In Program.cs:
builder.Services.AddBlazorWebFormsComponents();In your layout file (MainLayout.razor), add the <Page /> render component once. This subscribes to IPageService and emits <PageTitle> and <meta> tags:
@inherits LayoutComponentBase
<BlazorWebFormsComponents.Page />
<header>
<!-- ... -->
</header>
<main>
@Body
</main>Important:
WebFormsPageBaseprovides the code-behind API (Page.Title,IsPostBack). The<BlazorWebFormsComponents.Page />component does the rendering (<PageTitle>,<meta>tags). Both are required.
In App.razor or the host page <head>:
<script src="_content/Fritz.BlazorWebFormsComponents/js/Basepage.js"></script>⚠️ CRITICAL: The migration pipeline is a two-layer automated sequence. Both layers MUST run. Do NOT make any manual code fixes between Layer 1 and Layer 2. The migration pipeline measures script quality. Manual fixes between layers corrupt the measurement. If Layer 1 output has issues, those issues should be fixed in the script, not patched by hand.
The migration pipeline has two mandatory layers that run in strict sequence:
| Step | Layer | Executor | Description |
|---|---|---|---|
| 1 | Layer 1: Mechanical | Automated script (bwfc-migrate.ps1) | Tag transforms, expression conversion, file renaming, scaffolding |
| 2 | Layer 2: Structural | Copilot-assisted (this skill) | Data binding, lifecycle, templates, layouts |
| 3 | Build & verify | Copilot | dotnet build, fix any remaining compile errors |
| 4 | Report | Copilot | Document results |
You MUST run Layer 1 as a PowerShell script. Do NOT apply Layer 1 transforms manually.
.\migration-toolkit\scripts\bwfc-migrate.ps1 -Path "<source-webforms-project>" -Output "<blazor-output-dir>"-Path — path to the source Web Forms project directory (containing .aspx, .ascx, .master files)-Output — path to the target Blazor project directory (will be created if it doesn't exist)asp: prefix removal, runat="server" removal, expression conversion, URL rewriting, file renaming, scaffold generation (.csproj, Program.cs, _Imports.razor, App.razor, etc.)What Layer 1 handles:
asp: tag prefixesrunat="server" attributes<%: expr %> → @(expr), <%# Item.X %> → @context.X~/path → /path.aspx → .razor, .ascx → .razor, .master → .razor<asp:Content> wrappers<%@ Page %> directives to @page "/route"<form runat="server"> with <div> (preserves CSS block formatting context)After Layer 1 completes, immediately proceed to Layer 2. Do NOT fix, edit, or clean up any Layer 1 output first.
⚠️ MANDATORY — READ BEFORE STARTING LAYER 2: Open and read all three child documents in this skill's directory. They contain the detailed patterns, examples, and control translation tables needed for every transform below. Without them you will miss critical migration details.
CODE-TRANSFORMS.md— Code-behind lifecycle mapping (Page_Load→OnInitializedAsync,Page_PreRender→OnParametersSetAsync), event handler conversion, navigation patterns, data binding migration (SelectMethod delegates, template binding withContext="Item"), query string / route parameter conversion, and Master Page → Layout conversion with complete before/after examples.CONTROL-REFERENCE.md— Control translation tables for all 58 BWFC components across 6 categories (Simple, Form, Validation, Data, Navigation, AJAX), structural/infrastructure components (WebFormsPage,Page,NamingContainer,MasterPage,Content,ContentPlaceHolder,EmptyLayout),DataBinder.Evalcompatibility shim, theming infrastructure, and custom control base classes (WebControl,CompositeControl,HtmlTextWriter).AJAX-TOOLKIT.md— Ajax Control Toolkit extender migration (14 supported components), installation, Layer 1 automation, Layer 2 manual work (ServiceMethod wiring for AutoCompleteExtender, TargetControlID verification), before/after examples, and troubleshooting.
Layer 2 is where Copilot applies structural transforms to every generated .razor and .razor.cs file. Work through each file and apply ALL of the following:
⚠️ MANDATORY: SelectMethod MUST be preserved as a delegate. When the original Web Forms markup has
SelectMethod="MethodName", the migrated Blazor markup MUST haveSelectMethod="@service.MethodName"(or explicit lambda). Do NOT convert toItems=binding — this is the #1 recurring migration error.
SelectMethod — convert string method name to SelectHandler<ItemType> delegate (e.g., SelectMethod="@productService.GetProducts" if signature matches, or SelectMethod="@((maxRows, startRow, sort, out total) => service.GetProducts(maxRows, startRow, sort, out total))" for explicit wiring). BWFC's DataBoundComponent.OnAfterRenderAsync automatically calls the delegate to populate Items.ItemType attribute — BWFC data controls use ItemType (matches Web Forms DataBoundControl.ItemType). Do NOT change to TItem or any other name.Context="Item" to <ItemTemplate> elementsPage_Load → OnInitializedAsyncResponse.Redirect → NavigationManager.NavigateToEditForm where form validation is neededItems (for DataSource-originating data only): Items="@(_products ?? new())"SelectMethod is set, Items is auto-populated by the BWFC framework — do NOT also set Items[DatabaseProvider] review item. Use the detected EF Core package and connection string. Do NOT substitute providers (e.g., do not use SQLite when the original used SQL Server).@inject directives for required services (NavigationManager, DbContext, etc.)Session["key"] → scoped DI service patternsdotnet build and fix compile errorsThis skill covers Layers 1 and 2 of the three-layer pipeline. Use the related skills for Layer 3.
| Layer | What It Handles | Skill |
|---|---|---|
| Layer 1: Mechanical | Tag prefixes, runat, expressions, URLs, file renaming | ✅ This skill (automated via bwfc-migrate.ps1) |
| Layer 2: Structural | Data binding, code-behind lifecycle, templates, layouts | ✅ This skill (Copilot-assisted) |
| Layer 3: Architecture | State management, data access, auth, middleware | /bwfc-data-migration, /bwfc-identity-migration |
| 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 |
| 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 entirely (no Blazor equivalent): AutoEventWireup, CodeBehind/CodeFile, Inherits, EnableViewState/ViewStateMode, MasterPageFile, ValidateRequest, MaintainScrollPositionOnPostBack, ClientIDMode, EnableTheming, SkinID
| 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 |
| Web Forms | Blazor |
|---|---|
href="~/Products" | href="/Products" |
NavigateUrl="~/Products/<%: Item.ID %>" | NavigateUrl="@($"/Products/{context.ID}")" |
<%: GetRouteUrl("ProductRoute", new { id = Item.ID }) %> | @($"/Products/{context.ID}") or use BWFC's GetRouteUrlHelper extension (see below) |
Response.Redirect("~/Products") | NavigationManager.NavigateTo("/Products") |
BWFC GetRouteUrlHelper: BWFC provides a
GetRouteUrlHelperextension method onBaseWebFormsComponentthat wraps ASP.NET Core'sLinkGenerator. Inside any BWFC component, you can callthis.GetRouteUrl("RouteName", new { id = item.ID })directly — no manual URL construction needed. Register routes via ASP.NET Core's routing system and the helper maps them automatically.
| Web Forms | Blazor |
|---|---|
<asp:Content ContentPlaceHolderID="MainContent" runat="server"> | (remove — page body IS the content) |
<asp:Content ContentPlaceHolderID="HeadContent" runat="server"> | <HeadContent> ... </HeadContent> |
<asp:ContentPlaceHolder ID="MainContent" runat="server" /> | @Body (in layout) |
<form runat="server"> wrapper with <div> (preserves the id attribute and CSS block formatting context — many Web Forms stylesheets use position: relative offsets that depend on this wrapper as the containing block)<EditForm Model="@model"> insteadDetailed control mappings and code transformation patterns are in child documents:
Replace ViewState["key"] with component fields.
if (!IsPostBack) → works AS-IS with WebFormsPageBase (always enters the block). if (IsPostBack) (without !) → dead code in Blazor; flag for manual review and move logic to event handlers.
SqlDataSource, ObjectDataSource, EntityDataSource → injected services. See /bwfc-data-migration.
Blazor doesn't render component IDs. Use CssClass or explicit id attributes for CSS/JS targeting.
Add Context="Item" on template elements:
<ItemTemplate Context="Item">
@Item.PropertyName
</ItemTemplate>runat="server" on HTML ElementsRemove runat="server" from plain HTML elements. Use @ref if programmatic access needed.
// Web Forms: protected void Btn_Click(object sender, EventArgs e) { }
// Blazor: private void Btn_Click() { }TextMode="MultiLine" CasingBWFC uses Multiline (lowercase 'l'), not MultiLine. Silent failure if wrong.
Include during migration to prevent errors, remove when stable.
## Page: [PageName.aspx] → [PageName.razor]
### Layer 1 — Mechanical
- [ ] File renamed (.aspx → .razor)
- [ ] <%@ Page %> → @page "/route"
- [ ] asp: prefixes removed
- [ ] runat="server" removed
- [ ] Expressions converted
- [ ] URLs converted (~/ → /)
- [ ] <asp:Content> wrappers removed
- [ ] <form runat="server"> replaced with <div>
### Layer 2 — Structural
- [ ] SelectMethod string → SelectHandler delegate
- [ ] ItemType preserved (strip namespace prefix only)
- [ ] Data loading in OnInitializedAsync
- [ ] Event handlers converted
- [ ] Template Context="Item" added
- [ ] Navigation calls converted
### Verification
- [ ] Builds without errors
- [ ] Renders correctly
- [ ] Interactive features work
- [ ] No browser console errors9bf8669
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.