Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
EF Core 9.0 (EF9) был выпущен в ноябре 2024 года и является выпуском краткосрочной поддержки (STS). EF9 будет поддерживаться до 10 ноября 2026 г.
EF9 доступен как ежедневные сборки , которые содержат все последние функции EF9 и настройки API. Ежедневные сборки используются в приведенных здесь примерах.
Совет
Примеры можно запускать и отлаживать, скачав исходный код с GitHub. Каждый раздел ниже ссылается на исходный код, характерный для этого раздела.
Ef9 предназначен для .NET 8 и поэтому может использоваться с .NET 8 (LTS) или .NET 9.
Совет
Новые документы обновляются для каждой предварительной версии. Все примеры настроены для использования ежедневных сборок EF9, которые обычно имеют несколько дополнительных недель завершенной работы по сравнению с последней предварительной версией. Мы настоятельно рекомендуем использовать ежедневные сборки при тестировании новых функций, чтобы вы не тестировали устаревшие компоненты.
Azure Cosmos DB для NoSQL
EF 9.0 обеспечивает существенное улучшение поставщика EF Core для Azure Cosmos DB; значительные части поставщика были перезаписаны для предоставления новых функциональных возможностей, разрешения новых форм запросов и более эффективного выравнивания поставщика с Azure Cosmos DB рекомендациями. Ниже перечислены основные улучшения высокого уровня. Полный список см. в этой эпической проблеме.
Предупреждение
В рамках улучшений, которые вносятся в поставщик, необходимо вносить ряд критически важных изменений; Если вы обновляете существующее приложение, внимательно ознакомьтесь с разделом критических изменений.
Улучшение запросов с помощью ключей разделов и идентификаторов документов
Каждый документ, хранящийся в базе данных Azure Cosmos DB, имеет уникальный идентификатор ресурса. Кроме того, каждый документ может содержать "ключ секции", который определяет логическую секционирование данных таким образом, чтобы база данных была эффективно масштабирована. Дополнительные сведения о выборе ключей секций см. в разделе Partitioning и горизонтальное масштабирование в Azure Cosmos DB.
В EF 9.0 поставщик Azure Cosmos DB значительно лучше идентифицирует сравнения ключей раздела в запросах LINQ и извлекает их, чтобы убедиться, что ваши запросы отправляются только в соответствующий раздел. Это может значительно повысить производительность запросов и сократить расходы на RU. Например:
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, UserId и SessionId:
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 извлекает значения параметров tenantId, userId и 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","00aa00aa-bb11-cc22-dd33-44ee44ee44ee",7.0]' [Parameters=[]]
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a"))
Обратите внимание, что сравнения ключей разделов удалены из части WHERE и теперь используются в качестве ключей разделов для эффективного выполнения: ["Майкрософт","00aa00aa-bb11-cc22-dd33-44ee44ee44ee",7.0].
Дополнительные сведения см. в документации по запросам с ключами разделов.
Значительно улучшены возможности запросов LINQ
В EF 9.0 возможности перевода LINQ поставщика Azure Cosmos DB были значительно расширены, и поставщик теперь может выполнять значительно больше типов запросов. Полный список улучшений запросов слишком длинный для списка, но ниже приведены основные моменты.
- Полная поддержка примитивных коллекций EF, что позволяет выполнять запросы LINQ для коллекций, например ints или строк. См. что нового в EF8: примитивные коллекции для получения дополнительной информации.
- Поддержка произвольных запросов по немитивным коллекциям.
- Теперь поддерживаются множество дополнительных операторов LINQ: индексирование в коллекции,
Length/Count, ,ElementAtContainsи многие другие. - Поддержка статистических операторов, таких как
CountиSum. - Дополнительные переводы функций (см. документацию по отображениям функций для полного списка поддерживаемых переводов функций):
- Переводы для участников компонента
DateTimeиDateTimeOffset(DateTime.Year,DateTimeOffset.Month...). -
IsDefined и 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, что позволяет лучше взаимодействовать с другими инструментами. Например, .NET System.Text.Json также поддерживает полиморфизм, используя $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);
}
}
После этого используйте функцию VectorDistance в запросах LINQ для выполнения поиска сходства векторов:
var blogs = await context.Blogs
.OrderBy(s => EF.Functions.VectorDistance(s.Vector, vector))
.Take(5)
.ToListAsync();
Дополнительные сведения см. в документации по поиску векторов.
Поддержка разбивки на страницы
Теперь поставщик Azure Cosmos DB позволяет выполнять разбивку по результатам запроса с помощью маркеров continuation, что гораздо эффективнее и экономично, чем традиционное использование 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<T> маркер продолжения, который можно использовать для эффективного возобновления запроса на более поздний момент, загружая следующие 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 и предварительно скомпилированные запросы
Предупреждение
Предкомпиляция с использованием NativeAOT и предкомпиляция запросов являются высоко экспериментальными функциями и пока не подходят для использования в производственной среде. Поддержку, описанную ниже, следует рассматривать как инфраструктуру для окончательной функции, которая будет выпущена в будущей версии. Мы рекомендуем поэкспериментировать с текущей поддержкой и сообщить о вашем опыте, но не рекомендуем развертывать приложения EF NativeAOT в производственной среде.
EF 9.0 обеспечивает начальную экспериментальную поддержку .NET NativeAOT, что позволяет публиковать заранее скомпилированные приложения, которые используют EF для доступа к базам данных. Для поддержки запросов LINQ в режиме NativeAOT EF используется предварительная компиляция запросов: этот механизм статически определяет запросы EF LINQ и создает перехватчики C#, содержащие код для выполнения каждого конкретного запроса. Это может значительно сократить время запуска вашего приложения, так как процесс обработки и компиляции запросов LINQ в SQL перестаёт происходить при каждом запуске приложения. Вместо этого перехватчик каждого запроса содержит завершенный SQL для этого запроса, а также оптимизированный код для материализации результатов базы данных в виде объектов .NET.
Например, при выполнении программы со следующим запросом EF:
var blogs = await context.Blogs.Where(b => b.Name == "foo").ToListAsync();
EF создаст перехватчик C# в вашем проекте, который будет управлять выполнением запроса. Вместо обработки запроса и перевода его в SQL каждый раз при запуске программы, перехватчик имеет SQL, прямо встроенный в него (в данном случае для SQL Server), что позволяет программе запускаться значительно быстрее.
var relationalCommandTemplate = ((IRelationalCommandTemplate)(new RelationalCommand(materializerLiftableConstantContext.CommandBuilderDependencies, "SELECT [b].[Id], [b].[Name]\nFROM [Blogs] AS [b]\nWHERE [b].[Name] = N'foo'", new IRelationalParameter[] { })));
Кроме того, тот же перехватчик содержит код для материализации вашего объекта .NET из результатов базы данных.
var instance = new Blog();
UnsafeAccessor_Blog_Id_Set(instance) = dataReader.GetInt32(0);
UnsafeAccessor_Blog_Name_Set(instance) = dataReader.GetString(1);
В этом случае используется еще одна новая возможность .NET — небезопасные аксессоры для внедрения данных из базы данных в закрытые поля объекта.
Если вы заинтересованы в NativeAOT и хотите поэкспериментировать с передовыми функциями, дайте это попробовать! Просто помните, что эта функция должна считаться нестабильной, и в настоящее время имеет множество ограничений; Мы ожидаем стабилизировать его и сделать его более подходящим для использования в EF 10.
Дополнительные сведения см. на странице документации NativeAOT.
Перевод LINQ и SQL
Как и при каждом выпуске, EF9 включает большое количество улучшений в возможностях запросов LINQ. Новые запросы могут переводиться, и многие переводы SQL для поддерживаемых сценариев были улучшены для повышения производительности и удобства чтения.
Слишком большое количество улучшений, чтобы перечислить их здесь. Ниже выделены некоторые из более важных улучшений; смотрите этот вопрос, чтобы получить более полный список выполненных работ в версии 9.0.
Мы хотели бы поблагодарить Андреа Канчиани (@ranma42) за его многочисленные, высококачественные вклады в оптимизацию SQL, генерируемого EF Core!
Сложные типы: поддержка GroupBy и ExecuteUpdate
Группировка по
Совет
Код, показанный здесь, поступает из ComplexTypesSample.cs.
EF9 поддерживает группировку по сложному экземпляру типа. Например:
var groupedAddresses = await context.Stores
.GroupBy(b => b.StoreAddress)
.Select(g => new { g.Key, Count = g.Count() })
.ToListAsync();
EF преобразует это как группирование по каждому элементу сложного типа, который соответствует семантике сложных типов в качестве объектов значений. Например, на Azure SQL:
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]
Выполнить обновление
Совет
Код, показанный здесь, поступает из ExecuteUpdateSample.cs.
В EF9 аналогичным образом улучшена поддержка свойств сложного типа ExecuteUpdateAsync. Однако каждый элемент сложного типа должен быть явно указан. Например:
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'
Ранее необходимо было вручную перечислить различные свойства сложного типа в вызове ExecuteUpdateAsync.
Удалите ненужные элементы из 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.
Было введено несколько новых переводов, использующих функции SQL GREATEST и LEAST.
Внимание
Функции GREATEST и LEAST были интродуцированы для баз данных SQL Server/Azure SQL в версии 2022. Visual Studio 2022 по умолчанию устанавливает SQL Server 2019. Мы рекомендуем установить SQL Server Developer Edition 2022, чтобы попробовать эти новые переводы в EF9.
Например, запросы с помощью Math.Max или Math.Min теперь переводятся для Azure SQL с помощью 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-запросу и параметрам при использовании Azure SQL.
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 представляет 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 вводит метод 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 Blog".
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-запрос для разных параметризованных коллекций (только изменения значения параметра), но в некоторых ситуациях это может привести к проблемам с производительностью, так как база данных не может оптимально планировать запрос. Этот Constant метод можно использовать для возврата к предыдущему переводу.
Следующий запрос использует 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 , которое заставляет параметризацию примитивных коллекций явно (это поведение по умолчанию).
Совет
Метод Parameter переопределяет параметр контекста. Если вы хотите предотвратить параметризацию примитивных коллекций для большинства запросов (но не все), можно задать параметр TranslateParameterizedCollectionsToConstants контекста и использовать Parameter для запросов или отдельных переменных, которые требуется параметризировать.
Встроенные независимые подзапросы
Совет
Код, показанный здесь, поступает из QuerySample.cs.
В EF8 объект IQueryable, на который ссылается другой запрос, может выполняться в виде отдельной обходной схемы базы данных. Например, рассмотрим следующий запрос LINQ:
var dotnetPosts = context
.Posts
.Where(p => p.Title.Contains(".NET"));
var results = await dotnetPosts
.Where(p => p.Id > 2)
.Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
.Skip(2).Take(10)
.ToArrayAsync();
В 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 IQueryable инлайнится в dotnetPosts, что приводит к одной круговой поездке в базу данных.
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# для nullable значений
В 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])
который фильтрует сущности, для которых NullableIntOneNullableIntTwo задано значение 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функции, возвращающие результаты запроса какHashSet(#30033, внесено @wertzui). -
TimeOnly.FromDateTimeиFromTimeSpanтеперь переводятся на SQL Server (#33678). -
ToStringПерекрещенные перечисления теперь переводятся (#33706, внесенные @Danevandy99). -
string.Joinтеперь преобразуется в CONCAT_WS в неагрегирующем контексте на SQL Server (#28899). -
EF.Functions.PatIndexтеперь преобразуется в функцию SQL ServerPATINDEX, которая возвращает начальную позицию первого вхождения шаблона (#33702, @smnsht). -
SumиAverageтеперь работают для десятичных разрядов в SQLite (#33721, с участием от @ranma42). - Исправления и оптимизации в
string.StartsWithиEndsWith(#31482). -
Convert.To*Теперь методы могут принимать аргумент типаobject(#33891, внесенный @imangd). - Операция Exclusive-Or (XOR) теперь переводится на SQL Server (#34071, представлено @ranma42).
- Оптимизации в области допустимости NULL для операций
COLLATEиAT TIME ZONE(#34263, вклад от @ranma42). - Оптимизация для операций над
DISTINCT,IN,EXISTSи множествами (#34381, внесённая @ranma42).
Выше были упомянуты лишь некоторые из более значительных улучшений запросов в EF9, полный список см. в этом выпуске.
Миграции
Защита от параллельной миграции
EF9 представляет механизм блокировки для защиты от нескольких выполнений миграции одновременно, так как это может оставить базу данных в поврежденном состоянии. Это не происходит при развертывании миграции в рабочую среду с помощью рекомендуемых методов, но может произойти, если миграции применяются с помощью метода DbContext.Database.MigrateAsync() во время выполнения. Мы рекомендуем применять миграции при развертывании, а не как часть запуска приложения, но это может привести к более сложным архитектурам приложений (например, при использовании проектов .NET Aspire).
Дополнительные сведения см. в разделе "Блокировка миграции".
Примечание.
Если вы используете базу данных Sqlite, ознакомьтесь с потенциальными проблемами, связанными с этой функцией.
Предупреждение о том, что не удается выполнить несколько операций миграции внутри транзакции
Большинство операций, выполняемых во время миграции, защищены транзакцией. Это гарантирует, что если по какой-то причине миграция завершается ошибкой, база данных не оказывается в поврежденном состоянии. Однако некоторые операции не включаются в транзакцию (например, операции в таблицах SQL Server, оптимизированных для работы с памятью или операции изменения базы данных, такие как изменение параметров сортировки базы данных). Чтобы избежать повреждения базы данных в случае сбоя миграции, рекомендуется выполнить эти операции в изоляции с помощью отдельной миграции. EF9 теперь обнаруживает сценарий, когда миграция содержит несколько операций, одна из которых не может быть упакована в транзакцию и выдает предупреждение.
Улучшена инициализация данных
EF9 представил удобный способ заполнения данных, который заполняет базу данных начальными данными. DbContextOptionsBuilderтеперь содержит и методыUseSeeding, которые выполняются при инициализации DbContext (в составе UseAsyncSeeding).EnsureCreatedAsync
Примечание.
Если приложение запущено ранее, база данных уже может содержать примеры данных (которые были бы добавлены при первой инициализации контекста). Таким образом, следует проверить, UseSeedingUseAsyncSeeding существуют ли данные перед попыткой заполнения базы данных. Это можно сделать, выполнив простой запрос 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);
}
});
Дополнительные сведения см. здесь.
Другие улучшения миграции
- При изменении существующей таблицы в темпоральную таблицу SQL Server размер кода миграции значительно сократился.
Построение модели
Автоматически скомпилированные модели
Совет
Приведенный здесь код получен из примера NewInEFCore9.CompiledModels .
Скомпилированные модели могут улучшить время запуска для приложений с большими моделями, то есть с количеством типов сущностей в сотнях или тысячах. В предыдущих версиях EF Core необходимо создать скомпилированную модель вручную с помощью командной строки. Например:
dotnet ef dbcontext optimize
После выполнения команды необходимо добавить строку, например, .UseModel(MyCompiledModels.BlogsContextModel.Instance), чтобы сообщить 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, который может автоматически обновлять скомпилированную модель при создании проекта модели! Чтобы приступить к работе, установите Майкрософт. EntityFrameworkCore.Tasks пакет NuGet. Например:
dotnet add package Microsoft.EntityFrameworkCore.Tasks --version 9.0.0
Совет
Используйте версию пакета в приведенной выше команде, которая соответствует используемой версии EF Core.
Затем включите интеграцию, задав EFOptimizeContextEFScaffoldModelStage свойства в .csproj файле. Например:
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
<EFScaffoldModelStage>build</EFScaffoldModelStage>
</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
--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.
Теперь, когда модель изменяется, скомпилированная модель будет автоматически перестроена сразу после создания проекта.
Дополнительные сведения см. в статье интеграции MSBuild.
Примитивные коллекции доступные только для чтения
Совет
Код, показанный здесь, поступает из 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 :"При создании или перестроении индекса значение коэффициента заполнения определяет процент пространства на каждой странице конечного уровня, который будет заполнен данными, резервируя оставшуюся часть на каждой странице как свободное место для будущего роста".
Коэффициент заполнения можно задать для одного или составного первичного и альтернативного ключей и индексов с помощью HasFillFactor. Например:
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-hunter. Спасибо!
Сделать существующие соглашения по созданию моделей более расширяемыми
Совет
Код, показанный здесь, поступает из 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 HierarchyId
Совет
Код, показанный здесь, поступает из 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 имеет HierarchyId/4/1/3/1/ значение, то child1 получит HierarchyId "/4/1/3/1/1/" и child2 получит HierarchyId "/4/1/3/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 склонны вызывать непредвиденные последствия, поэтому мы просим таких людей, как вы, попробовать это и сообщить о любых негативных опытах, которые у вас возникли.