Чек-лист перед миграцией с .NET Framework на .NET
· Обновлено: · Го Комура · .NET, .NET Framework, C#, Модернизация, Разработка Windows, Миграция
История изменений (3 обновлений, последнее 31 Aug 2026)
Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.
- Русский текст исправлен, чтобы соответствовать японскому оригиналу.
- Восстановлены разделы 9–15 полного перевода. Утверждения статьи не менялись.
- Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
- Первая публикация
Цитирование статьи(DOI: 10.5281/zenodo.21619691)
Статья заархивирована на Zenodo. Ниже приведены DOI, который всегда ведёт к последней версии, и DOI, закреплённый за версией, которую вы читаете.
Го Комура (2026). Чек-лист перед миграцией с .NET Framework на .NET. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21619691 https://comcomponent.com/ru/blog/2026/03/15/003-dotnet-framework-to-dotnet-premigration-checklist/
- DOI (последняя версия)
- 10.5281/zenodo.21619691
- DOI (эта версия)
- 10.5281/zenodo.21619692
Скачать Excel-чек-лист с листами на японском и английском
Файл состоит из двух листов, Checklist-ja и Checklist-en. Чек-лист до старта из 13 глав сведён в 7 категорий и 34 пункта: политика / подготовка текущей стороны .NET Framework / тип приложения и выбор технологий / неподдерживаемые технологии и API, которые стоит проверить / зависимости, специфичные для Windows / общие библиотеки и доступ к данным / эксплуатация и build. Столбцы Status и Notes пустые, поэтому таблицу можно сразу использовать как инвентаризацию по проекту. Если просмотреть её до чтения статьи, ход до 13-й главы будет понятнее.
Сменить TargetFramework в .csproj на net10.0, обновить несколько пакетов NuGet — и если сборка прошла, всё готово.
…если бы миграция сводилась к этому, жить было бы спокойнее. На практике чаще получается иначе.
В реальных проектах на .NET Framework обычно дремлет немало допущений, о которых в обычной работе не думают: System.Web, WCF, Web Forms, старый packages.config, web.config.install.xdt, нативные DLL, COM / ActiveX, сторонние контролы, которые живут только в дизайнере, неявная ставка на x86, ResX, завязанный на дизайнер, старые сериализаторы.
Поэтому в миграции с .NET Framework на .NET по-настоящему важно не начать писать код, а сначала провести инвентаризацию. Если до старта разложить вопросы, миграция перестаёт быть «большой ставкой» и становится «последовательной работой по пунктам».
flowchart TB
accTitle: Инвентаризация меняет характер миграции
accDescr: Если до реализации провести инвентаризацию и разложить вопросы, миграция становится не большой ставкой, а последовательной работой по пунктам.
hid1["Скрытые неявные допущения"] --> inv1["Инвентаризация до старта раскладывает вопросы"]
inv1 --> tsk1["Последовательная работа по пунктам"]
hid1 -.->|"без инвентаризации"| bet1["Миграция становится большой ставкой"]
Рис. 1: В зависимости от того, разложены ли вопросы до старта, миграция оказывается ставкой или обычной работой.
В этой статье собрано, что проверить перед миграцией существующего бизнес-приложения на .NET Framework 4.x на текущий .NET. Основные объекты примерно такие:
- библиотеки классов
- консольные приложения
- службы Windows
- WinForms / WPF
- ASP.NET Framework (MVC / Web API / Web Forms)
- приложения на WCF
- приложения на EF6
Текст написан по состоянию на 2026-03-15. Сроки поддержки и рекомендации официальных инструментов меняются, поэтому если вы читаете статью заметно позже, сверьтесь и с официальными источниками.
1. Сначала вывод
Сначала только выводы, от которых трудно уйти.
- До миграции сначала наведите порядок на стороне .NET Framework. Официальное руководство Microsoft тоже рекомендует перед портированием поднять цель до .NET Framework 4.7.2 или новее, заранее перейти на
PackageReference, на SDK-стиль и обновить зависимости. - Сложность задаёт скорее модель приложения, чем объём кода. Библиотеки классов и консоль относительно лёгкие; ASP.NET Framework, Web Forms, WCF-сервер и WF (Workflow Foundation) обычно тяжелее.
- WinForms / WPF можно перенести на .NET, но они остаются Windows-only. Если это перепутать, получится классическая мина: перенос сделан, а в Linux-контейнер приложение так и не встаёт.
- Переход ASP.NET Framework → ASP.NET Core по сути миграция архитектуры. Небольшое приложение иногда можно перенести одним махом, но для крупного продакшена безопаснее сразу рассчитывать на поэтапную миграцию.
- WCF и EF6 иногда можно отделить от миграции runtime. Для WCF-клиента есть поддерживаемые пакеты под .NET, а EF6 после перехода на современный .NET можно отдельно мигрировать на EF Core.
- С другой стороны, создание AppDomain, .NET Remoting, CAS (Code Access Security), COM+, Workflow Foundation и зависимость от BinaryFormatter — красные флаги. Если не найти их заранее, трудозатраты взорвутся позже.
packages.config/install.ps1/XDT(XML Document Transform, механизм преобразованияweb.configи подобных файлов) / содержимоеcontent/ нативные DLL / COM / ActiveX / ставка на x86` часто проходят сборку и падают уже в runtime или в дизайнере, поэтому их нужно выявить до старта.- На март 2026 LTS — .NET 10. Для новой миграции естественно ориентироваться на текущую LTS.
- Миграция без тестов, замеров и отката опасна. Это не столько работа по реализации, сколько работа, в которой скрытые допущения выявляют по одному.
Карта знаний этой статьи
Миграция с .NET Framework на .NET требует инвентаризации до начала реализации; преобразование в PackageReference — преемник packages.config — и в проект в стиле SDK следует закончить до самой миграции. Создание нового AppDomain, .NET Remoting, COM+ и Workflow Foundation с .NET не совместимы; BinaryFormatter тоже может быть несовместим — в зависимости от ситуации. У WCF клиент и сервер различаются: на стороне сервера нужна перепроектировка на CoreWCF или gRPC. ASP.NET Core — преемник ASP.NET Framework, но Web Forms — это не та же модель приложения (app model); переход с EF6 на EF Core безопаснее отделить от миграции runtime и отложить. WinForms/WPF перенести можно, но они остаются Windows-only; зависимость от Windows-only API можно снизить пакетом Windows Compatibility Pack.
flowchart LR
accTitle: Карта знаний: контрольный список перед миграцией с .NET Framework на .NET
accDescr: Схема связей между форматом зависимостей, несовместимыми технологиями, обращением с WCF и EF6 и зависимостью от Windows-only API — тем, что нужно разобрать до миграции с .NET Framework на .NET
framework_to_dotnet_migration["миграция с .NET Framework на .NET"]
dotnet_framework[".NET Framework"]
dotnet[".NET (начиная с Core)"]
windows_compat_pack["Windows Compatibility Pack"]
package_reference["PackageReference"]
packages_config["packages.config"]
sdk_style_project["проект SDK-style"]
dotnet_upgrade_assistant[".NET Upgrade Assistant"]
github_copilot_modernization["модернизация с GitHub Copilot"]
net_standard_2_0[".NET Standard 2.0"]
appdomain["создание AppDomain"]
dotnet_remoting[".NET Remoting"]
com_plus["COM+ (System.EnterpriseServices)"]
workflow_foundation["Workflow Foundation"]
binaryformatter["BinaryFormatter"]
wcf_server["сервер WCF (хост службы WCF)"]
corewcf["CoreWCF"]
grpc["gRPC"]
ef_core["EF Core"]
ef6["EF6 (Entity Framework 6)"]
asp_net_core["ASP.NET Core"]
asp_net_framework["ASP.NET Framework (MVC/Web API)"]
web_forms["ASP.NET Web Forms"]
system_web["System.Web / HttpContext.Current"]
wpf["WPF"]
windows_forms["Windows Forms"]
windows_only_api_dependency["зависимость от Windows-only API"]
wcf_client["клиент WCF"]
framework_to_dotnet_migration -->|"требует"| dotnet_framework
dotnet -->|"преемник"| dotnet_framework
framework_to_dotnet_migration -.->|"использует"| windows_compat_pack
package_reference -->|"преемник"| packages_config
sdk_style_project -->|"должен предшествовать"| framework_to_dotnet_migration
package_reference -->|"должен предшествовать"| framework_to_dotnet_migration
dotnet_upgrade_assistant -->|"не рекомендуется"| framework_to_dotnet_migration
github_copilot_modernization -->|"рекомендуется для"| framework_to_dotnet_migration
framework_to_dotnet_migration -.->|"использует"| net_standard_2_0
appdomain -->|"несовместимо с"| dotnet
dotnet_remoting -->|"несовместимо с"| dotnet
com_plus -->|"несовместимо с"| dotnet
workflow_foundation -->|"несовместимо с"| dotnet
binaryformatter -.->|"несовместимо с"| dotnet
wcf_server -.->|"требует"| corewcf
wcf_server -.->|"требует"| grpc
corewcf -->|"реализует"| wcf_server
ef_core -->|"преемник"| ef6
framework_to_dotnet_migration -->|"должен предшествовать"| ef_core
asp_net_core -->|"преемник"| asp_net_framework
web_forms -.->|"несовместимо с"| asp_net_core
system_web -->|"несовместимо с"| dotnet
wpf -.->|"использует"| binaryformatter
windows_forms -.->|"использует"| binaryformatter
wpf -.->|"использует"| windows_only_api_dependency
windows_forms -.->|"использует"| windows_only_api_dependency
windows_compat_pack -.->|"снижает"| windows_only_api_dependency
dotnet -.->|"использует"| wcf_client
wcf_server -.->|"требует"| dotnet_framework
asp_net_framework -->|"требует"| dotnet_framework
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 30, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
2. Сначала решите, нужно ли мигрировать именно сейчас
Первое решение — не «как мигрировать», а действительно ли это приложение нужно мигрировать сейчас.
Если оставить вопрос размытым, легко получить технически верную, но слишком тяжёлую для бизнеса миграцию — или, наоборот, слишком долго откладывать то, что явно стоило бы перенести.
flowchart TB
accTitle: Вопрос, который решают первым
accDescr: Прежде чем выбирать способ миграции, нужно решить, действительно ли приложение мигрировать сейчас; иначе легко уйти в слишком тяжёлую миграцию или в чрезмерную отсрочку.
q1["Действительно ли мигрировать сейчас"] -->|"решить сначала"| how1["Как мигрировать"]
q1 -.->|"если оставить размытым"| bad1["Слишком тяжёлая миграция или чрезмерная отсрочка"]
Рис. 2: Если не решить «мигрировать ли сейчас» до «как мигрировать», оценка качается в одну из сторон.
2.1 Остаться на .NET Framework — тоже обычное решение
.NET Framework 4.8.1 поддерживается, пока стоит на поддерживаемой Windows. То есть это не история вида «если прямо сейчас не перевести всё на современный .NET, сразу опасно».
Но у решения остаться есть жёсткие ограничения.
- из Windows не выйти
- придётся дальше держать ASP.NET Web Forms и старый серверный стек
- выгоду от новой производительности, языковых возможностей и экосистемы .NET получить труднее
- легче разойтись с нынешними допущениями облака, контейнеров и CI/CD
Наоборот, если зависимость такого рода сильная, разумно пока стабильно эксплуатировать .NET Framework 4.8.1 и параллельно строить план замены на отдельном треке.
flowchart TB
accTitle: Остаться — тоже рациональный выбор
accDescr: При сильной зависимости от Web Forms, совместимости WCF-сервера, COM+ и подобного разумный путь — пока стабильно работать на .NET Framework 4.8.1 и отдельно планировать замену.
dep1["Сильная зависимость от legacy"] --> stay1["Пока стабильно держать 4.8.1"]
stay1 --> plan1["План замены на отдельном треке"]
stay1 -.-> lim1["Ограничения вроде Windows-only остаются"]
Рис. 3: Если зависимость глубокая, реалистичны две линии сразу: стабильная эксплуатация на 4.8.1 и план замены.
- очень много экранных наработок на Web Forms
- нужно жёстко сохранить совместимость WCF-сервера
- глубоко завязаны Workflow Foundation или COM+
- design-time компоненты третьих сторон не работают на современном .NET
- бизнес не допускает крупных изменений спецификации
2.2 Что меняется в зависимости от выбора
| Выбор | Что улучшается | Что остаётся / теряется | Когда уместно |
|---|---|---|---|
| Остаться на .NET Framework 4.8.1 | Легче не ломать существующий код и стабильно эксплуатировать | Windows-only, старая модель приложения, потолок модернизации | Сильная зависимость от legacy, сейчас приоритет бизнеса — стабильная эксплуатация |
| Перейти на современный .NET, оставаясь на Windows | Можно модернизировать runtime и инструментарий. Большая выгода от производительности, опыта разработки и SDK-стиля | Зависимость от Windows API остаётся. Кроссплатформенности не будет | Бизнес-приложения на WinForms / WPF, службах Windows, с Windows API |
| Перейти на современный .NET с прицелом на Linux / контейнеры / облако | Больше свободы в месте развёртывания. Легче обновить и модель эксплуатации | Windows-only API и app model нужно снять заранее | Хотите сдвинуть серверную часть в облако и обновить инфраструктуру |
Важно заранее решить не «хотим ли мигрировать», а куда нужно приземлиться после миграции.
flowchart TB
accTitle: Решают целевую точку посадки
accDescr: Важно заранее решить не «хотим ли мигрировать», а куда нужно приземлиться после миграции.
ask1["Хотим ли мигрировать"] -.->|"вопроса недостаточно"| dec1["Что решить сначала"]
land1["Куда приземлиться после миграции"] -->|"решить это"| dec1
dec1 --> path1["Появляются варианты и вопросы, на которые смотреть"]
Рис. 4: Если вопрос сменить с «хотим ли мигрировать» на «куда приземлиться», варианты начинают сходиться.
3. Четыре решения, которые стоит принять заранее
3.1 Целевая версия .NET
На момент написания, по политике поддержки Microsoft, LTS — .NET 10. При этом и .NET 8 LTS, и .NET 9 STS планируют снять с поддержки 2026-11-10. STS заканчивается в ту же дату, что LTS, потому что срок поддержки STS продлили с 18 до 24 месяцев. Если считать по старому правилу «STS — 18 месяцев», .NET 9 выглядит как окончание 2026-05-12, поэтому когда решение опирается на дату, сверяйтесь с официальной информацией о жизненном цикле.
Поэтому если вы только начинаете миграцию с .NET Framework, при отсутствии особых причин естественно целиться в текущую LTS.
Практическая логика здесь простая.
- Небольшая миграция, которую хотят закрыть быстро — сразу на текущую LTS
- Система, рассчитанная на долгую эксплуатацию, тоже по умолчанию на текущую LTS
- «Из-за существующей библиотеки хотим предыдущую LTS» как причина бывает, но решение принимают, глядя на дату окончания поддержки
flowchart TB
accTitle: Как выбирать целевую версию
accDescr: И для небольшой миграции, и для основной системы базой служит текущая LTS; предыдущую LTS рассматривают из-за существующих библиотек и только после проверки даты окончания поддержки.
tgt1["Выбрать целевой .NET"] -->|"по умолчанию"| lts1["Приземлиться на текущую LTS"]
tgt1 -->|"из-за существующих библиотек"| old1["Рассмотреть предыдущую LTS"]
old1 --> dt1["Решить по дате окончания поддержки"]
Рис. 5: Целевая версия по умолчанию — текущая LTS; старую LTS выбирают только с датой поддержки в руках.
3.2 Остаться Windows-only или целиться в кроссплатформенность
От этого решения сильно меняется, на что смотреть.
- Если остаётесь на Windows, реалистичный путь — модернизировать runtime через WPF / WinForms и Windows Compatibility Pack.
- Если в будущем нужны Linux и контейнеры, на раннем этапе инвентаризируйте Windows-only API:
System.Drawing.Common, реестр, WMI, EventLog, службы Windows, COM, Office Interop.
Если начать миграцию без этого решения, посередине разговор скручивается: «а стоило ли фиксироваться на Windows?», «нет, мы вообще-то хотели контейнеры».
flowchart TB
accTitle: Развилка Windows-only или кроссплатформа
accDescr: Если остаётесь на Windows, реалистичный путь — сначала модернизировать runtime через compatibility pack; если целитесь в Linux и контейнеры, Windows-only API нужно инвентаризировать рано.
aim1["Куда целимся"] -->|"остаёмся на Windows"| wn1["Модернизировать runtime через compatibility pack"]
aim1 -->|"в будущем и контейнеры"| xp1["Рано инвентаризировать Windows-only API"]
aim1 -.->|"начать, не решив"| twist1["Посередине разговор скручивается"]
Рис. 6: Если эту развилку не закрыть заранее, не ясно, на что смотреть, и разговор скручивается.
3.3 Одним махом или поэтапно
Форм миграции в целом три.
- одномоментная миграция, близкая к in-place
- side-by-side, когда старое и новое стоят рядом
- поэтапная миграция по route / library
Для приложений на ASP.NET Framework руководство Microsoft явно указывает incremental migration. Если продакшен останавливать нельзя, функций много и вокруг много зависимостей, с самого начала закладывать поэтапную миграцию обычно честнее.
flowchart TB
accTitle: Три формы миграции
accDescr: Есть одномоментная миграция, близкая к in-place, side-by-side сосуществование и поэтапный перенос по route или library; если продакшен нельзя останавливать, разумная база — поэтапная миграция.
typ1["Выбрать форму миграции"] --> t1["Одномоментная (ближе к in-place)"]
typ1 --> t2["side-by-side, старое и новое рядом"]
typ1 --> t3["Поэтапно по route / library"]
t3 -.-> fit1["Если продакшен нельзя останавливать, это база"]
Рис. 7: Форм три; для продакшена с большим числом функций, который нельзя останавливать, база — поэтапная миграция.
3.4 Что «вынести из текущего объёма миграции»
Миграции чаще проваливаются, потому что в один заход пытаются впихнуть слишком много.
Например, одновременное выполнение всего перечисленного обычно тяжело.
- .NET Framework → .NET
- ASP.NET Framework → ASP.NET Core
- EF6 → EF Core
- Windows-серверы → Linux-контейнеры
- смена платформы аутентификации
- смена платформы логирования / мониторинга
- миграция базы данных
Когда-нибудь всё это может понадобиться. Но нужно ли делать это одновременно — другой вопрос.
На практике лучше разделять так.
- Сначала модернизировать runtime и структуру проекта
- Затем перенести app model
- В конце обновить ORM, аутентификацию, облако и мониторинг
flowchart TB
accTitle: Порядок разделения без перегруза
accDescr: Сначала модернизируют runtime и структуру проекта, затем переносят app model, в конце обновляют ORM, аутентификацию, облако и мониторинг.
d1["Модернизировать runtime и структуру"] --> d2["Перенести app model"]
d2 --> d3["Обновить ORM, аутентификацию, облако, мониторинг"]
d1 -.->|"если делать всё сразу"| hv1["Миграция тяжелеет и чаще проваливается"]
Рис. 8: Если не делать всё сразу, а разделить runtime, app model и окружение, объём не раздувается.
4. База, которую стоит подготовить до старта
Руководство Microsoft по подготовке к портированию довольно практичное. Коротко: перед миграцией текущий проект на .NET Framework нужно сдвинуть ближе к современному входу.
4.1 Поднять цель до .NET Framework 4.7.2 или новее
Официальное руководство рекомендует перед портированием таргетировать .NET Framework 4.7.2 или новее. Причина в том, что даже когда .NET Standard не содержит существующий API как есть, проще опереться на более новую замену API.
На практике, если можно, понятнее ориентироваться на 4.8.1.
- проще с точки зрения поддержки
- удобно считать финальной стабильной точкой на стороне .NET Framework
- легче выстроить политику «сначала навести порядок на текущем Framework»
Что меняется, если сделать это заранее
- проще и стабильнее работать с общими библиотеками на
.NET Standard 2.0 - заранее снижается шум от старого runtime
- проще понять, проблема из-за возраста Framework или из-за перехода на современный .NET
4.2 Сдвинуться к PackageReference
Руководство по подготовке к портированию рекомендует перевести ссылки в формат PackageReference.
Если сделать это заранее, управление зависимостями становится заметно прозрачнее.
Что меняется при PackageReference
- ссылки на пакеты собираются в
csproj - транзитивные зависимости становятся обозримее
- допущения restore выравниваются со стороной современного .NET
- лучше стыковка с CLI / CI
Но здесь есть мины.
Типичные мины
В официальной документации NuGet явно указаны такие ограничения перехода с packages.config на PackageReference.
- встроенная миграция Visual Studio недоступна для проектов ASP.NET
- пакеты, которые зависят от
install.ps1/uninstall.ps1, могут работать не так, как ожидается - содержимое папки
contentиногда игнорируется - XDT-преобразования вроде
web.config.install.xdtне применяются - пакеты со старой раскладкой сборок прямо под
libиногда разрешаются некорректно
То есть это не «просто смена формата пакетов».
В классическом ASP.NET при установке NuGet-пакета часто переписывали web.config, поэтому при миграции скрытые допущения легко всплывают.
flowchart TB
accTitle: Неявные допущения, которые всплывают при PackageReference
accDescr: При переходе с packages.config на PackageReference install.ps1, XDT-преобразования и содержимое content могут не примениться, и скрытые допущения установки web.config всплывают.
cv1["Преобразование в PackageReference"] --> np1["install.ps1 иногда не выполняется"]
cv1 --> nx1["XDT-преобразования не применяются"]
cv1 --> nc1["Содержимое content иногда игнорируется"]
np1 --> exp1["Всплывают скрытые допущения"]
nx1 --> exp1
nc1 --> exp1
Рис. 9: Даже если казалось, что меняется только формат, допущения, завязанные на установку, всплывают разом.
Чем преобразовывать
Не только руками. Но у инструментов разная зона покрытия.
| Инструмент | Что умеет | Допущения и оговорки |
|---|---|---|
| Преобразование в Visual Studio | В обозревателе решений щёлкните правой кнопкой «Ссылки» или packages.config и выберите Migrate packages.config to PackageReference... |
Visual Studio 2017 15.7 или новее. Не работает для проектов ASP.NET и C++. Если пункта нет в меню, сначала восстановите NuGet или откройте диспетчер пакетов, затем щёлкните правой кнопкой снова |
| .NET Upgrade Assistant | Анализирует проект и проводит обновление пакетом | В официальной документации уже не рекомендуется; направляют на модернизацию через GitHub Copilot |
| Модернизация через GitHub Copilot | Помогает с оценкой, планом, правкой кода и проверкой. Центр текущих официальных рекомендаций | Как в 4.5, нужны Visual Studio, Copilot и код на C# |
| Ручной переход на SDK-стиль | Создаёте новый csproj и переносите только нужные элементы |
В проекте с небольшим числом файлов это иногда самый быстрый путь |
Перед запуском преобразование Visual Studio делает резервную копию проекта и в конце показывает отчёт (зависимости верхнего уровня, транзитивные зависимости, найденные проблемы совместимости). Если нужно откатиться, документация описывает такой путь: вернуть csproj и packages.config из папки резервной копии и в консоли диспетчера пакетов выполнить update-package -reinstall.
То есть попробовать можно так, чтобы было куда вернуться. Для оценки до старта быстрее всего преобразовать один проект и прочитать отчёт.
flowchart TB
accTitle: Пробовать преобразование так, чтобы можно было откатиться
accDescr: Преобразование Visual Studio сначала делает резервную копию и затем показывает отчёт, поэтому достаточно преобразовать один проект, прочитать отчёт и при необходимости вернуть файлы из копии.
bk1["Создаётся резервная копия"] --> cv2["Преобразовать один проект"]
cv2 --> rp1["Прочитать отчёт преобразования"]
rp1 -->|"если есть проблемы"| rv1["Вернуть файлы и переустановить пакеты"]
rp1 -->|"если проблем нет"| go1["Расширить на другие проекты"]
Рис. 10: Преобразование можно пробовать с копией и отчётом, поэтому сначала берут один проект.
4.3 Сдвинуться к SDK-стилю
Руководство по подготовке к портированию также рекомендует переход на формат проекта в SDK-стиле.
Это даёт заметный эффект.
Что меняется при SDK-стиле
csprojстановится намного короче- хорошо сочетается с
PackageReference - проще multi-targeting
- легче выстроить CI/CD вокруг
dotnet build/dotnet test/dotnet publish - конфигурация ближе к стороне современного .NET, поэтому дальше разница меньше
Иначе говоря, если прыгнуть на современный .NET со старым csproj и старым управлением NuGet, разница окажется слишком большой.
flowchart TB
accTitle: SDK-стиль уменьшает разницу
accDescr: Прыжок на современный .NET со старым csproj и старым NuGet даёт слишком большую разницу, поэтому сначала сдвигаются к SDK-стилю и приближают конфигурацию к современной стороне.
oldp["Старый csproj + старое управление NuGet"] -.->|"прыгнуть сразу"| big1["Разница слишком большая"]
oldp -->|"сначала SDK-стиль"| sdk1["Конфигурация ближе к современной стороне"]
sdk1 --> small1["Дальше разница меньше"]
Рис. 11: Если вставить SDK-стиль между шагами, прыжок на современный .NET становится меньше.
4.4 Сначала обновить зависимости
Это тоже по официальному руководству: зависимости сдвигают к последним доступным версиям, а если можно — к версиям с поддержкой .NET Standard.
Зачем делать это заранее
- раньше становится ясно, «живёт ли этот пакет на современном .NET»
- старые зависимости не превращаются в шум
- проще перевести общие библиотеки на
netstandard2.0 - на следующем этапе проще сосредоточиться именно на портировании кода
flowchart TB
accTitle: Зачем обновлять зависимости заранее
accDescr: Если сначала сдвинуть зависимости ближе к актуальным, раньше видно, работают ли они на современном .NET, меньше шума от старых пакетов, и дальше можно сосредоточиться на портировании кода.
upd1["Сначала обновить зависимости"] --> know1["Раньше видно поддержку современного .NET"]
upd1 --> noise1["Меньше шума от старых зависимостей"]
know1 --> foc1["Дальше можно сосредоточиться на портировании кода"]
noise1 --> foc1
Рис. 12: Чем раньше закрыть обновление зависимостей, тем больше сама миграция сводится к портированию.
4.5 Проверить и допущения официальных инструментов
На март 2026 центр рекомендаций Microsoft сместился к модернизации через GitHub Copilot. На практике удобнее смотреть на это не как на отдельные старые инструменты миграции, а как на единый поток помощи: оценка, план, правка кода, проверка.
Но текущая документация исходит из Visual Studio 2026 или поддерживаемой линейки Visual Studio 2022, GitHub Copilot и кода на C#.
Зачем это проверять
- меняется то, чего можно ждать от официальных инструментов
- можно выровнять допущения команды об IDE / build agent / расширениях
- можно сразу решить не ждать слишком много от автоматизации в решениях на VB.NET
Проекты с примесью VB.NET не редкость. Поэтому стоит заранее понять, насколько далеко помогут текущие официальные инструменты.
flowchart TB
accTitle: Где сейчас официальные инструменты и какие у них допущения
accDescr: .NET Upgrade Assistant уже не рекомендуется, центр рекомендаций сместился к модернизации через GitHub Copilot, но нужны Visual Studio, Copilot и C#, поэтому в решениях на VB.NET автоматизацию не стоит переоценивать.
ua1[".NET Upgrade Assistant"] -->|"не рекомендуется, направляют на"| cp1["Модернизация через GitHub Copilot"]
cp1 --> pre1["Допущения: Visual Studio + Copilot + C#"]
pre1 -.->|"если есть VB.NET"| vb1["Не ждать слишком много от автоматизации"]
Рис. 13: Центр инструментов сместился к Copilot-модернизации; если допущения не совпадают с площадкой, нужна отдельная подготовка.
5. Оценить сложность по типу проекта
О миграции часто говорят одной фразой «с .NET Framework на .NET», но на практике каждый тип проекта — отдельная игра.
5.1 Приблизительное ощущение сложности
| Тип | Ощущение сложности | Основные вопросы |
|---|---|---|
| Библиотека классов | Низкая—средняя | Совместимость API, зависимости, разделение целевых платформ |
| Консоль / batch / часть служб Windows | Низкая—средняя | Способ распространения, нативные зависимости, конфигурация |
| WinForms / WPF | Средняя | Остаётся Windows-only, дизайнер, сторонний UI, всё вокруг BinaryFormatter |
| ASP.NET MVC / Web API | Средняя—высокая | Миграция app model на ASP.NET Core, аутентификация, сессии, конфигурация, DI |
| ASP.NET Web Forms | Высокая | Большая разница в модели экрана, предполагается замена слоя UI |
| Клиент WCF | Средняя | Замена пакетов, контракты, конфигурация |
| Сервер WCF | Высокая | CoreWCF или редизайн на gRPC / HTTP API |
| Одновременно EF6 → EF Core | Высокая | ORM — другая вещь, отличия в поведении, история миграций |
5.2 Для библиотек классов ключ — как провести «границу общего»
Библиотеки классов переносить относительно легко. Но только когда библиотека действительно отделена так, как полагается библиотеке.
Сложность растёт при таких зависимостях:
- обращение к
System.Web - прямой взгляд в
HttpContext.Current - типы WPF / WinForms в публичном API
- слишком сильная опора на Windows API вроде реестра, WMI, EventLog
- зависимость от
AppDomainили Remoting
Проще смотреть так: если удаётся вынести только бизнес-логику — легко; если библиотека ещё и держит app model — тяжело.
flowchart TB
accTitle: Как смотреть на тяжесть библиотеки классов
accDescr: Библиотека классов, из которой можно вынести только бизнес-логику, лёгкая; если она держит System.Web, UI-типы, Windows API, AppDomain и прочий app model, миграция тяжелеет.
lib1["Смотрим библиотеку классов"] -->|"логику можно вынести отдельно"| lt1["Миграция лёгкая"]
lib1 -->|"внутри ещё и app model"| hv2["Миграция тяжёлая"]
hv2 -.-> sm1["System.Web, UI-типы, Windows API и т. п."]
Рис. 14: Сложность библиотеки задаёт не число строк, а то, сколько app model она в себе держит.
5.3 WinForms / WPF мигрируют относительно легко, но остаются Windows-only
WinForms и WPF можно перенести на .NET. Но оба остаются Windows-only фреймворками.
Если здесь ошибиться в ожиданиях, будет плохо.
- Что улучшится
- можно сесть на runtime, язык и SDK-стиль современного .NET
- легче приблизить CI/CD и управление пакетами к текущей практике
- проще получить отдельные улучшения производительности и сопровождения
- Что не изменится
- приложение останется Windows-only
- останутся проблемы совместимости UI-контролов и design-time компонентов
- проблемы ActiveX / COM / нативных DLL никуда не денутся
Кроме того, в WinForms / WPF иногда нужно проверить влияние BinaryFormatter. Особенно если custom-типы задействованы в clipboard, drag & drop, ResX или сериализации на этапе дизайна, проблема легко всплывает при подъёме цели на .NET 9 или новее.
flowchart TB
accTitle: Ожидания от миграции WinForms / WPF
accDescr: WinForms и WPF можно перенести на .NET и сесть на runtime, язык и SDK-стиль современного .NET, но Windows-only природа не меняется, и иногда нужно проверить влияние BinaryFormatter.
ui1["Переносим WinForms / WPF"] --> gain1["Можно сесть на выгоду современного .NET"]
ui1 --> keep1["Остаются Windows-only"]
ui1 -.-> bfc1["Нужно проверить BinaryFormatter"]
Рис. 15: Десктопный UI переносится, но свойство «только Windows» уезжает вместе с ним.
5.4 ASP.NET Framework — это миграция app model, а не runtime
Переход с ASP.NET Framework на ASP.NET Core в руководстве Microsoft прямо назван non-trivial. Дело не просто в смене имён API, а в том, что меняется лежащая в основе архитектура.
Различия чаще всего проявляются здесь:
- Hosting model
- Middleware pipeline
- Request processing model
- Session / Cache
- Authentication / Authorization
- Configuration
- Dependency Injection
- Logging / Monitoring
Для приложения на ASP.NET Framework сначала стоит проверить следующее.
- с каких route / endpoint можно начать перенос
- можно ли снять зависимость от
System.Webс общих библиотек - как выровнять аутентификацию / сессии / обработку исключений / логирование
- будет ли поэтапная миграция без остановки продакшена
Для крупных приложений реалистичнее с самого начала закладываться на incremental migration.
flowchart TB
accTitle: Миграция ASP.NET — это миграция app model
accDescr: Переход с ASP.NET Framework на ASP.NET Core — не замена имён API, а смена архитектуры hosting, middleware, аутентификации, конфигурации и DI; для крупных приложений реалистичная база — поэтапная миграция.
an1["ASP.NET Framework → Core"] --> am1["Меняется лежащая в основе архитектура"]
am1 --> pts1["hosting, middleware, аутентификация, конфигурация, DI"]
am1 -->|"крупное приложение"| inc1["Планировать как incremental migration"]
Рис. 16: Миграция ASP.NET — это не runtime, а app model; чем больше масштаб, тем сильнее нужна поэтапность.
5.5 С Web Forms начинают не с «переноса экранов», а с разделения ответственности
Web Forms — это не тот же app model, что у ASP.NET Core. Поэтому при оценке безопаснее не исходить из того, что экранные наработки уедут как есть.
На практике чаще начинают с такого разделения.
- отделить логику экрана от бизнес-логики
- разложить ответственность, зашитую в
Page/UserControl/ViewState - вынести бизнес-логику и доступ к данным в общую библиотеку
- UI собрать заново на другой модели: Razor Pages / MVC / Blazor
То есть в проекте на Web Forms важнее не план миграции runtime, а есть ли план разделения ответственности.
flowchart TB
accTitle: Web Forms начинают с разделения ответственности
accDescr: Web Forms — не тот же app model, что ASP.NET Core, поэтому начинают с отделения логики экрана от бизнес-логики, выноса логики в общую библиотеку и сборки UI на другой модели.
wf1["Экранные наработки Web Forms"] --> sep1["Отделить логику экрана от бизнес-логики"]
sep1 --> mv1["Вынести логику в общую библиотеку"]
mv1 --> re1["Собрать UI на другой модели"]
wf1 -.->|"если считать, что уедет как есть"| ngw1["Оценка разваливается"]
Рис. 17: Web Forms смотрят не как перенос активов, а как разделение ответственности и сборку на другой модели.
5.6 Клиента WCF и сервер WCF оценивают отдельно
Их не стоит смешивать.
Клиент WCF
Для WCF Client есть поддерживаемые NuGet-пакеты под современный .NET. Поэтому если вы только вызываете WCF, задача иногда оказывается не такой тяжёлой, как кажется.
Сервер WCF
Сторона, которая хостит службу WCF, — другое дело. В руководстве Microsoft в основном два пути модернизации.
- CoreWCF, чтобы сохранить совместимость с существующими клиентами
- сдвиг к современному RPC / HTTP вроде gRPC
Но CoreWCF не приносит весь WCF как есть, это subset. Для совместимости с существующими клиентами он подходит, но изменения кода и тесты обязательны.
flowchart TB
accTitle: WCF оценивают отдельно на клиенте и на сервере
accDescr: У WCF-клиента есть поддерживаемые пакеты под современный .NET, и задача иногда легче, чем кажется; на сервере нужно выбрать CoreWCF для совместимости или редизайн на gRPC.
wcf1["На какой стороне используется WCF"] -->|"клиент"| cl1["Перенос через поддерживаемый пакет"]
wcf1 -->|"сервер"| sv1["Выбрать путь"]
sv1 -->|"сохранить совместимость"| cw1["CoreWCF (subset)"]
sv1 -->|"редизайн"| gr1["gRPC / HTTP API"]
Рис. 18: У вызывающей стороны и у хоста разные и сложность, и варианты, поэтому оценивать их вместе нельзя.
6. Выявить технологии, которых в .NET нет или которые стопорят «как есть»
Это нужно сделать до старта обязательно. У Microsoft есть перечень технологий, которые работали в .NET Framework, но недоступны в .NET 6+.
6.1 Технологии, которые часто становятся красным флагом
| Технология | Состояние в .NET | Как к этому относиться |
|---|---|---|
Создание AppDomain, например AppDomain.CreateDomain |
Не поддерживается | Изоляцию думать через отдельный процесс / контейнер / AssemblyLoadContext |
| .NET Remoting | Не поддерживается | Перепроектировать на IPC, HTTP, gRPC, Socket, Pipe и т. п. |
| CAS / Security Transparency | Не поддерживается как граница безопасности | Думать через ОС / контейнер / разделение прав |
System.EnterpriseServices (COM+) |
Не поддерживается | Отделить и заменить дизайн, завязанный на COM+ |
| Workflow Foundation | Не поддерживается | Считать отдельной оценкой, включая альтернативы вроде CoreWF |
| WCF server | Из коробки не работает как есть | Выбирать CoreWCF или gRPC |
| BinaryFormatter | С .NET 9 реализация всегда бросает исключение | Миграция сериализатора, аудит ResX / clipboard / drag & drop |
6.2 AppDomain: «часть API осталась, но создание — отдельный вопрос»
Тема AppDomain немного запутанная.
В .NET часть поверхности API сохранена, но использование в духе создать новый AppDomain и изолировать код не поддерживается.
Поэтому если AppDomain использовали для следующего, нужен редизайн.
- изоляция плагинов
- выгрузка динамически загруженного кода
- изоляция кода с частичным доверием
- временное отделение среды выполнения
Перед миграцией смотреть нужно не только на то, встречается ли слово AppDomain, но и на то, для чего его использовали.
flowchart TB
accTitle: Решение зависит от того, как использовали AppDomain
accDescr: Даже если часть API AppDomain осталась, создание нового AppDomain для изоляции не поддерживается, поэтому отличают простое использование типа от изоляции и при изоляции перепроектируют через отдельный процесс или AssemblyLoadContext.
ad1["Встретился AppDomain"] -->|"только тип или сведения"| okc1["Часто можно идти дальше как есть"]
ad1 -->|"создавали ради изоляции"| rd1["Нужен редизайн"]
rd1 --> alt1["Отдельный процесс / контейнер / AssemblyLoadContext"]
Рис. 19: Нужен ли редизайн, решает не наличие слова, а то, для чего механизм использовали.
6.3 Remoting глубже, чем кажется
Помимо самого Remoting, в зону влияния иногда попадают и вызовы BeginInvoke() / EndInvoke() асинхронных делегатов. Это не сам Remoting, но в современном .NET это не поддерживается, поэтому перед миграцией это тоже нужно выявить.
При поиске безопаснее сразу смотреть и это:
System.Runtime.RemotingMarshalByRefObjectRealProxyBeginInvoke(/EndInvoke(
6.4 BinaryFormatter внезапно выходит на первый план в зависимости от target version
Чем старше кодовая база, тем выше шанс, что BinaryFormatter используется «без осознания».
- сохранённые данные
- кэш
- хранение сессий
- состояние плагинов
- clipboard / drag & drop
- ResX
- всё вокруг дизайнера WinForms / WPF
Начиная с .NET 9 реализации BinaryFormatter в runtime нет, и API всегда бросает PlatformNotSupportedException.
То есть это не «подумаем позже», а вопрос, который нужно аудировать в момент выбора target version.
flowchart TB
accTitle: Как BinaryFormatter выходит на первый план
accDescr: BinaryFormatter часто используют неосознанно в сохранённых данных, ResX и clipboard, а с .NET 9 API всегда бросает исключение, поэтому аудит нужен уже в момент выбора target version.
hid2["Неосознанное использование (ResX, clipboard и т. п.)"] --> tg1["Цель ставят на .NET 9 или новее"]
tg1 --> ex1["API всегда бросает исключение"]
ex1 --> aud1["Аудировать сразу в момент выбора"]
Рис. 20: BinaryFormatter начинает работать в момент выбора target, поэтому его не откладывают, а аудируют заранее.
6.5 Поисковые термины, которые стоит прогнать через grep в первую очередь
Ещё до старта достаточно поискать эти слова по всему решению — картина сильно меняется.
System.Web
HttpContext.Current
System.Runtime.Remoting
MarshalByRefObject
AppDomain
BinaryFormatter
ServiceHost
ChannelFactory
System.EnterpriseServices
Workflow
packages.config
web.config.install.xdt
install.ps1
DllImport
AxInterop
Microsoft.Office.Interop
Найти хотя бы одно из них не значит «сразу мимо». Это карта, которая показывает, что уедет по стандартному маршруту, а что окажется на отдельном треке.
Реальные команды поиска
Инструмент может быть rg (ripgrep) или PowerShell.
# PowerShell. Поиск по всему решению
Get-ChildItem -Recurse -Include *.cs,*.vb,*.config,*.csproj,*.vbproj |
Select-String -Pattern 'BinaryFormatter|System\.Runtime\.Remoting|MarshalByRefObject|AppDomain\.CreateDomain' |
Select-Object Path, LineNumber, Line
# ripgrep. Когда сначала нужны количества
rg -n --stats "BinaryFormatter|System\.Runtime\.Remoting|AppDomain\.CreateDomain" --glob "*.cs" --glob "*.vb"
Что делать, если нашли
Поиск — только вход, решение принимают после него. Для каждого термина следующее место проверки уже известно.
| Найденный термин | Что проверить дальше | Куда это сходится |
|---|---|---|
System.Web / HttpContext.Current |
Можно ли вынести из общей библиотеки. Можно ли на вызывающей стороне переложить в DTO | Снимать по стратегии 8.5 |
System.Runtime.Remoting / MarshalByRefObject / BeginInvoke |
Для чего был удалённый вызов. Между процессами, между машинами или просто асинхронный вызов | Редизайн на IPC, HTTP, gRPC, Task |
AppDomain |
Только имя типа или изоляция через CreateDomain |
Если ради изоляции — отдельный процесс или AssemblyLoadContext (6.2) |
BinaryFormatter |
Нужно ли потом читать записанные данные. Нет ли косвенного использования через ResX или clipboard | Перейти на другой сериализатор и спланировать чтение старых данных (6.4) |
ServiceHost / ChannelFactory |
Хост или клиент | Оценивать отдельно, как в 5.6 |
packages.config / install.ps1 / web.config.install.xdt |
Что переписывалось при установке | Мины из 4.2. Конфигурацию нужно явно забрать себе |
DllImport / AxInterop / Microsoft.Office.Interop |
Допущения о разрядности и полный набор файлов, нужных в runtime | Перейти к проверке bitness в 9.4 |
После этого получается не ответ «можно ли мигрировать», а список: что идёт стандартным маршрутом, а что — отдельной оценкой. Именно такой список и нужен до старта.
flowchart TB
accTitle: От поиска к списку
accDescr: Поиск — только вход; если по каждому найденному термину пройти следующее место проверки, получается список того, что идёт стандартным маршрутом, а что требует отдельной оценки.
grep1["Прогнать решение поисковыми терминами"] --> nxt1["По каждому термину пройти следующее место проверки"]
nxt1 --> map1["Получается список стандартного маршрута и отдельной оценки"]
grep1 -.-> note2["Находка сама по себе не значит «мимо»"]
Рис. 21: Поиск — вход; если дойти до решения, получается список, который нужен до старта.
7. Решить, насколько допустима привязка к Windows
Частое заблуждение при миграции — «перешли на .NET, значит стало кроссплатформенно». Такой магии нет. Если приложение глубоко связано с Windows, после миграции оно по-прежнему Windows-only.
7.1 Перенести, оставаясь Windows-only, — вполне реалистичный путь
У Microsoft есть Windows Compatibility Pack: он даёт доступ из современного .NET ко многим Windows API — реестр, WMI, EventLog, службы Windows, Directory Services и другие.
На практике это важно.
- сначала хотим перейти на современный .NET
- но пока из Windows не выходим
- поэтому зависимость от Windows API пока готовы допустить
Для такой площадки это сильный вариант.
Первой целью миграции вовсе не обязательно должна быть кроссплатформенность.
7.2 Но Windows-only API — ещё и «долг, который всплывёт позже»
Наличие Windows Compatibility Pack не значит, что можно расслабиться во всём.
- хотите поставить в Linux-контейнер
- хотите работать с прицелом на Kubernetes
- хотите, чтобы разработчики на macOS / Linux собирали тот же build
- в будущем хотите сократить число Windows VM в облаке
Если такие цели есть, зависимость от Windows API лучше сделать видимой уже сейчас.
flowchart TB
accTitle: Где уместен compatibility pack и чем он становится долгом
accDescr: Если пока остаётесь на Windows, compatibility pack позволяет временно допустить Windows API и перейти на современный .NET; если потом целитесь в контейнеры и облако, эту зависимость нужно сделать видимой сейчас как долг, который всплывёт позже.
now1["Сначала хотим современный .NET"] -->|"пока остаёмся на Windows"| pack1["Временно допустить зависимость через compatibility pack"]
pack1 -.->|"если потом контейнеры и облако"| debt1["Зависимость — долг, который всплывёт позже"]
debt1 --> vis1["Сделать видимой уже сейчас"]
Рис. 22: Compatibility pack — реалистичный мост, но эта зависимость в зависимости от цели становится долгом, поэтому её делают видимой.
7.3 System.Drawing.Common особенно часто понимают неправильно
System.Drawing.Common начиная с .NET 6 — библиотека только для Windows.
Если есть код обработки изображений или отрисовки текста, сначала нужно решить, каким путём идти.
- продолжать эксплуатацию на Windows
- или в будущем запускать также на Linux / macOS
В первом случае иногда можно пока оставить как есть. Во втором замену на SkiaSharp, ImageSharp и подобные нужно сразу включить в план миграции.
flowchart TB
accTitle: Развилка System.Drawing.Common
accDescr: System.Drawing.Common с .NET 6 — библиотека только для Windows, поэтому при эксплуатации на Windows её иногда можно пока оставить, а если нужны Linux и macOS, замену на SkiaSharp или ImageSharp сразу включают в план.
sd1["Используется System.Drawing.Common"] -->|"эксплуатация на Windows"| ok4["Иногда можно пока оставить как есть"]
sd1 -->|"нужны и Linux / macOS"| repl1["Заменить на SkiaSharp / ImageSharp"]
repl1 --> plan2["Сразу включить в план миграции"]
Рис. 23: System.Drawing.Common — Windows-only, поэтому обращение с ним с самого начала зависит от точки посадки.
7.4 Типичные признаки жёсткой привязки к Windows
Если есть такие ссылки или API, безопаснее оценивать проект исходя из того, что «как минимум сначала он мигрирует, оставаясь Windows-only».
Microsoft.Win32.RegistrySystem.ManagementSystem.Diagnostics.EventLogSystem.ServiceProcessSystem.DirectoryServicesSystem.DrawingDllImport/ P/Invoke- ссылки на COM
AxInterop.*Microsoft.Office.Interop.*
8. Способ вынесения общих библиотек меняет сложность
Для крупных решений не будет преувеличением сказать, что успех миграции определяется тем, как разрезаны общие библиотеки.
8.1 Сначала классифицировать
Библиотеки проще разложить, разбив на три большие категории.
- чистая бизнес-логика / доменная логика
- промежуточный слой с небольшой зависимостью от app model
- слой, плотно связанный с UI / Web / Windows API
Из них первой стоит переносить категорию 1.
- вычисления
- проверка правил
- DTO / контракты
- доменные сервисы
- простые преобразования данных
Если эту часть удаётся вынести чисто, сложность резко падает.
flowchart TB
accTitle: Три класса библиотек и порядок переноса
accDescr: Библиотеки делят на чистую бизнес-логику, промежуточный слой с зависимостью от app model и слой, плотно связанный с UI или Windows API; первой переносят чистую бизнес-логику.
c1["Чистая бизнес-логика"] -->|"переносить первой"| ez1["Если вынести чисто, сложность падает"]
c2["Промежуточный слой ближе к app model"] -->|"затем"| ez1
c3["Слой, плотно связанный с UI и Windows API"] -->|"в конце"| ez1
Рис. 24: Библиотеки делят на три слоя и сначала выносят чистую логику.
8.2 netstandard2.0 по-прежнему рабочий мост
По рекомендациям Microsoft, для общей библиотеки, которой нужно сосуществовать и со стороной .NET Framework, базовый вариант — .NET Standard 2.0.
Здесь важны два момента.
- .NET Framework не поддерживает
.NET Standard 2.1 - если общую библиотеку нужно ссылать и из старого, и из нового кода, 2.0 обычно оказывается практическим решением
flowchart TB
accTitle: Мост netstandard2.0
accDescr: .NET Framework не поддерживает .NET Standard 2.1, поэтому если общую библиотеку нужно ссылать и из старого, и из нового кода, практическим решением чаще оказывается .NET Standard 2.0.
fw1["Сторона .NET Framework"] -->|"может ссылаться"| ns1["Общая библиотека netstandard2.0"]
mn1["Сторона современного .NET"] -->|"может ссылаться"| ns1
ns21["netstandard2.1"] -.->|"Framework не поддерживает"| fw1
Рис. 25: Как мост для ссылок с обеих сторон реалистичен 2.0, а не 2.1.
8.3 Что меняется при netstandard2.0
| Подход | Что меняется | Когда уместно | На что смотреть |
|---|---|---|---|
Переход на netstandard2.0 |
Легко ссылаться и из старого, и из нового кода | Чистая бизнес-логика, общие контракты, утилиты | API, специфичные для app model, положить нельзя |
Multi-target (например net48;net10.0) |
Общий код сохраняется, при этом можно держать различия по окружениям | Библиотеки с небольшими различиями по окружениям | Растёт объём условной компиляции и управления сборкой |
Сразу сделать только net10.0 |
В перспективе самый чистый вариант | Новые слои, которым не нужно сосуществование старого и нового | Из .NET Framework ссылаться нельзя |
8.4 Режим совместимости — не панацея
У .NET Standard 2.0 есть режим совместимости, который позволяет ссылаться на библиотеки .NET Framework. Но это не волшебство, при котором всё прозрачно работает само.
Например, библиотеки, завязанные на API конкретной app model вроде WPF, остаются сложными. То есть даже говоря «общая библиотека», важно сузить её до ответственности, которую действительно можно разделять.
8.5 Для библиотек ASP.NET всё решает возможность снять System.Web
При поэтапной миграции ASP.NET Framework жизнь сильно усложняется, если общая библиотека напрямую висит на HttpContext.Current или System.Web.
Базовая стратегия тогда — один из следующих вариантов.
- вынести зависимость от
System.Webза пределы интерфейса - изменить код так, чтобы сведения из
HttpContextприходили как DTO - на переходный период использовать adapter
- если и это не помогает — снимать зависимость поэтапно через multi-target
8.6 Библиотеки поднимают в порядке leaf-first
В руководстве по incremental migration для ASP.NET явно сказано поднимать supporting library в порядке postorder depth-first, то есть с листьев.
Это работает не только для Web, но и для обычных решений в целом.
- зависимости уже подняты, поэтому верхние слои становятся обозримее
- проблемы совместимости легче локализовать
- легче тестировать каждую библиотеку отдельно
flowchart TB
accTitle: Поднимать leaf-first
accDescr: Если supporting library поднимать с листьев зависимостей, зависимости уже готовы, верхние слои становятся обозримее, проблемы совместимости локализуются, и библиотеки проще тестировать по отдельности.
lf1["Сначала поднять листовые библиотеки"] --> lf2["Затем промежуточный слой, чьи зависимости уже готовы"]
lf2 --> lf3["В конце верхний слой приложения"]
lf1 -.-> loc1["Проблемы можно локализовать и тестировать"]
Рис. 26: Если поднимать с листьев, к моменту верхнего слоя основание уже собрано.
9. Инвентаризировать NuGet / внешние зависимости / сторонние компоненты
Если сделать это небрежно, самая большая боль придёт во второй половине миграции.
9.1 Зависимости проще разложить, разбив на 4 вида
- публичные пакеты NuGet
- внутренние private package / internal library
- ссылки на локальные DLL
- COM / ActiveX / нативные DLL / SDK
Смотреть только на пункт 1 недостаточно. По-настоящему опасны 3 и 4.
flowchart TB
accTitle: Четыре вида зависимостей и где риск
accDescr: Зависимости делят на публичный NuGet, внутренние пакеты, ссылки на локальные DLL и COM / ActiveX / нативные DLL; одного публичного NuGet мало, по-настоящему опасны локальные DLL и COM / нативная сторона.
dp1["Инвентаризация зависимостей"] --> k1["Публичный NuGet и внутренние пакеты"]
dp1 --> k3["Ссылки на локальные DLL"]
dp1 --> k4["COM / ActiveX / нативные DLL"]
k3 --> dg1["По-настоящему опасны эти"]
k4 --> dg1
Рис. 27: Мало посчитать только публичный NuGet: сначала разбирают локальные DLL и COM / нативную сторону.
9.2 Что проверить по каждой зависимости
По каждой зависимости минимум нужно подтвердить следующее.
- таргетирует ли она современный .NET
- поддерживает ли
PackageReference - нормально ли живёт в SDK-стиле
- нет ли ограничений x86 / x64 / ARM64
- не зависит ли от design-time инструментов или расширений Visual Studio
- не предполагает ли install script / config transform
- продолжается ли поддержка
9.3 Сторонний UI / отчёты / design-time компоненты оценивают отдельно
В миграции WinForms / WPF / ASP.NET это ощущается особенно сильно.
- сетки
- движки отчётов
- компоненты вывода PDF
- компоненты графиков
- UI-библиотеки, встроенные в дизайнер
- обёртки ActiveX
Здесь задействован не только runtime, но и поддержка в design-time. Если в оценке миграции смотреть только на «компилируется ли», обычно промахиваются.
9.4 Нативные DLL и bitness смотрят обязательно
Даже если в эпоху .NET Framework казалось, что приложение работает как AnyCPU, на деле оно может зависеть от такого.
- COM, жёстко привязанный к
x86 - 32-bit ActiveX
- конкретная версия VC++ Runtime
- подписанные нативные DLL
Это не проблемы, которые внезапно родились из перехода на современный .NET, — это лишь всплывающие на поверхность изначально существовавшие ограничения. Именно поэтому их стоит сделать видимыми до миграции.
flowchart TB
accTitle: Как всплывают ограничения bitness
accDescr: Даже если казалось, что приложение работает как AnyCPU, оно может зависеть от x86-only COM, 32-bit ActiveX или конкретной версии VC++ Runtime, и это лишь старые ограничения, которые становятся видны при переходе на современный .NET.
any1["Казалось, что работает как AnyCPU"] -.-> hid3["Зависимость от x86 COM, 32-bit ActiveX"]
hid3 --> surf1["При миграции всплывают старые ограничения"]
surf1 --> pre2["Сделать видимыми до миграции"]
Рис. 28: Проблема bitness не порождается миграцией: просто становятся видны ограничения, которые были с самого начала.
10. EF6, сериализаторы и данные рассматривать как отдельную проблему
Миграцию runtime и перепроектирование доступа к данным / сериализации лучше по возможности держать как разные задачи — так обычно получается удачнее.
10.1 EF6 → EF Core — это не прямое обновление
В руководстве Microsoft по EF тоже сказано, что EF Core — это total rewrite EF6, и прямого пути обновления нет.
Поэтому для приложений на EF6 реалистичен такой порядок.
- сначала перейти на современный .NET
- при необходимости оставить EF6 и продолжать на нём работать
- затем перевести на EF Core уже отдельным проектом
Не совмещать миграцию runtime и миграцию ORM. Уже одно это заметно снижает сложность.
flowchart TB
accTitle: Разнести миграцию EF6 и runtime
accDescr: EF Core — полная переработка EF6 без прямого пути обновления, поэтому реалистичный порядок — сначала перейти на современный .NET, при необходимости оставить EF6, и лишь потом перевести на EF Core отдельным проектом.
e1["Сначала перейти на современный .NET"] --> e2["При необходимости оставить EF6"]
e2 --> e3["Потом EF Core отдельным проектом"]
e1 -.->|"если делать одновременно"| mix1["Путаются различия ORM и становится тяжелее"]
Рис. 29: Runtime и ORM не двигают одновременно: реалистичнее сначала перенести runtime, оставив EF6.
10.2 Что меняется, если оставить EF6
- Плюсы
- различия слоя доступа к данным можно отложить
- можно сосредоточиться на миграции business logic и app model
- «различия поведения EF Core» не примешиваются
- На что смотреть
- с точки зрения новой разработки основной вариант — EF Core
- у форм использования EF6 Designer / EDMX есть отдельные ограничения
10.3 Для EF6 на базе EDMX смотрят и design-time
В документации EF6 написано, что EF Designer напрямую не поддерживается в проектах .NET / .NET Standard и в проектах .NET Framework на SDK-стиле.
Для приложений на EDMX эти три пункта нужно рассматривать по отдельности.
- работает ли это во время выполнения
- можно ли пользоваться Designer
- как поступить со сгенерированным кодом
Если EDMX используется активно, безопаснее сразу заложить это в первоначальную оценку.
10.4 BinaryFormatter и своя сериализация легко становятся «скрытой зависимостью»
Сериализаторы легко упустить, если полагаться только на поиск по коду.
- формат сохранения
- обмен сообщениями
- кэш
- старые контракты WCF / SOAP
- ResX
- clipboard / drag & drop
Здесь замешана ещё и совместимость данных. То есть проверять нужно не просто «собралось ли», а читаются ли старые данные.
11. В объём миграции включать конфигурацию, распространение, эксплуатацию и CI/CD
Объект миграции — не только исходный код.
11.1 Файлы конфигурации
На стороне .NET Framework в app.config / web.config нередко лежит очень многое.
- connection string
- custom config section
- настройки endpoint WCF
- binding redirect
- diagnostics
- различные настройки ASP.NET
- результаты transform при установке пакетов
На стороне современного .NET способ хранения и путь чтения конфигурации в ряде случаев меняются. Поэтому «файлы конфигурации оставим на потом» — опасно.
Первый шаг — инвентаризация конфигурации.
- что лежит в файлах конфигурации
- что из этого обязательно при запуске приложения
- что относится к различиям по окружениям
- что автоматически подставляли NuGet или установщик
11.2 Способ распространения
Форму распространения тоже смотрят до старта.
- под IIS ли это
- служба Windows ли это
- Scheduled Task ли это
- ClickOnce / MSI / собственный installer
- предполагается ли on-premises сервер
- что лучше подходит: self-contained или framework-dependent
Даже если сам исполняемый модуль перенесён, если механизм распространения и запуска остаётся на старых допущениях, в конце всё упирается в тупик.
11.3 Логи, мониторинг, регламенты эксплуатации
Эксплуатационную сторону тоже легко упустить.
- предполагается ли Windows Event Log
- смотрят ли Performance Counter
- есть ли мониторинг на базе WMI
- зафиксированы ли служебная учётная запись и права
- предполагается ли, что логи пишутся в локальный файл
Совершенно обычная ситуация: код после миграции работает, а эксплуатация не выстраивается.
11.4 CI/CD и build agent
Перед миграцией проверяют и следующее.
- ставится ли нужный .NET SDK на build agent
- что делать с pipeline, завязанным на
nuget.exe/msbuild.exe - сдвигаться ли к
dotnetCLI - как обновить задания тестов, coverage, publish
- не завязаны ли внутренние шаблоны или reusable pipeline на старый формат
«Локально работает, а CI падает» — классика миграции.
flowchart TB
accTitle: Объект миграции — не только код
accDescr: Даже если исполняемый модуль перенесён, если файлы конфигурации, способ распространения, логи и мониторинг, CI/CD и build agent остаются на старых допущениях, в конце всё упирается в тупик, поэтому их тоже включают в объём миграции.
code1["Миграция исходного кода"] --> also1["Этим не заканчивается"]
also1 --> cfg1["Конфигурация и способ распространения"]
also1 --> ops1["Логи, мониторинг, эксплуатация"]
also1 --> ci1["CI/CD и build agent"]
ci1 -.-> gotcha1["Локально работает, а CI падает"]
Рис. 30: Пока не посчитаны конфигурация, распространение, эксплуатация и CI/CD, объём миграции ещё не посчитан.
12. Реалистичный порядок миграции
С учётом всего вышесказанного реалистичный ход обычно сводится примерно к следующему.
12.1 Сначала навести порядок на стороне текущего Framework
- Поднять до .NET Framework 4.7.2 или новее, по возможности до 4.8.1
- Обновить зависимости
- Пересмотреть
packages.config - По возможности сдвинуться к
PackageReferenceи SDK-стилю - Убедиться, что текущее приложение в этом состоянии действительно работает
Уже одно это заметно сокращает разницу на следующем этапе.
12.2 Сначала спасти shared library
Далее бизнес-логику и общие контракты сдвигают к netstandard2.0 или multi-target.
Порядок подъёма — leaf-first.
12.3 Для самого приложения стратегию меняют по app model
- библиотеки классов / консоль / часть служб идут сравнительно прямо
- WinForms / WPF модернизируют, оставаясь Windows-only
- ASP.NET MVC / Web API небольшие — одним махом, тяжёлые — поэтапно
- Web Forms исходят из замены экранного слоя и сначала выносят shared logic
- сервер WCF сначала решают: оставлять CoreWCF или делать редизайн на gRPC
12.4 Соблюдать правило «не делать всё сразу»
Особенно стоит избегать таких сочетаний.
- миграция runtime + полная замена ORM
- миграция runtime + смена платформы аутентификации
- миграция runtime + полный переход в облако
- миграция runtime + смена платформы мониторинга
- миграция runtime + обновление UI-фреймворка
Даже если всё это в итоге нужно, обычно лучше не складывать в один спринт.
12.5 Двигаться только после тестов и baseline
Как минимум перед стартом хочется иметь следующее.
- модульные тесты
- интеграционные тесты основных бизнес-потоков
- snapshot-проверку типичных экранов / API
- baseline по производительности
- способ проверки основных логов
- процедуру отката
Идти вперёд в состоянии, когда после миграции нельзя понять, «что именно сломалось», довольно опасно.
flowchart TB
accTitle: Что подготовить до того, как двигаться
accDescr: Если до миграции подготовить тесты, baseline по производительности, способ проверки логов и процедуру отката, после миграции можно понять, что сломалось; идти без этой подготовки опасно.
prep1["Сначала тесты и baseline"] --> mv2["Затем миграция"]
mv2 --> det1["Можно понять, что сломалось"]
non2["Идти без подготовки"] -.-> risk1["Сломается, и не найти где"]
Рис. 31: Миграцию определяет подготовка до старта: без baseline сломанное место не локализовать.
13. Чек-лист до старта
Ниже форма, которую можно сразу вставить в управление проектом.
13.1 Политика
- Можете одним предложением объяснить, зачем нужна миграция
- Определено, куда приземляемся: Windows-only современный .NET или кроссплатформенность в будущем
- Определена целевая версия .NET
- Определено, что не входит в текущий объём (переход на EF Core, обновление аутентификации, полный переход в облако и т. п.)
13.2 Подготовка текущей стороны .NET Framework
- Подняли до .NET Framework 4.7.2 или новее, по возможности до 4.8.1
- Обновили зависимости ближе к актуальным
- Выявили наличие
packages.config - Проверили, можно ли перейти на
PackageReference - Проверили, можно ли перейти на SDK-стиль
- Текущее приложение в этом состоянии собирается, запускается и проходит тесты
13.3 Тип приложения и выбор технологий
- Разбили сложность по типам: библиотека классов / десктоп / Web / WCF и т. д.
- Понимаем, что WinForms / WPF остаются Windows-only
- Понимаем, что ASP.NET Framework — это миграция app model на ASP.NET Core
- Включили в оценку замену UI-слоя Web Forms
- Оценили клиента и сервер WCF по отдельности
13.4 Неподдерживаемые технологии и API, которые стоит проверить
- Выявили зависимость от
AppDomain - Выявили Remoting /
MarshalByRefObject/BeginInvoke/EndInvoke - Выявили CAS / Security Transparency / COM+ / WF
- Выявили зависимость от BinaryFormatter
- Выявили зависимость от
System.Web
13.5 Зависимости, специфичные для Windows
- Выявили использование реестра, WMI, EventLog, служб Windows, Directory Services
- Выявили использование
System.Drawing.Common - Выявили COM / ActiveX / Office Interop / P/Invoke / нативные DLL
- Проверили ограничения x86 / x64 / ARM64
13.6 shared library и доступ к данным
- Классифицировали shared library на бизнес-логику / слой, тесно связанный с app model
- Выявили то, что можно перевести на
netstandard2.0 - Выявили библиотеки, которым нужен multi-target
- Определили, можно ли сначала перенести только runtime, сохранив EF6
- Проверили зависимость от EDMX / Designer
13.7 Эксплуатация и build
- Провели инвентаризацию файлов конфигурации
- Проверили способ распространения (IIS / Service / MSI / ClickOnce и т. д.)
- Проверили допущения о логах / мониторинге / правах / учётной записи выполнения
- Проверили, нужно ли обновлять CI/CD и build agent
- Подготовили процедуру отката
14. Итог
В миграции с .NET Framework на .NET важно не столько «какой командой переносить», сколько до старта увидеть, что идёт как есть, а что становится отдельной проблемой.
Если сузить до главного, это шесть пунктов.
- до миграции навести порядок на стороне .NET Framework
- разделить сложность по app model
- заранее выявить неподдерживаемые технологии
- решить, насколько допустима привязка к Windows
- решить, как резать shared library
- не складывать в один этап ORM, аутентификацию и переход в облако
Точность оценки миграции сильно зависит от первой недели. Иначе говоря, если за эту неделю удастся разложить вопросы, вторая половина будет заметно ближе к обычной разработке.
flowchart TB
accTitle: Как использовать первую неделю
accDescr: Если за первую неделю разложить, что идёт как есть, а что становится отдельной проблемой, точность оценки растёт, и вторая половина становится ближе к обычной разработке.
wk1["Первая неделя: разложить вопросы"] --> est1["Точность оценки растёт"]
est1 --> nor1["Дальше ближе к обычной разработке"]
wk1 -.-> pts2["Что идёт как есть, что отдельно"]
Рис. 32: Удастся ли первую неделю потратить на инвентаризацию — от этого зависит точность миграции в целом.
«Для начала просто попробуем переключиться на net10.0» — как разведка это неплохо.
Но для боевой миграции перед этим есть на что посмотреть. Именно это мы и разобрали в этой статье.
15. Справочные материалы
- Предварительные условия для портирования кода
- Обзор портирования с .NET Framework на .NET
- Технологии .NET Framework, недоступные в .NET 6 и более поздних версиях
- Что такое модернизация с GitHub Copilot
- Установка модернизации с GitHub Copilot
- Microsoft Learn, Обзор .NET Upgrade Assistant — сейчас не рекомендуется; предлагается модернизация с GitHub Copilot.
- Официальная политика поддержки .NET
- Microsoft Learn, Жизненный цикл Microsoft .NET and .NET Core — даты начала и окончания поддержки по версиям.
- Официальная политика поддержки .NET Framework
- Миграция ASP.NET Framework на ASP.NET Core с помощью инструментов
- Миграция с ASP.NET Framework на ASP.NET Core
- Get started with incremental ASP.NET to ASP.NET Core migration
- Use the Windows Compatibility Pack to port code to .NET
- .NET Standard
- Cross-platform targeting for .NET libraries
- Миграция с packages.config на PackageReference
- PackageReference in project files
- BinaryFormatter migration guide
- Руководство по миграции BinaryFormatter для Windows Forms
- WCF Client Support Policy
- CoreWCF Support Policy
- Why migrate WCF to ASP.NET Core gRPC
- Port from EF6 to EF Core
- Новые возможности EF6
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Как долго проработают приложения VB6 — поддержка среды выполнения и миграция на .NET
Как долго ещё проработают приложения на VB6? Среда выполнения поддерживается и в Windows 11, поддержка IDE уже закончилась. В статье — та...
CI/CD для WinForms / WPF: сборка, подпись и распространение в GitHub Actions
Практическое руководство по CI/CD для WinForms / WPF в GitHub Actions. Минимальный YAML сборки и тестов на windows-latest, нумерация верс...
Спящий режим, гибернация и Modern Standby: как не дать долгоживущему приложению остановиться ночью
Разбираем, почему долгоживущее Windows-приложение к утру оказывается остановленным: чем отличаются спящий режим S3, гибернация и Modern S...
Сетевые диски и UNC-пути: типичные ловушки ── как бизнес-приложению работать с файловым сервером (общей папкой)
Разбираем типичные сбои, когда бизнес-приложение пишет в общую папку или следит за ней. Почему службе не видна буква диска (Z:), какие пр...
Защита Windows-приложения от повторного запуска — именованный Mutex и активация окна при втором старте
Разбираем, как в бизнес-приложении Windows запретить повторный запуск через именованный Mutex. Разберём ловушку RDP из-за разницы Global\...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Использование и перенос существующих активов
Речь о разборе существующего кода на .NET Framework, Web Forms, WCF, COM / ActiveX и устаревшей практике NuGet, поэтому тема хорошо ложится на консультации по миграции унаследованных систем.
Технические консультации и ревью дизайна
Если до старта нужно зафиксировать объём миграции, как резать её на этапы и насколько допустима привязка к Windows, тему удобно вести как техническую консультацию и ревью архитектуры.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Что сделать в первую очередь перед миграцией с .NET Framework на .NET?
- До реализации сначала наведите порядок на стороне .NET Framework. Официальное руководство Microsoft тоже рекомендует перед портированием поднять версию до .NET Framework 4.7.2 или новее (на практике, если можно, — до 4.8.1), перевести packages.config на PackageReference, приблизить проекты к SDK-стилю и обновить зависимости ближе к актуальным. Если это сделать заранее, разница на этапе перехода на современный .NET сильно сокращается, и проще понять, проблема из-за возраста Framework или из-за самого перехода на .NET. Параллельно до старта проведите инвентаризацию неподдерживаемых технологий — AppDomain, Remoting, BinaryFormatter и подобных.
- Можно ли оставить приложение на .NET Framework?
- Да, это обычное решение. .NET Framework 4.8.1 поддерживается, пока стоит на поддерживаемой Windows, так что речь не о том, что без немедленного перехода на современный .NET система сразу окажется в опасности. Если экранов на Web Forms очень много, нужно жёстко сохранить совместимость WCF-сервера, глубоко завязаны Workflow Foundation или COM+, либо design-time компоненты третьих сторон не работают на современном .NET, разумно пока стабильно эксплуатировать 4.8.1 и параллельно строить план замены на отдельном треке. Ограничения при этом остаются: из Windows не выйти, выгоду от новой производительности и языковых возможностей .NET получить труднее.
- Какие технологии не переносятся на .NET или чаще всего стопорят работу?
- Создание AppDomain, .NET Remoting, CAS (Code Access Security), COM+ (System.EnterpriseServices) и Workflow Foundation в современном .NET не поддерживаются — это красные флаги, которые требуют пересмотра дизайна. WCF-сервер из коробки как есть не работает: придётся выбирать CoreWCF или редизайн на gRPC / HTTP API. Начиная с .NET 9 реализации BinaryFormatter в runtime нет, API всегда бросает исключение, поэтому нужен аудит сохранённых данных, ResX, clipboard и drag & drop. Отдельно смотрите install.ps1 / XDT-преобразования packages.config, нативные DLL, COM / ActiveX и неявную ставку на x86: сборка может пройти, а падение всплывёт уже в runtime.
- Если перенести WinForms или WPF на .NET, приложение станет кроссплатформенным?
- Нет. WinForms / WPF можно перенести на .NET, но они остаются Windows-only фреймворками. Миграция даёт современный runtime .NET, языковые возможности, SDK-стиль и лучшую стыковку с CI/CD — но в Linux-контейнер приложение от этого не поедет. Если в будущем нужны Linux и контейнеры, заранее инвентаризируйте Windows-only API: System.Drawing.Common, реестр, WMI, EventLog, службы Windows, COM, Office Interop. Если остаётесь на Windows, реалистичный путь — сначала модернизировать runtime через Windows Compatibility Pack.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.