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

EF Core 9 (EF9) — следующий выпуск после EF Core 8 и запланирован на выпуск в ноябре 2024 года.

EF9 доступен как ежедневные сборки , которые содержат все последние функции EF9 и настройки API. Примеры, приведенные здесь, используют эти ежедневные сборки.

Совет

Вы можете выполнить и выполнить отладку в примерах, скачав пример кода с GitHub. Каждый раздел ниже ссылается на исходный код, характерный для этого раздела.

EF9 предназначен для .NET 8 и, следовательно, может использоваться с .NET 8 (LTS) или предварительной версией .NET 9.

Совет

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

Azure Cosmos DB for NoSQL

EF 9.0 обеспечивает существенное улучшение поставщика EF Core для Azure Cosmos DB; Значительные части поставщика были перезаписаны, чтобы обеспечить новые функциональные возможности, разрешить новые формы запросов и лучше выровнять поставщика с рекомендациями Azure Cosmos DB. Ниже перечислены основные улучшения высокого уровня. Полный список см. в этой эпической проблеме.

Предупреждение

В рамках улучшений, которые вносятся в поставщик, необходимо вносить ряд критически важных изменений; Если вы обновляете существующее приложение, внимательно ознакомьтесь с разделом критических изменений.

Улучшение запросов с помощью ключей секций и идентификаторов документов

Каждый документ, хранящийся в базе данных Azure Cosmos DB, имеет уникальный идентификатор ресурса. Кроме того, каждый документ может содержать "ключ секции", который определяет логическую секционирование данных таким образом, чтобы база данных была эффективно масштабирована. Дополнительные сведения о выборе ключей секций можно найти в разделении и горизонтальном масштабировании в Azure Cosmos DB.

В EF 9.0 поставщик Azure Cosmos DB значительно лучше определяет сравнения ключей секций в запросах LINQ и извлекает их, чтобы убедиться, что запросы отправляются только в соответствующую секцию; Это может значительно повысить производительность запросов и сократить расходы на ЕЗ. Например:

var sessions = await context.Sessions
    .Where(b => b.PartitionKey == "someValue" && b.Username.StartsWith("x"))
    .ToListAsync();

В этом запросе поставщик автоматически распознает сравнение PartitionKeyпо; если мы рассмотрим журналы, мы увидим следующее:

Executed ReadNext (189.8434 ms, 2.8 RU) ActivityId='8cd669ed-2ca5-4f2b-8923-338899071361', Container='test', Partition='["someValue"]', Parameters=[]
SELECT VALUE c
FROM root c
WHERE STARTSWITH(c["Username"], "x")

Обратите внимание, что WHERE предложение не содержит PartitionKey: сравнение было "снято" и используется для выполнения запроса только в соответствующей секции. В предыдущих версиях сравнение было оставлено в WHERE предложении во многих ситуациях, что привело к выполнению запроса ко всем секциям, что привело к увеличению затрат и снижению производительности.

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

var somePartitionKey = "someValue";
var someId = 8;
var sessions = await context.Sessions
    .Where(b => b.PartitionKey == somePartitionKey && b.Id == someId)
    .SingleAsync();

В журналах показано следующее для этого запроса:

Executed ReadItem (73 ms, 1 RU) ActivityId='13f0f8b8-d481-47f0-bf41-67f7deb008b2', Container='test', Id='8', Partition='["someValue"]'

Здесь запрос SQL не отправляется вообще. Вместо этого поставщик выполняет чрезвычайно эффективное чтение точек (ReadItem API), которое напрямую извлекает документ с помощью ключа секции и идентификатора. Это наиболее эффективный и экономичный вид чтения, который можно выполнить в Azure Cosmos DB; Дополнительные сведения о точках чтения см. в документации по Azure Cosmos DB.

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

Ключи иерархических разделов

Совет

Код, показанный здесь, поступает из HierarchicalPartitionKeysSample.cs.

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

Ключи секций задаются с помощью API сборки модели, как правило, в DbContext.OnModelCreating. Для каждого уровня ключа секции должен быть сопоставлено свойство в типе сущности. Например, рассмотрим тип сущности UserSession :

public class UserSession
{
    // Item ID
    public Guid Id { get; set; }

    // Partition Key
    public string TenantId { get; set; } = null!;
    public Guid UserId { get; set; }
    public int SessionId { get; set; }

    // Other members
    public string Username { get; set; } = null!;
}

Следующий код задает трехуровневый ключ секции с помощью TenantIdи UserIdSessionId свойства:

modelBuilder
    .Entity<UserSession>()
    .HasPartitionKey(e => new { e.TenantId, e.UserId, e.SessionId });

Совет

Это определение ключа секции следует примеру, приведенному в разделе "Выбор ключей иерархических разделов" из документации по Azure Cosmos DB.

Обратите внимание, как, начиная с EF Core 9, свойства любого сопоставленного типа можно использовать в ключе секции. Для bool и числовых типов, таких как int SessionId свойство, значение используется непосредственно в ключе секции. Другие типы, такие как Guid UserId свойство, автоматически преобразуются в строки.

При выполнении запросов EF автоматически извлекает значения ключа секции из запросов и применяет их к API запросов Azure Cosmos DB, чтобы убедиться, что запросы ограничены соответствующим количеством возможных секций. Например, рассмотрим следующий запрос LINQ, который предоставляет все три значения ключа секции в иерархии:

var tenantId = "Microsoft";
var sessionId = 7;
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");

var sessions = await context.Sessions
    .Where(
        e => e.TenantId == tenantId
             && e.UserId == userId
             && e.SessionId == sessionId
             && e.Username.Contains("a"))
    .ToListAsync();

При выполнении этого запроса EF Core извлекает значения tenantIduserIdпараметров и sessionId параметров и передает их в API запросов Azure Cosmos DB в качестве значения ключа секции. Например, ознакомьтесь с журналами выполнения приведенного выше запроса:

info: 6/10/2024 19:06:00.017 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing SQL query for container 'UserSessionContext' in partition '["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a"))

