microsoft/migrating-webforms-to-blazor-server
> Migrates ASP.NET Web Forms applications to Blazor Server using Blazor patterns. Handles configuration, App.razor InteractiveServer render modes, static asset migration (Content/Scripts/Images to wwwroot), markup conversion (.aspx/.ascx/.master to .razor), control conversion to Razor syntax, lifecycle mapping (Page_Load to OnInitializedAsync), and Master pages to Layouts. Use when migrating Web Forms to Blazor Server, converting .aspx pages with CSS/JavaScript compatibility, or fixing build errors for System.Web.UI types (Page, Control),
npx skills add https://github.com/microsoft/upgrade-agent-plugins --skill migrating-webforms-to-blazor-server
Migrate ASP.NET Web Forms applications to Blazor Server using Blazor patterns. This skill provides complete migration guidance including project setup, configuration, and page conversion.
This skill covers end-to-end migration of Web Forms pages to Blazor components — replacing asp: server controls with HTML elements and Blazor directives, migrating code-behind lifecycles to component lifecycles, and converting master pages to layouts.
Reference guide for conditional/alternative approaches. These skills address specific migration scenarios you may encounter based on your project's data access patterns, authentication system, and technology stack. The workflow steps below contain explicit instructions on when to invoke each skill.
| Skill | Use For |
|-------|---------|
| managing-blazor-server-authentication | Authentication setup, cookie auth in circuits, HttpContext NULL errors |
| managing-blazor-server-data-access | Data access, Session state in circuits, DbContext factory pattern, shopping cart persistence (referenced in Step 5 for data layer migration) |
| migrating-mvc-static-files | Static file migration from Content/Scripts to wwwroot (executed in Step 2) |
Complete migration tasks in order. Reference the child documents for detailed transformation patterns and control mappings.
Migration Progress:
- [ ] Step 1: Prepare Blazor project
- [ ] Step 2: Migrate static assets to wwwroot
- [ ] Step 3: Configure _Imports.razor
- [ ] Step 4: Configure App.razor render mode
- [ ] Step 5: Copy/rename files
- [ ] Step 6: Transform markup to Blazor
- [ ] Step 7: Transform code-behind
- [ ] Step 8: Build and validate
Inspect the workspace and user intent to determine the migration approach automatically:
Check for explicit user instruction:
If user intent is unclear, check workspace:
<Project Sdk="Microsoft.NET.Sdk.Web"> and Blazor framework references)Apply the determined approach:
For side-by-side migration:
dotnet new blazor -n MyBlazorApp --interactivity Server
For in-place migration:
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
Program.cs with standard Blazor Server hosting setup if not already presentApp.razor, _Imports.razor, and a layout file if they don't existIf the Web Forms project has a Models/ directory or uses Entity Framework, check the Web.config connection strings to determine the database provider:
| Web.config Indicator | Database Provider | EF Core Package to Install |
|---------------------|-------------------|---------------------------|
| providerName="System.Data.SqlClient" | SQL Server | Microsoft.EntityFrameworkCore.SqlServer |
| providerName="System.Data.SQLite" | SQLite | Microsoft.EntityFrameworkCore.Sqlite |
| providerName="Npgsql" | PostgreSQL | Npgsql.EntityFrameworkCore.PostgreSQL |
| providerName="MySql.Data.MySqlClient" | MySQL | Pomelo.EntityFrameworkCore.MySql |
Add the detected provider to your Blazor project:
# Example for SQL Server
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
Important: Use the same database provider as the original Web Forms project. Do not change providers (e.g., SQL Server → SQLite) unless explicitly required, as this can cause data type compatibility issues.
Execute the migrating-mvc-static-files skill to migrate Content/Scripts/Images to wwwroot. Complete all steps in that skill, then return here for Blazor-specific additions. That skill handles:
After completing the static file migration skill, apply these Blazor-specific additions:
App.razor preserving bundle order:CSS order matters for cascade rules. Check App_Start/BundleConfig.cs for the StyleBundle's .Include() calls and add <link> tags in App.razor in the same order. Verify all referenced CSS files exist in wwwroot/css/.
<!DOCTYPE html>
<html>
<head>
<!-- Original site stylesheets - preserve BundleConfig.cs order -->
<link rel="stylesheet" href="css/bootstrap.min.css" />
<link rel="stylesheet" href="css/Site.css" />
</head>
<body>
<!-- Original site scripts -->
<script src="js/jquery-3.7.1.min.js"></script>
<script src="js/bootstrap.bundle.min.js"></script>
<script src="js/Site.js"></script>
</body>
</html>
> ⚠️ WARNING: The bootstrap NuGet package does not expose files through _content/bootstrap/ in Blazor Server. Copy Bootstrap files from the original Content/ folder to wwwroot/css/ to preserve the exact version and avoid class name changes required by version upgrades.
Search for additional static asset folders beyond Content/Scripts/Images (e.g., Pics, Files, Uploads, Documents). Check for image/document file types and hardcoded paths in service classes, then migrate any found folders to wwwroot/ preserving their names.
_Imports.razor@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
> Note: The @using static import lets you write InteractiveServer as shorthand in App.razor. Do not add @rendermode InteractiveServer as a line in _Imports.razor — @rendermode is a directive attribute that belongs on component instances, not a standalone directive.
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.
Based on the migration approach determined in Step 1:
For side-by-side migration: Copy files from the WebForms project to the Blazor project and rename them. Place them in the Blazor project respecting Blazor conventions (pages in /Components/Pages/, layouts in /Components/Layout/, shared components in /Components/).
For in-place migration: Rename files in place within the same project. They can stay in their current locations initially.
| Original | New Name | Notes |
|----------|----------|-------|
| MyPage.aspx | MyPage.razor | Page markup |
| MyPage.aspx.cs | MyPage.razor.cs | Code-behind (keep as partial class) |
| MyControl.ascx | MyControl.razor | User control markup |
| MyControl.ascx.cs | MyControl.razor.cs | User control code-behind |
| Site.Master | MainLayout.razor | Master page → Layout |
| Site.Master.cs | MainLayout.razor.cs | Layout code-behind |
Models and Business Logic directories:
If the WebForms project has Models/, BLL/, BusinessLogic/, or Services/ folders, copy all .cs files to the Blazor project preserving directory structure.
> Data layer migration: EF6 namespace updates, DbContext configuration, connection strings, and session state migration are handled by managing-blazor-server-data-access. Execute that skill when:
> - Build errors reference System.Data.Entity namespaces
> - User asks about data access, Entity Framework, or database setup
> - Session state or shopping cart functionality needs migration
> - After markup migration is complete (if deferring data concerns)
Apply these mechanical transformations to each .razor file:
Control conversions (native HTML/Blazor):
<asp:Label> → <label> or @value inline<asp:TextBox> → <input @bind="value" /><asp:Button> → <button @onclick="Handler">Text</button><asp:DropDownList> → <select @bind="value">@foreach...</select><asp:GridView> → <table> with @foreach rows (or QuickGrid for advanced scenarios)<asp:ListView> / <asp:Repeater> → @foreach loop with custom markup<asp:RequiredFieldValidator> etc. → <EditForm> with <DataAnnotationsValidator> and <ValidationMessage For="..."><asp:Login> → <form method="post"> → minimal API endpoint (see managing-blazor-server-authentication)<asp:LoginView> → <AuthorizeView> with <NotAuthorized> / <Authorized> templates<asp:LoginName> → @context.User.Identity?.Name inside <AuthorizeView>Directive conversions:
<%@ Page Title="X" ... %> → @page "/route" (derive route from file path)<%@ Master ... %> → @inherits LayoutComponentBase<%@ Control ... %> → (remove entirely for user controls)<%@ Import Namespace="X" %> → @using XAutoEventWireup, CodeBehind, Inherits, EnableViewState, MasterPageFileExpression conversions:
<%: expr %> → @(expr)<%= expr %> → @(expr)<%# Item.Property %> → @context.Property (in templates)<%# Eval("Property") %> → @context.Property<%# Bind("Property") %> → @bind-Value="context.Property"<%-- comment --%> → @* comment *@<% if (x) { %> → @if (x) {<% } %> → }URL conversions:
href="~/path" → href="/path"NavigateUrl="~/path" → NavigateUrl="/path"ImageUrl="~/images/x.png" → ImageUrl="/images/x.png"Content/Layout conversions:
<asp:Content ContentPlaceHolderID="MainContent">...</asp:Content> wrappers (keep inner content)<asp:Content ContentPlaceHolderID="HeadContent"> → <HeadContent><asp:ContentPlaceHolder ID="MainContent" /> → @Body (in layouts)<form runat="server"> → <div> (preserves CSS block formatting context)Read ref/markup-transforms.md for complete transformation tables and examples.
For each .razor.cs file, apply these structural transformations. Read ref/code-transforms.md for detailed patterns:
Lifecycle conversions:
Page_Load → OnInitializedAsync (for first-load initialization)Page_PreRender → OnParametersSetAsync (for pre-render logic)if (!IsPostBack) guards — OnInitializedAsync runs once on first render (no postback concept)> ⚠️ Parameter-driven data loading: If the page loads data based on route parameters
> or query strings (e.g., category ID, product ID), use OnParametersSetAsync instead of
> OnInitializedAsync. Unlike Web Forms postbacks, Blazor reuses component instances when
> only the route parameter changes — OnInitializedAsync won't re-run, but
> OnParametersSetAsync will.
Event handler signatures:
object sender, EventArgs e parametersprotected void → private void (or async Task)Navigation:
Response.Redirect("~/path") → NavigationManager.NavigateTo("/path")@inject NavigationManager NavigationManager to .razor filePage title:
Page.Title = "..." in code-behind → <PageTitle>...</PageTitle> in markupData loading:
OnInitializedAsync using injected services@foreach@inject ProductService ProductService)State management:
ViewState["key"] → component field (private string _value;)Session["key"] → scoped DI service patternRead ref/code-transforms.md for detailed patterns.
Build the project and verify migration completeness:
dotnet build
After successful build, review migration completeness:
Search .razor files to verify Web Forms server controls were replaced:
<input, <div, <button, <select, @foreach loops<asp: prefixes or runat="server" attributes indicate incomplete migration_Imports.razor: @using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
If existing CSS or JavaScript depends on Web Forms HTML structure:
If migration issues remain after build validation, revisit the transformation references above and re-check lifecycle, data-binding, and control mapping patterns.
Controls: Remove asp: prefix, runat="server", and replace with native HTML
<!-- Before --> <asp:Label ID="lblName" runat="server" Text="Hello" />
<!-- After --> <label>Hello</label>
<!-- Before --> <asp:TextBox ID="Name" runat="server" />
<!-- After --> <input @bind="model.Name" />
<!-- Before --> <asp:Button Text="Submit" OnClick="Submit_Click" runat="server" />
<!-- After --> <button @onclick="Submit_Click">Submit</button>
Lifecycle: Convert Page_Load to OnInitializedAsync
// Before
protected void Page_Load(object sender, EventArgs e) { }
// After
protected override async Task OnInitializedAsync() { }
For detailed transformation patterns, control mappings, and troubleshooting:
Migration scope:
File transformations:
.razor extensionasp: prefixes removed from controlsrunat="server" attributes removed~/ URLs replaced with /Data binding:
SqlDataSource, ObjectDataSource, EntityDataSource) replaced with injected servicesOnInitializedAsync / OnParametersSetAsync) as appropriate@foreach, components such as QuickGrid when needed)Code-behind:
Page_Load → OnInitializedAsyncResponse.Redirect → NavigationManager.NavigateToValidation:
Take microsoft/migrating-webforms-to-blazor-server from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.