ASP.NET Web Forms to Blazor Migration Guide

How to migrate ASP.NET Web Forms to Blazor page lifecycle mapping, control conversion, state management, authentication, and phased migration strategy.

ASP.NET Web Forms Blazor

ASP.NET Web Forms to Blazor Migration Guide

  • Sunday, August 23, 2026

How to migrate ASP.NET Web Forms to Blazor page lifecycle mapping, control conversion, state management, authentication, and phased migration strategy.

ASP.NET Web Forms to Blazor is one of the most technically satisfying migrations in the .NET ecosystem and one of the most frequently misunderstood. It is not an upgrade in the conventional sense. There is no tooling that converts .aspx pages to Blazor components automatically, and the page lifecycle that Web Forms developers have worked with for twenty years has no direct equivalent in Blazor. What the migration offers instead is a genuine architectural improvement: component-based UI, C# throughout the stack, proper separation of concerns, and first-class support on .NET 10 with a long-term support commitment.

This guide covers the honest picture of what migrating from Web Forms to Blazor actually involves what maps cleanly, what requires rethinking, and how to structure the migration so your application keeps running throughout.

Why Blazor Specifically, Not ASP.NET Core MVC or Razor Pages?

Before getting into the how, it's worth being direct about when Blazor is the right migration destination versus the alternatives.

Blazor is well-suited when your organization already invests heavily in .NET and wants to keep one language and one tooling stack across the product and when you want to modernize existing Web Forms or MVC apps without moving your team to a JavaScript-only stack.

Choose Blazor over Razor Pages when:

  • Your Web Forms application is highly interactive complex forms, real-time updates, dynamic UI based on user input and you want to preserve that interactivity in C# without building a separate JavaScript frontend
  • Your team is strong in C# and has little JavaScript experience Blazor lets them own the full stack
  • You're building on top of ABP Framework or ASP.NET Zero, both of which have mature Blazor editions that provide authentication, multi-tenancy, and module architecture out of the box
  • The application needs SignalR-based real-time features that Blazor Server handles natively

Choose Razor Pages or MVC over Blazor when:

  • The application is primarily server-rendered with minimal interactivity forms that submit and show results Razor Pages is simpler and has less overhead
  • SEO is critical and the application can't use Blazor's prerendering effectively
  • The team wants the most direct migration path with the lowest conceptual distance from Web Forms

The Fundamental Conceptual Shift

Web Forms developers need to understand one thing before writing a line of Blazor code: the postback model does not exist in Blazor. In Web Forms, every user interaction that requires server processing triggers a postback the entire page or an UpdatePanel sends a request to the server, the server processes it and rebuilds the page, and the response updates the browser. This model is what ViewState exists to support.

Blazor replaces this with a component model:

Web Forms conceptBlazor equivalent
.aspx pageBlazor component (.razor file)
Code-behind (.aspx.cs)@code { } block or partial class
Server controls (<asp:Button>, <asp:GridView>)Blazor components (<Button>, <QuickGrid>) or HTML elements with event handlers
PostbackEvent callback (@onclick, @onchange)
ViewStateComponent state ([Parameter], private fields)
UpdatePanelComponent re-render (automatic on state change)
Master pagesBlazor layouts (MainLayout.razor)
User controls (.ascx)Reusable Blazor components
Session["key"]ISessionStorageService, scoped services, or Blazor Server's circuit state
Response.Redirect()NavigationManager.NavigateTo()
Page.IsPostBackOnInitializedAsync() lifecycle method
Label.Text = valueData binding (@value, StateHasChanged())
Page_LoadOnInitialized() / OnParametersSet()

Understanding this mapping is the foundation for every Web Forms-to-Blazor conversion decision.

Choosing a Blazor Hosting Model

The first architectural decision in a Web Forms to Blazor migration is which Blazor hosting model to target. Blazor Server works best when you need real-time UI, central control, and secure handling of sensitive data on the server. Blazor WebAssembly is better when you want client-heavy experiences, offline capabilities, or to offload compute from your backend.

