Пошаговый план миграции ASP.NET Web Forms на ASP.NET Core и Blazor

РубрикаМиграцияУровеньОпытный Чтение10 минОпубликовано ТегиASP.NETWeb FormsBlazorМиграция

Переписать Web Forms-приложение за один заход почти никому не удаётся. Рабочий путь другой: новое приложение встаёт перед старым и забирает страницы по одной, пока старое не опустеет.

Главное. Web Forms мигрируют частями: логика выносится в общие библиотеки, новое ASP.NET Core приложение встаёт перед старым через YARP, а страницы переезжают по одной с общей сессией и авторизацией.

Почему не «переписать с нуля»

Web Forms работает только на .NET Framework 4.x и не будет перенесён в современный .NET. Платформа поддерживается вместе с Windows, но новых возможностей не получает, а найти разработчиков, готовых развивать ViewState и UpdatePanel, с каждым годом сложнее. Поэтому вопрос не в том, мигрировать ли, а в том, как сделать это без остановки бизнеса.

Проект «новая версия с нуля» обычно выглядит так: старое приложение заморожено, новое догоняет его год, а за это время бизнес успевает попросить десяток доработок в старом. В итоге две системы, ни одна не готова. Инкрементальная миграция по схеме Strangler Fig устроена иначе: обе версии работают одновременно, пользователь видит один сайт, а маршруты переезжают постепенно.

Шаг 1. Инвентаризация

Прежде чем писать код, нужно понять масштаб. Хватит таблицы на несколько десятков строк, но собранной честно:

  • список всех .aspx, .ascx, .ashx и .asmx с частотой использования из логов IIS: часто окажется, что треть страниц никто не открывал годами;
  • зависимости от System.Web в бизнес-логике: HttpContext.Current, Session, Cache, Server.MapPath внутри классов, которые должны быть чистыми;
  • сторонние контролы (Telerik, DevExpress, Infragistics) и их аналоги для Blazor;
  • механизм аутентификации: Forms Authentication, Windows, членство ASP.NET Membership;
  • состояние: что хранится в Session и ViewState и насколько это критично;
  • язык кода: для VB.NET важно, что компоненты Blazor и Razor Pages пишутся на C#, а библиотеки на VB.NET можно подключать без изменений.

Шаг 2. Освобождаем бизнес-логику от System.Web

Самая полезная работа делается ещё внутри старого проекта. Вся логика, которая не касается интерфейса (расчёты, доступ к данным, интеграции), выносится в отдельные библиотеки классов. Обращения к HttpContext.Current заменяются параметрами или интерфейсами, которые передаются извне.

Целевой формат таких библиотек: .NET Standard 2.0 или мультитаргетинг net48 плюс net8.0. Тогда одна и та же сборка подключается и к старому Web Forms-сайту, и к новому приложению на ASP.NET Core. VB.NET это полностью поддерживает, переписывать логику на C# не нужно.

Billing.Core.vbprojXML
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <!-- одна библиотека для старого и нового приложения -->
    <TargetFrameworks>net48;net8.0</TargetFrameworks>
    <RootNamespace>Billing.Core</RootNamespace>
    <OptionStrict>On</OptionStrict>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
  </ItemGroup>
</Project>
Если в библиотеке остался вызов HttpContext.Current, мультитаргетинг не соберётся под net8.0. Это удобный способ найти все скрытые зависимости от System.Web: компилятор покажет их списком.

Шаг 3. Новое приложение перед старым

Создаётся пустое приложение ASP.NET Core, которое принимает все запросы. Маршруты, уже перенесённые в новый код, оно обслуживает само, а остальные проксирует в старое приложение через YARP (Yet Another Reverse Proxy). Пользователь ходит на один домен и не замечает границы.

