EF Core 9 (EF9) 中的中断性变更

此页面记录了可能会导致现有应用程序从 EF Core 8 更新到 EF Core 9 时中断的 API 和行为变更。 如果从早期版本的 EF Core 进行更新,请务必查看之前的中断性变更:

目标 Framework

EF Core 9 面向 .NET 8。 这意味着面向 .NET 8 的现有应用程序可以继续面向它。 面向较旧的 .NET、.NET Core 和 .NET Framework 版本的应用程序需要面向 .NET 8 或 .NET 9 才能使用 EF Core 9。

总结

注意

如果使用 Azure Cosmos DB,请参阅以下有关 Azure Cosmos DB 重大更改的单独部分

中断性变更 影响
如果存在挂起的模型更改,则应用迁移时会引发异常
在显式事务中应用迁移时会引发异常
使用 EF 工具时找不到 Microsoft.EntityFrameworkCore.Design
EF.Functions.Unhex() 现在返回 byte[]?
SqlFunctionExpression 的为 Null 性参数的 arity 已验证
ToString() 方法现在为 null 实例返回空字符串
共享框架依赖项已更新为 9.0.x

影响较大的更改

如果存在挂起的模型更改,则应用迁移时会引发异常

跟踪问题 #33732

旧行为

如果模型与上次迁移相比有未完成的更改,调用 Migrate 时,这些更改不会与其他迁移同时应用。

新行为

从 EF Core 9.0 开始,如果模型与上次迁移相比有未决的更改,当调用 dotnet ef database updateMigrateMigrateAsync 时,将引发异常。

上下文“DbContext”的模型具有挂起的更改。 在更新数据库之前添加新的迁移。 通过将事件 ID“RelationalEventId.PendingModelChangesWarning”传递给“DbContext.OnConfiguring”或“AddDbContext”中的“ConfigureWarnings”方法,可以抑制或记录此异常。

原因

在进行模型更改后忘记添加新迁移是一项常见错误,在某些情况下很难诊断。 新的异常可确保应用模型在应用迁移后与数据库匹配。

缓解措施

有几个常见情况可能会引发此异常:

  • 根本没有迁移。 当数据库通过其他方式更新时,这很常见。
    • 缓解措施:如果不打算使用迁移来管理数据库架构,请移除 Migrate 调用,否则请添加迁移MigrateAsync
  • 至少有一个迁移,但缺少模型快照。 手动创建的迁移通常会这样。
    • 缓解措施:使用 EF 工具添加新迁移,这将更新模型快照
  • 该模型不是由开发人员修改的,但它以非确定性方式生成,导致 EF 检测到它已修改。 在提供给 HasData() 的对象中使用 new DateTime()DateTime.NowDateTime.UtcNowGuid.NewGuid() 时,这种情况很常见。
    • 缓解措施:添加新迁移,检查其内容以查找原因,以及将动态数据替换为模型中的静态硬编码值。 修复模型后,应重新创建迁移。 如果动态数据必须用于初始化,请考虑使用 新的初始化模式 而不是 HasData()
  • 创建上次迁移所针对的提供程序与用于应用迁移的提供程序不同。
  • 迁移是通过替换某些 EF 服务来动态生成的或选择的。
    • 缓解措施:在这种情况下,警告为误报,应禁止显示

      options.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))

如果你的方案不属于上述任一情况,并且每次添加新迁移都会创建相同的迁移,或者仍会引发空迁移和异常,则创建一个小重现项目,并将其与 EF 团队共享

在显式事务中应用迁移时会引发异常

跟踪问题 #17578

旧行为

为灵活应用迁移,通常使用以下模式:

await dbContext.Database.CreateExecutionStrategy().ExecuteAsync(async () =>
{
    await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
    await dbContext.Database.MigrateAsync(cancellationToken);
    await transaction.CommitAsync(cancellationToken);
});

新行为

从 EF Core 9.0 开始,MigrateMigrateAsync 调用将启动事务并使用 ExecutionStrategy 执行命令,如果应用使用上述模式,则会引发异常:

针对警告“Microsoft.EntityFrameworkCore.Migrations.MigrationsUserTransactionWarning”生成错误:在应用迁移之前启动了事务。 这可以防止获取数据库锁,因此数据库将不会受到并发迁移应用程序的保护。 事务和执行策略已根据需要由 EF 管理。 删除外部事务。 通过将事件 ID“RelationalEventId.MigrationsUserTransactionWarning”传递给“DbContext.OnConfiguring”或“AddDbContext”中的“ConfigureWarnings”方法,可以抑制或记录此异常。

原因

使用显式事务可防止获取数据库锁,因此数据库将不会受到并发迁移应用程序的保护,它还会限制 EF 在内部管理事务的方式。

缓解措施

如果事务中只有一个数据库调用,则删除外部事务和 ExecutionStrategy

await dbContext.Database.MigrateAsync(cancellationToken);

否则,如果你的方案需要显式的事务处理,且你有其他方式来防止同时应用迁移,那么可以忽略该警告:

options.ConfigureWarnings(w => w.Ignore(RelationalEventId.MigrationsUserTransactionWarning))

影响中等的更改

使用 EF 工具时找不到 Microsoft.EntityFrameworkCore.Design

跟踪问题 #35265

旧行为

以前,EF 工具需要以以下方式引用 Microsoft.EntityFrameworkCore.Design

    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="*.0.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>

新行为

从 .NET SDK 9.0.200 开始,调用 EF 工具时会引发异常:

无法加载文件或程序集“Microsoft.EntityFrameworkCore.Design,Culture=neutral,PublicKeyToken=null”。 系统找不到指定的文件。

原因

EF 工具依赖于 .NET SDK 的未记录行为,该行为导致私有资产包含在生成的 .deps.json 文件中。 此内容已在 sdk#45259 中修复。 遗憾的是,为此进行的 EF 更改达不到 EF 9.0.x 版本的服务标准,因此将在 EF 10 中解决。

缓解措施

在 EF 10 发布前,作为解决方法,可以将 Design 程序集引用标记为可发布:

    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <Publish>true</Publish>
    </PackageReference>

这会将它包含在生成的 .deps.json 文件中,但将 Microsoft.EntityFrameworkCore.Design.dll 复制到输出和发布文件夹会产生副作用。

影响较小的更改

EF.Functions.Unhex() 现在返回 byte[]?

跟踪问题 #33864

旧行为

EF.Functions.Unhex() 函数之前被批注为返回 byte[]

新行为

从 EF Core 9.0 开始,Unhex() 现在批注为返回 byte[]?

原因

Unhex() 转换为 SQLite unhex 函数,该函数对无效输入返回 NULL。 因此,对于这些情况,Unhex() 返回了 null,这违反了注释。

缓解措施

如果你确定传递给 Unhex() 的文本内容表示有效的十六进制字符串,则只需添加一个 null 包容运算符作为调用永远不会返回 null 的断言:

var binaryData = await context.Blogs.Select(b => EF.Functions.Unhex(b.HexString)!).ToListAsync();

否则,在 Unhex() 的返回值上添加对 null 的运行时检查。

SqlFunctionExpression 的为 Null 性参数的 arity 已验证

跟踪问题 #33852

旧行为

以前,可以创建具有不同参数数量和为 Null 性传播参数的 SqlFunctionExpression

新行为

从 EF Core 9.0 开始,如果参数数量和为 Null 性传播参数不匹配,EF 将引发异常。

原因

参数数量和为 Null 性传播参数不匹配可能会导致意外行为。

缓解措施

请确保 argumentsPropagateNullability 的元素数与 arguments 相同。 如有疑问,请使用 false 来获取为 Null 性参数。

ToString() 方法现在为 null 实例返回空字符串

跟踪问题 #33941

旧行为

以前,当参数值为 ToString() 时,EF 为 null 方法返回了不一致的结果。 例如,ToString() 对于 bool? 值的 null 属性,返回 null;但对于值为 bool? 的非属性表达式 null,返回True。 其他数据类型的行为也不一致,例如 ToString() 值枚举上的 null 返回空字符串。

新行为

从 EF Core 9.0 开始,当参数值为 ToString() 时,null 方法现在在所有情况下都始终返回空字符串。

原因

旧的行为在不同的数据类型和情况下是不一致的,也与 C# 行为不一致。

缓解措施

若要恢复到旧行为,请相应地重写查询:

var newBehavior = context.Entity.Select(x => x.NullableBool.ToString());
var oldBehavior = context.Entity.Select(x => x.NullableBool == null ? null : x.NullableBool.ToString());