For most Web Forms migrations, Blazor Server is the right starting point:

  • It runs on the server closer to the Web Forms execution model your team already understands
  • It has full access to server-side resources (databases, files, internal APIs) without CORS or API layer concerns
  • Initial load is fast the server renders the initial HTML before SignalR takes over
  • It works on browsers that don't support WebAssembly
  • Debugging is simpler standard .NET debugging in Visual Studio works

Blazor WebAssembly becomes the better choice when:

  • Offline capability is a requirement
  • You're building a SaaS product where reducing server load at scale matters significantly
  • The application is API-first with a clean separation between backend and frontend

.NET 10 Blazor United (Auto mode) the current recommended option for new projects and migrations targeting .NET 10 allows mixing both models in the same application. Components can be configured individually for server-side or WebAssembly rendering. For a migration, starting with InteractiveServer render mode and selectively enabling InteractiveWebAssembly for specific components as needed is a practical path.

Phase 1: Setting Up the Blazor Project Alongside Web Forms

The safest migration approach runs the Blazor application alongside the existing Web Forms application using YARP as a reverse proxy the same Strangler Fig pattern used for other .NET migrations. Users are routed to Blazor pages as they're migrated; unmigrated pages continue to be served by Web Forms.

Create the new Blazor project:

# Create a new Blazor Web App targeting .NET 10
dotnet new blazor -n YourApp.Blazor \
    --interactivity Server \
    --framework net10.0
// appsettings.json routes unmigrated pages to Web Forms
{
  "ReverseProxy": {
    "Routes": {
      "webforms-fallback": {
        "ClusterId": "webforms",
        "Match": {
          "Path": "{**catch-all}"
        }
      }
    },
    "Clusters": {
      "webforms": {
        "Destinations": {
          "webforms-destination": {
            "Address": "https://localhost:44300/"
          }
        }
      }
    }
  }
}

As each page is migrated to Blazor, add a specific route match in YARP that takes priority over the catch-all traffic for migrated pages goes to Blazor, everything else still goes to Web Forms.

Phase 2: The Layout Replacing Master Pages

Web Forms Master Pages (Site.Master) become Blazor layouts. The conversion is straightforward:

Web Forms Site.Master:

<%@ Master Language="C#" %>
<!DOCTYPE html>
<html>
<head>
    <title>My Application</title>
    <asp:ContentPlaceHolder ID="head" runat="server" />
</head>
<body>
    <nav>
        <a href="/">Home</a>
        <a href="/products">Products</a>
    </nav>
    <main>
        <asp:ContentPlaceHolder ID="MainContent" runat="server" />
    </main>
    <footer>© 2026 My Company</footer>
</body>
</html>

Blazor MainLayout.razor:

@inherits LayoutComponentBase

<!DOCTYPE html>
<html lang="en">
<head>
    <title>My Application</title>
    @HeadContent
</head>
<body>
    <nav>
        <NavLink href="/">Home</NavLink>
        <NavLink href="/products">Products</NavLink>
    </nav>
    <main>
        @Body  <!-- replaces ContentPlaceHolder ID="MainContent" -->
    </main>
    <footer>© 2026 My Company</footer>

    <script src="_framework/blazor.web.js"></script>
</body>
</html>

Multiple Master Pages (different layouts for authenticated vs unauthenticated areas) become multiple Blazor layout files each page component specifies which layout to use with @layout.

Phase 3: Converting Pages and Controls

Converting a Web Forms Page to a Blazor Component

Web Forms Products.aspx:

<%@ Page Language="C#" MasterPageFile="~/Site.Master"
         CodeBehind="Products.aspx.cs" Inherits="YourApp.Products" %>

