Start Debugging

Миграция приложения Blazor Server на Blazor United (Blazor Web App) в .NET 11

Пошаговый чек-лист для перевода отдельного приложения Blazor Server на унифицированный шаблон Blazor Web App в .NET 11 с сохранением каждой страницы в режиме InteractiveServer без изменения поведения.

Если у вас есть отдельное приложение Blazor Server (dotnet new blazorserver) и вы хотите перевести его на унифицированный шаблон, который во время цикла предварительной версии .NET 8 имел прозвище “Blazor United” и был выпущен как Blazor Web App, миграция в основном механическая и обычно занимает полдня для небольшого приложения, от одного до трёх дней для крупного. Ничего в коде ваших компонентов менять не нужно. Меняется хост: _Host.cshtml исчезает, маршрутизация переезжает в компонент Routes, в Program.cs MapBlazorHub меняется на MapRazorComponents, и вы явно объявляете render mode. Держите каждую страницу на @rendermode InteractiveServer, и поведение останется идентичным Blazor Server эпохи .NET 7. Единственное, что кусается, это двойное выполнение пререндеринга. Это руководство ориентировано на .NET 11 (на момент написания предварительная версия, GA запланирован на ноябрь 2026) и набор пакетов Microsoft.AspNetCore.Components 11.0.x.

Зачем уходить с отдельного шаблона Server

Отдельный шаблон blazorserver по-прежнему работает в .NET 11, так что это не вынужденная миграция. Делайте её, когда одно из этого станет реальным результатом, которого вы хотите, не раньше:

Если ничего из этого не применимо, изменение <TargetFramework>net11.0</TargetFramework> в вашем существующем отдельном приложении Server является допустимой альтернативой и не является этой миграцией.

Что ломается

ОбластьИзменениеСерьёзность
Хост-страницаPages/_Host.cshtml и _Layout.cshtml заменяются корневым хост-компонентом App.razorhigh
Маршрутизация<Router> переезжает из App.razor в новый Routes.razorhigh
Запуск в Program.csAddServerSideBlazor() + MapBlazorHub() + MapFallbackToPage("/_Host") заменяются на AddRazorComponents().AddInteractiveServerComponents() + MapRazorComponents<App>().AddInteractiveServerRenderMode()high
Render modeИнтерактивность включается по выбору на компонент или глобально; нет неявного “всё приложение интерактивно”high
ПререндерингOnInitialized/OnInitializedAsync по умолчанию выполняются дважды (проход пререндера + интерактивный проход)medium
Клиентский скрипт_framework/blazor.server.js становится _framework/blazor.web.jsmedium
Подключение аутентификацииКомпонент CascadingAuthenticationState заменяется на AddCascadingAuthenticationState() в DImedium
Доступ к HttpContextHttpContext доступен только во время статического SSR, а не внутри интерактивного компонентаmedium
Семантика App.razorApp.razor больше не маршрутизатор; это оболочка HTML-документаlow

Чек-лист перед стартом

Шаги миграции

1. Поднимите target framework и пакеты

Отредактируйте .csproj. SDK остаётся Microsoft.NET.Sdk.Web.

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

Поднимите любые ссылки на пакеты Microsoft.AspNetCore.Components.* до 11.0.x. Удалите Microsoft.AspNetCore.Components.Web, если он указан явно; он входит во framework reference для проектов веб-SDK.

Проверка: dotnet restore завершается, а dotnet build падает только с ошибками о хост-странице и Program.cs (что ожидаемо на этом этапе), а не с ошибками разрешения пакетов.

2. Создайте хост-компонент App.razor

В отдельном шаблоне Server HTML-документ живёт в Pages/_Host.cshtml и Pages/_Layout.cshtml. Перенесите эту разметку в новый корневой App.razor (сначала удалите старый маршрутизатор App.razor или переименуйте его). Tag helper <component>, который запускал корень, становится <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>

Два изменения, которые легко пропустить: скрипт это blazor.web.js (не blazor.server.js), а <HeadOutlet /> вместе с <Routes /> это компоненты, поэтому они принимают тот render mode, который вы им назначите на шаге 6.

Проверка: файл компилируется как компонент Razor (без директивы @page, без @model).

3. Перенесите маршрутизацию в Routes.razor