共享框架依赖项已更新为 9.0.x

旧行为

使用 Microsoft.NET.Sdk.Web SDK 并以 net8.0 为目标的应用程序将从共享框架中解析 System.Text.JsonMicrosoft.Extensions.Caching.MemoryMicrosoft.Extensions.Configuration.AbstractionsMicrosoft.Extensions.LoggingMicrosoft.Extensions.DependencyModel 等包,因此这些程序集通常不会与应用一起部署。

新行为

虽然 EF Core 9.0 仍然支持 net8.0,但它现在引用了 9.0.x 版本的 System.Text.JsonMicrosoft.Extensions.Caching.MemoryMicrosoft.Extensions.Configuration.AbstractionsMicrosoft.Extensions.LoggingMicrosoft.Extensions.DependencyModel。 以 net8.0 为目标的应用将无法利用共享框架来避免部署这些程序集。

原因

匹配的依赖项版本包含最新的安全修补程序,并使用它们简化了 EF Core 的服务模型。

缓解措施

将应用更改为以 net9.0 为目标以获得先前的行为。

Azure Cosmos DB 重大更改

在 9.0 版本中,为了使 Azure Cosmos DB 提供程序更好,已经进行了大量的工作。 这些更改包括许多影响重大的重大更改;如果要升级现有应用程序,请仔细阅读以下内容。

中断性变更 影响
鉴别器属性现在命名为 $type,而不是 Discriminator
默认情况下,id 属性不再包含鉴别器
JSON id 属性将映射到键
不再支持通过 Azure Cosmos DB 提供程序来同步 I/O
SQL 查询现在必须直接投影 JSON 值
未定义的结果现在会自动从查询结果中筛选出来
错误转换的查询不再转换
HasIndex 现在引发而不是被忽略
在 9.0.0-rc.2 之后,IncludeRootDiscriminatorInJsonId 已重命名为 HasRootDiscriminatorInJsonId

影响较大的更改

鉴别器属性现在命名为 $type,而不是 Discriminator

跟踪问题 #34269

旧行为

EF 会自动将鉴别器属性添加到 JSON 文档中,以标识文档所代表的实体类型。 在 EF 的早期版本中,此 JSON 属性默认命名为 Discriminator

新行为

从 EF Core 9.0 开始,鉴别器属性现在默认称为 $type。 如果 Azure Cosmos DB 中有来自早期版本 EF 的现有文档,这些文档将使用旧的 Discriminator 命名,升级到 EF 9.0 后,对这些文档的查询将失败。

原因

一种新兴的 JSON 做法在需要标识文档类型的方案中使用 $type 属性。 例如,.NET 的 System.Text.Json 还支持多态性,使用 $type 作为其默认的鉴别器属性名称 (docs)。 为了与生态系统的其余部分保持一致,并使其更容易与外部工具进行互操作,默认值已更改。

缓解措施

最简单的缓解措施是只需将鉴别器属性的名称配置为 Discriminator,就像之前一样:

modelBuilder.Entity<Session>().HasDiscriminator<string>("Discriminator");

对所有顶级实体类型执行此操作将使 EF 的行为与以前一样。

此时,如果需要,还可以更新所有文档以使用新的 $type 命名。

默认情况下,id 属性现在只包含 EF 键属性

跟踪问题 #34179

旧行为

以前,EF 将实体类型的鉴别器值插入到文档的 id 属性中。 例如,如果保存了一个 Blog 实体类型,其 Id 属性包含 8,则 JSON id 属性将包含 Blog|8

新行为

从 EF Core 9.0 开始,JSON id 属性不再包含鉴别器值,只包含键属性的值。 对于上面的示例,JSON id 属性是 8。 如果 Azure Cosmos DB 中的现有文档来自以前版本的 EF,则这些文档在 JSON id 属性中具有鉴别器值,升级到 EF 9.0 后,对这些文档的查询将失败。

原因

由于 JSON id 属性必须是唯一的,因此之前向其添加了鉴别器,以允许存在具有相同键值的不同实体。 例如,这允许在同一个容器和分区中同时具有一个 Blog 和一个 Post 以及一个包含值 8 的 Id 属性。 这与关系数据库数据建模模式更为一致,其中每个实体类型都映射到自己的表,因此有自己的键空间。

