Новые возможности EF Core 5.0

В приведенном ниже списке указаны ключевые новые функции EF Core 5.0. Полный перечень проблем в этом выпуске см. в средстве отслеживания проблем.

EF Core 5.0 — это основной выпуск. Он содержит несколько критических изменений, касающихся улучшений API и поведения. Такие изменения могут негативно повлиять на работу существующих приложений.

Многие ко многим

В EF Core 5.0 поддерживаются связи "многие ко многим" без явного сопоставления таблицы соединения.

Например, рассмотрим такие типы сущностей:

public class Post
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Tag> Tags { get; set; }
}

public class Tag
{
    public int Id { get; set; }
    public string Text { get; set; }
    public ICollection<Post> Posts { get; set; }
}

По соглашению EF Core 5.0 распознает их как связь "многие ко многим" и автоматически создает в базе данных таблицу соединения PostTag. Данные могут запрашиваться и обновляться без явных ссылок на таблицу соединения, что значительно упрощает код. При необходимости таблицу соединений по-прежнему можно настроить для явного выполнения запросов.

Дополнительные сведения см. в полной документации по связи "многие ко многим".

Разделенные запросы

Начиная с EF Core 3.0, EF Core всегда создает один SQL-запрос для каждого LINQ-запроса. Это обеспечивает согласованность данных, возвращаемых в рамках ограничений используемого режима транзакций. Однако если запрос использует Include или проекцию для возврата нескольких связанных коллекций, это может привести к снижению производительности.

Теперь EF Core 5.0 позволяет разделять один LINQ-запрос, включая связанные коллекции, на несколько SQL-запросов. Это может значительно повысить производительность, но может также привести к несогласованности возвращаемых результатов в случае изменения данных между двумя запросами. Для устранения этой проблемы и обеспечения согласованности с разделенными запросами можно использовать сериализуемые транзакции или транзакции с моментальными снимками, но это может привести к дополнительным затратам на производительность и вызвать различия в поведении.

Рассмотрим запрос, который извлекает два уровня связанных коллекций с помощью Include:

var artists = context.Artists
    .Include(e => e.Albums)
    .ToList();

Если используется поставщик SQLite, EF Core по умолчанию создаст следующий SQL:

SELECT a."Id", a."Name", a0."Id", a0."ArtistId", a0."Title"
FROM "Artists" AS a
LEFT JOIN "Album" AS a0 ON a."Id" = a0."ArtistId"
ORDER BY a."Id", a0."Id"

При использовании разделенных запросов вместо этого создается следующий код SQL.

SELECT a."Id", a."Name"
FROM "Artists" AS a
ORDER BY a."Id"

SELECT a0."Id", a0."ArtistId", a0."Title", a."Id"
FROM "Artists" AS a
INNER JOIN "Album" AS a0 ON a."Id" = a0."ArtistId"
ORDER BY a."Id"

Разделение запросов можно включить, разместив новый оператор AsSplitQuery в любом месте запроса LINQ или глобально в модели OnConfiguring. Дополнительные сведения см. в полной документации по разделенным запросам.

Простое ведение журнала и улучшенная диагностика

В EF Core 5.0 появился простой способ настройки ведения журнала с помощью нового метода LogTo. Следующий код задает вывод в консоль сообщений журнала, включая весь код SQL, создаваемый EF Core.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.LogTo(Console.WriteLine);

Кроме того, теперь можно вызвать метод ToQueryString для любого запроса LINQ, извлекая код SQL, который будет выполнен запросом.

Console.WriteLine(
    ctx.Artists
    .Where(a => a.Name == "Pink Floyd")
    .ToQueryString());

Наконец, различные типы EF Core были дополнены расширенным свойством DebugView, которое предоставляет подробные сведения о внутренних компонентах. Например, можно обратиться к ChangeTracker.DebugView, чтобы узнать, какие сущности отслеживаются в данный момент времени.

Дополнительные сведения см. в документации по ведению журнала и перехватам.

Включение с фильтрацией

Метод Include теперь поддерживает фильтрацию включенных сущностей.

var blogs = context.Blogs
    .Include(e => e.Posts.Where(p => p.Title.Contains("Cheese")))
    .ToList();

Этот запрос возвращает блоги со всеми связанными записями, но только в том случае, если заголовок записи содержит Cheese.

Дополнительные сведения см. в полной документации по функции "Включение с фильтрацией".

Сопоставление типа "одна таблица на тип" (TPT)

По умолчанию в EF Core иерархия наследования типов .NET сопоставляется с одной таблицей базы данных. Это называется сопоставлением "одна таблица на иерархию" (TPH). В EF Core 5.0 каждый тип .NET в иерархии наследования также может сопоставляться с отдельной таблицей базы данных. Это называется сопоставлением "одна таблица на тип" (TPT).

Например, возьмем следующую модель с сопоставленной иерархией.

public class Animal
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Cat : Animal
{
    public string EducationLevel { get; set; }
}

public class Dog : Animal
{
    public string FavoriteToy { get; set; }
}

При использовании сопоставления "одна таблица на тип" (TPT) для каждого типа в иерархии создается таблица базы данных.

CREATE TABLE [Animals] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NULL,
    CONSTRAINT [PK_Animals] PRIMARY KEY ([Id])
);

CREATE TABLE [Cats] (
    [Id] int NOT NULL,
    [EducationLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Cats_Animals_Id] FOREIGN KEY ([Id]) REFERENCES [Animals] ([Id]) ON DELETE NO ACTION,
);

CREATE TABLE [Dogs] (
    [Id] int NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Dogs_Animals_Id] FOREIGN KEY ([Id]) REFERENCES [Animals] ([Id]) ON DELETE NO ACTION,
);

Дополнительные сведения см. в полной документации по сопоставлению "одна таблица на тип" (TPT).

Гибкое сопоставление сущностей

Типы сущностей обычно сопоставляются с таблицами или представлениями так, что EF Core будет извлекать содержимое таблицы или представления при запросе соответствующего типа. EF Core 5.0 добавляет дополнительные варианты сопоставления, в которых сущность может быть сопоставлена с SQL-запросом (называемым определяющим запросом) или с функцией с табличным значением.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>().ToSqlQuery(
        @"SELECT Id, Name, Category, BlogId FROM posts
          UNION ALL
          SELECT Id, Name, ""Legacy"", BlogId from legacy_posts");

    modelBuilder.Entity<Blog>().ToFunction("BlogsReturningFunction");
}

Функции с табличным значением также могут сопоставляться с методом .NET, а не с DbSet, что позволяет передавать параметры. При этом сопоставление можно настроить с HasDbFunction.

Наконец, теперь можно сопоставлять сущность с представлением при запросе (или с функцией или определяющим запросом) и с таблицей при обновлении.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Blog>()
        .ToTable("Blogs")
        .ToView("BlogsView");
}

Общие типы сущностей и контейнеры свойств

В EF Core 5.0 один тип CLR может быть сопоставлен с несколькими различными типами сущностей, которые называются общими. Хотя с этой функцией можно использовать любой тип CLR, .NET Dictionary предлагает полезный вариант применения, который мы называем контейнерами свойств.

public class ProductsContext : DbContext
{
    public DbSet<Dictionary<string, object>> Products => Set<Dictionary<string, object>>("Product");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.SharedTypeEntity<Dictionary<string, object>>("Product", b =>
        {
            b.IndexerProperty<int>("Id");
            b.IndexerProperty<string>("Name").IsRequired();
            b.IndexerProperty<decimal>("Price");
        });
    }
}

Впоследствии можно выполнять запросы к этим сущностям и обновлять их так же, как и в случае с обычными типами сущностей с собственным выделенным типом CLR. Дополнительные сведения можно найти в документации по контейнерам свойств.

Обязательные зависимости "один к одному"

В EF Core 3.1 зависимая сторона связи "один к одному" всегда считалась необязательной. Это было особенно очевидно при использовании принадлежащих сущностей, поскольку все их столбцы создавались в базе данных как допускающие значение NULL, даже если они были настроены в соответствии с требованиями модели.

В EF Core 5.0 навигацию к принадлежащей сущности теперь можно настроить как обязательную зависимость. Например:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>(b =>
    {
        b.OwnsOne(e => e.HomeAddress,
            b =>
            {
                b.Property(e => e.City).IsRequired();
                b.Property(e => e.Postcode).IsRequired();
            });
        b.Navigation(e => e.HomeAddress).IsRequired();
    });
}

DbContextFactory

В EF Core 5.0 введены методы AddDbContextFactory и AddPooledDbContextFactory для регистрации фабрики с целью создания экземпляров DbContext в контейнере внедрения зависимостей приложения. Это может быть полезно, если в коде приложения требуется создавать и удалять экземпляры контекста вручную.