Создайте Routes.razor в корне проекта и вставьте блок <Router>, который раньше жил в старом 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>

Проверка: dotnet build больше не жалуется на отсутствующий тип Routes.

4. Включите сокращения render mode в _Imports.razor

Добавьте эту строку в _Imports.razor, чтобы вы могли писать InteractiveServer вместо полного указания:

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

Проверка: @rendermode InteractiveServer в компоненте разрешается без ошибки using.

5. Перепишите Program.cs

Замените регистрацию Blazor Server и эндпойнты на эквиваленты 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();

Удалите AddServerSideBlazor(), app.MapBlazorHub() и app.MapFallbackToPage("/_Host"). Вызов app.UseAntiforgery() новый и обязательный; шаблон Web App включает middleware antiforgery по умолчанию, и отправки форм без него падают.

Проверка: dotnet build завершается с нулём ошибок.

6. Сделайте приложение интерактивным (глобальный render mode)

Миграция с наименьшим риском делает всё приложение InteractiveServer, точно воспроизводя старое поведение. Установите render mode на <Routes /> и <HeadOutlet /> внутри App.razor:

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

Проверка: запустите dotnet run, откройте приложение и подтвердите, что интерактивные компоненты (кнопки, @onclick, отправки EditForm) работают и что браузер держит открытый WebSocket к /_blazor. Когда это заработает, позже вы сможете опускать отдельные страницы до @rendermode StaticServer или поднимать острова до WebAssembly, но это работа после миграции.

7. Обработайте двойное выполнение пререндеринга

Это единственное изменение поведения, которое удивляет людей. С интерактивным render mode Blazor сначала пререндерит статический HTML, затем рендерит снова поверх живого circuit, поэтому OnInitialized и OnInitializedAsync выполняются дважды. У старого значения по умолчанию отдельного Server (render-mode="ServerPrerendered") было то же свойство, но многие приложения использовали render-mode="Server" и никогда этого не видели.

У вас есть три варианта. Самый чистый в .NET 11 это декларативный атрибут [PersistentState] (добавлен в .NET 10): получить данные один раз во время пререндера, сериализовать в HTML, восстановить в интерактивном проходе.

// .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();
    }
}

Если вы вообще не хотите получать данные во время пререндера, отключите пререндеринг для этой границы:

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

Проверка: поставьте точку останова или строку журнала в каждый OnInitializedAsync с побочными эффектами и подтвердите, что он выполняется один раз на реальную навигацию, а не дважды.

8. Переподключите аутентификацию и авторизацию

Если ваше приложение использовало <CascadingAuthenticationState>, обёрнутый вокруг маршрутизатора, удалите этот компонент и зарегистрируйте его в DI вместо этого, затем замените RouteView на AuthorizeRouteView в Routes.razor.

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

Проверка: зайдите на страницу [Authorize] без входа в систему и подтвердите, что вас перенаправляет, затем войдите и подтвердите доступ.

9. Удалите мёртвые хост-файлы

Удалите Pages/_Host.cshtml, Pages/_Layout.cshtml и любой вызов app.MapRazorPages(), который существовал только для обслуживания _Host. Уберите AddRazorPages() из Program.cs, если больше ничто не использует Razor Pages.

Проверка: dotnet build чист, и приложение по-прежнему обслуживает каждый маршрут.

Проверка: дымовой тест после миграции

Прогоните всё это перед слиянием:

План отката

Эта миграция обратима только если вы сохранили ветку с шага перед стартом. После того как вы удалите _Host.cshtml и перепишете Program.cs, нет переключателя на месте обратно к модели отдельного Server. Откатывайтесь через checkout коммита до миграции, а не редактированием вперёд. Поскольку изменение структурное, а не миграция данных, в вашей базе данных или хранилище ничего отменять не нужно. Создайте ветку, выполните работу, проверьте по дымовому тесту и сливайте только когда всё зелёное.

Грабли, на которые мы наступили

Пункт назначения здесь почти всегда “каждая страница на InteractiveServer, пререндеринг обработан”. Это миграция, которая не меняет ничего из того, что видит пользователь. Добавление страниц Static Server, островов WebAssembly и компонентов Auto это награда, которую вы собираете потом, по одному компоненту за раз, без дальнейшей структурной встряски.

Связанное

Источники

Comments

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

< Назад