Start Debugging

Migrar una app de Blazor Server a Blazor United (Blazor Web App) en .NET 11

Una lista de pasos para mover una app de Blazor Server independiente a la plantilla unificada Blazor Web App en .NET 11, manteniendo cada página en InteractiveServer sin cambios de comportamiento.

Si tienes una app de Blazor Server independiente (dotnet new blazorserver) y quieres moverla a la plantilla unificada que se apodó “Blazor United” durante el ciclo de versión preliminar de .NET 8 y que se publicó como Blazor Web App, la migración es en su mayoría mecánica y suele tomar medio día para una app pequeña, de uno a tres días para una grande. Nada del código de tus componentes tiene que cambiar. Lo que cambia es el host: _Host.cshtml desaparece, el enrutamiento se mueve a un componente Routes, Program.cs cambia MapBlazorHub por MapRazorComponents, y declaras un render mode de forma explícita. Mantén cada página en @rendermode InteractiveServer y el comportamiento será idéntico al de Blazor Server de la era .NET 7. Lo único que muerde es la doble ejecución del prerenderizado. Esta guía apunta a .NET 11 (versión preliminar al momento de escribir, con GA programada para noviembre de 2026) y al conjunto de paquetes Microsoft.AspNetCore.Components 11.0.x.

Por qué dejar la plantilla Server independiente

La plantilla blazorserver independiente sigue funcionando en .NET 11, así que esta no es una migración forzada. Hazla cuando uno de estos sea un resultado real que deseas, no antes:

Si ninguno de esos aplica, un cambio <TargetFramework>net11.0</TargetFramework> en tu app de Server independiente existente es una alternativa válida y no es esta migración.

Qué se rompe

ÁreaCambioSeveridad
Página hostPages/_Host.cshtml y _Layout.cshtml se reemplazan por un componente host raíz App.razorhigh
Enrutamiento<Router> se mueve fuera de App.razor a un nuevo Routes.razorhigh
Inicio en Program.csAddServerSideBlazor() + MapBlazorHub() + MapFallbackToPage("/_Host") reemplazados por AddRazorComponents().AddInteractiveServerComponents() + MapRazorComponents<App>().AddInteractiveServerRenderMode()high
Render modeLa interactividad es opt-in por componente o global; no hay un “toda la app es interactiva” implícitohigh
PrerenderizadoOnInitialized/OnInitializedAsync se ejecutan dos veces (pasada de prerender + pasada interactiva) de forma predeterminadamedium
Script de cliente_framework/blazor.server.js pasa a ser _framework/blazor.web.jsmedium
Cableado de autenticaciónEl componente CascadingAuthenticationState se reemplaza por AddCascadingAuthenticationState() en DImedium
Acceso a HttpContextHttpContext solo está disponible durante el SSR estático, no dentro de un componente interactivomedium
Semántica de App.razorApp.razor ya no es el enrutador; es el shell del documento HTMLlow

Lista previa al vuelo

Pasos de migración

1. Sube el target framework y los paquetes

Edita el .csproj. El SDK sigue siendo Microsoft.NET.Sdk.Web.

<!-- .NET 11 -->
<PropertyGroup>
  <TargetFramework>net11.0</TargetFramework>
  <Nullable>enable</Nullable>
  <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

Sube cualquier referencia de paquete Microsoft.AspNetCore.Components.* a 11.0.x. Elimina Microsoft.AspNetCore.Components.Web si se referencia explícitamente; es parte de la referencia de framework para los proyectos del SDK web.

Verifica: dotnet restore se completa y dotnet build falla solo con errores sobre la página host y Program.cs (esperado en este punto), no con errores de resolución de paquetes.

2. Crea el componente host App.razor

En la plantilla Server independiente, el documento HTML vive en Pages/_Host.cshtml y Pages/_Layout.cshtml. Mueve ese marcado a un nuevo App.razor raíz (elimina primero el viejo enrutador App.razor, o renómbralo). El tag helper <component> que arrancaba la raíz pasa a ser <Routes />.

@* .NET 11 - App.razor is now the HTML document shell, not the router *@
<!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="app.css" />
    <link rel="stylesheet" href="YourApp.styles.css" />
    <HeadOutlet />
</head>
<body>
    <Routes />
    <script src="_framework/blazor.web.js"></script>
</body>
</html>

Dos cambios fáciles de pasar por alto: el script es blazor.web.js (no blazor.server.js), y <HeadOutlet /> junto con <Routes /> son componentes, así que adoptan el render mode que les asignes en el paso 6.

Verifica: el archivo compila como un componente Razor (sin directiva @page, sin @model).

3. Mueve el enrutamiento a Routes.razor

Crea Routes.razor en la raíz del proyecto y pega el bloque <Router> que solía vivir en el viejo App.razor.

@* .NET 11 - Routes.razor holds the router that used to be in App.razor *@
<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>
        <PageTitle>Not found</PageTitle>
        <LayoutView Layout="@typeof(Layout.MainLayout)">
            <p role="alert">Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

Verifica: un dotnet build ya no se queja de un tipo Routes faltante.

4. Habilita las abreviaturas de render mode en _Imports.razor