services.AddDbContextFactory<SomeDbContext>(b =>
    b.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test"));

На этом этапе службы приложений, такие как контроллеры ASP.NET Core, могут добавляться с помощью IDbContextFactory<TContext> и использовать его для создания экземпляров контекста.

public class MyController : Controller
{
    private readonly IDbContextFactory<SomeDbContext> _contextFactory;

    public MyController(IDbContextFactory<SomeDbContext> contextFactory)
        => _contextFactory = contextFactory;

    public void DoSomeThing()
    {
        using (var context = _contextFactory.CreateDbContext())
        {
            // ...
        }
    }
}

Дополнительные сведения см. в полной документации по DbContextFactory.

Перестроение таблиц SQLite

По сравнению с другими базами данных таблицы SQLite относительно ограничены возможностями обработки схем. Например, в этой системе не поддерживается удаление столбца из существующей таблицы. EF Core 5.0 позволяет обойти эти ограничения путем автоматического создания новой таблицы, копирования в нее данных из старой таблицы, удаления старой таблицы и переименования новой. Таким образом, таблица перестраивается, в связи с чем к ней можно безопасно применять не поддерживаемые ранее операции миграции.

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

Параметры сортировки базы данных

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

Например, следующий параметр настраивает столбец Name в SQL Server как чувствительный к регистру символов, в связи с чем все создаваемые для этого столбца индексы будут работать соответствующим образом.

modelBuilder
    .Entity<User>()
    .Property(e => e.Name)
    .UseCollation("SQL_Latin1_General_CP1_CS_AS");

Дополнительные сведения см. в полной документации по параметрам сортировки и чувствительности к регистру.

Счетчики событий

В EF Core 5.0 предоставляются счетчики событий, которые можно использовать для отслеживания производительности приложения и выявления различных аномалий. Просто подключитесь к процессу, в котором выполняется EF, с помощью средства dotnet-counters.

> dotnet counters monitor Microsoft.EntityFrameworkCore -p 49496

[Microsoft.EntityFrameworkCore]
    Active DbContexts                                               1
    Execution Strategy Operation Failures (Count / 1 sec)           0
    Execution Strategy Operation Failures (Total)                   0
    Optimistic Concurrency Failures (Count / 1 sec)                 0
    Optimistic Concurrency Failures (Total)                         0
    Queries (Count / 1 sec)                                     1,755
    Queries (Total)                                            98,402
    Query Cache Hit Rate (%)                                      100
    SaveChanges (Count / 1 sec)                                     0
    SaveChanges (Total)                                             1

Дополнительные сведения см. в полной документации по счетчикам событий.

Другие функции

Построение модели

  • Были представлены API построения модели, позволяющие упростить настройку функций сравнения значений.
  • Теперь вычисляемые столбцы можно настроить как хранимые или виртуальные.
  • Точность и масштаб теперь можно настраивать с помощью текучего API.
  • Были представлены новые API-интерфейсы построения модели для свойств навигации.
  • Были представлены новые API-интерфейсы построения модели для полей, аналогичные свойствам.
  • Типы .NET PhysicalAddress и IPAddress теперь можно сопоставлять со строковыми столбцами базы данных.
  • Резервное поле теперь можно настраивать с помощью нового атрибута [BackingField].
  • Теперь разрешены резервные поля, допускающие значения NULL. За счет этого обеспечивается лучшая поддержка генерируемых хранилищем значений по умолчанию в тех случаях, когда значение по умолчанию CLR не является хорошим значением-меткой (то есть bool).
  • С помощью нового атрибута [Index] можно указывать в типе сущности индекс вместо использования текучего API.
  • С помощью нового атрибута [Keyless] можно настраивать тип сущности как не имеющий ключа.
  • По умолчанию EF Core теперь рассматривает дискриминатор как полный, то есть не видит значения дискриминатора, которые не настроены приложением в модели. Такое поведение позволяет в определенной степени повысить производительность и может быть отключено, если столбец дискриминатора может содержать неизвестные значения.

Query

  • В исключениях ошибок преобразования запросов теперь приводятся более явные причины возникновения сбоя, помогающие выявить проблему.
  • Запросы без отслеживания теперь могут выполнять разрешение идентификаторов, что позволяет исключить возвращение нескольких экземпляров сущности для одного и того же объекта базы данных.
  • Добавлена поддержка GroupBy с условными статистическими выражениями (например, GroupBy(o => o.OrderDate).Select(g => g.Count(i => i.OrderDate != null))).
  • Добавлена поддержка преобразования оператора Distinct над элементами группы перед выполнением статистического вычисления.
  • Преобразование Reverse.
  • Оптимизировано связанное с DateTime преобразование для SQL Server (например, DateDiffWeek, DateFromParts).
  • Преобразование новых методов для массивов байтов (например, Contains, Length, SequenceEqual).
  • Преобразование некоторых дополнительных побитовых операторов, например дополнения до двух.
  • Преобразование FirstOrDefault для строк.
  • Оптимизировано преобразование запросов, связанных с семантикой значений NULL, что позволяет получить более строгие и эффективные запросы.
  • Сопоставленные пользователем функции теперь можно аннотировать для управления распространением значений NULL, что также позволяет получить более строгие и эффективные запросы.
  • Значительно сокращен код SQL, содержащий блоки CASE.
  • Функцию SQL Server DATALENGTH теперь можно вызывать в запросах с помощью нового метода EF.Functions.DataLength.
  • EnableDetailedErrors добавляет дополнительные сведения в исключения.

Сохранение

  • Перехват и события SaveChanges.
  • Реализованы API-интерфейсы для управления точками сохранения транзакций. Кроме того, EF Core будет автоматически создавать точку сохранения при вызове SaveChanges, если транзакция уже выполняется, и выполнять откат к этой точке в случае сбоя.
  • Идентификатор транзакции может явно задаваться приложением, что позволяет упростить корреляцию событий транзакций в журнале и других местах.
  • Заданный по умолчанию максимальный размер пакета для SQL Server изменен до 42 по результатам анализа производительности пакетной обработки.

Миграция и формирование шаблонов

  • Таблицы теперь можно исключать из миграции.
  • Новая команда dotnet ef migrations list показывает, какие миграции еще не были применены к базе данных (команда Get-Migration выполняет те же действия в консоли управления пакетами).
  • Скрипты миграции теперь содержат инструкции транзакций, необходимые для более эффективной обработки сбоев приложения миграции.
  • Столбцы для несопоставленных базовых классов теперь упорядочиваются после столбцов для сопоставленных типов сущностей. Обратите внимание, что это касается только вновь созданных таблиц: порядок столбцов для существующих таблиц остается неизменным.
  • Теперь при создании миграции можно учитывать, является ли создаваемая миграция идемпотентной и будут ли выходные данные выполняться немедленно или создаваться в виде скрипта.
  • Добавлены новые параметры командной строки, позволяющие задавать пространства имен для миграции и формирования шаблонов.
  • Команда dotnet ef database update теперь принимает новый параметр --connection, позволяющий задать строку подключения.
  • При формировании шаблонов для существующих баз данных имена таблиц преобразуются в форму единственного числа, поэтому таблицы с именами People и Addresses будут формироваться по типам сущностей с именами Person и Address. Исходные имена базы данных по-прежнему можно сохранить.
  • Новый параметр --no-onconfiguring может указывать EF Core на необходимость исключить OnConfiguring при формировании шаблона модели.

Azure Cosmos DB

Sqlite

  • Теперь поддерживаются вычисляемые столбцы.
  • Теперь получение двоичных и строковых данных с помощью GetBytes, GetChars и GetTextReader осуществляется более эффективно за счет использования SqliteBlob и потоков.
  • Инициализация SqliteConnection выполняется в отложенном режиме.

Другой

  • Теперь можно создавать прокси-объекты отслеживания изменений, которые автоматически реализуют интерфейсы INotifyPropertyChanging и INotifyPropertyChanged. Таким образом обеспечивается альтернативный подход к отслеживанию изменений, при котором не требуется проверка на наличие изменений при вызове SaveChanges.
  • Теперь возможно изменение DbConnection или строки подключения в уже инициализированном экземпляре DbContext.
  • Новый метод ChangeTracker.Clear очищает DbContext от всех отслеживаемых сущностей. Обычно это не требуется, если выполняется рекомендация по созданию нового экземпляра контекста с кратким сроком существования для каждой единицы работы. Однако если необходимо сбросить состояние экземпляра DbContext, то использование нового метода Clear() является более эффективным и надежным подходом, чем массовое отсоединение всех сущностей.
  • Программы командной строки EF Core теперь автоматически задают для переменных среды ASPNETCORE_ENVIRONMENTиDOTNET_ENVIRONMENT значение Development. Это обеспечивает согласованную работу при использовании универсального узла и ASP.NET Core во время разработки.
  • Пользовательские аргументы командной строки могут передаваться в IDesignTimeDbContextFactory<TContext>, что позволяет приложениям управлять способами создания и инициализации контекста.
  • Теперь в SQL Server можно настроить коэффициент заполнения индекса.
  • Новое IsRelational свойство можно использовать для различения при использовании реляционного поставщика и нереляционного поставщика (например, в памяти).