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:
- Rich interactivity without writing JavaScript for most UI scenarios.
- Component reusability across pages and even across different projects.
- Full .NET runtime access on the client (Blazor WebAssembly) or near-zero latency UI updates (Blazor Server).
- Unified development model — you can share models, validations, and business logic between client and server seamlessly.
- Future-proofing your application for .NET’s evolving ecosystem, including .NET MAUI Blazor Hybrid and Blazor United (server-side rendering + interactivity).
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:
- Razor Pages → Blazor Components: Convert individual pages or partial views into interactive Blazor components while keeping the rest of the application intact.
- MVC Views → Blazor Pages: Replace controller-returned views with Blazor-routed pages, often combined with a shared backend API.
- Hybrid approach: Run Blazor Server or WebAssembly alongside existing Razor Pages/MVC, gradually moving functionality over.
- Full migration: Replace the entire UI layer with a Blazor app, using the existing ASP.NET Core backend as an API or retaining it for authentication.
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:
- Pages with heavy JavaScript/jQuery interactivity — these are prime candidates for Blazor components.
- Forms and validation logic — Blazor’s form components and data annotations integrate beautifully.
- Dashboard-style pages with real-time updates — Blazor Server’s SignalR connection excels here.
- Static content pages (About, Contact) — these may not need immediate migration.
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
- Start small: Migrate a single page or a shared component first, such as a navbar or a product card. Gain confidence and then expand.
- Keep backend services untouched: Reuse existing services, repositories, and business logic. Only change the presentation layer.
- Embrace component composition: Break down large views into smaller reusable Blazor components. This improves maintainability and aligns with the framework’s strengths.
- Handle JavaScript interop wisely: For functionality not yet available in Blazor (e.g., complex charting libraries), use
IJSRuntimeto call JavaScript. Wrap it in a service to keep components clean. - Manage state appropriately: Use cascading parameters, dependency-injected state containers, or libraries like Fluxor for complex state. Avoid tightly coupling components to the database context directly.
- Prerender when possible: Blazor Server’s
ServerPrerenderedmode improves perceived performance and SEO. Use it on your initial host page. - Test incrementally: Write bUnit tests for migrated components before removing the old page to ensure parity.
- Plan for SEO if using Blazor WebAssembly: Consider prerendering or using Blazor Server for public-facing content; otherwise, implement server-side rendering strategies.
Common Pitfalls and How to Avoid Them
- Over-migrating static content: Not every page needs interactivity. Keep static informational pages as Razor Pages or MVC views to reduce complexity.
- Ignoring the client/server difference: Blazor Server keeps state on the server; WebAssembly runs entirely on the client. Don’t design components assuming unlimited server access for WebAssembly — always use async API calls.
- Mismatched routing: Ensure routes in Blazor don’t conflict with existing MVC/Pages routes unless you intend to replace them. Use distinct URL spaces during migration.
- Large components causing latency in Blazor Server: Heavy rendering or frequent UI updates can cause lag. Use virtualization, limit state updates, and consider switching to WebAssembly for highly interactive parts.
- Losing authentication state in WebAssembly: Cookie-based authentication doesn’t work in WebAssembly. You must switch to token-based auth or use the backend for identity via API.
- Neglecting JavaScript interop complexity: Some existing JS libraries may need a wrapper component. Plan for a gradual replacement rather than trying to remove all JS at once.
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.