Switch Blazor Interactivity
Switching an existing Power Portals Pro project from one interactivity mode to another is a series of edits to Program.cs, App.razor, the .Client project, and (when adding interactivity) the Account pages. The framework itself does not need any changes — only the host wiring does. The steps below cover the common transitions.
Tip
If your customizations live in a small number of pages, the lowest-friction path is to scaffold a fresh project in the target mode and copy your customizations across. Comparing your current host to a freshly generated reference is also useful when chasing down a discrepancy after a manual switch.
dotnet new powerportalspro -o MyPortal-Reference --interactivity Auto
Server → WebAssembly or Auto
These steps add WebAssembly to a Server-only host. The same edits apply whether you're targeting WebAssembly-only or Auto — the only differences are which builder methods you chain in Program.cs and which render mode App.razor returns. Both are called out per step.
1. Convert the .Client project to a WebAssembly app
Open .Client/MyApp.Client.csproj and change the SDK from Microsoft.NET.Sdk.Razor to Microsoft.NET.Sdk.BlazorWebAssembly. Add the WebAssembly framework and Power Portals Pro client packages:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" />
<PackageReference Include="PowerPortalsPro.Web.Client" />
<PackageReference Include="PowerPortalsPro.Web.Blazor.FluentUI" />
<PackageReference Include="PowerPortalsPro.Web.Common" />
</ItemGroup>
</Project>
Add the WebAssembly server-side hosting package to the server host's .csproj as well — it provides the middleware that serves the WASM bundle:
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" />
2. Register interactive WebAssembly components on the server
In the server Program.cs, replace the component registration with the matching builder calls. AddAuthenticationStateSerialization() serializes the authenticated user across the Server→WASM boundary so the cascading AuthenticationState is consistent on both runtimes:
// Replace
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
// With (Auto)
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents()
.AddAuthenticationStateSerialization();
// Or (WebAssembly)
builder.Services.AddRazorComponents()
.AddInteractiveWebAssemblyComponents()
.AddAuthenticationStateSerialization();
3. Map the WebAssembly render mode
Update app.MapRazorComponents<App>() to chain the matching render-mode endpoints. The AddAdditionalAssemblies call already points at the .Client assembly's _Imports in the templates, so it doesn't change here:
// Auto
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(MyApp.Client._Imports).Assembly);
// WebAssembly
app.MapRazorComponents<App>()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(MyApp.Client._Imports).Assembly);
4. Update App.razor's PageRenderMode
In App.razor's PageRenderMode getter, return the new render mode. Pin /Account/* to InteractiveWebAssemblyRenderMode(prerender: false) first, then return the default for everything else:
// In App.razor's PageRenderMode getter
if (HttpContext.Request.Path.StartsWithSegments("/Account"))
return new InteractiveWebAssemblyRenderMode(prerender: false);
// Auto
return new InteractiveAutoRenderMode();
// WebAssembly
return new InteractiveWebAssemblyRenderMode();
The Account-route pin is required because the framework's IAuthService is only registered in the WASM client's DI graph. Without the pin, Auto's server-side prerender fails to resolve [Inject] IAuthService on a cold session.
5. Add .Client/Program.cs
Create a Program.cs in the .Client project. This sets up the WebAssembly host, registers the HttpClient with the cookie-forwarding handler (so authenticated calls from the WASM client to /api/* see the same auth cookie as the browser), and calls AddPowerPortalsProWebClient to register the WASM-side framework services. The UserPowerPortalsProWebClient call pre-fetches the cross-cutting localization strings at startup so the first paint doesn't flash key-as-fallback text:
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.FluentUI.AspNetCore.Components;
using PowerPortalsPro.Web.Blazor.FluentUI;
using PowerPortalsPro.Web.Client;
using PowerPortalsPro.Web.Client.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddFluentUIComponents();
builder.Services.AddSingleton<CookieCredentialsHandler>();
builder.Services.AddHttpClient("PowerPortalsPro", client =>
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<CookieCredentialsHandler>();
builder.Services.AddSingleton(sp =>
sp.GetRequiredService<IHttpClientFactory>().CreateClient("PowerPortalsPro"));
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthenticationStateDeserialization();
builder.Services.AddPowerPortalsProWebClient();
var app = builder.Build();
await app.UserPowerPortalsProWebClient(
LocalizationBaselines.Default.Concat(new[] { "app" }).ToArray());
await app.RunAsync();
6. Switch from form-post to JSON Auth endpoints
Server hosts use MapAdditionalIdentityEndpoints() for form-post Identity pages. WebAssembly and Auto hosts use MapAuthEndpoints<TUser>() instead — the .Client's IAuthService wrapper calls these JSON endpoints under /api/auth/*:
// Add to the server's Program.cs (after app.UsePowerPortalsProWebServer)
app.MapAuthEndpoints<PortalUser>();
// Optionally swap the form-post Identity endpoints for the JSON ones
// (delete the line below — it's only consumed by server-rendered Account pages)
// app.MapAdditionalIdentityEndpoints();
When the host runs both Server and WASM Account pages (uncommon), both endpoint registrations can coexist. The default templates pick one or the other based on the interactivity mode.
7. Move the Account pages to the .Client project
Power Portals Pro ships two parallel sets of Account pages, one for each render context:
- Delete the server-rendered pages under
Components/Account/Pages/and the helper classes (IdentityRedirectManager,IdentityUserAccessor,IdentityComponentsEndpointRouteBuilderExtensions,CookieLoginController) from the server project. - Add the WASM Account pages under
.Client/Pages/Account/. The fastest way is to scaffold a fresh project with--interactivity Autoand copy them across — they cover Login, Register, ForgotPassword, ResetPassword, ConfirmEmail, ExternalLogin, and the full Manage/* surface. - If you customized any of the server-rendered Account pages (custom validation, extra fields), port those customizations over to the WASM equivalents — they implement the same UX over
IAuthServiceinstead ofUserManager.
8. Optional — scoped exception handler for /api/*
When WebAssembly is in play, exceptions thrown from /api/* need to round-trip to the WASM client as RFC 9457 problem+json so the client-side PowerPortalsProService can rehydrate the original CLR type. The templates wire this with a scoped UseExceptionHandler for /api/* only — Developer Exception Page still handles server-rendered page errors elsewhere:
if (app.Environment.IsDevelopment())
{
app.UseWebAssemblyDebugging();
app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments("/api"),
branch => branch.UseExceptionHandler());
}
WebAssembly or Auto → Server
Reverse the steps above:
- Drop
AddInteractiveWebAssemblyComponents()andAddAuthenticationStateSerialization()from the server's component registration; keepAddInteractiveServerComponents()only. - Replace
.AddInteractiveWebAssemblyRenderMode()(and anyInteractiveServerRenderModechained alongside) with a single.AddInteractiveServerRenderMode()inMapRazorComponents<App>(). - In
App.razor'sPageRenderMode, drop the/Account/*pin and returnnew InteractiveServerRenderMode()for every interactive route. - Move the Account pages back to the server project under
Components/Account/Pages/using theUserManager-direct pattern (or scaffold a fresh Server project and copy them across). Re-addIdentityRedirectManager/IdentityUserAccessorand remove the.Client/Pages/Account/set. - Replace
app.MapAuthEndpoints<PortalUser>()withapp.MapAdditionalIdentityEndpoints(). - Change the
.Clientproject's SDK back toMicrosoft.NET.Sdk.Razor, drop the WebAssembly package references, and delete.Client/Program.csand.Client/wwwroot/.
Auto ↔ WebAssembly
The cheapest switch — the project layout, packages, and Account pages are identical between the two. Only three things change:
- In
App.razor'sPageRenderMode, swapnew InteractiveAutoRenderMode()fornew InteractiveWebAssemblyRenderMode()(or vice versa) on the default branch. The/Account/*pin stays the same in both modes. - In server
Program.cs, add or removeAddInteractiveServerComponents()inAddRazorComponents()— Auto needs it, WebAssembly-only doesn't. - In server
Program.cs, add or remove.AddInteractiveServerRenderMode()inMapRazorComponents<App>().
Verifying the Switch
After making the changes:
- Build the solution. Most wiring mistakes show up at compile time — missing render-mode methods, unresolved types, or stale Account-page references.
- Sign in and out. Auth is the most disruptive thing to switch — exercise the full Login → Manage Profile → Logout cycle to confirm the new endpoints are wired correctly.
- Click an interactive page. A button click on a grid or editor proves interactivity is reaching the right runtime. Server mode shows a SignalR connection in dev tools' WebSocket tab; WebAssembly shows the runtime files under
/_framework/in the network tab. - Check the WASM bundle on first load. For WebAssembly and Auto modes, the network tab on a fresh load (incognito or cleared cache) should show the runtime + framework + portal assemblies streaming in.
- Verify the Account/Manage pages. Profile changes, password changes, and external-login linking each exercise a different endpoint — confirm they all complete cleanly.
Note
Watch out for service-lifetime mismatches when moving services between the server and the WASM client. The WASM host runs as a single scope per session, but the framework's caching services are registered as singletons — if you register your own service as
Transienton the WASM side, per-instance state silently resets every resolve. UseSingletonon the WASM side for any service that holds mutable state.