EF 9.0 通常会更改映射,使其更符合常见的 Azure Cosmos DB NoSQL 做法和期望,而不是符合来自关系数据库的用户的期望。 此外,在 id 属性中设置鉴别器值会使外部工具和系统更难与 EF 生成的 JSON 文档交互;这样的外部系统通常不知道 EF 鉴别器值,默认情况下,EF 鉴别器值来自 .NET 类型。

缓解措施

最简单的缓解方法是简单地配置 EF,将鉴别器包含在 JSON id 属性中,如前所述。 为此,引入了一个新的配置选项:

modelBuilder.Entity<Session>().HasDiscriminatorInJsonId();

对所有顶级实体类型执行此操作将使 EF 的行为与以前一样。

此时,如果需要,还可以更新所有文档以重写其 JSON id 属性。 请注意,只有当不同类型的实体在同一容器中不共享相同的 id 值时,才能执行此操作。

JSON id 属性将映射到键

跟踪问题 #34179

旧行为

以前,EF 会创建映射到 JSON id 属性的影子属性,除非已将其中一个属性明确映射到 id

新行为

从 EF Core 9 开始,键属性将按约定映射到 JSON id 属性(如果可能)。 这意味着,键属性将不再保留在文档中具有相同值的不同名称下,因此使用文档且依赖存在的此属性的非 EF 代码将不再正常运行。

原因

EF 9.0 通常会更改映射,使其更符合常见的 Azure Cosmos DB NoSQL 做法和预期。 在文档中存储键值两次并不常见。

缓解措施

如果想要保留 EF Core 8 行为,最简单的缓解措施是使用出于此目的引入的新配置选项:

modelBuilder.Entity<Session>().HasShadowId();

对所有顶级实体类型执行此操作将使 EF 的行为与以前一样。 或者,可以使用一个调用将其应用于模型中的所有实体类型:

modelBuilder.HasShadowIds();

影响中等的更改

不再支持通过 Azure Cosmos DB 提供程序来同步 I/O

跟踪问题 #32563

旧行为

以前,调用同步方法(如 ToListSaveChanges )会导致 EF Core 在对 Azure Cosmos DB SDK 执行异步调用时阻止同步使用 .GetAwaiter().GetResult()。 这可能会导致死锁。

新行为

从 EF Core 9.0 开始,EF 现在会在尝试使用同步 I/O 时默认引发。 异常消息为“Azure Cosmos DB 不支持同步 I/O。 在使用 Entity Framework Core 访问 Azure Cosmos DB 时,请确保仅使用和正确等待异步方法。 有关详细信息,请参阅 https://aka.ms/ef-cosmos-nosync。”

原因

异步方法上的同步阻塞可能会导致死锁,而 Azure Cosmos DB SDK 又仅支持异步方法。

缓解措施

在 EF Core 9.0 中,可以通过以下方法防止该错误:

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

也就是说,应用程序应停止将同步 API 与 Azure Cosmos DB 配合使用,因为 Azure Cosmos DB SDK 不支持此功能。 在 EF Core 的未来版本中,将移除可防止该异常的功能,之后使用异步 API 便成为了唯一的选项。

SQL 查询现在必须直接投影 JSON 值

跟踪问题 #25527

旧行为

以前,EF 生成的查询如下:

SELECT c["City"] FROM root c

此类查询会导致 Azure Cosmos DB 将每个结果包装在 JSON 对象中,如下所示:

[
    {
        "City": "Berlin"
    },
    {
        "City": "México D.F."
    }
]
新行为

从 EF Core 9.0 开始,EF 现在将 VALUE 修饰符添加到查询,如下所示:

SELECT VALUE c["City"] FROM root c

此类查询会导致 Azure Cosmos DB 直接返回值,而不进行包装:

[
    "Berlin",
    "México D.F."
]

如果应用程序使用 SQL 查询,那么在升级到 EF 9.0 后,这些查询可能会被破坏,因为它们不包括 VALUE 修饰符。

原因

在一些情况下,将每个结果包装在一个额外的 JSON 对象中可能会导致性能下降,导致 JSON 结果有效负载膨胀,并且不是使用 Azure Cosmos DB 的自然方式。

缓解措施