<asp:Content ContentPlaceHolderID="MainContent" runat="server">
    <h2>Products</h2>
    <asp:Label ID="ErrorLabel" runat="server" ForeColor="Red" />
    <asp:GridView ID="ProductGrid" runat="server"
                  AutoGenerateColumns="False">
        <Columns>
            <asp:BoundField DataField="Name" HeaderText="Name" />
            <asp:BoundField DataField="Price" HeaderText="Price"
                            DataFormatString="{0:C}" />
            <asp:TemplateField HeaderText="Actions">
                <ItemTemplate>
                    <asp:Button ID="EditBtn" runat="server" Text="Edit"
                        CommandArgument='<%# Eval("Id") %>'
                        OnClick="EditBtn_Click" />
                </ItemTemplate>
            </asp:TemplateField>
        </Columns>
    </asp:GridView>
</asp:Content>

Web Forms Products.aspx.cs:

public partial class Products : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            LoadProducts();
        }
    }

    private void LoadProducts()
    {
        var products = ProductService.GetAll();
        ProductGrid.DataSource = products;
        ProductGrid.DataBind();
        ErrorLabel.Text = string.Empty;
    }

    protected void EditBtn_Click(object sender, EventArgs e)
    {
        var btn = (Button)sender;
        var id = int.Parse(btn.CommandArgument);
        Response.Redirect($"/EditProduct.aspx?id={id}");
    }
}

Blazor Products.razor:

@page "/products"
@rendermode InteractiveServer
@inject IProductService ProductService
@inject NavigationManager Navigation

<PageTitle>Products</PageTitle>

<h2>Products</h2>

@if (!string.IsNullOrEmpty(_errorMessage))
{
    <p class="text-danger">@_errorMessage</p>
}

@if (_products is null)
{
    <p>Loading...</p>
}
else
{
    <QuickGrid Items="@_products.AsQueryable()" class="table">
        <PropertyColumn Property="@(p => p.Name)" Title="Name" />
        <PropertyColumn Property="@(p => p.Price)"
                        Title="Price" Format="C" />
        <TemplateColumn Title="Actions">
            <button class="btn btn-sm btn-primary"
                    @onclick="() => EditProduct(context.Id)">
                Edit
            </button>
        </TemplateColumn>
    </QuickGrid>
}

@code {
    private List<Product>? _products;
    private string _errorMessage = string.Empty;

    // Replaces Page_Load with !IsPostBack
    protected override async Task OnInitializedAsync()
    {
        await LoadProductsAsync();
    }

    private async Task LoadProductsAsync()
    {
        try
        {
            _products = await ProductService.GetAllAsync();
        }
        catch (Exception ex)
        {
            _errorMessage = "Failed to load products. Please try again.";
            // log ex
        }
    }

    // Replaces EditBtn_Click + Response.Redirect
    private void EditProduct(int id)
    {
        Navigation.NavigateTo($"/products/edit/{id}");
    }
}

Key observations in this conversion:

  • Page_Load with !IsPostBack → OnInitializedAsync() which runs once on component initialization
  • GridView with DataBind() → QuickGrid with direct property binding
  • Response.Redirect() → Navigation.NavigateTo()
  • ErrorLabel.Text = value → bound field @_errorMessage that Blazor renders automatically
  • No ViewState state is held in component fields (_products, _errorMessage)

Converting Web Forms Form Submission

Web Forms EditProduct.aspx.cs:

protected void SaveBtn_Click(object sender, EventArgs e)
{
    if (Page.IsValid)
    {
        var product = new Product
        {
            Id = int.Parse(Request.QueryString["id"]),
            Name = NameTextBox.Text,
            Price = decimal.Parse(PriceTextBox.Text)
        };

        try
        {
            ProductService.Update(product);
            Response.Redirect("/Products.aspx?saved=true");
        }
        catch (Exception ex)
        {
            ErrorLabel.Text = "Save failed: " + ex.Message;
        }
    }
}

Blazor EditProduct.razor:

@page "/products/edit/{Id:int}"
@rendermode InteractiveServer
@inject IProductService ProductService
@inject NavigationManager Navigation

