EF Core 8 (EF8) 中的中断性变更

此页面将记录 API 和可能导致现有应用程序从 EF Core 7 更新到 EF Core 8 时中断的行为更改。 如果从早期版本的 EF Core 进行更新,请确保查看以前的中断性变更:

目标 Framework

EF Core 8 面向 .NET 8。 面向旧版 .NET、.NET Core 和 .NET Framework 版本的应用程序需要更新到面向 .NET 8。

总结

中断性变更 影响
LINQ 查询中的 Contains 可能会在较旧的 SQL Server 版本上停止工作
JSON 中的枚举默认存储为整数而不是字符串
SQL Server datetime 现已搭建到 .NET DateOnlyTimeOnly
具有数据库生成值的布尔列不再搭建基架为可为 null
SQLite Math 方法现在转换为 SQL
ITypeBase 在某些 API 中替换 IEntityType
ValueGenerator 表达式必须使用公共 API
ExcludeFromMigrations 不再排除 TPC 层次结构中的其他表
非阴影整数键保留到 Cosmos 文档
关系模型在编译的模型中生成
基架可能会生成不同的导航名称
鉴别器现在具有最大长度
SQL Server 键值比较时不区分大小写

影响较大的更改

LINQ 查询中的 Contains 可能会在较旧的 SQL Server 版本上停止工作

跟踪问题 #13617

旧行为

以前,当 Contains 运算符在 LINQ 查询中使用参数化值列表时,EF 会生成效率低下但适用于所有 SQL Server 版本的 SQL。

新行为

从 EF Core 8.0 开始,EF 现在可生成更高效的 SQL,但在 SQL Server 2014 及更低版本上不受支持。

请注意,较新的 SQL Server 版本可能配置了较旧的兼容级别,也会导致它们与新的 SQL 不兼容。 这也可能发生在从以前的本地 SQL Server 实例迁移来的 Azure SQL 数据库,该数据库保留了旧的兼容级别。

原因

EF Core 为 Contains 生成的上一个 SQL 将参数化值作为常量插入到 SQL 中。 例如,以下 LINQ 查询:

var names = new[] { "Blog1", "Blog2" };

var blogs = await context.Blogs
    .Where(b => names.Contains(b.Name))
    .ToArrayAsync();

...将转换为以下 SQL:

SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] IN (N'Blog1', N'Blog2')

在 SQL 中插入常量值会导致许多性能问题,从而造成查询计划缓存失败,并导致不必要地逐出其他查询。 新的 EF Core 8.0 转换使用 SQL Server OPENJSON 函数,以改为将值作为 JSON 数组传输。 这解决了上述技术固有的性能问题:但函数 OPENJSON 在 SQL Server 2014 及更低版本中不可用。

有关此变更的详细信息,请查看此博客文章

缓解措施

如果数据库是 SQL Server 2016 (13.x) 或更高版本,或者使用的是 Azure SQL,请通过以下命令检查已配置的数据库兼容性级别:

SELECT name, compatibility_level FROM sys.databases;

如果兼容级别低于 130 (SQL Server 2016),请考虑将其修改为较新的值(文档)。

否则,如果数据库版本确实低于 SQL Server 2016,或者设置为由于某种原因而无法更改的旧兼容级别,请将 EF Core 配置为还原到较旧且效率较低的 SQL,如下所示:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlServer(@"<CONNECTION STRING>", o => o.UseCompatibilityLevel(120));

JSON 中的枚举默认存储为整数而不是字符串

跟踪问题 #13617

旧行为

在 EF7 中,映射到 JSON 的枚举默认存储为 JSON 文档中的字符串值。

新行为

从 EF Core 8.0 开始,EF 现在默认将枚举映射到 JSON 文档中的整数值。

原因

默认情况下,EF 始终将枚举映射到关系数据库中的数字列。 由于 EF 支持查询 JSON 中的值与列和参数中的值的交互,因此 JSON 中的值与非 JSON 列中的值匹配非常重要。

缓解措施

若要继续使用字符串,请使用转换配置枚举属性。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<User>().Property(e => e.Status).HasConversion<string>();
}

或者,对于枚举类型的所有属性:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<StatusEnum>().HaveConversion<string>();
}

影响中等的更改

SQL Server datetime 现已搭建到 .NET DateOnlyTimeOnly

跟踪问题 #24507

旧行为