若要缓解问题,只需将 VALUE 修饰符添加到 SQL 查询的投影中,如下所示。

未定义的结果现在会自动从查询结果中筛选出来

跟踪问题 #25527

旧行为

以前,EF 生成的查询如下:

SELECT c["City"] FROM root c

此类查询会导致 Azure Cosmos DB 将每个结果包装在 JSON 对象中,如下所示:

[
    {
        "City": "Berlin"
    },
    {
        "City": "México D.F."
    }
]

如果任何结果未定义(例如,文档中缺少 City 属性),则返回一个空文档,EF 将为该结果返回 null

新行为

从 EF Core 9.0 开始,EF 现在将 VALUE 修饰符添加到查询,如下所示:

SELECT VALUE c["City"] FROM root c

此类查询会导致 Azure Cosmos DB 直接返回值,而不进行包装:

[
    "Berlin",
    "México D.F."
]

Azure Cosmos DB 的行为是自动从结果中筛选出 undefined 值;这意味着,如果文档中缺少 City 属性之一,查询将只返回一个结果,而不是两个结果,其中一个是 null

原因

在一些情况下,将每个结果包装在一个额外的 JSON 对象中可能会导致性能下降,导致 JSON 结果有效负载膨胀,并且不是使用 Azure Cosmos DB 的自然方式。

缓解措施

如果为未定义的结果获取 null 值对应用程序很重要,请使用新的 undefined 运算符将 null 值合并到 EF.Functions.Coalesce

var users = await context.Customer
    .Select(c => EF.Functions.CoalesceUndefined(c.City, null))
    .ToListAsync();

错误转换的查询不再转换

跟踪问题 #34123

旧行为

以前,EF 转换的查询如下:

var sessions = await context.Sessions
    .Take(5)
    .Where(s => s.Name.StartsWith("f"))
    .ToListAsync();

但是,此查询的 SQL 转换不正确:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Session") AND STARTSWITH(c["Name"], "f"))
OFFSET 0 LIMIT @__p_0

在 SQL 中,WHERE 子句在 OFFSET 子句LIMIT求值;但在上面的 LINQ 查询中,Take 运算符出现在 Where 运算符之前。 因此,此类查询可能会返回不正确的结果。

新行为

从 EF Core 9.0 开始,此类查询不再转换,并引发异常。

原因

不正确的转换可能会导致无提示数据损坏,这可能会在应用程序中引入难以发现的错误。 EF 总是倾向于通过提前引发来快速失败,而不是可能导致数据损坏。

缓解措施

如果对以前的行为感到满意,并且想要执行相同的 SQL,只需围绕 LINQ 运算符的顺序进行交换:

var sessions = await context.Sessions
    .Where(s => s.Name.StartsWith("f"))
    .Take(5)
    .ToListAsync();

遗憾的是,Azure Cosmos DB 目前不支持 SQL 子查询中的 OFFSETLIMIT 子句,而这正是原始 LINQ 查询的正确转换所需要的。

影响较小的更改

HasIndex 现在引发而不是被忽略

跟踪问题 #34023

旧行为

以前,EF Cosmos DB 提供程序忽略了对HasIndex 的调用。

新行为

现在,如果指定了 HasIndex,则会引发提供程序。

原因

在 Azure Cosmos DB, 中,默认情况下对所有属性编制索引,无需指定索引。 虽然可以定义自定义索引策略,但 EF 目前不支持此策略,可以在没有 EF 支持的情况下通过 Azure 门户完成。 由于 HasIndex 调用没有任何作用,因此不再允许它们。

缓解措施

删除对 HasIndex 的任何调用。

在 9.0.0-rc.2 之后,IncludeRootDiscriminatorInJsonId 已重命名为 HasRootDiscriminatorInJsonId

跟踪问题 #34717

旧行为

IncludeRootDiscriminatorInJsonId API 是在 9.0.0 rc.1 中引入的。

新行为

对于 EF Core 9.0 的最终版本,API 已重命名为 HasRootDiscriminatorInJsonId

原因

另一个相关的 API 被重命名为以 Has 而不是 Include 开始,因此为了一致性,这个 API 也被重命名。

缓解措施

如果代码使用的是 IncludeRootDiscriminatorInJsonId API,只需将其更改为引用 HasRootDiscriminatorInJsonId 即可。