<PageTitle>Edit Product</PageTitle>

<EditForm Model="@_model" OnValidSubmit="SaveAsync">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <div class="mb-3">
        <label for="name">Name</label>
        <InputText id="name" @bind-Value="_model.Name"
                   class="form-control" />
        <ValidationMessage For="@(() => _model.Name)" />
    </div>

    <div class="mb-3">
        <label for="price">Price</label>
        <InputNumber id="price" @bind-Value="_model.Price"
                     class="form-control" />
        <ValidationMessage For="@(() => _model.Price)" />
    </div>

    @if (!string.IsNullOrEmpty(_errorMessage))
    {
        <p class="text-danger">@_errorMessage</p>
    }

    <button type="submit" class="btn btn-primary">Save</button>
    <a href="/products" class="btn btn-secondary ms-2">Cancel</a>
</EditForm>

@code {
    [Parameter] public int Id { get; set; }

    private ProductEditModel _model = new();
    private string _errorMessage = string.Empty;

    protected override async Task OnInitializedAsync()
    {
        var product = await ProductService.GetByIdAsync(Id);
        if (product is not null)
        {
            _model = new ProductEditModel
            {
                Name = product.Name,
                Price = product.Price
            };
        }
    }

    private async Task SaveAsync()
    {
        try
        {
            await ProductService.UpdateAsync(new Product
            {
                Id = Id,
                Name = _model.Name,
                Price = _model.Price
            });
            Navigation.NavigateTo("/products?saved=true");
        }
        catch (Exception ex)
        {
            _errorMessage = $"Save failed: {ex.Message}";
        }
    }
}

public class ProductEditModel
{
    [Required]
    [MaxLength(200)]
    public string Name { get; set; } = string.Empty;

    [Range(0.01, 99999.99)]
    public decimal Price { get; set; }
}

Converting User Controls to Blazor Components

Web Forms User Controls (.ascx) map directly to Blazor components. The conversion follows the same pattern markup to .razor, code-behind to @code { }, properties to [Parameter] attributes.

Web Forms ProductCard.ascx:

<%@ Control Language="C#" CodeBehind="ProductCard.ascx.cs" %>
<div class="product-card">
    <h3><%= ProductName %></h3>
    <p>$<%= Price %></p>
</div>
public partial class ProductCard : UserControl
{
    public string ProductName { get; set; }
    public decimal Price { get; set; }
}

Blazor ProductCard.razor:

<div class="product-card">
    <h3>@ProductName</h3>
    <p>@Price.ToString("C")</p>
</div>

@code {
    [Parameter] public string ProductName { get; set; } = string.Empty;
    [Parameter] public decimal Price { get; set; }
}

Phase 4: State Management Replacing ViewState and Session

ViewState and Session are the two Web Forms state mechanisms that need the most deliberate replacement strategy.

ViewState → Component State

ViewState in Web Forms serializes control values between postbacks so they survive the request cycle. In Blazor, component state is held in memory as C# fields no serialization needed for Blazor Server (the circuit maintains server-side state). The pattern is direct:

// Web Forms ViewState
ViewState["SelectedCategory"] = categoryId;
var selected = (int)ViewState["SelectedCategory"];

// Blazor component field
private int _selectedCategory;
// Blazor automatically re-renders when state changes via @bind or StateHasChanged()

For state that must survive navigation or page refresh in Blazor Server, use ISessionStorageService (via the Blazored.SessionStorage NuGet package) or ProtectedSessionStorage (built into ASP.NET Core for encrypted session values).

Session → Scoped Services or ProtectedSessionStorage

// Web Forms session
Session["CartId"] = cartId;
var cartId = (Guid)Session["CartId"];

// Blazor Server scoped service (persists for the circuit lifetime)
// Register: builder.Services.AddScoped<CartState>();
public class CartState
{
    public Guid CartId { get; private set; } = Guid.NewGuid();
    public List<CartItem> Items { get; } = new();
}

