Convert Blazor Server App to Blazor Web App
This skill helps an agent convert a pre-.NET 8 Blazor Server app into a .NET 8+ Blazor Web App. The old hosting model uses AddServerSideBlazor/MapBlazorHub with a _Host.cshtml Razor Page as the entry point. The new Blazor Web App model uses AddRazorComponents/MapRazorComponents with an App.razor root component, enabling per-component render modes, enhanced navigation, streaming rendering, and other .NET 8+ features. The converted app uses InteractiveServer render mode to preserve existing interactive behavior.
When to Use
- Migrating a Blazor Server app from .NET 6 or .NET 7 to .NET 8+
- App currently uses
AddServerSideBlazor() and MapBlazorHub() in Program.cs (or Startup.cs)
- App uses
Pages/_Host.cshtml (or _Host.razor) as the host page with Component Tag Helpers
- Want to adopt new Blazor Web App features while keeping interactive server rendering
When Not to Use
- The app already uses
AddRazorComponents and MapRazorComponents. It is already a Blazor Web App — no conversion is needed. Stop here and tell the user the app is already using the Blazor Web App model.
- Blazor WebAssembly or hosted Blazor WebAssembly app — these have a different migration path
- The app should stay on the legacy Blazor Server hosting model (just update TFM and packages)
- The app targets .NET Framework — it must be migrated to .NET first
Inputs
| Input |
Required |
Description |
| Blazor Server project |
Yes |
The .csproj and source files of the Blazor Server app |
| Target framework |
Yes |
.NET 8 or later (e.g., net8.0, net9.0, net10.0) |
Program.cs or Startup.cs |
Yes |
The app's service and middleware configuration |
_Host.cshtml location |
Recommended |
Usually Pages/_Host.cshtml; may be _Host.razor in some projects |
Workflow
Commit strategy: Commit after each logical step so the migration is reviewable and bisectable.
Step 1: Update the project file
Update the .csproj file:
- Change the Target Framework Moniker (TFM) to the target version:
<TargetFramework>net8.0</TargetFramework>
- Update all
Microsoft.AspNetCore.*, Microsoft.EntityFrameworkCore.*, Microsoft.Extensions.*, and System.Net.Http.Json package references to the matching version.
For non-Blazor project file changes (nullable reference types, implicit usings, HTTP/3 support, etc.), see the general ASP.NET Core migration guide.
Step 2: Create Routes.razor from App.razor
The old App.razor contains the <Router> component. This content moves to a new Routes.razor file so that App.razor can become the root HTML document component.
- Create a new file
Routes.razor in the project root.
- Move the entire content of
App.razor into Routes.razor.
- If the content is wrapped in
<CascadingAuthenticationState>, remove that wrapper (it will be replaced by a service in Step 5).
- Leave
App.razor empty for the next step.
The resulting Routes.razor should look similar to:
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
If the app uses <AuthorizeRouteView> instead of <RouteView>, keep it — it works the same way in Blazor Web Apps.
Step 3: Convert _Host.cshtml to App.razor
Move the HTML shell from Pages/_Host.cshtml into the now-empty App.razor and transform it from a Razor Page into a Razor component:
Remove Razor Page directives — delete @page "/", @using Microsoft.AspNetCore.Components.Web, @namespace, and @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers.
Add component injection — if using environment-conditional error UI, add:
@inject IHostEnvironment Env
Fix the base tag — replace <base href="~/" /> with <base href="/" />.
Replace HeadOutlet Component Tag Helper — replace:
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
with:
<HeadOutlet @rendermode="InteractiveServer" />
Replace App Component Tag Helper with Routes — replace:
<component type="typeof(App)" render-mode="ServerPrerendered" />
with:
<Routes @rendermode="InteractiveServer" />
Replace Environment Tag Helpers — replace:
<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>
with:
@if (Env.IsDevelopment())
{
<text>
An unhandled exception has occurred. See browser dev tools for details.
</text>
}
else
{
<text>
An error has occurred. This app may no longer respond until reloaded.
</text>
}
Update the Blazor script — replace:
<script src="_framework/blazor.server.js"></script>
with:
<script src="_framework/blazor.web.js"></script>
Add render mode import — add to _Imports.razor:
@using static Microsoft.AspNetCore.Components.Web.RenderMode
Delete Pages/_Host.cshtml (and Pages/_Host.cshtml.cs if it exists).
Prerendering note: If the original app used render-mode="Server" (not "ServerPrerendered"), prerendering was disabled. Preserve this by using new InteractiveServerRenderMode(prerender: false) instead of InteractiveServer for both HeadOutlet and Routes.
Step 4: Update Program.cs
Make the following changes to Program.cs (or Startup.cs if the app uses the older hosting pattern):
Replace Blazor Server services — replace:
builder.Services.AddServerSideBlazor();
with:
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
If AddServerSideBlazor had options configured (e.g., circuit options, hub options, detailed errors), migrate them to AddInteractiveServerComponents:
// Old:
builder.Services.AddServerSideBlazor(options =>
{
options.DetailedErrors = true;
options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(10);
});
// New:
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents(options =>
{
options.DetailedErrors = true;
options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(10);
});
Replace Blazor endpoint mapping — replace:
app.MapBlazorHub();
with:
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
Ensure there is a using statement for the project's root namespace so that App resolves to the App.razor component.
Remove the fallback route — delete:
app.MapFallbackToPage("/_Host");
Remove explicit routing middleware — delete if present:
app.UseRouting();
Endpoint routing is the default and explicit UseRouting() is no longer needed.
Add antiforgery middleware — add after UseAuthentication/UseAuthorization if present:
app.UseAntiforgery();
AddRazorComponents registers antiforgery services automatically, but the middleware must be explicitly added to the pipeline. Without it, form POST requests fail with 400 errors.
Step 5: Migrate CascadingAuthenticationState (if present)
If the app used <CascadingAuthenticationState> to wrap the router:
- Remove the
<CascadingAuthenticationState> component wrapper (already done in Step 2 if following this workflow).
- Add the cascading authentication state service in
Program.cs:builder.Services.AddCascadingAuthenticationState();
The component wrapper approach does not work across render mode boundaries in Blazor Web Apps. The service-based approach provides Task<AuthenticationState> as a cascading value to all components regardless of render mode.
Step 6: Recommended improvements (optional)
These are optional modernization improvements — not required for the conversion to work. If you suggest any of these, state explicitly that they are optional.
- Replace
UseStaticFiles with MapStaticAssets (.NET 9+): app.MapStaticAssets() provides optimized static file serving with fingerprinting, pre-compression, and content-based ETags. See MapStaticAssets documentation.
- Add
@attribute [StreamRendering] to pages with async data loading (OnInitializedAsync) for improved perceived performance. The page renders its initial synchronous content immediately and re-renders when async data arrives.
- Update CSS isolation bundle reference if the
<link> tag referenced a _Host assembly name; ensure it matches the project's actual assembly name: <link href="{AssemblyName}.styles.css" rel="stylesheet" />.
- For other non-Blazor improvements (minimal hosting, HTTP/3, output caching, etc.), see the general ASP.NET Core migration guide.
Step 7: Verify the migration
- Build the project targeting the new framework. Confirm no compile errors.
- Search for remaining references to removed APIs:
AddServerSideBlazor
MapBlazorHub
MapFallbackToPage
blazor.server.js
_Host.cshtml
- Run the app and verify:
- Pages load and render correctly
- Interactive features work (forms, event handlers, SignalR circuits)
- Navigation between pages works
- Authentication and authorization flows work if present
- Run existing tests.
Validation
Common Pitfalls
| Pitfall |
Solution |
Missing UseAntiforgery() middleware |
AddRazorComponents registers antiforgery services, but the middleware must be explicitly added. Place app.UseAntiforgery() after UseAuthentication/UseAuthorization. Without it, form POST requests fail with 400 errors. |
Forgetting to replace blazor.server.js with blazor.web.js |
The old script does not work with the Blazor Web App model. Replace all references to _framework/blazor.server.js with _framework/blazor.web.js. |
Not removing <CascadingAuthenticationState> wrapper |
The component wrapper does not work across render mode boundaries in Blazor Web Apps. Use builder.Services.AddCascadingAuthenticationState() instead. |
Leaving app.UseRouting() in the pipeline |
Explicit UseRouting() is no longer needed and can interfere with endpoint routing. Remove it unless other middleware specifically requires it. |
Using InteractiveServer when prerendering was disabled |
If the original app used render-mode="Server" (not "ServerPrerendered"), use new InteractiveServerRenderMode(prerender: false) to preserve the same behavior. Using InteractiveServer enables prerendering which can cause unexpected issues with components that depend on JS interop during initialization. |
Not migrating AddServerSideBlazor circuit options |
If circuit options, hub options, or detailed error settings were configured, migrate them to AddInteractiveServerComponents(options => { ... }). Otherwise those settings are silently lost. |
UseAntiforgery() placed before authentication middleware |
The antiforgery middleware must be placed after UseAuthentication and UseAuthorization. Placing it before causes antiforgery validation to run before the user identity is established. |
| CSS isolation bundle link has wrong assembly name |
If the <link href="{Name}.styles.css"> tag referenced the old project name, update it to match the current assembly name. |
More Info
1---2name: convert-blazor-server-to-webapp3description: Converts a pre-.NET 8 Blazor Server app into a .NET 8+ Blazor Web App by updating project files, migrating entry points, and adopting new render modes.4license: MIT5---67# Convert Blazor Server App to Blazor Web App89This skill helps an agent convert a pre-.NET 8 Blazor Server app into a .NET 8+ Blazor Web App. The old hosting model uses `AddServerSideBlazor`/`MapBlazorHub` with a `_Host.cshtml` Razor Page as the entry point. The new Blazor Web App model uses `AddRazorComponents`/`MapRazorComponents` with an `App.razor` root component, enabling per-component render modes, enhanced navigation, streaming rendering, and other .NET 8+ features. The converted app uses `InteractiveServer` render mode to preserve existing interactive behavior.1011## When to Use1213- Migrating a Blazor Server app from .NET 6 or .NET 7 to .NET 8+14- App currently uses `AddServerSideBlazor()` and `MapBlazorHub()` in `Program.cs` (or `Startup.cs`)15- App uses `Pages/_Host.cshtml` (or `_Host.razor`) as the host page with Component Tag Helpers16- Want to adopt new Blazor Web App features while keeping interactive server rendering1718## When Not to Use1920- **The app already uses `AddRazorComponents` and `MapRazorComponents`.** It is already a Blazor Web App — no conversion is needed. Stop here and tell the user the app is already using the Blazor Web App model.21- Blazor WebAssembly or hosted Blazor WebAssembly app — these have a different migration path22- The app should stay on the legacy Blazor Server hosting model (just update TFM and packages)23- The app targets .NET Framework — it must be migrated to .NET first2425## Inputs2627| Input | Required | Description |28|-------|----------|-------------|29| Blazor Server project | Yes | The `.csproj` and source files of the Blazor Server app |30| Target framework | Yes | .NET 8 or later (e.g., `net8.0`, `net9.0`, `net10.0`) |31| `Program.cs` or `Startup.cs` | Yes | The app's service and middleware configuration |32| `_Host.cshtml` location | Recommended | Usually `Pages/_Host.cshtml`; may be `_Host.razor` in some projects |3334## Workflow3536> **Commit strategy:** Commit after each logical step so the migration is reviewable and bisectable.3738### Step 1: Update the project file3940Update the `.csproj` file:41421. Change the Target Framework Moniker (TFM) to the target version:43 ```xml44 <TargetFramework>net8.0</TargetFramework>45 ```462. Update all `Microsoft.AspNetCore.*`, `Microsoft.EntityFrameworkCore.*`, `Microsoft.Extensions.*`, and `System.Net.Http.Json` package references to the matching version.4748For non-Blazor project file changes (nullable reference types, implicit usings, HTTP/3 support, etc.), see the [general ASP.NET Core migration guide](https://learn.microsoft.com/aspnet/core/migration/70-to-80).4950### Step 2: Create `Routes.razor` from `App.razor`5152The old `App.razor` contains the `<Router>` component. This content moves to a new `Routes.razor` file so that `App.razor` can become the root HTML document component.53541. Create a new file `Routes.razor` in the project root.552. Move the entire content of `App.razor` into `Routes.razor`.563. If the content is wrapped in `<CascadingAuthenticationState>`, remove that wrapper (it will be replaced by a service in Step 5).574. Leave `App.razor` empty for the next step.5859The resulting `Routes.razor` should look similar to:6061```razor62<Router AppAssembly="@typeof(Program).Assembly">63 <Found Context="routeData">64 <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />65 <FocusOnNavigate RouteData="@routeData" Selector="h1" />66 </Found>67 <NotFound>68 <LayoutView Layout="@typeof(MainLayout)">69 <p>Sorry, there's nothing at this address.</p>70 </LayoutView>71 </NotFound>72</Router>73```7475If the app uses `<AuthorizeRouteView>` instead of `<RouteView>`, keep it — it works the same way in Blazor Web Apps.7677### Step 3: Convert `_Host.cshtml` to `App.razor`7879Move the HTML shell from `Pages/_Host.cshtml` into the now-empty `App.razor` and transform it from a Razor Page into a Razor component:80811. **Remove Razor Page directives** — delete `@page "/"`, `@using Microsoft.AspNetCore.Components.Web`, `@namespace`, and `@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers`.82832. **Add component injection** — if using environment-conditional error UI, add:84 ```razor85 @inject IHostEnvironment Env86 ```87883. **Fix the base tag** — replace `<base href="~/" />` with `<base href="/" />`.89904. **Replace HeadOutlet Component Tag Helper** — replace:91 ```html92 <component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />93 ```94 with:95 ```razor96 <HeadOutlet @rendermode="InteractiveServer" />97 ```98995. **Replace App Component Tag Helper with Routes** — replace:100 ```html101 <component type="typeof(App)" render-mode="ServerPrerendered" />102 ```103 with:104 ```razor105 <Routes @rendermode="InteractiveServer" />106 ```1071086. **Replace Environment Tag Helpers** — replace:109 ```html110 <environment include="Staging,Production">111 An error has occurred. This application may no longer respond until reloaded.112 </environment>113 <environment include="Development">114 An unhandled exception has occurred. See browser dev tools for details.115 </environment>116 ```117 with:118 ```razor119 @if (Env.IsDevelopment())120 {121 <text>122 An unhandled exception has occurred. See browser dev tools for details.123 </text>124 }125 else126 {127 <text>128 An error has occurred. This app may no longer respond until reloaded.129 </text>130 }131 ```1321337. **Update the Blazor script** — replace:134 ```html135 <script src="_framework/blazor.server.js"></script>136 ```137 with:138 ```html139 <script src="_framework/blazor.web.js"></script>140 ```1411428. **Add render mode import** — add to `_Imports.razor`:143 ```razor144 @using static Microsoft.AspNetCore.Components.Web.RenderMode145 ```1461479. **Delete `Pages/_Host.cshtml`** (and `Pages/_Host.cshtml.cs` if it exists).148149**Prerendering note:** If the original app used `render-mode="Server"` (not `"ServerPrerendered"`), prerendering was disabled. Preserve this by using `new InteractiveServerRenderMode(prerender: false)` instead of `InteractiveServer` for both `HeadOutlet` and `Routes`.150151### Step 4: Update `Program.cs`152153Make the following changes to `Program.cs` (or `Startup.cs` if the app uses the older hosting pattern):1541551. **Replace Blazor Server services** — replace:156 ```csharp157 builder.Services.AddServerSideBlazor();158 ```159 with:160 ```csharp161 builder.Services.AddRazorComponents()162 .AddInteractiveServerComponents();163 ```164165 If `AddServerSideBlazor` had options configured (e.g., circuit options, hub options, detailed errors), migrate them to `AddInteractiveServerComponents`:166 ```csharp167 // Old:168 builder.Services.AddServerSideBlazor(options =>169 {170 options.DetailedErrors = true;171 options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(10);172 });173174 // New:175 builder.Services.AddRazorComponents()176 .AddInteractiveServerComponents(options =>177 {178 options.DetailedErrors = true;179 options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(10);180 });181 ```1821832. **Replace Blazor endpoint mapping** — replace:184 ```csharp185 app.MapBlazorHub();186 ```187 with:188 ```csharp189 app.MapRazorComponents<App>()190 .AddInteractiveServerRenderMode();191 ```192193 Ensure there is a `using` statement for the project's root namespace so that `App` resolves to the `App.razor` component.1941953. **Remove the fallback route** — delete:196 ```csharp197 app.MapFallbackToPage("/_Host");198 ```1992004. **Remove explicit routing middleware** — delete if present:201 ```csharp202 app.UseRouting();203 ```204 Endpoint routing is the default and explicit `UseRouting()` is no longer needed.2052065. **Add antiforgery middleware** — add after `UseAuthentication`/`UseAuthorization` if present:207 ```csharp208 app.UseAntiforgery();209 ```210 `AddRazorComponents` registers antiforgery services automatically, but the middleware must be explicitly added to the pipeline. Without it, form POST requests fail with 400 errors.211212### Step 5: Migrate `CascadingAuthenticationState` (if present)213214If the app used `<CascadingAuthenticationState>` to wrap the router:2152161. Remove the `<CascadingAuthenticationState>` component wrapper (already done in Step 2 if following this workflow).2172. Add the cascading authentication state service in `Program.cs`:218 ```csharp219 builder.Services.AddCascadingAuthenticationState();220 ```221222The component wrapper approach does not work across render mode boundaries in Blazor Web Apps. The service-based approach provides `Task<AuthenticationState>` as a cascading value to all components regardless of render mode.223224### Step 6: Recommended improvements (optional)225226These are optional modernization improvements — not required for the conversion to work. If you suggest any of these, state explicitly that they are optional.227228- **Replace `UseStaticFiles` with `MapStaticAssets`** (.NET 9+): `app.MapStaticAssets()` provides optimized static file serving with fingerprinting, pre-compression, and content-based ETags. See [MapStaticAssets documentation](https://learn.microsoft.com/aspnet/core/fundamentals/static-files#mapstaticassets).229- **Add `@attribute [StreamRendering]`** to pages with async data loading (`OnInitializedAsync`) for improved perceived performance. The page renders its initial synchronous content immediately and re-renders when async data arrives.230- **Update CSS isolation bundle reference** if the `<link>` tag referenced a `_Host` assembly name; ensure it matches the project's actual assembly name: `<link href="{AssemblyName}.styles.css" rel="stylesheet" />`.231- For other non-Blazor improvements (minimal hosting, HTTP/3, output caching, etc.), see the [general ASP.NET Core migration guide](https://learn.microsoft.com/aspnet/core/migration/70-to-80).232233### Step 7: Verify the migration2342351. Build the project targeting the new framework. Confirm no compile errors.2362. Search for remaining references to removed APIs:237 - `AddServerSideBlazor`238 - `MapBlazorHub`239 - `MapFallbackToPage`240 - `blazor.server.js`241 - `_Host.cshtml`2423. Run the app and verify:243 - Pages load and render correctly244 - Interactive features work (forms, event handlers, SignalR circuits)245 - Navigation between pages works246 - Authentication and authorization flows work if present2474. Run existing tests.248249## Validation250251- [ ] No references to `AddServerSideBlazor` remain252- [ ] No references to `MapBlazorHub` remain253- [ ] No references to `MapFallbackToPage("/_Host")` remain254- [ ] No references to `blazor.server.js` remain255- [ ] `Pages/_Host.cshtml` has been deleted256- [ ] `App.razor` serves as the root component with a full HTML document structure257- [ ] `Routes.razor` contains the `<Router>` configuration258- [ ] `Program.cs` uses `AddRazorComponents().AddInteractiveServerComponents()`259- [ ] `Program.cs` uses `MapRazorComponents<App>().AddInteractiveServerRenderMode()`260- [ ] `app.UseAntiforgery()` is present in the middleware pipeline261- [ ] If the app used `<CascadingAuthenticationState>`, it has been replaced with `AddCascadingAuthenticationState()` service registration262- [ ] App builds and runs successfully on the target framework263264## Common Pitfalls265266| Pitfall | Solution |267|---------|----------|268| Missing `UseAntiforgery()` middleware | `AddRazorComponents` registers antiforgery services, but the middleware must be explicitly added. Place `app.UseAntiforgery()` after `UseAuthentication`/`UseAuthorization`. Without it, form POST requests fail with 400 errors. |269| Forgetting to replace `blazor.server.js` with `blazor.web.js` | The old script does not work with the Blazor Web App model. Replace all references to `_framework/blazor.server.js` with `_framework/blazor.web.js`. |270| Not removing `<CascadingAuthenticationState>` wrapper | The component wrapper does not work across render mode boundaries in Blazor Web Apps. Use `builder.Services.AddCascadingAuthenticationState()` instead. |271| Leaving `app.UseRouting()` in the pipeline | Explicit `UseRouting()` is no longer needed and can interfere with endpoint routing. Remove it unless other middleware specifically requires it. |272| Using `InteractiveServer` when prerendering was disabled | If the original app used `render-mode="Server"` (not `"ServerPrerendered"`), use `new InteractiveServerRenderMode(prerender: false)` to preserve the same behavior. Using `InteractiveServer` enables prerendering which can cause unexpected issues with components that depend on JS interop during initialization. |273| Not migrating `AddServerSideBlazor` circuit options | If circuit options, hub options, or detailed error settings were configured, migrate them to `AddInteractiveServerComponents(options => { ... })`. Otherwise those settings are silently lost. |274| `UseAntiforgery()` placed before authentication middleware | The antiforgery middleware must be placed after `UseAuthentication` and `UseAuthorization`. Placing it before causes antiforgery validation to run before the user identity is established. |275| CSS isolation bundle link has wrong assembly name | If the `<link href="{Name}.styles.css">` tag referenced the old project name, update it to match the current assembly name. |276277## More Info278279- [Convert a Blazor Server app into a Blazor Web App](https://learn.microsoft.com/aspnet/core/migration/70-to-80#convert-a-blazor-server-app-into-a-blazor-web-app) — the official step-by-step migration guide280- [ASP.NET Core Blazor render modes](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes) — understanding InteractiveServer, InteractiveWebAssembly, and InteractiveAuto281- [Migrate CascadingAuthenticationState to services](https://learn.microsoft.com/aspnet/core/migration/70-to-80#migrate-the-cascadingauthenticationstate-component-to-cascading-authentication-state-services) — replacing the component wrapper with a service282- [MapStaticAssets](https://learn.microsoft.com/aspnet/core/fundamentals/static-files#mapstaticassets) — optimized static file serving in .NET 9+283- [Migrate from ASP.NET Core 7.0 to 8.0](https://learn.microsoft.com/aspnet/core/migration/70-to-80) — general migration guide for all ASP.NET Core changes284- [Stream rendering with Blazor](https://learn.microsoft.com/aspnet/core/blazor/components/render-modes#streaming-rendering) — `@attribute [StreamRendering]` for async data loading285- [Cascading values and render mode boundaries](https://learn.microsoft.com/aspnet/core/blazor/components/cascading-values-and-parameters#cascading-valuesparameters-and-render-mode-boundaries) — why cascading parameters do not cross render mode boundaries