以前,当使用 datetime 列搭建 SQL Server 数据库时,EF 将生成类型为 DateTimeTimeSpan 的实体属性。

新行为

从 EF Core 8.0 开始,datetime 被搭建为 DateOnlyTimeOnly

原因

DateOnlyTimeOnly 是在 .NET 6.0 中引入的,非常适合映射数据库日期和时间类型。 DateTime 特别包含未使用的时间组件,在将其映射到 date 时可能会造成混淆,而 TimeSpan 表示时间间隔,可能包括天数而不是一天中事件发生的时间。 使用新类型可以防止 bug 和混淆,并提供清晰的意图。

缓解措施

此更改仅影响定期将其数据库重新搭建到 EF 代码模型(“数据库优先”流)的用户。

建议通过修改代码以使用新搭建的 DateOnlyTimeOnly 类型来应对这一更改。 但是,如果这不可能,可以编辑基架模板以还原到以前的映射。 为此,请按照本页中所述设置模板。 然后,编辑 EntityType.t4 文件,找到生成实体属性的位置(搜索 property.ClrType),并将代码更改为以下内容:

        var clrType = property.GetColumnType() switch
        {
            "date" when property.ClrType == typeof(DateOnly) => typeof(DateTime),
            "date" when property.ClrType == typeof(DateOnly?) => typeof(DateTime?),
            "time" when property.ClrType == typeof(TimeOnly) => typeof(TimeSpan),
            "time" when property.ClrType == typeof(TimeOnly?) => typeof(TimeSpan?),
            _ => property.ClrType
        };

        usings.AddRange(code.GetRequiredUsings(clrType));

        var needsNullable = Options.UseNullableReferenceTypes && property.IsNullable && !clrType.IsValueType;
        var needsInitializer = Options.UseNullableReferenceTypes && !property.IsNullable && !clrType.IsValueType;
#>
    public <#= code.Reference(clrType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#

具有数据库生成值的布尔列不再搭建基架为可为 null

跟踪问题 #15070

旧行为

以前,具有数据库默认约束的不可为 null 的 bool 列被搭建基架为可为 null 的 bool? 属性。

新行为

从 EF Core 8.0 开始,不可为 null 的 bool 列始终被搭建基架为不可为 null 的属性。

原因

如果 bool 属性的值为 false(CLR 默认值),则该属性的值不会发送到数据库。 如果数据库中该列的默认值为 true,则即使属性值为 false,数据库中的值最终仍为 true。 然而,在 EF8 中,sentinel 用于确定属性的值是否可以更改。 对于数据库生成的值为 truebool 属性,会自动完成此操作,这意味着不再需要将属性搭建基架为可为 null。

缓解措施

此更改仅影响定期将其数据库重新搭建到 EF 代码模型(“数据库优先”流)的用户。

建议通过修改代码以使用不可为 null 的布尔属性来应对这一更改。 但是,如果这不可能,可以编辑基架模板以还原到以前的映射。 为此,请按照本页中所述设置模板。 然后,编辑 EntityType.t4 文件,找到生成实体属性的位置(搜索 property.ClrType),并将代码更改为以下内容:

#>
        var propertyClrType = property.ClrType != typeof(bool)
                              || (property.GetDefaultValueSql() == null && property.GetDefaultValue() != null)
            ? property.ClrType
            : typeof(bool?);
#>
    public <#= code.Reference(propertyClrType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#
<#

影响较小的更改

现在,SQLite Math 方法会转换为 SQL

跟踪问题 #18843

旧行为

以前只会将 Math 上的 Abs、Max、Min 和 Round 方法转换为 SQL。 如果所有其他成员出现在查询的最终 Select 表达式中,则会在客户端上计算这些成员。

新行为

在 EF Core 8.0 中,具有相应 SQLite 数学函数的所有 Math 方法会转换为 SQL。

这些数学函数已在默认情况下提供的本机 SQLite 库中启用(通过依赖于 SQLitePCLRaw.bundle_e_sqlite3 NuGet 包)。 SQLitePCLRaw.bundle_e_sqlcipher 提供的库中也启用了它们。 如果使用其中一个库,则应用程序不应受到此更改的影响。

但通过其他方式包括本机 SQLite 库的应用程序可能无法启用数学函数。 在这些情况下,方法 Math 将转换为 SQL,并且在执行时不会遇到此类函数错误。

原因

