How to migrate your Blazor Server app to .NET 8

October 3, 2023 · 11 minute read · Tags: blazor

It’s one thing to start a brand new project with .NET 8.

But what if you have an existing Blazor Server project which you want to upgrade?

How easy is it to migrate to the new framework, and what should you watch out for along the way?

Let’s find out!

Please note: this article has been updated to reflect some minor changes in .NET 8 Release Candidate 2.

Up the .NET version

This first part is by far the easiest.

You can change your existing Blazor Server app to target .NET 8 with one change in your project’s .csproj file.

<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>      
    </PropertyGroup>

</Project>

The TargetFramework is the key here, and indicates you want this project to run using .NET 8.

If you have Microsoft NuGet packages references, you will probably want to up them too.

<ItemGroup>
	<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0-8.0.0-rc.2.23480.1" />
</ItemGroup>

In this case I’ve updated the reference to Microsoft.EntityFrameworkCore to the latest publicly available .NET 8 release candidate.

If you have other projects in your solution you’ll likely to need to upgrade them to .NET 8 too at this point.

If, at this point, you run into compile errors, it’s probably because of any NuGet packages you’ve upgraded.

You’ll need to tackle each of those compile errors as you encounter them (and obviously the details will vary depending on which NuGet packages you’re using).

Once you can compile, it’s worth running any automated tests you have, then check your app in the browser to make sure everything still works!

Switch to ‘SSR first mode’

That’s the boring bit done, now to embrace .NET 8’s SSR render mode.

I’ve found the easiest way to think about .NET 8 static server-side rendering mode is as the foundation on which everything else is built.

If your app were a cake, SSR would be the sponge base 🎂

In practice this means telling .NET you want to route incoming requests to your Razor components first and foremost. Then let it take matters on from there.

Specifically, you need a little config to tell it to route incoming requests to one specific Razor component (of your choosing).

Program.cs

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

...

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

The key here is the call to AddRazorComponents.

While here I’ve also added interactive server components and enabled interactive server render mode (that’s Blazor server to you and me).

The next few steps can be a little confusing so let’s outline what we’re going to do, then walk through the exact steps, and take a moment to review what we’re doing (and why).

First we need to create an App razor component.

This is where we indicated (in the code above) we wanted to route incoming requests, to see if they can be handled via a razor component.

Secondly we need to migrate our existing code from _Host.cshtml page across to the new App.razor component.

That’s because App.razor is going to become the new root page for our application.

Migrate from _Host.cshtml to App.razor

Traditionally Blazor Server apps have been served via an initial Razor Page, usually called _Host.cshtml.

This acts as the entry point for your web app (and is where you reference stylesheets, scripts etc.)

You also likely have a separate App.razor component which handles routing for your Razor Components.

The usual flow is something like this:

flowchart TB Browser-->|Request '/Counter'| _Host _Host -->|Render App| App subgraph _ App --> Counter end

In .NET 8 we can switch out that _Host.cshtml razor page for a razor component instead.

The aim is to change the flow to this:

flowchart TB Browser-->|Request '/Counter'| App subgraph _ App --> Routes Routes --> Counter end

The easiest way to change this flow is to:

  1. Rename your existing App.razor component file to Routes.razor
  2. Rename _Host.cshtml page to App.razor

Then make a few small tweaks to your new App.razor component.

You’re aiming for something like this:

App.razor

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <base href="/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css"/>
    <link href="css/site.css" rel="stylesheet"/>
    <link href="BlazorMigrationDemo.styles.css" rel="stylesheet"/>
    <link rel="icon" type="image/png" href="favicon.png"/>
    <HeadOutlet/>
</head>

<body>
<Routes/>

<div id="blazor-error-ui">
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

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

</body>
</html>

Here are the key changes:

Remove superfluous markup

Right at the top of this file is some code you needed when this was a razor page.

Now you’re converting it to a razor component you can get rid of all the following lines:

@page "/"
@using Microsoft.AspNetCore.Components.Web
@namespace BlazorMigrationDemo.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Update the base link to point to “/” instead of “~/”.

<base href="/" />

Watch out for this one, it’s sneaky and will leave you wondering why your app isn’t picking up CSS styles if you miss it.

Replace the head component

_Host.cshtml used the component tag helper to render the HeadOutlet for your components.

Now you’re using a Razor Component (App.razor) instead of the usual Razor Page, you can just reference the HeadOutlet component directly.

From this:

<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered"/>

To this:

 <HeadOutlet/>

Remove the root component

Your razor page likely also used a component tag helper to render your app, like this:

<component type="typeof(App)" render-mode="ServerPrerendered"/>

This isn’t needed any more - you can delete it.

Tidy up the blazor UI

There previous UI for handling errors in your Blazor app probably used the environment tag helper.

This isn’t available in Razor components.

<div id="blazor-error-ui">
    <environment include="Staging,Production">
        An error has occurred. This application may no longer respond until reloaded.
    </environment>
    <environment include="Development">
        An unhandled exception has occurred. See browser dev tools for details.
    </environment>
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

At the time of writing the answer is to simplify this to always show the same error message.

This may change when .NET 8 reaches RTM.

<div id="blazor-error-ui">
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

Remove the Blazor Server JS script

_Host.cshtml referenced a Blazor server JavaScript file which is no longer necessary (.NET will load this dynamically as needed).

You can safely remove this line:

<script src="_framework/blazor.server.js"></script>

In its place you’ll want to include this script (to enable Blazor’s new enhanced navigation and form handling).

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

Render your new Routes component

Remember how you renamed your old App.razor component to Routes.razor.

The time has arrived to use that Routes component. You can render it inside the <body> element of your (new) App.razor component.

<body>
<Routes/>
</body>

Remove NotFound from your router in Routes.razor

Changes to the way requests are handled by Blazor in .NET 8 means the NotFound template in Routes.razor is now redundant.

It can be safely removed.

Which should leave you with something like this:

<Router AppAssembly="@typeof(Routes).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1"/>
    </Found>
</Router>

Check your progress

With these changes you’ve ended up with a simplified App.razor component which has HTML, script tags and stylesheet links.

Take another look at this new flow for requests to your app:

flowchart TB Browser-->|Request '/Counter'| App subgraph _ App --> Routes Routes --> Counter end

When a request comes in .NET will forward it to App (which lives in App.Razor).

It will then render the Routes component (Routes.razor) which invokes the Router to handle the request (and ultimately forward it to the relevant component).

Make it interactive

At this point your app should work when you spin it up and view it in the browser, with one major caveat.

Everything will now be rendered using static server-side rendering.

This means your components are rendered once on the server, and plain HTML returned to the browser.

You may find this is sufficient for some of your existing components, especially if they’re predominantly showing data.

However the chances are your existing components require some level of interactivity.

As you’re migrating from Blazor server one option here is to make everything run using interactive server mode.

This would effectively give you the exact same result you had with .NET 7 (or earlier).

The good news is this is straightforward - you can simply make the Routes and HeadOutlet components use the new InteractiveServer render mode

<!DOCTYPE html>
<html lang="en">
<head>
    ...
    <HeadOutlet @rendermode="@RenderMode.InteractiveServer"/>
</head>

<body>
<Routes @rendermode="@RenderMode.InteractiveServer"/>
</body>

...

With that change you should find everything works as it did before.

Watch out for pre-rendering here.

With this approach your Routes and HeadOutlet components will be pre-rendered on the server, before being rendered interactively using Blazor Server.

This means they’re rendered once on the server (and static HTML returned to the browser). Then when the initial static HTML has been rendered Blazor server kicks in to make the component(s) interactive.

This can be useful to avoid making the user wait for Blazor Server to kick in before they see something in the browser.

But, if you weren’t using pre-rendering before, you may not want to use it now.

In that case, you can disable it:

<Routes @rendermode="@(new InteractiveServerRenderMode(false))"/> 

Here we instantiate InteractiveServerRenderMode ourselves instead of using the RenderMode.InteractiveServerRenderMode shortcut.

In so doing we can pass false to the prerender parameter, and disable pre-rendering for the Routes component.