// Injected into components: @inject CartState Cart

// Blazor ProtectedSessionStorage for values that
// must survive page refresh
@inject ProtectedSessionStorage SessionStorage

await SessionStorage.SetAsync("CartId", cartId);
var result = await SessionStorage.GetAsync<Guid>("CartId");

Phase 5: Authentication

Web Forms Forms Authentication → ASP.NET Core Cookie Authentication:

Web Forms Forms Authentication works through the FormsAuthentication static class and web.config configuration. In Blazor, authentication integrates through ASP.NET Core's standard middleware.

// Blazor's Program.cs cookie authentication
builder.Services.AddAuthentication(
    CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/login";
        options.LogoutPath = "/logout";
        options.ExpireTimeSpan = TimeSpan.FromHours(8);
    });

builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthorizationCore();

// In App.razor wrap in CascadingAuthenticationState
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData"
                                DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <RedirectToLogin />
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
    </Router>
</CascadingAuthenticationState>

Protecting pages with [Authorize]:

@page "/admin"
@attribute [Authorize(Roles = "Admin")]

<h1>Admin Dashboard</h1>

ASP.NET Membership/Identity → ASP.NET Core Identity:

The user data migration follows the same approach described in the .NET Framework to .NET 10 guide scaffold the existing database, use a custom IPasswordHasher<T> to handle legacy password hashes during the transition period.

Phase 6: Common Web Forms Controls Blazor Equivalents

Web Forms ControlBlazor EquivalentNotes
<asp:GridView><QuickGrid> (.NET 8+)QuickGrid is built into .NET add Microsoft.AspNetCore.Components.QuickGrid
<asp:Repeater>@foreach loop in markupDirect and idiomatic
<asp:DropDownList><InputSelect>Part of Blazor's EditForm components
<asp:TextBox><InputText> or <input>InputText works inside EditForm
<asp:CheckBox><InputCheckbox>Same pattern
<asp:Button><button @onclick="...">Standard HTML button with event handler
<asp:LinkButton><button class="btn-link" @onclick="...">HTML button styled as link
<asp:Label><span>@value</span>Direct binding
<asp:ValidationSummary><ValidationSummary />Inside EditForm
<asp:RequiredFieldValidator>[Required] data annotationOn model class
<asp:RangeValidator>[Range(min, max)]On model class
<asp:UpdatePanel>Component re-renderAutomatic no equivalent needed
<asp:ScriptManager>Not neededBlazor handles scripts
<asp:FileUpload><InputFile>Handles streaming large files
<asp:Literal>@((MarkupString)htmlString)For rendering raw HTML

Phase 7: JavaScript Interop for Remaining Third-Party Libraries

Some Web Forms pages use jQuery plugins, charting libraries, or other JavaScript dependencies. Blazor doesn't eliminate JavaScript it provides JS Interop for calling JavaScript from C# and vice versa.

// Inject IJSRuntime for JS interop
@inject IJSRuntime JS

// Call a JavaScript function from C#
await JS.InvokeVoidAsync("initializeChart", "myChart", chartData);

// Get a value from JavaScript
var scrollPosition = await JS.InvokeAsync<int>("getScrollPosition");

For common Web Forms JavaScript dependencies:

jQuery UI date picker → Blazor's component libraries (MudBlazor, Radzen, Telerik) include date pickers natively in C# no jQuery needed

jQuery DataTables → QuickGrid handles most grid requirements; for advanced features, Telerik Grid or DevExpress Grid for Blazor

Chart.js → Wrap via JS Interop, or use Blazor-native charting (ApexCharts.Blazor, ChartJs.Blazor)

Bootstrap modals → MudBlazor MudDialog or Radzen RadzenDialog fully C#-managed

The goal is minimizing JS Interop every JavaScript dependency you eliminate is a reduction in the cognitive overhead of maintaining the application in C# alone.

Frequently Asked Questions

How long does a Web Forms to Blazor migration take?