SQLite 在版本 3.35.0 中添加了内置数学函数。 尽管它们默认处于禁用状态,但它们已变得非常普遍,因此我们决定在 EF Core SQLite 提供程序中为它们提供默认转换。

我们还与 Eric Sink 就 SQLitePCLRaw 项目进行了协作,以在作为该项目的一部分提供的所有本机 SQLite 库中启用数学函数。

缓解措施

修复中断的最简单方法是尽可能通过指定 SQLITE_ENABLE_MATH_FUNCTIONS 编译时选项启用本机 SQLite 库中的数学函数。

如果不控制本机库的编译,还可以通过使用 Microsoft.Data.Sqlite API 在运行时自行创建函数来修复中断。

sqliteConnection
    .CreateFunction<double, double, double>(
        "pow",
        Math.Pow,
        isDeterministic: true);

或者,可以通过将 Select 表达式拆分为由 AsEnumerable 分隔的两个部分来强制执行客户端计算。

// Before
var query = dbContext.Cylinders
    .Select(
        c => new
        {
            Id = c.Id
            // May throw "no such function: pow"
            Volume = Math.PI * Math.Pow(c.Radius, 2) * c.Height
        });

// After
var query = dbContext.Cylinders
    // Select the properties you'll need from the database
    .Select(
        c => new
        {
            c.Id,
            c.Radius,
            c.Height
        })
    // Switch to client-eval
    .AsEnumerable()
    // Select the final results
    .Select(
        c => new
        {
            Id = c.Id,
            Volume = Math.PI * Math.Pow(c.Radius, 2) * c.Height
        });

ITypeBase 在某些 API 中替换 IEntityType

跟踪问题 #13947

旧行为

以前,所有映射的结构类型都是实体类型。

新行为

随着在 EF8 中引入复杂类型,一些以前使用 IEntityType 的 API 现在使用 ITypeBase,以便 API 可以与实体或复杂类型一起使用。 这包括:

  • IProperty.DeclaringEntityType 现已过时,应改用 IProperty.DeclaringType
  • IEntityTypeIgnoredConvention 现已过时,应改用 ITypeIgnoredConvention
  • 现在,IValueGeneratorSelector.Select 接受 ITypeBase,它可以但不一定是 IEntityType

原因

随着在 EF8 中引入复杂类型,这些 API 可以与 IEntityTypeIComplexType 一起使用。

缓解措施

旧 API 已过时,但在 EF10 之前不会移除。 应尽快更新代码以使用新的 API。

ValueConverter 和 ValueComparer 表达式必须为已编译的模型使用公共 API

跟踪问题 #24896

旧行为

以前,ValueConverterValueComparer 定义未包含在已编译的模型中,因此可能包含任意代码。

新行为

现在,EF 从 ValueConverterValueComparer 对象中提取表达式,并在已编译的模型中包含这些 C#。 这意味着这些表达式只能使用公共 API。

为什么

EF 团队正在逐步将更多构造移动到已编译的模型中,以支持未来将 EF Core 与 AOT 配合使用。

缓解措施

公开比较器使用的 API。 例如,考虑此简单转换器:

public class MyValueConverter : ValueConverter<string, byte[]>
{
    public MyValueConverter()
        : base(v => ConvertToBytes(v), v => ConvertToString(v))
    {
    }

    private static string ConvertToString(byte[] bytes)
        => ""; // ... TODO: Conversion code

    private static byte[] ConvertToBytes(string chars)
        => Array.Empty<byte>(); // ... TODO: Conversion code
}

若要将此转换器用于 EF8 的已编译模型中,必须公开 ConvertToStringConvertToBytes 方法。 例如:

public class MyValueConverter : ValueConverter<string, byte[]>
{
    public MyValueConverter()
        : base(v => ConvertToBytes(v), v => ConvertToString(v))
    {
    }

    public static string ConvertToString(byte[] bytes)
        => ""; // ... TODO: Conversion code

    public static byte[] ConvertToBytes(string chars)
        => Array.Empty<byte>(); // ... TODO: Conversion code
}

ExcludeFromMigrations 不再排除 TPC 层次结构中的其他表

跟踪问题 #30079

旧行为

以前,对 TPC 层次结构中的表使用 ExcludeFromMigrations 也会排除层次结构中的其他表。

新行为

从 EF Core 8.0 开始,ExcludeFromMigrations 不再影响其他表。

为什么

