Что нового в EF Core 9: функции, которые вам нужно знать

· 11 мин чтения

Entity Framework Core 9 был выпущен вместе с .NET 9 в ноябре 2024 года, и, потратив немало времени на работу с ним в нескольких проектах, я могу сказать, что это один из самых значимых выпусков за последнее время. Не потому, что он заново изобретает велосипед, а потому, что он оттачивает те области, в которых EF Core исторически вызывал больше всего проблем — преобразование запросов, производительность и работу с современными шаблонами данных.

В этом посте я расскажу о функциях, которые оказали наибольшее влияние на мою повседневную работу. Если вы все еще используете EF Core 8 (или даже 7), это должно дать вам четкое представление о том, что вас ждет по другую сторону обновления.

EF Core 9 в экосистеме .NET 9

EF Core 9 ориентирован на .NET 8 и .NET 9, а это значит, что вам не обязательно обновлять все приложение до .NET 9, чтобы воспользоваться большинством этих функций. Тем не менее, некоторые улучшения AOT и производительности тесно связаны с изменениями среды выполнения .NET 9, поэтому вы получите максимальную отдачу, пройдя весь путь.

Выпуск следует за нечетной/четной периодичностью, установленной Microsoft: выпуски с нечетными номерами (например, .NET 9) имеют стандартную поддержку (STS) с 18 месяцами поддержки, а четные выпуски (например, .NET 8) — долгосрочную поддержку (LTS). Имейте это в виду при планировании графика обновления.

Улучшения перевода LINQ

Именно здесь большинство разработчиков сразу почувствуют разницу. EF Core 9 добился значительных успехов в переводе выражений LINQ в SQL, что действительно имеет смысл.

Лучшие переводы GroupBy

Если вы когда-либо писали запрос GroupBy в EF Core и в итоге получали предупреждения при оценке на стороне клиента или странный SQL, вы знаете, что это за боль. EF Core 9 обрабатывает гораздо более широкий набор сценариев GroupBy непосредственно в SQL.

[[[ТОК_2]]]

В предыдущих версиях запросы, включающие агрегирование свойств навигации внутри GroupBy иногда возвращались к оценке клиента. EF Core 9 аккуратно преобразует это в один SQL-запрос с помощью GROUP BY, SUM, AVG и COUNT.

Сложные проекции и подзапросы

Вложенные подзапросы и сложные проекции также получили серьезное обновление. Рассмотрим что-то вроде этого:

[[[ТОК_8]]]

EF Core 9 теперь может преобразовать все это выражение в SQL, не запуская оценку на стороне клиента. Сгенерированный запрос использует коррелированные подзапросы и боковые соединения, где это необходимо, а план SQL значительно более эффективен, чем тот, который создавался в более ранних версиях.

Параметризованные примитивные коллекции

Одним из выдающихся улучшений LINQ является возможность передавать коллекции примитивных значений непосредственно в запросы:

[[[ТОК_9]]]

В EF Core 8 это было преобразовано с использованием предложений IN со встроенными значениями, что означало, что кэш плана запроса нельзя было повторно использовать при изменении списка. EF Core 9 правильно параметризует эти коллекции, отправляя их как структурированный параметр. Это очень важно для кэширования плана запроса на SQL Server и PostgreSQL.## Массовые операции — ExecuteUpdate и ExecuteDelete

ExecuteUpdate и ExecuteDelete были представлены в EF Core 7, но EF Core 9 существенно расширяет возможности, которые вы можете с ними делать.

Более сложные выражения обновления

Теперь вы можете использовать более сложные выражения в ExecuteUpdate, включая ссылки на другие таблицы через свойства навигации:

await context.Products
    .Where(p => p.Category.IsDiscontinued)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.IsAvailable, false)
        .SetProperty(p => p.DiscontinuedDate, DateTimeOffset.UtcNow)
        .SetProperty(p => p.Price, p => p.Price * 0.5m));

При этом генерируется один оператор UPDATE с JOIN для таблицы категорий — нет необходимости загружать сущности в память, нет накладных расходов на отслеживание изменений.

Условное массовое удаление с подзапросами

Массовые удаления с помощью фильтров подзапросов теперь полностью поддерживаются:

await context.AuditLogs
    .Where(log => log.CreatedAt < DateTime.UtcNow.AddYears(-2))
    .Where(log => !context.ProtectedRecords
        .Any(pr => pr.AuditLogId == log.Id))
    .ExecuteDeleteAsync();

