← Back to DevBytes

Migrating from ASP.NET Core to Blazor: Step-by-Step Guide

What Is Blazor and Why Migrate?

Blazor is a modern web framework from Microsoft that allows you to build interactive web UIs using C# and .NET instead of JavaScript. It runs on the client side via WebAssembly (Blazor WebAssembly) or on the server with a real-time SignalR connection (Blazor Server). Migrating an existing ASP.NET Core application—whether it uses MVC, Razor Pages, or traditional server-rendered views—to Blazor lets you transform your application into a component-based, single-page application (SPA) while keeping the entire codebase in .NET.

The migration matters because it unlocks:

Whether you’re looking to modernize a legacy MVC app or incrementally enhance a Razor Pages site, Blazor provides a natural migration path that doesn’t require a full rewrite.

Understanding the Migration Scope

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Before diving into code, it’s important to define the migration scope. Not every part of an ASP.NET Core application must become a Blazor component immediately. Common migration scenarios include:

Blazor Server is often the easiest starting point for migration because it shares the same server-side DI container, authentication, and session state as your existing ASP.NET Core app. Blazor WebAssembly introduces a clear client/server separation and requires additional API endpoints, but it’s ideal for offline-capable or static-hosted apps.

Step-by-Step Migration Guide

Step 1: Evaluate Your Current ASP.NET Core Application

Start by auditing your existing application. Identify:

Plan an incremental migration. For example, begin with a single interactive page or a shared component like a navigation menu, then expand.

Step 2: Set Up the Blazor Environment

If you want to add Blazor to an existing ASP.NET Core project, you can do so without creating a separate project. For Blazor Server, add the required services and endpoints.

Option A: Add Blazor Server to an existing ASP.NET Core app

In your Program.cs, register Blazor services and map the Blazor hub:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Existing services (MVC, Razor Pages, etc.)
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

// Add Blazor Server services
builder.Services.AddServerSideBlazor();
// Optional: Add circuit handler for detailed options
builder.Services.AddScoped<YourBlazorService>();

var app = builder.Build();

app.UseStaticFiles();
app.UseRouting();

// Existing middleware
app.MapControllers();
app.MapRazorPages();

// Map Blazor SignalR hub and set fallback
app.MapBlazorHub();
app.MapFallbackToPage("/_Host"); // For Blazor Server, often a Razor Page

app.Run();

Then add a _Host.cshtml Razor Page that hosts the Blazor app. Typically placed under Pages:

@page "/"
@namespace YourApp.Pages
@using Microsoft.AspNetCore.Components.Web

<html lang="en">
<head>
    <base href="/" />
    <!-- Stylesheets, etc. -->
    <component type="typeof(YourApp.App)" render-mode="ServerPrerendered" />
</head>
<body>
    <script src="_framework/blazor.server.js"></script>
</body>
</html>

Option B: Create a new Blazor Server or WebAssembly project and reference your existing shared libraries. This is cleaner for a full migration. Use the .NET CLI or Visual Studio templates.

Step 3: Create Your First Blazor Component

Let’s migrate a simple Razor view that displays a list of products. The original Razor view (Products.cshtml) might look like:

@model IEnumerable<Product>
<h2>Product List</h2>
<ul>
@foreach (var product in Model)
{
    <li>@product.Name - @product.Price.ToString("C")</li>
}
</ul>

The equivalent Blazor component (ProductList.razor) becomes:

@* ProductList.razor *@
@using YourApp.Models
@inject IProductService ProductService

<h2>Product List</h2>
<ul>
@foreach (var product in products)
{
    <li>@product.Name - @product.Price.ToString("C")</li>
}
</ul>

@code {
    private IEnumerable<Product> products;

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

Here, IProductService is injected via DI, and data is fetched asynchronously in OnInitializedAsync. No controller action is needed; the component handles its own data loading.

Step 4: Migrating Controllers and Pages to Components

When migrating MVC controller actions, you’ll move from request-response patterns to component-based routing. In Blazor, use the @page directive to define routes.

Original MVC controller:

// ProductsController.cs
public async Task<IActionResult> Index()
{
    var products = await _productService.GetProductsAsync();
    return View(products);
}

The Blazor page that replaces this action:

@page "/products"
@* ProductPage.razor *@
@inject IProductService ProductService

<h1>Products</h1>
<ProductList></ProductList>  <!-- Reuse the component -->

@code {
    // Page-specific logic if needed
}

Route parameters are also straightforward. For a detail page:

@page "/product/{id:int}"
@inject IProductService ProductService

<h2>Product Details</h2>
<div>@product?.Name</div>

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

    private Product product;

    protected override async Task OnParametersSetAsync()
    {
        product = await ProductService.GetProductByIdAsync(Id);
    }
}

This replaces the MVC pattern of /Product/Details/5 and the associated controller method.

