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 版本上停止工作
旧行为
以前,当 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 中的枚举默认存储为整数而不是字符串
旧行为
在 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 date
和 time
现已搭建到 .NET DateOnly
和 TimeOnly
旧行为
以前,当使用 date
或 time
列搭建 SQL Server 数据库时,EF 将生成类型为 DateTime 和 TimeSpan 的实体属性。
新行为
从 EF Core 8.0 开始,date
和 time
被搭建为 DateOnly 和 TimeOnly。
原因
DateOnly 和 TimeOnly 是在 .NET 6.0 中引入的,非常适合映射数据库日期和时间类型。 DateTime 特别包含未使用的时间组件,在将其映射到 date
时可能会造成混淆,而 TimeSpan 表示时间间隔,可能包括天数而不是一天中事件发生的时间。 使用新类型可以防止 bug 和混淆,并提供清晰的意图。
缓解措施
此更改仅影响定期将其数据库重新搭建到 EF 代码模型(“数据库优先”流)的用户。
建议通过修改代码以使用新搭建的 DateOnly 和 TimeOnly 类型来应对这一更改。 但是,如果这不可能,可以编辑基架模板以还原到以前的映射。 为此,请按照本页中所述设置模板。 然后,编辑 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
旧行为
以前,具有数据库默认约束的不可为 null 的 bool
列被搭建基架为可为 null 的 bool?
属性。
新行为
从 EF Core 8.0 开始,不可为 null 的 bool
列始终被搭建基架为不可为 null 的属性。
原因
如果 bool
属性的值为 false
(CLR 默认值),则该属性的值不会发送到数据库。 如果数据库中该列的默认值为 true
,则即使属性值为 false
,数据库中的值最终仍为 true
。 然而,在 EF8 中,sentinel 用于确定属性的值是否可以更改。 对于数据库生成的值为 true
的 bool
属性,会自动完成此操作,这意味着不再需要将属性搭建基架为可为 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
旧行为
以前只会将 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
旧行为
以前,所有映射的结构类型都是实体类型。
新行为
随着在 EF8 中引入复杂类型,一些以前使用 IEntityType
的 API 现在使用 ITypeBase
,以便 API 可以与实体或复杂类型一起使用。 这包括:
IProperty.DeclaringEntityType
现已过时,应改用IProperty.DeclaringType
。IEntityTypeIgnoredConvention
现已过时,应改用ITypeIgnoredConvention
。- 现在,
IValueGeneratorSelector.Select
接受ITypeBase
,它可以但不一定是IEntityType
。
原因
随着在 EF8 中引入复杂类型,这些 API 可以与 IEntityType
或 IComplexType
一起使用。
缓解措施
旧 API 已过时,但在 EF10 之前不会移除。 应尽快更新代码以使用新的 API。
ValueConverter 和 ValueComparer 表达式必须为已编译的模型使用公共 API
旧行为
以前,ValueConverter
和 ValueComparer
定义未包含在已编译的模型中,因此可能包含任意代码。
新行为
现在,EF 从 ValueConverter
和 ValueComparer
对象中提取表达式,并在已编译的模型中包含这些 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 的已编译模型中,必须公开 ConvertToString
和 ConvertToBytes
方法。 例如:
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 层次结构中的其他表
旧行为
以前,对 TPC 层次结构中的表使用 ExcludeFromMigrations
也会排除层次结构中的其他表。
新行为
从 EF Core 8.0 开始,ExcludeFromMigrations
不再影响其他表。
为什么
旧行为是一个 bug,导致无法使用迁移来跨项目管理层次结构。
缓解措施
在应排除的任何其他表上显式使用 ExcludeFromMigrations
。
非阴影整数键保留到 Cosmos 文档
旧行为
以前,符合作为合成键属性条件的非阴影整数属性不会保留到 JSON 文档中,而是在导出时重新合成。
新行为
从 EF Core 8.0 开始,这些属性现已保留。
原因
旧行为是一个 bug,导致符合合成键条件的属性无法保留到 Cosmos。
缓解措施
如果不应保留属性的值,则从模型中排除该属性。
此外,可以通过将 Microsoft.EntityFrameworkCore.Issue31664
AppContext 开关设置为 true
来完全禁用此行为,有关详细信息,请参阅适合于库使用者的 AppContext。
AppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue31664", isEnabled: true);
关系模型在编译的模型中生成
旧行为
以前,即使在使用编译的模型时,关系模型也会在运行时计算。
新行为
从 EF Core 8.0 开始,关系模型已成为生成编译模型的一部分。 但是,对于特别大的模型,生成的文件可能无法编译。
原因
这样做是为了进一步缩短启动时间。
缓解措施
编辑生成的 *ModelBuilder.cs
文件并删除行 AddRuntimeAnnotation("Relational:RelationalModel", CreateRelationalModel());
和方法 CreateRelationalModel()
。
基架可能会生成不同的导航名称
旧行为
以前,当从现有数据库搭建 DbContext
和实体类型基架时,关系的导航名称有时派生自多个外键列名称的公用前缀。
新行为
从 EF Core 8.0 开始,复合外键中列名称的公用前缀不再用于生成导航名称。
原因
这是一个模糊不清的命名规则,有时会生成非常糟糕的名称,例如,S
、Student_
,甚至只是 _
。 如果没有此规则,则不会再生成奇怪的名称,并且导航的命名约定也会更简单,从而更易于理解和预测将生成的名称。
缓解措施
EF Core Power Tools 可以选择以旧方式生成导航。 或者,可以使用 T4 模板完全自定义生成的代码。 这可用于示范基架关系的外键属性,并使用适合代码的任何规则来生成所需的导航名称。
鉴别器现在具有最大长度
旧行为
以前,为 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 键值比较时不区分大小写
旧行为
以前,使用 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);
});
}
按不同顺序应用多个 AddDbContext 调用
旧行为
以前,当对 AddDbContext
、AddDbContextPool
、AddDbContextFactory
或 AddPooledDbContextFactor
的多个调用具有相同的上下文类型但配置冲突时,第一个调用获胜。
新行为
从 EF Core 8.0 开始,上次调用的配置将优先。
原因
这被更改为与新方法 ConfigureDbContext
一致,该方法可用于在 Add*
方法之前或之后添加配置。
缓解措施
反转 Add*
调用的顺序。
EntityTypeAttributeConventionBase 已替换为 TypeAttributeConventionBase
新行为
在 EF Core 8.0 中,EntityTypeAttributeConventionBase
被重命名为 TypeAttributeConventionBase
。
原因
TypeAttributeConventionBase
更好地表示了功能,因为它现在可以用于复杂类型和实体类型。
缓解措施
将 EntityTypeAttributeConventionBase
用法替换为 TypeAttributeConventionBase
。