Для общих вещей Microsoft выпустила пакет Microsoft.AspNetCore.SystemWebAdapters. Он позволяет обоим приложениям разделять аутентификацию и сессию: новое приложение запрашивает их у старого по защищённому каналу. Это снимает главный страх миграции: пользователю не придётся логиниться дважды, а корзина не потеряется на переходе между страницами.

Program.csC#
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSystemWebAdapters()
    .AddRemoteAppClient(o =>
    {
        // адрес старого Web Forms-приложения и общий ключ
        o.RemoteAppUrl = new(builder.Configuration["LegacyUrl"]!);
        o.ApiKey = builder.Configuration["RemoteAppApiKey"]!;
    })
    .AddAuthenticationClient(true)   // общая авторизация
    .AddSessionClient();             // общая сессия

builder.Services.AddHttpForwarder(); // YARP
builder.Services.AddRazorComponents().AddInteractiveServerComponents();

var app = builder.Build();
app.UseAntiforgery();
app.UseSystemWebAdapters();
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
// всё, что не нашло маршрута, уходит в старое приложение
app.MapForwarder("/{**catch-all}", app.Configuration["LegacyUrl"]!)
   .Add(e => ((RouteEndpointBuilder)e).Order = int.MaxValue);
app.Run();

Шаг 4. Переносим страницы

Порядок переноса выбирают по соотношению пользы и риска. Хорошо начинать со страниц, которые часто используются, но мало связаны с остальными: справочники, отчёты, личный кабинет. Сложные мастера с многошаговым ViewState оставляют на потом.

Для каждой страницы решается, во что её превращать. Статичный контент и простые формы хорошо ложатся на Razor Pages или Blazor со статическим серверным рендерингом. Интерактивные экраны с таблицами и фильтрами переносят в Blazor с режимом Interactive Server, он ближе всего к модели событий Web Forms. Модель «серверного события на кнопку» сохраняется, только без постбэка всей страницы.

  • MasterPage превращается в layout-компонент, UserControl в обычный Razor-компонент;
  • события Page_Load и Button_Click становятся OnInitializedAsync и методами-обработчиками;
  • ViewState заменяется полями компонента, а данные между страницами передаются явно;
  • валидаторы заменяются EditForm и DataAnnotations.

Шаг 5. Выключаем старое

Когда в YARP почти не остаётся проксируемых маршрутов, полезно посмотреть в логи старого приложения. Если туда неделями не приходит запросов, кроме служебных, его можно переводить в режим только для чтения и затем выключать. Финальные шаги: перенести аутентификацию в новое приложение, убрать SystemWebAdapters, удалить прокси.

Реалистичный срок для среднего корпоративного приложения на 100-200 страниц составляет от полугода до полутора лет. Зато всё это время система работает, а доработки делаются уже в новом коде.

Как это делают сегодня

Для подготовки проекта Microsoft предлагает инструменты модернизации в Visual Studio, которые анализируют зависимости и помогают с переносом проектов на SDK-формат. Для сторонних контролов у основных вендоров есть Blazor-линейки, а свободные библиотеки вроде MudBlazor и FluentUI Blazor закрывают типовые формы и таблицы. Если часть команды пишет только на VB.NET, сохраняйте на нём библиотеки логики, а интерфейс делайте на C#.

Вопросы и ответы

Можно ли писать Blazor-компоненты на VB.NET?

Нет, Razor поддерживает только C#. Но всю бизнес-логику можно оставить в библиотеках на VB.NET под .NET 8 и вызывать из компонентов.

Что делать с сессией на время миграции?

Пакет SystemWebAdapters позволяет новому приложению читать и писать сессию старого. Объекты в сессии должны сериализоваться, поэтому заранее проверьте, что там не лежат DataSet или соединения.

Обязательно ли переходить именно на Blazor?

Нет. Для простых страниц хватит Razor Pages или MVC, а если фронтенд уже на React или Vue, ASP.NET Core может быть только API. Blazor выбирают, когда команда сильна в .NET и хочется сохранить серверную модель.

Читайте также