Step 5: Handling Data and Services

One of the greatest advantages of migration is that you can reuse your existing .NET services almost unchanged. The DI container works the same way. For Blazor Server, scoped services live for the duration of the circuit (user session). For Blazor WebAssembly, services are typically registered as singletons or scoped to the client, and you’ll call server APIs via HTTP.

Register your existing services in the Blazor app’s Program.cs:

builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<ICartService, CartService>();
// If using EF Core, register DbContext as scoped (Blazor Server)
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

For Blazor WebAssembly, you would register an HTTP client wrapper and call a Web API controller that uses the service:

builder.Services.AddHttpClient<IProductService, ProductServiceClient>(client =>
{
    client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
});

Then in a component, inject and use it exactly as before.

Step 6: State Management and Interactivity

Replace client-side JavaScript interactivity with Blazor’s event binding and lifecycle methods. For example, a button that toggles visibility with jQuery becomes a simple boolean field and a conditional in the template.

Old approach (Razor + JS):

<button onclick="togglePanel()">Toggle</button>
<div id="panel" style="display:none">Content</div>
<script>
function togglePanel() {
    var panel = document.getElementById('panel');
    panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
}
</script>

Blazor equivalent:

<button @onclick="TogglePanel">Toggle</button>
@if (showPanel)
{
    <div>Content</div>
}

@code {
    private bool showPanel;

    private void TogglePanel()
    {
        showPanel = !showPanel;
    }
}

Forms gain full validation support without external scripts:

<EditForm Model="@formModel" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <InputText @bind-Value="formModel.Name" />
    <button type="submit">Save</button>
</EditForm>

@code {
    private ProductFormModel formModel = new();
    private async Task HandleValidSubmit()
    {
        await ProductService.SaveAsync(formModel);
    }
}

Step 7: Authentication and Authorization Migration

If your existing app uses ASP.NET Core Identity, you can keep the same authentication infrastructure. For Blazor Server, the user’s security context is available via AuthenticationStateProvider and standard [Authorize] attributes. No migration is needed on the server side.

In Blazor Server, simply add the same services:

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie();
// Or AddIdentity...
builder.Services.AddAuthorization();
// ...
app.UseAuthentication();
app.UseAuthorization();

Then protect Blazor pages with @attribute [Authorize] or use <AuthorizeView> in components.

For Blazor WebAssembly, you typically use token-based authentication (JWT). Add a custom AuthenticationStateProvider that reads tokens and configure the HttpClient to send them. The migration involves moving from server-side cookie redirects to API-based login/logout, but your Identity backend can remain the same.

Step 8: Styling and UI Integration

Most CSS and layout files can be moved as-is. Blazor supports global stylesheets in wwwroot and component-specific isolated CSS files (Component.razor.css). Migrate your existing _Layout.cshtml into a MainLayout.razor component:

@* MainLayout.razor *@
@inherits LayoutComponentBase
<div class="page">
    <header>...</header>
    <main>
        @Body
    </main>
</div>

Shared partials like navigation menus become Blazor components that you place in the layout.

Step 9: Testing and Debugging

Testing a Blazor app is different from MVC. Use bUnit for unit testing components:

// Example using bUnit and xUnit
[Fact]
public void ProductList_RendersProducts()
{
    var ctx = new TestContext();
    ctx.Services.AddSingleton<IProductService>(new MockProductService());
    var cut = ctx.RenderComponent<ProductList>();
    Assert.Contains("Product Name", cut.Markup);
}

Debugging can be done with Visual Studio or browser DevTools. For Blazor Server, you can set breakpoints directly in component code. For WebAssembly, use the browser’s developer tools with .NET debugging support.

Step 10: Deployment and Incremental Rollout

You don’t have to flip a switch overnight. Use a reverse proxy (like YARP) or conditional routing to serve Blazor pages for specific paths while keeping old MVC pages for others. For example, configure the ASP.NET Core app to route /new-ui/* to the Blazor app and keep /legacy/* to Razor Pages. Gradually move routes over.

When deploying Blazor Server, ensure sticky sessions if using load balancers. Blazor WebAssembly can be deployed as static files to a CDN. Both can be hosted within the same ASP.NET Core process.

Best Practices for a Smooth Migration

Common Pitfalls and How to Avoid Them

Conclusion

Migrating from ASP.NET Core to Blazor is a journey that transforms your server-rendered application into a modern, interactive web experience while preserving your investment in .NET. By taking an incremental, component-focused approach, you can move at your own pace—reusing services, authentication, and business logic—while gradually introducing the rich interactivity that Blazor offers. Whether you choose Blazor Server for its simplicity or Blazor WebAssembly for its client-side power, the path is clear and supported by Microsoft’s long-term vision for .NET web development. Start with a single component today, and watch your application evolve into a fully interactive, component-driven masterpiece.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles