Миграция с EF Core 6 на EF Core 11: критические изменения, которые действительно бьют
Руководство по миграции с фиксированными версиями, с EF Core 6.0 на EF Core 11.0, проходящее через критические изменения EF7, 8, 9, 10 и 11, которые ломают реальные приложения: Encrypt=True, Contains через OPENJSON, PendingModelChangesWarning, нативный столбец json и разделение SqlClient 7.0.
Переход с EF Core 6.0 на EF Core 11.0 — это пять мажорных версий за один прыжок, и болезненные части почти никогда не являются переименованиями API. Это тихие изменения поведения: строка подключения, работавшая годами, теперь выбрасывает ошибку SSL, запрос Contains внезапно достигает тайм-аута на старом SQL Server, а развёртывание прерывается, потому что EF решил, что в вашей модели есть ожидающие изменения. Закладывайте полдня на небольшой сервис и от двух до четырёх дней на монолит с нетривиальной моделью, пользовательскими конвертерами значений и потоком scaffolding в режиме database-first. Ничто из этого не является дверью в одну сторону на уровне базы данных, но два изменения (значение по умолчанию Encrypt в EF Core 7 и исключение PendingModelChangesWarning в EF Core 9) не дадут вашему приложению запуститься в первый же день, если вы не запланируете их.
Это руководство фиксирует Microsoft.EntityFrameworkCore 6.0 как источник и 11.0 как цель, работающие на .NET 11. Поскольку нижняя граница target framework у EF Core по пути растёт (EF Core 7 требует .NET 6, EF Core 8 и 9 требуют .NET 8, EF Core 10 требует .NET 10, а EF Core 11 требует .NET 11), это также миграция среды выполнения. Если вы ещё не перевели среду выполнения, сделайте это сначала, используя контрольный список миграции с .NET 8 на .NET 11, и возвращайтесь.
Зачем мигрировать сейчас
- EF Core 6.0 вышел из поддержки в ноябре 2024 года. Вы запускаете неподдерживаемый слой данных против поддерживаемой среды выполнения — худшее из обоих миров для проверки безопасности.
- EF Core 11 приносит реальный выигрыш в запросах бесплатно: трансляцию
Containsна основеOPENJSON(трижды доработанную с EF 8), более точные оценки кардинальности через несколько скалярных параметров и нативный тип столбцаjsonSQL Server с первоклассной индексацией. ExecuteUpdateиExecuteDelete, добавленные в EF Core 7 и улучшаемые в каждой версии с тех пор, превращают записи на основе множеств в одну SQL-инструкцию. Если вы всё ещё загружаете сущности, чтобы их изменить, вы оставляете на столе один-два порядка величины. Смотрите ExecuteUpdate против загрузки сущностей и SaveChanges для бенчмарка.- Защитные ограждения миграций, появившиеся в EF 9 (обнаружение ожидающих изменений модели) и EF 11 (обнаружение отсутствия миграций), отлавливают целый класс инцидентов в продакшене типа «база данных и модель тихо разошлись».
Что ломается
Это совокупный список по всем пяти версиям. Серьёзность — это вероятность того, что сломается типичное приложение, а не сложность исправления.
| Область | Изменение | Версия | Серьёзность |
|---|---|---|---|
| Подключения к SQL Server | Encrypt теперь по умолчанию true; недоверенные сертификаты выбрасывают исключение | EF 7 | высокая |
| SaveChanges с триггерами | Путь с предложением OUTPUT ломается на таблицах с триггерами или некоторыми вычисляемыми столбцами | EF 7 | высокая |
| Необязательные связи | Осиротевшие зависимые сущности больше не удаляются автоматически при разрыве связи | EF 7 | средняя |
Contains по списку | Транслируется через OPENJSON; не работает ниже SQL Server 2016 / уровня совместимости 130 | EF 8 | высокая |
Производительность Contains | План OPENJSON может сильно деградировать на некоторых нагрузках | EF 8 | высокая |
| Enum в столбцах JSON | По умолчанию хранятся как int вместо string | EF 8 | высокая |
| Строковые ключи на SQL Server | Сравниваются без учёта регистра в трекере изменений | EF 8 | средняя |
| Применение миграций | PendingModelChangesWarning теперь выбрасывает исключение при Migrate() | EF 9 | высокая |
| Миграции в транзакции | Внешняя транзакция вокруг Migrate() теперь выбрасывает исключение | EF 9 | высокая |
EF.Constant / EF.Parameter | Выбрасывают InvalidCastException внутри скомпилированных запросов | EF 9 | низкая |
| Инструменты EF, мультицелевые проекты | Теперь требуется --framework | EF 10 | средняя |
| Параметризованные коллекции | Трансляция по умолчанию теперь — несколько скалярных параметров | EF 10 | низкая |
| Хранение JSON на SQL Server | JSON в nvarchar(max) мигрирует на нативный json при уровне совместимости 170 / Azure SQL | EF 10 | низкая |
| Migrate без миграций | По умолчанию выбрасывает исключение вместо журналирования | EF 11 | низкая |
Microsoft.Data.SqlClient 7.0 | Зависимости аутентификации Entra ID выносятся в отдельный пакет | EF 11 | средняя |
Авторитетные списки по версиям приведены в конце. Прочитайте страницы EF 7, EF 8 и EF 9 перед началом; именно эти три несут изменения высокой серьёзности.
Предварительный контрольный список
- Переведите среду выполнения на .NET 11 и сначала подтвердите чистый
dotnet testна старых пакетах EF Core 6. Вы хотите, чтобы за раз менялась только одна переменная, чтобы первое «красное» после повышения EF было однозначным. - Проведите инвентаризацию своего провайдера. SQL Server, SQLite, PostgreSQL (Npgsql) и Cosmos — у каждого свои критические изменения. Это руководство сосредоточено на SQL Server и указывает на SQLite там, где он отличается.
- Проверьте версию и уровень совместимости вашего SQL Server. Изменению
Containsиз EF 8 нужен уровень совместимости 130 или выше:-- run against your target database SELECT name, compatibility_level FROM sys.databases; - Найдите
Database.Migrate(иMigrateAsync(. Каждое место вызова — кандидат на исключение об ожидающих изменениях из EF 9 и исключение о явной транзакции из EF 9. - Найдите
.HasConversion<string>()на enum и любые свойства enum, отображённые внутрь owned-типов, сопоставленных с JSON. Это изменение enum в JSON из EF 8. - Отметьте, используете ли вы аутентификацию Entra ID (Azure AD) в какой-либо строке подключения (
Authentication=Active Directory Default, управляемая идентичность, сервисный принципал). Это разделение SqlClient из EF 11. - Создайте ветку для миграции и сделайте резервную копию базы данных. Миграции, изменяющие схему (максимальная длина дискриминатора, нативный
json), генерируются автоматически и должны быть проверены до запуска против продакшена.
Шаги миграции
-
Поднимите все пакеты EF Core до 11.0 за один раз. Не поднимайтесь по одной версии за раз; критические изменения накопительны и задокументированы по версиям, поэтому один прыжок с открытой документацией быстрее, чем пять промежуточных компиляций. Обновите
Microsoft.EntityFrameworkCore, провайдер (Microsoft.EntityFrameworkCore.SqlServer) иMicrosoft.EntityFrameworkCore.Design. Проверьте черезdotnet restoreиdotnet buildи считайте первые ошибки компиляции истинным объёмом работ.<!-- src/MyApp.csproj, EF Core 11 on .NET 11 --> <ItemGroup> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="11.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="11.0.0" PrivateAssets="all" /> </ItemGroup> -
Обновите инструмент
dotnet efдо соответствующей мажорной версии. Инструмент 6.x не может прочитать модель 11.0. Проверьте черезdotnet ef --versionи подтвердите11.0.x.dotnet tool update --global dotnet-ef --version 11.* -
Сначала исправьте значение по умолчанию
Encrypt=True. Это изменение EF Core 7, которое живёт вMicrosoft.Data.SqlClient, а не в EF, поэтому переключателя на стороне EF нет. На машине разработчика без доверенного сертификата сервера ваше первое подключение выбрасывает ошибку SSL. Для локальной разработки добавьтеTrustServerCertificate=True; в продакшене установите действительный сертификат. Проверьте, открыв одно подключение:dotnet ef dbcontext infoдолжен подключиться без ошибки провайдера SSL.Server=localhost;Database=App;Trusted_Connection=True;TrustServerCertificate=True -
Обработайте защитные ограждения миграций в каждом месте вызова
Migrate(). EF Core 9 выбрасываетPendingModelChangesWarning, если модель отличается от последней миграции, а EF Core 11 выбрасываетMigrationsNotFound, если миграций нет вовсе. Если вы управляете схемой через миграции, исправление — добавить недостающую миграцию. Если вы управляете схемой иначе (Dapper, DACPAC, написанный вручную SQL) и вызываетеMigrate()лишь по привычке, удалите вызов или подавите предупреждения. Проверьте, запустивdotnet ef migrations has-pending-model-changesи получив чистый результат.// EF Core 11. Only suppress if you intentionally manage schema elsewhere. protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.ConfigureWarnings(w => { w.Ignore(RelationalEventId.PendingModelChangesWarning); w.Ignore(RelationalEventId.MigrationsNotFound); }); -
Удалите любую явную транзакцию, оборачивающую
Migrate(). Распространённый паттерн «устойчивой миграции» (начать транзакцию, мигрировать, зафиксировать, внутри стратегии выполнения) выбрасываетMigrationsUserTransactionWarningв EF Core 9, потому что EF теперь управляет транзакцией и блокировкой базы данных сам. Удалите обёртку и вызывайтеMigrateAsyncнапрямую. Проверьте, что приложение запускается и применяет миграции один раз.// EF Core 9+. EF manages the transaction and execution strategy. await dbContext.Database.MigrateAsync(cancellationToken); -
Подтвердите уровень совместимости SQL Server для изменения
Contains. Еслиsys.databasesсообщает уровень ниже 130, трансляцияOPENJSONиз EF Core 8 не сработает во время выполнения. Поднимите уровень, если можете, или зафиксируйте режим трансляции. Проверьте, запустив запрос, использующий.Where(x => list.Contains(x.Id)), и подтвердив корректный SQL.// EF Core 10+: pick the translation strategy explicitly. // Constant = pre-EF8 inlining, Parameter = OPENJSON, MultipleParameters = EF10 default. protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlServer(connectionString, o => o.UseParameterizedCollectionMode(ParameterTranslationMode.MultipleParameters)); -
Зафиксируйте хранение enum в JSON, если вы полагаетесь на строковые значения. EF Core 8 изменил enum внутри owned-типов, сопоставленных с JSON, со строк на целые числа. Существующие документы, записанные EF 6, содержат строки; после обновления EF читает их как целые числа и падает. Принудительно задайте конвертацию в строку, чтобы старые данные оставались читаемыми. Проверьте, прогнав туда-обратно сущность со свойством enum в JSON-столбце.
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) => configurationBuilder.Properties<OrderStatus>().HaveConversion<string>(); -
Добавьте миграцию для изменений схемы, которые теперь хочет EF. EF Core 8 задаёт столбцам дискриминатора TPH ограниченную максимальную длину, а EF Core 10 сопоставляет JSON-столбцы с нативным типом
jsonна Azure SQL или при уровне совместимости 170. Сгенерируйте миграцию, прочитайте её и только затем применяйте. Проверьте обзором сгенерированных операцийAlterColumn.dotnet ef migrations add UpgradeToEfCore11 dotnet ef migrations script --idempotent --output migrate.sql -
Вынесите аутентификацию Entra ID, если вы её используете. EF Core 11 переходит на
Microsoft.Data.SqlClient7.0, который удаляет зависимости аутентификации Azure из основного пакета. Если строка подключения использует аутентификациюActive Directory, добавьте пакет расширений. Проверьте, подключившись с управляемой идентичностью в развёрнутом окружении, а не только локально.<PackageReference Include="Microsoft.Data.SqlClient.Extensions.Azure" Version="7.0.0" />
Проверка
Выполните этот дымовой тест после миграции, по порядку:
dotnet buildчист, включая разрешение ссылки наMicrosoft.EntityFrameworkCore.Design(инструменты EF 11 больше не подтягивают её транзитивно).dotnet ef migrations has-pending-model-changesсообщает об отсутствии ожидающих изменений.- Приложение запускается, и
Migrate()(если вы его вызываете) применяется чисто, безPendingModelChangesWarningилиMigrationsNotFound. dotnet testзелёный. Обратите внимание на тесты, утверждающие сгенерированные строки SQL; трансляцииContainsи параметризованных коллекций изменились, поэтому снапшот-утверждения нужно будет обновить.- Выполните запрос, фильтрующий по
Containsнад списком, и подтвердите, что он выполняется, а не просто компилируется. - Выборочно проверьте сущность, сопоставленную с JSON, со свойством enum и сущность со строковым ключом, используемую в связи, на корректность значений.
План отката
Изменения пакетов и кода обратимы: откатите ветку, восстановите пакеты EF Core 6.0 и понизьте инструмент dotnet-ef. Риск — миграция схемы из шага 8. Изменения максимальной длины дискриминатора и нативного json меняют базу данных, и EF Core 6.0 не будет знать о миграции, помеченной EF Core 11. Если вам нужна возможность откатить среду выполнения после применения этой миграции, сначала сгенерируйте скрипт отката (dotnet ef migrations script UpgradeToEfCore11 PreviousMigration) и храните его вместе с релизом. Без этого скрипта изменение схемы фактически необратимо для бинарника EF 6.
Спотыкания, с которыми мы столкнулись
Тайм-аут Contains — самый коварный. Запрос компилируется, возвращает корректные результаты и проходит все тесты на небольшом наборе данных. Затем продакшен-таблица с миллионами строк попадает на план OPENJSON, и запрос достигает тайм-аута. Команда EF дорабатывала это трижды: EF 8 ввёл OPENJSON, EF 9 добавил TranslateParameterizedCollectionsToConstants, а EF 10 сменил значение по умолчанию на несколько скалярных параметров. Если вы видите регрессию, аварийный клапан на уровне запроса — EF.Constant(list).Contains(...), чтобы встроить значения в этот один запрос, оставив глобальное значение по умолчанию нетронутым. Руководство по обнаружению N+1 и руководство по разделению запросов охватывают соседние ловушки формы запроса, которые стоит проверить в том же проходе.
Строковые ключи без учёта регистра тихо меняют сопоставление. EF Core 8 заставил провайдер SQL Server сравнивать значения строковых ключей без учёта регистра в трекере изменений, чтобы соответствовать тому, как SQL Server сопоставляет внешние ключи. Если ваш код полагался на то, что "ABC" и "abc" — разные ключи в памяти, трекер изменений теперь трактует их как одну сущность. Исправление — пользовательский ValueComparer с учётом регистра на этих ключах, но сначала подтвердите, что вы действительно на это полагаетесь; большинству приложений нужно новое поведение.
Исключение об ожидающих изменениях срабатывает на динамических seed-данных. Модель, которая делает seed через HasData с использованием DateTime.UtcNow или Guid.NewGuid(), выглядит для EF «изменённой» при каждой сборке, поэтому EF 9 выбрасывает PendingModelChangesWarning, даже если вы ничего не меняли. Замените динамические значения на статические константы в seed или перейдите на паттерн seeding из EF 9. Этот легко ошибочно принять за настоящий баг миграции.
Вывод scaffolding в режиме database-first меняет форму. Если вы повторно делаете scaffolding из базы данных, EF 8 теперь генерирует DateOnly и TimeOnly для столбцов date и time, убирает nullable-обёртку у логических столбцов со значением по умолчанию и иначе именует навигации для составных внешних ключей. Ничто из этого не ломает работающее приложение, но создаёт большой шумный diff, который легко принять за ошибку. Делайте повторный scaffolding в отдельном коммите, чтобы diff можно было просмотреть.
Пять мажорных версий звучит тяжелее, чем есть на самом деле. Два изменения остановят ваше приложение намертво при первом запуске (значение по умолчанию Encrypt и исключение миграции), а одно — скрытая ловушка производительности (трансляция Contains). Запланируйте эти три, сгенерируйте и прочитайте единственную миграцию схемы, а остальное — это повышения версий пакетов и зелёный прогон тестов. Для более широкой стороны среды выполнения того же обновления контрольный список миграции с .NET 8 на .NET 11 охватывает изменения framework, ASP.NET Core и C# 14, которые идут вместе с этим.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.