Start Debugging

Миграция с 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, и возвращайтесь.

Зачем мигрировать сейчас

Что ломается

Это совокупный список по всем пяти версиям. Серьёзность — это вероятность того, что сломается типичное приложение, а не сложность исправления.

ОбластьИзменениеВерсияСерьёзность
Подключения к SQL ServerEncrypt теперь по умолчанию true; недоверенные сертификаты выбрасывают исключениеEF 7высокая
SaveChanges с триггерамиПуть с предложением OUTPUT ломается на таблицах с триггерами или некоторыми вычисляемыми столбцамиEF 7высокая
Необязательные связиОсиротевшие зависимые сущности больше не удаляются автоматически при разрыве связиEF 7средняя
Contains по спискуТранслируется через OPENJSON; не работает ниже SQL Server 2016 / уровня совместимости 130EF 8высокая
Производительность ContainsПлан OPENJSON может сильно деградировать на некоторых нагрузкахEF 8высокая
Enum в столбцах JSONПо умолчанию хранятся как int вместо stringEF 8высокая
Строковые ключи на SQL ServerСравниваются без учёта регистра в трекере измененийEF 8средняя
Применение миграцийPendingModelChangesWarning теперь выбрасывает исключение при Migrate()EF 9высокая
Миграции в транзакцииВнешняя транзакция вокруг Migrate() теперь выбрасывает исключениеEF 9высокая
EF.Constant / EF.ParameterВыбрасывают InvalidCastException внутри скомпилированных запросовEF 9низкая
Инструменты EF, мультицелевые проектыТеперь требуется --frameworkEF 10средняя
Параметризованные коллекцииТрансляция по умолчанию теперь — несколько скалярных параметровEF 10низкая
Хранение JSON на SQL ServerJSON в nvarchar(max) мигрирует на нативный json при уровне совместимости 170 / Azure SQLEF 10низкая
Migrate без миграцийПо умолчанию выбрасывает исключение вместо журналированияEF 11низкая
Microsoft.Data.SqlClient 7.0Зависимости аутентификации Entra ID выносятся в отдельный пакетEF 11средняя

Авторитетные списки по версиям приведены в конце. Прочитайте страницы EF 7, EF 8 и EF 9 перед началом; именно эти три несут изменения высокой серьёзности.

Предварительный контрольный список

  1. Переведите среду выполнения на .NET 11 и сначала подтвердите чистый dotnet test на старых пакетах EF Core 6. Вы хотите, чтобы за раз менялась только одна переменная, чтобы первое «красное» после повышения EF было однозначным.
  2. Проведите инвентаризацию своего провайдера. SQL Server, SQLite, PostgreSQL (Npgsql) и Cosmos — у каждого свои критические изменения. Это руководство сосредоточено на SQL Server и указывает на SQLite там, где он отличается.
  3. Проверьте версию и уровень совместимости вашего SQL Server. Изменению Contains из EF 8 нужен уровень совместимости 130 или выше:
    -- run against your target database
    SELECT name, compatibility_level FROM sys.databases;
  4. Найдите Database.Migrate( и MigrateAsync(. Каждое место вызова — кандидат на исключение об ожидающих изменениях из EF 9 и исключение о явной транзакции из EF 9.
  5. Найдите .HasConversion<string>() на enum и любые свойства enum, отображённые внутрь owned-типов, сопоставленных с JSON. Это изменение enum в JSON из EF 8.
  6. Отметьте, используете ли вы аутентификацию Entra ID (Azure AD) в какой-либо строке подключения (Authentication=Active Directory Default, управляемая идентичность, сервисный принципал). Это разделение SqlClient из EF 11.
  7. Создайте ветку для миграции и сделайте резервную копию базы данных. Миграции, изменяющие схему (максимальная длина дискриминатора, нативный json), генерируются автоматически и должны быть проверены до запуска против продакшена.

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

  1. Поднимите все пакеты 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>
  2. Обновите инструмент dotnet ef до соответствующей мажорной версии. Инструмент 6.x не может прочитать модель 11.0. Проверьте через dotnet ef --version и подтвердите 11.0.x.

    dotnet tool update --global dotnet-ef --version 11.*
  3. Сначала исправьте значение по умолчанию 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
  4. Обработайте защитные ограждения миграций в каждом месте вызова 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);
        });
  5. Удалите любую явную транзакцию, оборачивающую Migrate(). Распространённый паттерн «устойчивой миграции» (начать транзакцию, мигрировать, зафиксировать, внутри стратегии выполнения) выбрасывает MigrationsUserTransactionWarning в EF Core 9, потому что EF теперь управляет транзакцией и блокировкой базы данных сам. Удалите обёртку и вызывайте MigrateAsync напрямую. Проверьте, что приложение запускается и применяет миграции один раз.

    // EF Core 9+. EF manages the transaction and execution strategy.
    await dbContext.Database.MigrateAsync(cancellationToken);
  6. Подтвердите уровень совместимости 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));
  7. Зафиксируйте хранение enum в JSON, если вы полагаетесь на строковые значения. EF Core 8 изменил enum внутри owned-типов, сопоставленных с JSON, со строк на целые числа. Существующие документы, записанные EF 6, содержат строки; после обновления EF читает их как целые числа и падает. Принудительно задайте конвертацию в строку, чтобы старые данные оставались читаемыми. Проверьте, прогнав туда-обратно сущность со свойством enum в JSON-столбце.

    protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
        => configurationBuilder.Properties<OrderStatus>().HaveConversion<string>();
  8. Добавьте миграцию для изменений схемы, которые теперь хочет 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
  9. Вынесите аутентификацию Entra ID, если вы её используете. EF Core 11 переходит на Microsoft.Data.SqlClient 7.0, который удаляет зависимости аутентификации Azure из основного пакета. Если строка подключения использует аутентификацию Active Directory, добавьте пакет расширений. Проверьте, подключившись с управляемой идентичностью в развёрнутом окружении, а не только локально.

    <PackageReference Include="Microsoft.Data.SqlClient.Extensions.Azure" Version="7.0.0" />

Проверка

Выполните этот дымовой тест после миграции, по порядку:

  1. dotnet build чист, включая разрешение ссылки на Microsoft.EntityFrameworkCore.Design (инструменты EF 11 больше не подтягивают её транзитивно).
  2. dotnet ef migrations has-pending-model-changes сообщает об отсутствии ожидающих изменений.
  3. Приложение запускается, и Migrate() (если вы его вызываете) применяется чисто, без PendingModelChangesWarning или MigrationsNotFound.
  4. dotnet test зелёный. Обратите внимание на тесты, утверждающие сгенерированные строки SQL; трансляции Contains и параметризованных коллекций изменились, поэтому снапшот-утверждения нужно будет обновить.
  5. Выполните запрос, фильтрующий по Contains над списком, и подтвердите, что он выполняется, а не просто компилируется.
  6. Выборочно проверьте сущность, сопоставленную с 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.

< Назад