Agrega esta línea a _Imports.razor para que puedas escribir InteractiveServer en lugar de calificarlo por completo:

@* .NET 11 *@
@using static Microsoft.AspNetCore.Components.Web.RenderMode

Verifica: @rendermode InteractiveServer en un componente se resuelve sin un error de using.

5. Reescribe Program.cs

Cambia el registro de Blazor Server y los endpoints por los equivalentes de Razor Components.

// .NET 11, C# 14 - Blazor Web App startup
var builder = WebApplication.CreateBuilder(args);

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

// keep your existing app services here (DbContext, HttpClient, etc.)

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();

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

app.Run();

Elimina AddServerSideBlazor(), app.MapBlazorHub() y app.MapFallbackToPage("/_Host"). La llamada app.UseAntiforgery() es nueva y obligatoria; la plantilla Web App habilita el middleware de antiforgery de forma predeterminada y los envíos de formularios fallan sin ella.

Verifica: dotnet build se completa con cero errores.

6. Vuelve la app interactiva (render mode global)

La migración de menor riesgo vuelve toda la app InteractiveServer, reproduciendo el viejo comportamiento exactamente. Establece el render mode en <Routes /> y <HeadOutlet /> dentro de App.razor:

@* .NET 11 - global InteractiveServer, matches standalone Blazor Server behaviour *@
<HeadOutlet @rendermode="InteractiveServer" />
...
<Routes @rendermode="InteractiveServer" />

Verifica: ejecuta dotnet run, abre la app, y confirma que los componentes interactivos (botones, @onclick, envíos de EditForm) funcionan y que el navegador mantiene un WebSocket abierto hacia /_blazor. Una vez que esto funcione, más adelante puedes bajar páginas individuales a @rendermode StaticServer o escalar islas a WebAssembly, pero eso es trabajo posterior a la migración.

7. Maneja la doble ejecución del prerenderizado

Este es el único cambio de comportamiento que sorprende a la gente. Con un render mode interactivo, Blazor prerenderiza primero el HTML estático, luego renderiza de nuevo sobre el circuito en vivo, así que OnInitialized y OnInitializedAsync se ejecutan dos veces. El viejo predeterminado de Server independiente (render-mode="ServerPrerendered") tenía la misma propiedad, pero muchas apps usaban render-mode="Server" y nunca lo vieron.

Tienes tres opciones. La más limpia en .NET 11 es el atributo declarativo [PersistentState] (agregado en .NET 10): hace fetch una vez durante el prerender, lo serializa en el HTML, lo restaura en la pasada interactiva.

// .NET 11 - fetch once, survive the prerender-to-interactive handoff
public partial class Dashboard : ComponentBase
{
    [PersistentState]
    public List<Order>? Orders { get; set; }

    [Inject] public required IOrderService OrderService { get; init; }

    protected override async Task OnInitializedAsync()
    {
        // Orders is non-null on the interactive pass: state was restored,
        // so the service is not hit a second time.
        Orders ??= await OrderService.GetRecentAsync();
    }
}

Si no quieres hacer fetch durante el prerender en absoluto, deshabilita el prerenderizado para ese límite:

@* .NET 11 - skip the prerender pass entirely for this component tree *@
<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />

Verifica: pon un breakpoint o una línea de registro en cada OnInitializedAsync con efectos secundarios y confirma que se ejecuta una vez por navegación real, no dos.

8. Recablea la autenticación y la autorización

Si tu app usaba <CascadingAuthenticationState> envolviendo el enrutador, elimina ese componente y regístralo en DI en su lugar, luego cambia RouteView por AuthorizeRouteView en Routes.razor.

// .NET 11 - Program.cs
builder.Services.AddCascadingAuthenticationState();
@* .NET 11 - Routes.razor, authorized routing *@
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />

Verifica: visita una página [Authorize] sin haber iniciado sesión y confirma que se te redirige, luego inicia sesión y confirma el acceso.

9. Elimina los archivos host muertos

Quita Pages/_Host.cshtml, Pages/_Layout.cshtml y cualquier llamada app.MapRazorPages() que existiera solo para servir _Host. Elimina AddRazorPages() de Program.cs si nada más usa Razor Pages.

Verifica: dotnet build está limpio y la app sigue sirviendo cada ruta.

Verificación: la prueba de humo posterior a la migración

Ejecuta todas estas antes de hacer merge:

Plan de reversión

Esta migración es reversible solo si conservaste la rama del paso previo al vuelo. Una vez que eliminas _Host.cshtml y reescribes Program.cs, no hay un interruptor in situ para volver al modelo Server independiente. Revierte haciendo checkout del commit previo a la migración, no editando hacia adelante. Como el cambio es estructural y no una migración de datos, no hay nada que deshacer en tu base de datos o almacenamiento. Crea una rama, haz el trabajo, verifica contra la prueba de humo, y haz merge solo cuando esté en verde.

Tropiezos que tuvimos

El destino aquí es casi siempre “cada página en InteractiveServer, prerenderizado manejado”. Esa es la migración que no cambia nada que el usuario pueda ver. Agregar páginas Static Server, islas WebAssembly y componentes Auto es la recompensa que cobras después, un componente a la vez, sin más agitación estructural.

Relacionado

Fuentes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Volver