Consider using forms instead of enabling server interactivity

Finally, it’s worth looking at your components at this point.

If they are only interactive in the sense that they take user input and send it to your backend logic/database, you might find you can switch from interactive server render mode to SSR and forms instead.

Take this feedback form:

@page "/FormDemo"

<h3>Send us feedback!</h3>

<EditForm Model="@FeedbackModel" OnValidSubmit="SendFeedback">
    <InputText class="form-control" @bind-Value="FeedbackModel.Email"/>
    <InputTextArea class="form-control" @bind-Value="FeedbackModel.Message"></InputTextArea>
    <button type="submit">
        Send
    </button>
</EditForm>
@code {
    
    public Feedback FeedbackModel { get; set; } = new Feedback();

    public class Feedback
    {
        public string Email { get; set; }
        public string Message { get; set; }
    }

    private void SendFeedback()
    {
        Console.WriteLine("Feedback received from: " + FeedbackModel.Email + "message: " + FeedbackModel.Message);
    }

}

In Blazor server this would run interactively, but in .NET 8 you have the option to handle this using static server-side rendering and traditional form posts instead.

You’ll need to modify your EditForm to give it a FormName and set its method to POST.

<EditForm Model="@FeedbackModel" OnValidSubmit="SendFeedback" FormName="feedbackForm" method="POST">
    ...
</EditForm>

You’ll also need to modify your Model parameter (FeedbackModel in this case) to use the new [SupplyParameterFromForm] attribute.

@code {
    
    [SupplyParameterFromForm]
    public Feedback FeedbackModel { get; set; } = new Feedback();

    ...

}

With this, when the form is submitted it will be routed to your component and ASP.NET’s model binding will kick in to take the posted form values and use them to populate the FeedbackModel property.

Gotchas and things to watch out for

Here are some gotchas I’ve run into when migrating projects.

I’ll add to this list as things crop up.

NotFound is no longer relevant

With Blazor Server/WASM prior to .NET 8 you could provide a template to be displayed when someone attempted to navigate to a route which didn’t exist.

You did this via the NotFound parameter for the Router.

<Router ...>
	<Found Context="routeData">
		...	
	</Found>
	<NotFound>
		<p>Eeek, page not found!</p>
	</NotFound>
</Router>

With the changes to how .NET routes requests to your components in .NET 8 this NotFound template will never kick in (and so can be removed from your Router).

To handle 404s you can instead rely on the native mechanism for this in ASP.NET.

One way is to add this to your program.cs.

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");

Then create a Razor component to handle requests routed to /StatusCode.

StatusCode.razor

@page "/StatusCode/{responseCode}"

<h3>StatusCode @ResponseCode</h3>
@code {

    [Parameter]
    public string ResponseCode { get; set; }

}

Watch out for <script> tags in interactive components

When rendering components statically on the server you can now include <script> tags.

Indeed you’ve seen this in App.razor already.

But, if you attempt to do this with a component that’s running interactively (on the server or via WASM) you’ll hit JavaScript errors.

There used to be a warning for this which explained it wasn’t possible to include <script> tags in a razor component.

That warning was removed (because it is now possible, when running in SSR mode) but it does mean you’ll trip up if you try the same thing when using either of the interactive modes.

In conclusion

Upgrading from .NET 7 (or earlier) and Blazor Server to .NET 8 is typically fairly painless.

The key is to enable routing to components, then migrate the existing _Host.cshtml entry point to an equivalent razor component.

From there you can choose to make your entire app run interactively on the server (which is equivalent to your existing Blazor Server apps) or default to static server-side rendering and enable interactivity on a case by case basis.

Join the Practical ASP.NET Newsletter

Ship better Blazor apps, faster. One practical tip every Tuesday.

I respect your email privacy. Unsubscribe with one click.

    Next up

    How to upload a file with Blazor SSR in .NET 8?
    How to handle file uploads without using an interactive render mode?
    3 simple design tips to improve your Web UI
    Spruce up your features
    The quickest way to integrate PayPal checkout with Blazor SSR in .NET 8
    JavaScript Interop works differently with Blazor Server-side rendering