
Пошаговый план миграции ASP.NET Web Forms на ASP.NET Core и Blazor
Переписать Web Forms-приложение за один заход почти никому не удаётся. Рабочий путь другой: новое приложение встаёт перед старым и забирает страницы по одной, пока старое не опустеет.
Почему не «переписать с нуля»
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# не нужно.
<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>Шаг 3. Новое приложение перед старым
Создаётся пустое приложение ASP.NET Core, которое принимает все запросы. Маршруты, уже перенесённые в новый код, оно обслуживает само, а остальные проксирует в старое приложение через YARP (Yet Another Reverse Proxy). Пользователь ходит на один домен и не замечает границы.
Для общих вещей Microsoft выпустила пакет Microsoft.AspNetCore.SystemWebAdapters. Он позволяет обоим приложениям разделять аутентификацию и сессию: новое приложение запрашивает их у старого по защищённому каналу. Это снимает главный страх миграции: пользователю не придётся логиниться дважды, а корзина не потеряется на переходе между страницами.
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 страниц составляет от полугода до полутора лет. Зато всё это время система работает, а доработки делаются уже в новом коде.
Как это делают сегодня
Вопросы и ответы
Можно ли писать Blazor-компоненты на VB.NET?
Нет, Razor поддерживает только C#. Но всю бизнес-логику можно оставить в библиотеках на VB.NET под .NET 8 и вызывать из компонентов.
Что делать с сессией на время миграции?
Пакет SystemWebAdapters позволяет новому приложению читать и писать сессию старого. Объекты в сессии должны сериализоваться, поэтому заранее проверьте, что там не лежат DataSet или соединения.
Обязательно ли переходить именно на Blazor?
Нет. Для простых страниц хватит Razor Pages или MVC, а если фронтенд уже на React или Vue, ASP.NET Core может быть только API. Blazor выбирают, когда команда сильна в .NET и хочется сохранить серверную модель.