旧行为是一个 bug,导致无法使用迁移来跨项目管理层次结构。

缓解措施

在应排除的任何其他表上显式使用 ExcludeFromMigrations

非阴影整数键保留到 Cosmos 文档

跟踪问题 #31664

旧行为

以前,符合作为合成键属性条件的非阴影整数属性不会保留到 JSON 文档中,而是在导出时重新合成。

新行为

从 EF Core 8.0 开始,这些属性现已保留。

原因

旧行为是一个 bug,导致符合合成键条件的属性无法保留到 Cosmos。

缓解措施

如果不应保留属性的值,则从模型中排除该属性。 此外,可以通过将 Microsoft.EntityFrameworkCore.Issue31664AppContext 开关设置为 true 来完全禁用此行为,有关详细信息,请参阅适合于库使用者的 AppContext

AppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue31664", isEnabled: true);

关系模型在编译的模型中生成

跟踪问题 #24896

旧行为

以前,即使在使用编译的模型时,关系模型也会在运行时计算。

新行为

从 EF Core 8.0 开始,关系模型已成为生成编译模型的一部分。 但是,对于特别大的模型,生成的文件可能无法编译。

原因

这样做是为了进一步缩短启动时间。

缓解措施

编辑生成的 *ModelBuilder.cs 文件并删除行 AddRuntimeAnnotation("Relational:RelationalModel", CreateRelationalModel()); 和方法 CreateRelationalModel()

基架可能会生成不同的导航名称

跟踪问题 #27832

旧行为

以前,当从现有数据库搭建 DbContext 和实体类型基架时,关系的导航名称有时派生自多个外键列名称的公用前缀。

新行为

从 EF Core 8.0 开始,复合外键中列名称的公用前缀不再用于生成导航名称。

原因

这是一个模糊不清的命名规则,有时会生成非常糟糕的名称,例如,SStudent_,甚至只是 _。 如果没有此规则,则不会再生成奇怪的名称,并且导航的命名约定也会更简单,从而更易于理解和预测将生成的名称。

缓解措施

EF Core Power Tools 可以选择以旧方式生成导航。 或者,可以使用 T4 模板完全自定义生成的代码。 这可用于示范基架关系的外键属性,并使用适合代码的任何规则来生成所需的导航名称。

鉴别器现在具有最大长度

跟踪问题 #10691

旧行为

以前,为 TPH 继承映射创建的鉴别器列在 SQL Server/Azure SQL 上配置为 nvarchar(max),或者在其他数据库上配置为等效的未绑定字符串类型。

新行为

从 EF Core 8.0 开始,会创建具有最大长度的鉴别器列,其涵盖所有已知的鉴别器值。 EF 将生成迁移以执行此更改。 但如果以某种方式约束鉴别器列(例如,作为索引的一部分),则由迁移创建的 AlterColumn 可能会失败。

原因

当已知所有可能值的长度时,nvarchar(max) 列效率低下且不必要。

缓解措施

列大小可以显式取消绑定:

modelBuilder.Entity<Foo>()
    .Property<string>("Discriminator")
    .HasMaxLength(-1);

SQL Server 键值比较时不区分大小写

跟踪问题 #27526

旧行为

以前,使用 SQL Server/Azure SQL 数据库提供程序跟踪具有字符串键的实体时,将使用默认的 .NET 区分大小写的序号比较器来比较键值。

新行为

从 EF Core 8.0 开始,使用默认的 .NET 不区分大小写的序号比较器比较 SQL Server/Azure SQL 字符串键值。

原因

默认情况下,SQL Server 在比较匹配项的外键值与主体键值时,使用不区分大小写的比较。 这意味着当 EF 使用区分大小写的比较时,可能不会在应当将外键连接到主体键时执行此操作。

缓解措施

可以通过设置自定义 ValueComparer 来使用区分大小写的比较。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var comparer = new ValueComparer<string>(
        (l, r) => string.Equals(l, r, StringComparison.Ordinal),
        v => v.GetHashCode(),
        v => v);

    modelBuilder.Entity<Blog>()
        .Property(e => e.Id)
        .Metadata.SetValueComparer(comparer);

    modelBuilder.Entity<Post>(
        b =>
        {
            b.Property(e => e.Id).Metadata.SetValueComparer(comparer);
            b.Property(e => e.BlogId).Metadata.SetValueComparer(comparer);
        });
}