Это преобразуется в DELETE с подзапросом NOT EXISTS — именно то, что вы написали бы вручную. Никаких сущностей не загружено, никаких обращений туда и обратно.

Улучшения столбца JSON

Столбцы JSON были одной из самых интересных функций в последних выпусках EF Core, а EF Core 9 развивает их дальше.

Запрос внутри JSON

Теперь вы можете фильтровать и проецировать данные из столбцов JSON с улучшенной поддержкой перевода:

public class Order
{
    public int Id { get; set; }
    public string CustomerName { get; set; }
    public ShippingAddress Address { get; set; } // Stored as JSON
    public List<OrderNote> Notes { get; set; }   // Stored as JSON
}

public class ShippingAddress
{
    public string Street { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string ZipCode { get; set; }
    public string Country { get; set; }
}

public class OrderNote
{
    public DateTime CreatedAt { get; set; }
    public string Text { get; set; }
    public string Author { get; set; }
}

Конфигурация в вашем DbContext:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>(builder =>
    {
        builder.OwnsOne(o => o.Address, ab =>
        {
            ab.ToJson();
        });

        builder.OwnsMany(o => o.Notes, nb =>
        {
            nb.ToJson();
        });
    });
}

Теперь вы можете делать запросы непосредственно в JSON:

var newYorkOrders = await context.Orders
    .Where(o => o.Address.State == "NY")
    .OrderByDescending(o => o.Notes.Count)
    .Select(o => new
    {
        o.CustomerName,
        o.Address.City,
        LatestNote = o.Notes
            .OrderByDescending(n => n.CreatedAt)
            .Select(n => n.Text)
            .FirstOrDefault()
    })
    .ToListAsync();

EF Core 9 генерирует правильные вызовы JSON_VALUE и JSON_QUERY для SQL Server (или эквивалента у других поставщиков), а перевод охватывает гораздо более широкий диапазон операций LINQ с элементами JSON, чем раньше.

Обновление свойств JSON

Одним из проблем в EF Core 8 было то, что обновление одного свойства внутри столбца JSON приводило к переписыванию всего документа JSON. EF Core 9 улучшает эту ситуацию за счет более детального отслеживания изменений для типов, отображаемых в формате JSON, и по возможности генерирует более целевые обновления.

var order = await context.Orders.FindAsync(orderId);
order.Address.ZipCode = "10001";
await context.SaveChangesAsync();

У поддерживаемых поставщиков это может привести к более целенаправленной модификации JSON, а не к переписыванию всего большого двоичного объекта.

Сложные типы — объекты-значения без идентификатора

Сложные типы — это одна из функций, которую так ждали специалисты по предметно-ориентированному проектированию. В отличие от принадлежащих типов, сложные типы не имеют идентичности — это чистые объекты значений.

[ComplexType]
public record Money(decimal Amount, string Currency);

[ComplexType]
public record DateRange(DateTime Start, DateTime End);

public class Project
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Money Budget { get; set; }
    public DateRange Timeline { get; set; }
}

Они сохраняются в виде объединенных столбцов в родительской таблице — Budget_Amount, Budget_Currency, Timeline_Start, Timeline_End — без необходимости использования отдельной таблицы или какого-либо ключа.

Ключевое отличие от принадлежащих типов: сложные типы сравниваются по значению, а не по ссылке. Два экземпляра Money с одинаковыми Amount и Currency считаются равными, независимо от того, какой сущности они принадлежат.

var expensiveProjects = await context.Projects
    .Where(p => p.Budget.Amount > 100_000m && p.Budget.Currency == "USD")
    .OrderByDescending(p => p.Budget.Amount)
    .ToListAsync();

Это напрямую приводит к фильтрации по сглаженным столбцам — чистой, эффективной и именно такой, как вы ожидаете.

Поддержка HierarchyId для SQL Server

Если вы когда-либо работали с иерархическими данными в SQL Server — организационными диаграммами, деревьями категорий, файловыми системами — вы знаете, что HierarchyId — это встроенный тип для этого. EF Core 9 обеспечивает первоклассную поддержку.

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Title { get; set; }
    public HierarchyId PathFromCeo { get; set; }
}

Теперь вы можете напрямую запрашивать иерархические отношения:

var managerId = HierarchyId.Parse("/1/3/");