For a small Web Forms application (under 20 pages, minimal third-party controls), typically 4–8 weeks. For a medium application (20–60 pages, mix of controls, complex business logic), 3–6 months. For large enterprise Web Forms applications (60+ pages, heavy GridView/Repeater usage, complex validation, many user controls), 6–18 months using the phased Strangler Fig approach. The controlling variable isn't page count it's ViewState complexity, the volume of inline code in .aspx pages, and the extent to which business logic is embedded in code-behind rather than separated into services.

Is Blazor Server or Blazor WebAssembly better for migrating Web Forms?

Blazor Server for most Web Forms migrations. The execution model (server-side logic, UI updates pushed to browser) is conceptually closer to Web Forms than WebAssembly's client-side model. Blazor Server has full access to server-side resources without needing an API layer, debugging works with standard .NET tools, and the migration requires fewer architectural changes. Consider Blazor WebAssembly or the .NET 10 Auto mode after the initial migration is stable, for applications where reducing server load at scale becomes a priority.

Can we reuse Web Forms business logic in Blazor?

Yes and this is the most important efficiency in the migration. Business logic that is properly separated from the UI layer in Web Forms code-behind (in service classes, repositories, domain objects) ports directly to Blazor with no changes. The migration cost is concentrated in the UI layer converting .aspx pages to .razor components and code-behind event handlers to Blazor event callbacks. Well-structured Web Forms applications with a service layer typically find that 60–70% of their codebase (the non-UI code) needs minimal changes.

What happens to our existing Web Forms pages during migration?

Using the Strangler Fig pattern with YARP as the reverse proxy, Web Forms pages continue to be served by the existing application while Blazor pages are being developed. Users experience no downtime they're routed to Blazor for migrated pages and Web Forms for unmigrated ones through the same URL structure. The old Web Forms application is retired page by page rather than requiring a single cutover event.

Do we need to rewrite all our JavaScript?

Not necessarily. Blazor JS Interop allows calling existing JavaScript from C#, so existing jQuery plugins and charting libraries can be wrapped rather than replaced immediately. The recommended approach is to wrap JavaScript dependencies initially (to get the Blazor migration working) and then gradually replace them with Blazor-native components from libraries like MudBlazor, Radzen, or Telerik which eliminates the JS Interop overhead entirely for those features. Not all JavaScript can or should be eliminated custom animations, complex browser APIs, and niche JavaScript libraries are reasonable to keep as wrapped interop calls.

Is Blazor ready for enterprise production use in 2026?

Yes. Blazor lets you keep one language, one tooling stack, and one pool of developers across your product and it integrates with ASP.NET Core APIs, identity, and cloud-native patterns on Azure. Microsoft uses Blazor internally for production applications. The framework ships with .NET 10 LTS and carries Microsoft's long-term support commitment. Enterprise UI component libraries (MudBlazor, Telerik, DevExpress, Syncfusion, Radzen) have all reached maturity with comprehensive component sets. The primary limitation for enterprise use is the Blazor WebAssembly initial download size for large applications Blazor Server, which is the recommended starting point for Web Forms migrations, does not have this limitation.

Closing

Migrating from ASP.NET Web Forms to Blazor is a significant project more involved than a .NET Framework to .NET 10 upgrade because it genuinely requires rethinking the UI architecture rather than updating it. The payoff is commensurate: a component-based, C#-throughout application that deploys on modern cloud infrastructure, integrates cleanly with AI features via Microsoft Agent Framework, and recruits from a growing developer pool rather than a shrinking one.

The Strangler Fig approach migrating page by page with YARP routing between old and new is what makes this migration viable for production applications. You don't need a big-bang cutover or an extended freeze on new features. The old application keeps running; the new one takes over page by page until the migration is complete.

If you're evaluating a Web Forms to Blazor migration for an enterprise application, our software modernization team can assess your specific codebase and provide a realistic scope including which pages are straightforward conversions and which require more architectural work before Blazor can replace them.