Обратите внимание, что сравнения ключей секций были удалены из WHERE предложения и вместо этого используются в качестве ключа секции для эффективного выполнения: ["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]

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

Значительно улучшены возможности запросов LINQ

В EF 9.0 возможности перевода LINQ поставщика Azure Cosmos DB были значительно расширены, и поставщик теперь может выполнять значительно больше типов запросов. Полный список улучшений запросов слишком длинный для списка, но ниже приведены основные моменты.

  • Полная поддержка примитивных коллекций EF, что позволяет выполнять запросы LINQ для коллекций, например ints или строк. Дополнительные сведения см . в статье "Новые возможности EF8: примитивные коллекции ".
  • Поддержка произвольных запросов по немитивным коллекциям.
  • Теперь поддерживаются множество дополнительных операторов LINQ: индексирование в коллекции, Length/Count, , ContainsElementAtи многие другие.
  • Поддержка статистических операторов, таких как Count и Sum.
  • Дополнительные переводы функций (см . документацию по сопоставлениям функций для полного списка поддерживаемых переводов):
    • Переводы элементов DateTime и DateTimeOffset элементов компонента (DateTime.Year... DateTimeOffset.Month).
    • EF.Functions.IsDefined и EF.Functions.CoalesceUndefined теперь разрешать работу со значениями undefined .
    • string.Containsи StartsWith EndsWith теперь поддерживается StringComparison.OrdinalIgnoreCase.

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

Улучшенная модель, согласованная со стандартами Azure Cosmos DB и JSON

EF 9.0 сопоставляется с документами Azure Cosmos DB более естественными способами для базы данных документов на основе JSON и помогает взаимодействовать с другими системами, обращаюющимися к документам. Хотя это влечет за собой критические изменения, API существуют, которые позволяют вернуться к поведению до 9.0 во всех случаях.

Упрощенные id свойства без дискриминации

Во-первых, предыдущие версии EF вставляют дискриминационные значения в свойство JSON id , создавая такие документы, как:

{
    "id": "Blog|1099",
    ...
}

Это было сделано, чтобы разрешить документам различных типов (например, блог и запись) и одному значению ключа (1099) существовать в одной секции контейнера. Начиная с EF 9.0, id свойство содержит только значение ключа:

{
    "id": 1099,
    ...
}

Это более естественный способ сопоставления с JSON и упрощает взаимодействие внешних средств и систем с документами JSON, созданными EF; такие внешние системы обычно не знают о дискриминационных значениях EF, которые по умолчанию являются производными от типов .NET.

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

Дискриминированное свойство, переименованное в $type

Ранее было названо Discriminatorдискриминационные свойства по умолчанию. EF 9.0 изменяет значение по умолчанию $type:

{
    "id": 1099,
    "$type": "Blog",
    ...
}

Это следует за новым стандартом для полиморфизма JSON, что позволяет лучше взаимодействовать с другими инструментами. Например. System.Text.Json в NET также поддерживает полиморфизм, используя $type в качестве имени свойства по умолчанию (docs).

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

Поиск сходства векторов (предварительная версия)

Azure Cosmos DB теперь предлагает предварительную версию поддержки поиска сходства векторов. Векторный поиск является основной частью некоторых типов приложений, включая ИИ, семантический поиск и другие. Azure Cosmos DB позволяет хранить векторы непосредственно в документах вместе с остальными данными, что означает, что вы можете выполнять все запросы к одной базе данных. Это может значительно упростить архитектуру и удалить необходимость дополнительного выделенного решения векторной базы данных в стеке. Дополнительные сведения о поиске векторов Azure Cosmos DB см. в документации.

После правильной настройки контейнера Azure Cosmos DB использование векторного поиска с помощью EF является простым вопросом добавления свойства вектора и его настройки:

public class Blog
{
    ...

    public float[] Vector { get; set; }
}

public class BloggingContext
{
    ...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .Property(b => b.Embeddings)
            .IsVector(DistanceFunction.Cosine, dimensions: 1536);
    }
}

После этого используйте функцию EF.Functions.VectorDistance() в запросах LINQ для выполнения поиска сходства векторов:

var blogs = await context.Blogs
    .OrderBy(s => EF.Functions.VectorDistance(s.Vector, vector))
    .Take(5)
    .ToListAsync();

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

Поддержка разбивки на страницы

Поставщик Azure Cosmos DB теперь позволяет выполнять разбивку по результатам запроса с помощью маркеров продолжения, что гораздо эффективнее и экономично, чем традиционное использование Skip и Take:

var firstPage = await context.Posts
    .OrderBy(p => p.Id)
    .ToPageAsync(pageSize: 10, continuationToken: null);

var continuationToken = firstPage.ContinuationToken;
foreach (var post in page.Values)
{
    // Display/send the posts to the user
}

Новый ToPageAsync оператор возвращает CosmosPageмаркер продолжения, который можно использовать для эффективного возобновления запроса на более позднюю точку, извлекая следующие 10 элементов:

var nextPage = await context.Sessions.OrderBy(s => s.Id).ToPageAsync(10, continuationToken);

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

FromSql для более безопасного запроса SQL

Поставщик Azure Cosmos DB разрешил sql-запрос через FromSqlRaw. Однако этот API может быть подвержен атакам внедрения SQL, если предоставленные пользователем данные интерполируются или объединяются в SQL. В EF 9.0 теперь можно использовать новый FromSql метод, который всегда интегрирует параметризованные данные в качестве параметра за пределами SQL:

var maxAngle = 8;
_ = await context.Blogs
    .FromSql($"SELECT VALUE c FROM root c WHERE c.Angle1 <= {maxAngle}")
    .ToListAsync();

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

Ролевой доступ

Azure Cosmos DB для NoSQL включает встроенную систему управления доступом на основе ролей (RBAC). Теперь эта поддержка поддерживается EF9 для всех операций плоскости данных. Однако пакет SDK Azure Cosmos DB не поддерживает RBAC для операций плоскости управления в Azure Cosmos DB. Используйте API управления Azure вместо EnsureCreatedAsync RBAC.

Синхронный ввод-вывод теперь заблокирован по умолчанию

Azure Cosmos DB для NoSQL не поддерживает синхронные (блокирующие) API из кода приложения. Ранее EF маскировал это, блокируя для вас асинхронные вызовы. Однако это поощряет синхронное использование ввода-вывода, что является плохой практикой и может привести к взаимоблокировкам. Поэтому, начиная с EF 9, исключение возникает при попытке синхронного доступа. Например:

Синхронный ввод-вывод можно использовать сейчас, настроив уровень предупреждения соответствующим образом. Например, в OnConfiguring типе DbContext :

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.ConfigureWarnings(b => b.Ignore(CosmosEventId.SyncNotSupported));

Обратите внимание, что мы планируем полностью удалить поддержку синхронизации в EF 11, поэтому начните обновление, чтобы использовать асинхронные методы, как ToListAsync и SaveChangesAsync как можно скорее!

AOT и предварительно скомпилированные запросы

Как упоминалось в вводном представлении, существует много работы, чтобы разрешить EF Core работать без JIT-компиляции. Вместо этого EF компилируется заранее (AOT) все необходимое для выполнения запросов в приложении. Эта компиляция AOT и связанная обработка будут выполняться в рамках создания и публикации приложения. На этом этапе в выпуске EF9 существует не так много доступных приложений, которые можно использовать разработчиком приложений. Однако для тех, кто заинтересован, завершенные проблемы в EF9, поддерживающие AOT и предварительно скомпилированные запросы, являются следующими:

Ознакомьтесь с примерами использования предварительно скомпилированных запросов по мере объединения взаимодействия.

Перевод LINQ и SQL

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

Слишком большое количество улучшений, чтобы перечислить их здесь. Ниже выделены некоторые из более важных улучшений; См . эту проблему , чтобы получить более полный список выполненных работ в версии 9.0.

Мы хотели бы обратиться к Андреа Канчиани (@ranma42) за его многочисленные, высококачественные вклады в оптимизацию SQL, которая создается EF Core!

Сложные типы: поддержка GroupBy и ExecuteUpdate

GroupBy

Совет

Код, показанный здесь, поступает из ComplexTypesSample.cs.

EF9 поддерживает группировку по сложному экземпляру типа. Например:

var groupedAddresses = await context.Stores
    .GroupBy(b => b.StoreAddress)
    .Select(g => new { g.Key, Count = g.Count() })
    .ToListAsync();

EF преобразует это как группирование по каждому элементу сложного типа, который соответствует семантике сложных типов в качестве объектов значений. Например, в SQL Azure:

SELECT [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode], COUNT(*) AS [Count]
FROM [Stores] AS [s]
GROUP BY [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode]

ExecuteUpdate

Совет

Код, показанный здесь, поступает из ExecuteUpdateSample.cs.

Аналогичным образом, в EF9 ExecuteUpdate также было улучшено принятие свойств сложного типа. Однако каждый элемент сложного типа должен быть явно указан. Например:

var newAddress = new Address("Gressenhall Farm Shop", null, "Beetley", "Norfolk", "NR20 4DR");

await context.Stores
    .Where(e => e.Region == "Germany")
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.StoreAddress, newAddress));

Это создает SQL, который обновляет каждый столбец, сопоставленный с сложным типом:

UPDATE [s]
SET [s].[StoreAddress_City] = @__complex_type_newAddress_0_City,
    [s].[StoreAddress_Country] = @__complex_type_newAddress_0_Country,
    [s].[StoreAddress_Line1] = @__complex_type_newAddress_0_Line1,
    [s].[StoreAddress_Line2] = NULL,
    [s].[StoreAddress_PostCode] = @__complex_type_newAddress_0_PostCode
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'

Ранее необходимо вручную вывести список различных свойств сложного типа в ExecuteUpdate вызове.

Ненужимые элементы из SQL

Ранее EF иногда создавал SQL, содержащий элементы, которые на самом деле не нужны; В большинстве случаев они, возможно, были необходимы на более ранней стадии обработки SQL и были оставлены позади. EF9 теперь обрезает большинство таких элементов, что приводит к более компактным и, в некоторых случаях, более эффективному SQL.

Обрезка таблицы

В первом примере SQL, создаваемый EF, иногда содержал joIN в таблицы, которые фактически не нужны в запросе. Рассмотрим следующую модель, которая использует сопоставление наследования таблиц на тип (TPT):

public class Order
{
    public int Id { get; set; }
    ...

    public Customer Customer { get; set; }
}

public class DiscountedOrder : Order
{
    public double Discount { get; set; }
}

public class Customer
{
    public int Id { get; set; }
    ...

    public List<Order> Orders { get; set; }
}

public class BlogContext : DbContext
{
    ...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Order>().UseTptMappingStrategy();
    }
}

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

var customers = await context.Customers.Where(o => o.Orders.Any()).ToListAsync();

EF8 создал следующий SQL:

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
    SELECT 1
    FROM [Orders] AS [o]
    LEFT JOIN [DiscountedOrders] AS [d] ON [o].[Id] = [d].[Id]
    WHERE [c].[Id] = [o].[CustomerId])

Обратите внимание, что запрос содержал соединение с DiscountedOrders таблицей, даже если на нее не ссылались столбцы. EF9 создает обрезаемый SQL без соединения:

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
    SELECT 1
    FROM [Orders] AS [o]
    WHERE [c].[Id] = [o].[CustomerId])

Прорезка проекции

Аналогичным образом давайте рассмотрим следующий запрос:

var orders = await context.Orders
    .Where(o => o.Amount > 10)
    .Take(5)
    .CountAsync();

В EF8 этот запрос создал следующий SQL:

SELECT COUNT(*)
FROM (
    SELECT TOP(@__p_0) [o].[Id]
    FROM [Orders] AS [o]
    WHERE [o].[Amount] > 10
) AS [t]

Обратите внимание, что проекция [o].[Id] не требуется в подзапросе, так как внешнее выражение SELECT просто подсчитывает строки. EF9 создает следующее:

SELECT COUNT(*)
FROM (
    SELECT TOP(@__p_0) 1 AS empty
    FROM [Orders] AS [o]
    WHERE [o].[Amount] > 10
) AS [s]

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

Переводы с участием GREATEST/LEAST

Совет

Код, показанный здесь, поступает из LeastGreatestSample.cs.

Были введены несколько новых переводов, использующих GREATEST функции и LEAST функции SQL.

Внимание

Функции GREATEST были представлены в базах данных SQL Server или Azure SQL в версии 2022 года.LEAST Visual Studio 2022 устанавливает SQL Server 2019 по умолчанию. Мы рекомендуем установить SQL Server Developer Edition 2022 , чтобы попробовать эти новые переводы в EF9.

Например, запросы с помощью Math.Max или Math.Min теперь переводятся для SQL Azure, используя GREATEST и LEAST соответственно. Например:

var walksUsingMin = await context.Walks
    .Where(e => Math.Min(e.DaysVisited.Count, e.ClosestPub.Beers.Length) > 4)
    .ToListAsync();

Этот запрос преобразуется в следующий SQL при использовании EF9 для SQL Server 2022:

SELECT [w].[Id], [w].[ClosestPubId], [w].[DaysVisited], [w].[Name], [w].[Terrain]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
WHERE LEAST((
    SELECT COUNT(*)
    FROM OPENJSON([w].[DaysVisited]) AS [d]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Beers]) AS [b])) >

Math.Min также Math.Max можно использовать для значений примитивной коллекции. Например:

var pubsInlineMax = await context.Pubs
    .SelectMany(e => e.Counts)
    .Where(e => Math.Max(e, threshold) > top)
    .ToListAsync();

Этот запрос преобразуется в следующий SQL при использовании EF9 для SQL Server 2022:

SELECT [c].[value]
FROM [Pubs] AS [p]
CROSS APPLY OPENJSON([p].[Counts]) WITH ([value] int '$') AS [c]
WHERE GREATEST([c].[value], @__threshold_0) > @__top_1

Наконец, RelationalDbFunctionsExtensions.Least и RelationalDbFunctionsExtensions.Greatest можно использовать для непосредственного Least вызова или Greatest функции в SQL. Например:

var leastCount = await context.Pubs
    .Select(e => EF.Functions.Least(e.Counts.Length, e.DaysVisited.Count, e.Beers.Length))
    .ToListAsync();

Этот запрос преобразуется в следующий SQL при использовании EF9 для SQL Server 2022:

SELECT LEAST((
    SELECT COUNT(*)
    FROM OPENJSON([p].[Counts]) AS [c]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[DaysVisited]) AS [d]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Beers]) AS [b]))
FROM [Pubs] AS [p]

Принудительное или запретить параметризацию запросов

Совет

Код, показанный здесь, поступает из QuerySample.cs.

За исключением некоторых особых случаев, EF Core параметризует переменные, используемые в запросе LINQ, но включает константы в созданном SQL. Например, рассмотрим следующий метод запроса:

async Task<List<Post>> GetPosts(int id)
    => await context.Posts
        .Where(e => e.Title == ".NET Blog" && e.Id == id)
        .ToListAsync();

Это преобразуется в следующие параметры SQL и параметров при использовании SQL Azure:

Executed DbCommand (1ms) [Parameters=[@__id_0='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = @__id_0

Обратите внимание, что EF создал константу в SQL для блога .NET, так как это значение не изменится с запроса на запрос. Использование константы позволяет проверять это значение ядром СУБД при создании плана запроса, что может привести к более эффективному запросу.

С другой стороны, значение id параметризуется, так как один и тот же запрос может выполняться с множеством различных значений.id Создание константы в этом случае приведет к загрязнением кэша запросов с большим количеством запросов, которые отличаются только значениями id . Это очень плохо для общей производительности базы данных.

Как правило, эти значения по умолчанию не должны быть изменены. Однако EF Core 8.0.2 представляет EF.Constant метод, который заставляет EF использовать константу, даже если параметр будет использоваться по умолчанию. Например:

async Task<List<Post>> GetPostsForceConstant(int id)
    => await context.Posts
        .Where(e => e.Title == ".NET Blog" && e.Id == EF.Constant(id))
        .ToListAsync();

Теперь перевод содержит константу для id значения:

Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = 1

Метод EF.Parameter

EF9 вводит EF.Parameter метод, чтобы сделать противоположное. То есть принудительно ef использовать параметр, даже если значение является константой в коде. Например:

async Task<List<Post>> GetPostsForceParameter(int id)
    => await context.Posts
        .Where(e => e.Title == EF.Parameter(".NET Blog") && e.Id == id)
        .ToListAsync();

Перевод теперь содержит параметр для строки блога .NET:

Executed DbCommand (1ms) [Parameters=[@__p_0='.NET Blog' (Size = 4000), @__id_1='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = @__p_0 AND [p].[Id] = @__id_1

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

EF8 изменил способ преобразования некоторых запросов, использующих примитивные коллекции. Если запрос LINQ содержит параметризованную примитивную коллекцию, EF преобразует его содержимое в JSON и передает его в виде одного значения параметра запроса:

async Task<List<Post>> GetPostsPrimitiveCollection(int[] ids)
    => await context.Posts
        .Where(e => e.Title == ".NET Blog" && ids.Contains(e.Id))
        .ToListAsync();

Это приведет к следующему переводу в SQL Server:

Executed DbCommand (5ms) [Parameters=[@__ids_0='[1,2,3]' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (
    SELECT [i].[value]
    FROM OPENJSON(@__ids_0) WITH ([value] int '$') AS [i]
)

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

Следующий запрос используется EF.Constant для этого эффекта:

async Task<List<Post>> GetPostsForceConstantCollection(int[] ids)
    => await context.Posts
        .Where(
            e => e.Title == ".NET Blog" && EF.Constant(ids).Contains(e.Id))
        .ToListAsync();

Результирующий SQL выглядит следующим образом:

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (1, 2, 3)

Кроме того, EF9 вводит TranslateParameterizedCollectionsToConstants параметр контекста, который можно использовать для предотвращения параметризации примитивов для всех запросов. Мы также добавили дополнение TranslateParameterizedCollectionsToParameters , которое заставляет параметризацию примитивных коллекций явно (это поведение по умолчанию).

Совет

Метод EF.Parameter переопределяет параметр контекста. Если вы хотите предотвратить параметризацию примитивных коллекций для большинства запросов (но не все), можно задать параметр TranslateParameterizedCollectionsToConstants контекста и использовать EF.Parameter для запросов или отдельных переменных, которые требуется параметризировать.

Встроенные вложенные запросы без корректных

Совет

Код, показанный здесь, поступает из QuerySample.cs.

В EF8 объект IQueryable, на который ссылается другой запрос, может выполняться в виде отдельной обходной схемы базы данных. Например, рассмотрим следующий запрос LINQ:

var dotnetPosts = context
    .Posts
    .Where(p => p.Title.Contains(".NET"));

var results = dotnetPosts
    .Where(p => p.Id > 2)
    .Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
    .Skip(2).Take(10)
    .ToArray();

В EF8 запрос dotnetPosts выполняется в виде одного кругового пути, а затем окончательные результаты выполняются в качестве второго запроса. Например, в SQL Server.

SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%'

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_1 ROWS FETCH NEXT @__p_2 ROWS ONLY

В EF9 встраиваются в dotnetPosts структуру, что приводит к тому, IQueryable что одна база данных круговая поездка:

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata], (
    SELECT COUNT(*)
    FROM [Posts] AS [p0]
    WHERE [p0].[Title] LIKE N'%.NET%')
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY

Агрегатные функции по вложенным запросам и агрегатам в SQL Server

EF9 улучшает перевод некоторых сложных запросов с помощью агрегатных функций, состоящих из вложенных запросов или других агрегатных функций. Ниже приведен пример такого запроса:

var latestPostsAverageRatingByLanguage = await context.Blogs.
    Select(x => new
    {
        x.Language,
        LatestPostRating = x.Posts.OrderByDescending(xx => xx.PublishedOn).FirstOrDefault().Rating
    })
    .GroupBy(x => x.Language)
    .Select(x => x.Average(xx => xx.LatestPostRating))
    .ToListAsync();

Во-первых, Select вычисляется LatestPostRating для каждого Post из них, для которого требуется вложенный запрос при переводе в SQL. Далее в запросе эти результаты агрегируются с помощью Average операции. Результирующий SQL выглядит следующим образом при запуске в SQL Server:

SELECT AVG([s].[Rating])
FROM [Blogs] AS [b]
OUTER APPLY (
    SELECT TOP(1) [p].[Rating]
    FROM [Posts] AS [p]
    WHERE [b].[Id] = [p].[BlogId]
    ORDER BY [p].[PublishedOn] DESC
) AS [s]
GROUP BY [b].[Language]

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

var topRatedPostsAverageRatingByLanguage = await context.Blogs.
Select(x => new
{
    x.Language,
    TopRating = x.Posts.Max(x => x.Rating)
})
.GroupBy(x => x.Language)
.Select(x => x.Average(xx => xx.TopRating))
.ToListAsync();

Примечание.

Это изменение не влияет на Sqlite, который поддерживает агрегаты по вложенным запросам (или другим агрегатам) и не поддерживает LATERAL JOIN (APPLY). Ниже приведен SQL для первого запроса, запущенного в Sqlite:

SELECT ef_avg((
    SELECT "p"."Rating"
    FROM "Posts" AS "p"
    WHERE "b"."Id" = "p"."BlogId"
    ORDER BY "p"."PublishedOn" DESC
    LIMIT 1))
FROM "Blogs" AS "b"
GROUP BY "b"."Language"

Запросы с помощью count != 0 оптимизированы

Совет

Код, показанный здесь, поступает из QuerySample.cs.

В EF8 был переведен следующий запрос LINQ для использования функции SQL COUNT :

var blogsWithPost = await context.Blogs
    .Where(b => b.Posts.Count > 0)
    .ToListAsync();

EF9 теперь создает более эффективный перевод с помощью EXISTS:

SELECT "b"."Id", "b"."Name", "b"."SiteUri"
FROM "Blogs" AS "b"
WHERE EXISTS (
    SELECT 1
    FROM "Posts" AS "p"
    WHERE "b"."Id" = "p"."BlogId")

Семантика C# для операций сравнения со значениями, допускаемыми значением NULL

В EF8 сравнения между элементами, допускаемыми значением NULL, не выполнялись правильно для некоторых сценариев. В C#, если одно или оба операнда имеют значение NULL, результат операции сравнения имеет значение false; в противном случае сравниваются содержащиеся значения операндов. В EF8 мы использовали для преобразования сравнений с использованием семантики null базы данных. Это приведет к получению результатов, отличных от аналогичного запроса с помощью LINQ to Objects. Кроме того, мы создадим различные результаты при сравнении в фильтре и проекции. Некоторые запросы также будут создавать различные результаты между Sql Server и Sqlite/Postgres.

Например, запрос:

var negatedNullableComparisonFilter = await context.Entities
    .Where(x => !(x.NullableIntOne > x.NullableIntTwo))
    .Select(x => new { x.NullableIntOne, x.NullableIntTwo }).ToListAsync();

приведет к возникновению следующего КОДА SQL:

SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE NOT ([e].[NullableIntOne] > [e].[NullableIntTwo])

который фильтрует сущности, для которых NullableIntOne NullableIntTwo задано значение NULL.

В EF9 мы производим:

SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE CASE
    WHEN [e].[NullableIntOne] > [e].[NullableIntTwo] THEN CAST(0 AS bit)
    ELSE CAST(1 AS bit)
END = CAST(1 AS bit)

Аналогичное сравнение, выполняемое в проекции:

var negatedNullableComparisonProjection = await context.Entities.Select(x => new
{
    x.NullableIntOne,
    x.NullableIntTwo,
    Operation = !(x.NullableIntOne > x.NullableIntTwo)
}).ToListAsync();

привело к следующему SQL:

SELECT [e].[NullableIntOne], [e].[NullableIntTwo], CASE
    WHEN NOT ([e].[NullableIntOne] > [e].[NullableIntTwo]) THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END AS [Operation]
FROM [Entities] AS [e]

значение , возвращающее false сущности, для которых NullableIntOne NullableIntTwo задано значение NULL (а не true ожидалось в C#). Выполнение того же сценария в Sqlite, созданном:

SELECT "e"."NullableIntOne", "e"."NullableIntTwo", NOT ("e"."NullableIntOne" > "e"."NullableIntTwo") AS "Operation"
FROM "Entities" AS "e"

это приводит к Nullable object must have a value исключению, так как перевод создает null значение для случаев, когда NullableIntOne или NullableIntTwo имеет значение NULL.

EF9 теперь правильно обрабатывает эти сценарии, создавая результаты, согласованные с объектами LINQ to Objects и различными поставщиками.

Это улучшение было внесено @ranma42. Спасибо!

Order Перевод операторов и OrderDescending операторов LINQ

EF9 обеспечивает перевод операций упрощенного упорядочивания LINQ (Order и OrderDescending). Эти действия аналогичны OrderBy/OrderByDescending , но не требуют аргумента. Вместо этого они применяют упорядочение по умолчанию для сущностей, это означает упорядочение на основе значений первичного ключа и для других типов, упорядочение на основе самих значений.

Ниже приведен пример запроса, который использует преимущества упрощенных операторов упорядочивания:

var orderOperation = await context.Blogs
    .Order()
    .Select(x => new
    {
        x.Name,
        OrderedPosts = x.Posts.OrderDescending().ToList(),
        OrderedTitles = x.Posts.Select(xx => xx.Title).Order().ToList()
    })
    .ToListAsync();

Этот запрос эквивалентен следующему:

var orderByEquivalent = await context.Blogs
    .OrderBy(x => x.Id)
    .Select(x => new
    {
        x.Name,
        OrderedPosts = x.Posts.OrderByDescending(xx => xx.Id).ToList(),
        OrderedTitles = x.Posts.Select(xx => xx.Title).OrderBy(xx => xx).ToList()
    })
    .ToListAsync();

и создает следующий SQL:

SELECT [b].[Name], [b].[Id], [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata], [p0].[Title], [p0].[Id]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Posts] AS [p0] ON [b].[Id] = [p0].[BlogId]
ORDER BY [b].[Id], [p].[Id] DESC, [p0].[Title]

Примечание.

Order и OrderDescending методы поддерживаются только для коллекций сущностей, сложных типов или скаляров. Они не будут работать над более сложными проекциями, например коллекции анонимных типов, содержащих несколько свойств.

Это улучшение было внесено в состав команды EF @bricelam. Спасибо!

Улучшен перевод оператора логического отрицания (!)

EF9 приносит много оптимизаций вокруг SQL CASE/WHEN, COALESCEотрицания и различных других конструкций; большинство из них были внесли Андреа Канчиани (@ranma42) - большое спасибо за все это! Ниже мы рассмотрим лишь несколько этих оптимизаций вокруг логического отрицания.

Давайте рассмотрим следующий запрос:

var negatedContainsSimplification = await context.Posts
    .Where(p => !p.Content.Contains("Announcing"))
    .Select(p => new { p.Content }).ToListAsync();

В EF8 мы создадим следующий SQL:

SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE NOT (instr("p"."Content", 'Announcing') > 0)

В EF9 мы "заталкиваем" NOT операцию в сравнение:

SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE instr("p"."Content", 'Announcing') <= 0

Другим примером, применимым к SQL Server, является отрицаемая условная операция.

var caseSimplification = await context.Blogs
    .Select(b => !(b.Id > 5 ? false : true))
    .ToListAsync();

В EF8 используется для выполнения вложенных CASE блоков:

SELECT CASE
    WHEN CASE
        WHEN [b].[Id] > 5 THEN CAST(0 AS bit)
        ELSE CAST(1 AS bit)
    END = CAST(0 AS bit) THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]

В EF9 мы удалили вложение:

SELECT CASE
    WHEN [b].[Id] > 5 THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]

В SQL Server при проецирование незначаемого логическое свойство:

var negatedBoolProjection = await context.Posts.Select(x => new { x.Title, Active = !x.Archived }).ToListAsync();

EF8 создаст CASE блок, так как сравнения не могут отображаться в проекции непосредственно в запросах SQL Server:

SELECT [p].[Title], CASE
   WHEN [p].[Archived] = CAST(0 AS bit) THEN CAST(1 AS bit)
   ELSE CAST(0 AS bit)
END AS [Active]
FROM [Posts] AS [p]

В EF9 этот перевод был упрощен и теперь использует побитовую не (~):

SELECT [p].[Title], ~[p].[Archived] AS [Active]
FROM [Posts] AS [p]

Улучшена поддержка AZURE SQL и Azure Synapse

EF9 обеспечивает большую гибкость при указании типа ЦЕЛЕВОго SQL Server. Вместо настройки EF UseSqlServerс помощью теперь можно указать UseAzureSql или UseAzureSynapse. Это позволяет EF создавать лучшие SQL при использовании Azure SQL или Azure Synapse. EF может воспользоваться преимуществами конкретных функций базы данных (например , выделенного типа для JSON в Azure SQL) или обойти свои ограничения (например ESCAPE , предложение недоступно при использовании LIKE в Azure Synapse).

Другие улучшения запросов

  • Примитивные коллекции, запрашивающие поддержку в EF8 , были расширены для поддержки всех ICollection<T> типов. Обратите внимание, что это относится только к параметрам и встроенным коллекциям — примитивные коллекции, которые являются частью сущностей, по-прежнему ограничены массивами, списками и в EF9 также массивами и списками только для чтения.
  • Новые ToHashSetAsync функции, возвращающие результаты запроса как a HashSet (#30033, внесенные @wertzui).
  • TimeOnly.FromDateTime и FromTimeSpan теперь переведены на SQL Server (No 33678).
  • ToString Перекрещенные перечисления теперь переводятся (#33706, внесенные @Danevandy99).
  • string.Joinтеперь преобразуется в CONCAT_WS в нерегларегированном контексте в SQL Server (#28899).
  • EF.Functions.PatIndex теперь преобразуется в функцию SQL Server PATINDEX , которая возвращает начальную позицию первого вхождения шаблона (#33702, @smnsht).
  • Sum и Average теперь работает для десятичных разрядов в SQLite (#33721, внося свой вклад в @ranma42).
  • Исправления и оптимизации в string.StartsWith и EndsWith (#31482).
  • Convert.To* Теперь методы могут принимать аргумент типа object (#33891, внесенный @imangd).
  • Операция exclusive-Or (XOR) теперь преобразуется в SQL Server (#34071, внося в @ranma42).
  • Оптимизация вокруг допустимости значений NULL для операций AT TIME ZONE (#34263, вносимая @ranma42).COLLATE
  • Оптимизация для DISTINCT операций over EXISTS INи set (#34381, вносимая @ranma42).

Выше были только некоторые из более важных улучшений запросов в EF9; См . эту проблему для более полного описания.

Миграции

Улучшены миграции темпоральных таблиц

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

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AlterTable(
        name: "Blogs")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<string>(
        name: "SiteUri",
        table: "Blogs",
        type: "nvarchar(max)",
        nullable: false,
        oldClrType: typeof(string),
        oldType: "nvarchar(max)")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<string>(
        name: "Name",
        table: "Blogs",
        type: "nvarchar(max)",
        nullable: false,
        oldClrType: typeof(string),
        oldType: "nvarchar(max)")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<int>(
        name: "Id",
        table: "Blogs",
        type: "int",
        nullable: false,
        oldClrType: typeof(int),
        oldType: "int")
        .Annotation("SqlServer:Identity", "1, 1")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart")
        .OldAnnotation("SqlServer:Identity", "1, 1");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodEnd",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodStart",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
}

В EF9 та же операция теперь приводит к значительно меньшей миграции:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AlterTable(
        name: "Blogs")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodEnd",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:TemporalIsPeriodEndColumn", true);

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodStart",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:TemporalIsPeriodStartColumn", true);
}

Защита от параллельной миграции

EF9 представляет механизм блокировки для защиты от нескольких выполнений миграции одновременно, так как это может оставить базу данных в поврежденном состоянии. Это не происходит при развертывании миграции в рабочую среду с помощью рекомендуемых методов, но может произойти, если миграции применяются во время выполнения с помощью DbContext.Database.Migrate() метода. Рекомендуется применять миграции во время развертывания, а не как часть запуска приложения, но это может привести к более сложным архитектурам приложений (например , при использовании проектов .NET Aspire.

Примечание.

Если вы используете базу данных Sqlite, ознакомьтесь с потенциальными проблемами, связанными с этой функцией.

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

Большинство операций, выполняемых во время миграции, защищены транзакцией. Это гарантирует, что если по какой-то причине миграция завершается ошибкой, база данных не находится в поврежденном состоянии. Однако некоторые операции не упаковываются в транзакцию (например, операции с оптимизированными для памяти таблицами SQL Server или операции изменения базы данных, например изменение параметров сортировки базы данных). Чтобы избежать повреждения базы данных в случае сбоя миграции, рекомендуется выполнить эти операции в изоляции с помощью отдельной миграции. EF9 теперь обнаруживает сценарий, когда миграция содержит несколько операций, одна из которых не может быть упакована в транзакцию и выдает предупреждение.

Улучшено начальное значение данных

EF9 представил удобный способ заполнения данных, который заполняет базу данных начальными данными. DbContextOptionsBuilderтеперь содержит и методыUseSeeding, которые выполняются при инициализации DbContext (в составе EnsureCreatedAsync).UseAsyncSeeding

Примечание.

Если приложение запущено ранее, база данных уже может содержать примеры данных (которые были бы добавлены при первой инициализации контекста). Таким образом, следует проверить, UseSeeding UseAsyncSeeding существуют ли данные перед попыткой заполнения базы данных. Это можно сделать, выполнив простой запрос EF.

Ниже приведен пример использования этих методов.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;ConnectRetryCount=0")
        .UseSeeding((context, _) =>
        {
            var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url == "http://test.com");
            if (testBlog == null)
            {
                context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
                context.SaveChanges();
            }
        })
        .UseAsyncSeeding(async (context, _, cancellationToken) =>
        {
            var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b => b.Url == "http://test.com", cancellationToken);
            if (testBlog == null)
            {
                context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
                await context.SaveChangesAsync(cancellationToken);
            }
        });

Дополнительные сведения см. здесь.

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

Автоматически скомпилированные модели

Совет

Приведенный здесь код получен из примера NewInEFCore9.CompiledModels .

Скомпилированные модели могут улучшить время запуска для приложений с большими моделями, то есть количество типов сущностей в 100-х или 1000-х годах. В предыдущих версиях EF Core необходимо создать скомпилированную модель вручную с помощью командной строки. Например:

dotnet ef dbcontext optimize

После выполнения команды необходимо добавить строку, например, .UseModel(MyCompiledModels.BlogsContextModel.Instance) чтобы OnConfiguring сообщить EF Core использовать скомпилированную модель.

Начиная с EF9, эта .UseModel строка больше не требуется, если тип приложения DbContext находится в том же проекте или сборке, что и скомпилированная модель. Вместо этого скомпилированная модель будет обнаружена и используется автоматически. Это можно увидеть, имея журнал EF всякий раз, когда он создает модель. При запуске простого приложения ef показано создание модели при запуске приложения:

Starting application...
>> EF is building the model...
Model loaded with 2 entity types.

Выходные данные запуска в dotnet ef dbcontext optimize проекте модели:

PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> dotnet ef dbcontext optimize

Build succeeded in 0.3s

Build succeeded in 0.3s
Build started...
Build succeeded.
>> EF is building the model...
>> EF is building the model...
Successfully generated a compiled model, it will be discovered automatically, but you can also call 'options.UseModel(BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> 

Обратите внимание, что выходные данные журнала указывают на то, что модель была создана при выполнении команды. Если теперь мы снова запустите приложение, после перестроения, но без внесения изменений в код, выходные данные:

Starting application...
Model loaded with 2 entity types.

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

Интеграция MSBuild

При приведенном выше подходе скомпилированная модель по-прежнему должна создаваться вручную при изменении типов сущностей или DbContext конфигурации. Однако EF9 поставляется с пакетом MSBuild и целевым пакетом, который может автоматически обновлять скомпилированную модель при создании проекта модели! Чтобы приступить к работе, установите пакет NuGet Microsoft.EntityFrameworkCore.Tasks . Например:

dotnet add package Microsoft.EntityFrameworkCore.Tasks --version 9.0.0-preview.4.24205.3

Совет

Используйте версию пакета в приведенной выше команде, которая соответствует используемой версии EF Core.

Затем включите интеграцию, задав свойству EFOptimizeContext .csproj файл. Например:

<PropertyGroup>
    <EFOptimizeContext>true</EFOptimizeContext>
</PropertyGroup>

Существуют дополнительные, необязательные свойства MSBuild для управления сборкой модели, эквивалентные параметрам, переданным в командной строке dotnet ef dbcontext optimize. Например:

Свойство MSBuild Description
EFOptimizeContext Установите для true включения автоматически скомпилированных моделей.
DbContextName Используемый класс DbContext. Имя класса только или полное имя с пространствами имен. Если этот параметр опущен, EF Core найдет класс контекста. Если существует несколько классов контекста, этот параметр является обязательным.
EFStartupProject Относительный путь к запуску проекта. Значение по умолчанию — текущая папка.
EFTargetNamespace Пространство имен, используемое для всех созданных классов. По умолчанию создается из корневого пространства имен и выходного каталога, а также скомпилированныхModels.

В нашем примере необходимо указать проект запуска:

<PropertyGroup>
  <EFOptimizeContext>true</EFOptimizeContext>
  <EFStartupProject>..\App\App.csproj</EFStartupProject>
</PropertyGroup>

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

Optimizing DbContext...
dotnet exec --depsfile D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.deps.json
  --additionalprobingpath G:\packages 
  --additionalprobingpath "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages" 
  --runtimeconfig D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.runtimeconfig.json G:\packages\microsoft.entityframeworkcore.tasks\9.0.0-preview.4.24205.3\tasks\net8.0\..\..\tools\netcoreapp2.0\ef.dll dbcontext optimize --output-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\obj\Release\net8.0\ 
  --namespace NewInEfCore9 
  --suffix .g 
  --assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\bin\Release\net8.0\Model.dll --startup-assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.dll 
  --project-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model 
  --root-namespace NewInEfCore9 
  --language C# 
  --nullable 
  --working-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App 
  --verbose 
  --no-color 
  --prefix-output 

При запуске приложения показано, что скомпилированная модель обнаружена, поэтому модель не создается снова:

Starting application...
Model loaded with 2 entity types.

Теперь, когда модель изменяется, скомпилированная модель будет автоматически перестроена сразу после создания проекта.

Примечание.

Мы работаем над некоторыми проблемами производительности с изменениями, внесенными в скомпилированную модель в EF8 и EF9. Дополнительные сведения см . в статье о проблеме 33483# .

Примитивные коллекции только для чтения

Совет

Код, показанный здесь, поступает из PrimitiveCollectionsSample.cs.

EF8 представила поддержку сопоставлений массивов и изменяемых списков примитивных типов. Это было развернуто в EF9, чтобы включить коллекции и списки только для чтения. В частности, EF9 поддерживает коллекции, типизированные как IReadOnlyList, IReadOnlyCollectionили ReadOnlyCollection. Например, в следующем коде DaysVisited будет сопоставляться с соглашением как примитивная коллекция дат:

public class DogWalk
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ReadOnlyCollection<DateOnly> DaysVisited { get; set; }
}

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

    public class Pub
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public IReadOnlyCollection<string> Beers { get; set; }

        private List<DateOnly> _daysVisited = new();
        public IReadOnlyList<DateOnly> DaysVisited => _daysVisited;
    }

Затем эти коллекции можно использовать в запросах обычным образом. Например, следующий LINQ-запрос:

var walksWithADrink = await context.Walks.Select(
    w => new
    {
        WalkName = w.Name,
        PubName = w.ClosestPub.Name,
        Count = w.DaysVisited.Count(v => w.ClosestPub.DaysVisited.Contains(v)),
        TotalCount = w.DaysVisited.Count
    }).ToListAsync();

Который преобразуется в следующий SQL в SQLite:

SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", (
    SELECT COUNT(*)
    FROM json_each("w"."DaysVisited") AS "d"
    WHERE "d"."value" IN (
        SELECT "d0"."value"
        FROM json_each("p"."DaysVisited") AS "d0"
    )) AS "Count", json_array_length("w"."DaysVisited") AS "TotalCount"
FROM "Walks" AS "w"
INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"

Указание коэффициента заполнения для ключей и индексов

Совет

Код, показанный здесь, поступает из ModelBuildingSample.cs.

EF9 поддерживает спецификацию коэффициента заполнения SQL Server при использовании миграций EF Core для создания ключей и индексов. Из документации SQL Server "При создании или перестроении индекса значение коэффициента заполнения определяет процент пространства на каждой странице конечного уровня, который будет заполнен данными, резервируя оставшуюся часть на каждой странице в качестве свободного места для будущего роста".

Коэффициент заполнения можно задать для одного или составного первичного и альтернативного ключей и индексов. Например:

modelBuilder.Entity<User>()
    .HasKey(e => e.Id)
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasAlternateKey(e => new { e.Region, e.Ssn })
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasIndex(e => new { e.Name })
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasIndex(e => new { e.Region, e.Tag })
    .HasFillFactor(80);

При применении к существующим таблицам это приведет к изменению коэффициента заполнения на ограничение:

ALTER TABLE [User] DROP CONSTRAINT [AK_User_Region_Ssn];
ALTER TABLE [User] DROP CONSTRAINT [PK_User];
DROP INDEX [IX_User_Name] ON [User];
DROP INDEX [IX_User_Region_Tag] ON [User];

ALTER TABLE [User] ADD CONSTRAINT [AK_User_Region_Ssn] UNIQUE ([Region], [Ssn]) WITH (FILLFACTOR = 80);
ALTER TABLE [User] ADD CONSTRAINT [PK_User] PRIMARY KEY ([Id]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Name] ON [User] ([Name]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Region_Tag] ON [User] ([Region], [Tag]) WITH (FILLFACTOR = 80);

Это улучшение было внесено @deano-охотником. Спасибо!

Сделать существующие соглашения по созданию моделей более расширяемыми

Совет

Код, показанный здесь, поступает из CustomConventionsSample.cs.

В EF7 были представлены соглашения о создании общедоступных моделей для приложений. В EF9 мы облегчили расширение некоторых существующих соглашений. Например, код для сопоставления свойств по атрибуту в EF7 :

public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
    public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
        : base(dependencies)
    {
    }

    public override void ProcessEntityTypeAdded(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionContext<IConventionEntityTypeBuilder> context)
        => Process(entityTypeBuilder);

    public override void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        if ((newBaseType == null
             || oldBaseType != null)
            && entityTypeBuilder.Metadata.BaseType == newBaseType)
        {
            Process(entityTypeBuilder);
        }
    }

    private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
    {
        foreach (var memberInfo in GetRuntimeMembers())
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                entityTypeBuilder.Property(memberInfo);
            }
            else if (memberInfo is PropertyInfo propertyInfo
                     && Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
            {
                entityTypeBuilder.Ignore(propertyInfo.Name);
            }
        }

        IEnumerable<MemberInfo> GetRuntimeMembers()
        {
            var clrType = entityTypeBuilder.Metadata.ClrType;

            foreach (var property in clrType.GetRuntimeProperties()
                         .Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
            {
                yield return property;
            }

            foreach (var property in clrType.GetRuntimeFields())
            {
                yield return property;
            }
        }
    }
}

В EF9 это можно упростить до следующих:

public class AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
    : PropertyDiscoveryConvention(dependencies)
{
    protected override bool IsCandidatePrimitiveProperty(
        MemberInfo memberInfo, IConventionTypeBase structuralType, out CoreTypeMapping? mapping)
    {
        if (base.IsCandidatePrimitiveProperty(memberInfo, structuralType, out mapping))
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                return true;
            }

            structuralType.Builder.Ignore(memberInfo.Name);
        }

        mapping = null;
        return false;
    }
}

Обновление ApplyConfigurationsFromAssembly для вызова недоступных конструкторов

В предыдущих версиях EF Core ApplyConfigurationsFromAssembly метод создает только экземпляры типов конфигурации с открытыми конструкторами без параметров. В EF9 мы улучшили сообщения об ошибках, созданные при сбое, а также включили создание экземпляров неогласованным конструктором. Это полезно при совместном поиске конфигурации в частном вложенном классе, который никогда не должен быть создан кодом приложения. Например:

public class Country
{
    public int Code { get; set; }
    public required string Name { get; set; }

    private class FooConfiguration : IEntityTypeConfiguration<Country>
    {
        private FooConfiguration()
        {
        }

        public void Configure(EntityTypeBuilder<Country> builder)
        {
            builder.HasKey(e => e.Code);
        }
    }
}

В стороне, некоторые люди думают, что этот шаблон является боминацией, потому что он связывает тип сущности с конфигурацией. Другие люди считают, что это очень полезно, так как он совместно находит конфигурацию с типом сущности. Давайте не обсудим это здесь. :-)

Идентификатор иерархии SQL Server

Совет

Код, показанный здесь, поступает из HierarchyIdSample.cs.

Создание пути Sugar для HierarchyId

Добавлена поддержка первого класса для типа SQL Server HierarchyId в EF8. В EF9 добавлен метод сахара, чтобы упростить создание дочерних узлов в структуре дерева. Например, следующий код запрашивает существующую сущность со свойством HierarchyId :

var daisy = await context.Halflings.SingleAsync(e => e.Name == "Daisy");

Это HierarchyId свойство можно использовать для создания дочерних узлов без явной обработки строк. Например:

var child1 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1), "Toast");
var child2 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 2), "Wills");

Если daisy имеет /4/1/3/1/HierarchyId значение , то child1 получит HierarchyId "/4/1/3/1/1/" и child2 получит HierarchyId "/4/1/1/1/2/".

Чтобы создать узел между этими двумя дочерними элементами, можно использовать дополнительный подуровневый элемент. Например:

var child1b = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1, 5), "Toast");

При этом создается узел с элементом HierarchyId /4/1/3/1/1.5/, помещающим его между child1 и child2.

Это улучшение было внесено @Rezakazemi890. Спасибо!

Средства

Меньше перестроек

Средство dotnet ef командной строки по умолчанию создает проект перед выполнением средства. Это связано с тем, что не перестроение перед запуском средства является общим источником путаницы, когда вещи не работают. Опытные разработчики могут использовать --no-build этот вариант, чтобы избежать этой сборки, которая может быть медленной. Однако даже этот --no-build параметр может привести к повторному построению проекта при следующем построении за пределами средств EF.

Мы считаем, что вклад сообщества из @Suchiman исправил это. Тем не менее, мы также осознаем, что изменения вокруг поведения MSBuild имеют тенденцию иметь непредвиденные последствия, поэтому мы просим людей, как вы, чтобы попробовать это и сообщить о любых негативных опытах у вас есть.