// All direct and indirect reports
var allReports = await context.Employees
    .Where(e => e.PathFromCeo.IsDescendantOf(managerId))
    .Where(e => e.PathFromCeo != managerId) // exclude the manager
    .OrderBy(e => e.PathFromCeo)
    .ToListAsync();

// Direct reports only (one level down)
var directReports = await context.Employees
    .Where(e => e.PathFromCeo.GetAncestor(1) == managerId)
    .ToListAsync();

// Get an employee's depth in the hierarchy
var employeesWithDepth = await context.Employees
    .Select(e => new
    {
        e.Name,
        e.Title,
        Level = e.PathFromCeo.GetLevel()
    })
    .OrderBy(e => e.Level)
    .ToListAsync();
```Все это преобразуется в собственные методы SQL Server `HierarchyId`. Если вы реализуете древовидные структуры с самоссылающимися внешними ключами и рекурсивными CTE, это гораздо более чистый подход.

## Скомпилированные модели и поддержка AOT

Разработчики, заботящиеся о производительности, оценят постоянные инвестиции в скомпилированные модели и поддержку предварительной компиляции (AOT).

### Скомпилированные модели

Скомпилированные модели предварительно создают метаданные модели, которые EF Core обычно создает при запуске. Для больших моделей (например, сотен объектов) это может значительно сократить время холодного запуска.

```bash
dotnet ef dbcontext optimize --output-dir CompiledModels --namespace MyApp.CompiledModels

Затем подключите его:

builder.Services.AddDbContext<AppDbContext>(options =>
    options
        .UseSqlServer(connectionString)
        .UseModel(MyApp.CompiledModels.AppDbContextModel.Instance));

В EF Core 9 скомпилированные модели являются более полными — они поддерживают больше функций сопоставления и генерируют меньший объем выходных данных. Для модели, содержащей около 400 объектов, время запуска может сократиться с нескольких секунд до почти мгновенного.

Ход компиляции AOT

Полная встроенная поддержка AOT для EF Core все еще находится в стадии разработки, но EF Core 9 добился значительных успехов. Многие пути кода с большим количеством отражений были реорганизованы для обеспечения возможности обрезки, а скомпилированные модели являются ключевой частью истории AOT. Если вы ориентируетесь на такие сценарии, как функции Azure или микросервисы, где важен холодный запуск, эти улучшения имеют непосредственное значение.

Обновления поставщика Cosmo DB

Поставщик Azure Cosmos DB продолжает развиваться с помощью EF Core 9. Некоторые заметные улучшения:

Обработка ключей раздела

Поставщик теперь поддерживает иерархические ключи разделов и более разумно обрабатывает фильтры ключей разделов:

public class TenantDocument
{
    public string Id { get; set; }
    public string TenantId { get; set; }  // Partition key
    public string Region { get; set; }     // Sub-partition key
    public string Content { get; set; }
}
// This query now correctly uses the partition key for routing
var docs = await context.TenantDocuments
    .Where(d => d.TenantId == "tenant-42" && d.Region == "us-east")
    .Where(d => d.Content.Contains("important"))
    .ToListAsync();

Улучшен перевод LINQ в NoSQL

Больше операций LINQ теперь переводится на диалект SQL Cosmos DB, включая улучшенную поддержку Contains, Any, операций с вложенными массивами и математических функций. Запросы, которые раньше возвращались к оценке клиента, теперь обрабатываются на стороне сервера.

Поддержка векторного поиска

В EF Core 9 на ранней стадии реализована поддержка поиска по сходству векторов с помощью Cosmos DB, что полезно, если вы создаете приложения, которые интегрируются с внедрениями или поиском на основе искусственного интеллекта:

var results = await context.Documents
    .OrderBy(d => EF.Functions.VectorDistance(d.Embedding, queryVector))
    .Take(10)
    .ToListAsync();

Улучшения миграции

Миграции получили некоторые улучшения качества жизни, которые делают работу с ними менее болезненной в командной среде.

Временные таблицы в миграции

Миграции теперь обрабатывают конфигурацию темпоральной таблицы более изящно, с надлежащей поддержкой столбцов периода и именования таблиц истории:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Employee>()
        .ToTable("Employees", b => b.IsTemporal(t =>
        {
            t.HasPeriodStart("ValidFrom");
            t.HasPeriodEnd("ValidTo");
            t.UseHistoryTable("EmployeeHistory");
        }));
}

Идемпотентные сценарии

Команда Script-Migration (и ее эквивалент в CLI) по умолчанию создает более качественные идемпотентные сценарии с улучшенной обработкой крайних случаев, связанных с изменениями схемы, которые зависят от данных, существующих в определенных состояниях.

Пакеты миграции

Пакеты миграции, которые упаковывают ваши миграции в автономный исполняемый файл для развертывания, более надежны в EF Core 9 благодаря улучшенным отчетам об ошибках и логике повторных попыток в случае временных сбоев.

dotnet ef migrations bundle --self-contained -r linux-x64

В результате создается двоичный файл, который можно запустить в конвейере CI/CD без необходимости установки .NET SDK на целевой объект развертывания.

Тесты производительностиВот некоторые грубые тесты из моего собственного тестирования. Они взяты из проекта, насчитывающего примерно 200 сущностей, работающего на SQL Server 2022, измеренного с помощью BenchmarkDotNet. Ваши цифры будут различаться, но относительные улучшения должны быть одинаковыми.

СценарийEF Core 8EF Core 9Улучшение
Сборка модели (холодный старт)1850 мс320 мс~5,8 раз быстрее (скомпилировано)
Простой запрос (один объект по ПК)0,42 мс0,38 мс~10% быстрее
Сложный запрос (объединение + агрегация)3,1 мс2,4 мс~ на 23% быстрее
Массовое обновление (10 тыс. строк)145 мс118 мс~ на 19% быстрее
Запрос столбца JSON2,8 мс1,9 мс~32% быстрее
СохранитьИзменения (100 объектов)48 мс41 мс~ на 15 % быстрее

Улучшение скомпилированной модели является наиболее значительным, но устойчивые улучшения по всем направлениям суммируются, особенно в сценариях с высокой пропускной способностью, когда вы выполняете тысячи запросов в секунду.

Обновление с EF Core 8

Если вы используете EF Core 8, путь обновления относительно гладкий. Вот контрольный список:

1. Обновите свои пакеты:

<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />

2. Проверьте наличие критических изменений. Список EF Core 9 относительно короткий по сравнению с некоторыми предыдущими выпусками. Наиболее примечательные из них:

  • Некоторые ранее устаревшие API были удалены.
  • Изменения в том, как преобразуются определенные запросы GroupBy (теперь они выполняются на стороне сервера, что меняет поведение, если вы полагались на оценку клиента).
  • Незначительные изменения в выходных данных миграции.

3. Повторно сгенерируйте скомпилированные модели, если вы их используете. Формат изменился, поэтому старые скомпилированные модели не будут работать с EF Core 9.

4. Запустите набор тестов. Обратите особое внимание на запросы, которые ранее оценивались на клиенте — теперь они могут оцениваться на сервере, что обычно лучше, но может выявить различия в данных.

5. Проверьте свои запросы Cosmos DB, если вы используете этого поставщика. Улучшенные переводы означают, что некоторые запросы будут выполняться по-другому (обычно быстрее), но стоит убедиться, что результаты идентичны.

Минимальный апгрейд для типичного проекта выглядит так:

dotnet add package Microsoft.EntityFrameworkCore --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.0
dotnet build
dotnet test

Если все компилируется и тесты проходят успешно, возможно, вы в хорошей форме. Если у вас возникнут проблемы, документация по критическим изменениям EF Core 9 содержит подробные инструкции по миграции для каждого изменения.

Подведение итогов

EF Core 9 — это не революционный выпуск, а эволюционный, и именно таким он и должен был быть. Одни только улучшения LINQ оправдывают обновление для большинства проектов, а такие функции, как усовершенствования столбцов JSON, сложные типы и поддержка HierarchyId открывают шаблоны, которые ранее были неуклюжими или невозможными.

Если бы мне пришлось выбрать три функции, которые оказали наибольшее влияние на мои проекты:

  1. Параметризованные коллекции примитивов — поскольку эффективность кэша плана запроса имеет значение в масштабе
  2. Усовершенствования столбцов JSON — потому что шаблон гибридного реляционного документа невероятно полезен.
  3. Скомпилированные модели — поскольку время запуска напрямую влияет на продуктивность разработчиков и скорость развертывания.Команда EF Core добилась уверенного прогресса со времен EF Core 5, и версия 9 продолжает эту тенденцию. Если вы уже используете EF Core 8, обновление сопряжено с низким риском и высокой прибылью. Если вы используете что-то старое, сейчас лучшее время, чтобы совершить прыжок.

Удачного кодирования — и удачных запросов.