此页面记录了 API 和行为更改,这些更改可能会中断从 EF Core 9 更新到 EF Core 10 的现有应用程序。 如果从早期版本的 EF Core 进行更新,请务必查看之前的中断性变更:
总结
注释
如果使用 Microsoft.Data.Sqlite,请参阅 以下关于 Microsoft.Data.Sqlite 中断性变更的单独部分。
影响中等的更改
EF 工具现在要求为多目标项目指定框架
旧行为
以前,EF 工具(dotnet-ef)可用于面向多个框架的项目,而无需指定要使用的框架。
新行为
从 EF Core 10.0 开始,在针对多个框架的项目上运行 EF 工具时(使用<TargetFrameworks>而不是<TargetFramework>),必须使用--framework选项显式指定目标框架。 如果没有此选项,将引发以下错误:
项目面向多个框架。 使用 --framework 选项指定要使用的目标框架。
为什么
在 EF Core 10 中,这些工具开始依赖于 ResolvePackageAssets MSBuild 任务来获取有关项目依赖项的更准确的信息。 但是,如果项目面向多个目标框架(TFM),则此任务不可用。 该解决方案要求用户选择应使用哪个框架。
缓解措施
在面向多个框架的项目上运行任何 EF 工具命令时,请使用此选项 --framework 指定目标框架。 例如:
dotnet ef migrations add MyMigration --framework net9.0
dotnet ef database update --framework net9.0
dotnet ef migrations script --framework net9.0
如果项目文件如下所示:
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
</PropertyGroup>
运行 EF 工具时,需要选择其中一个框架(例如 net9.0)。
影响较低的更改
应用程序名称现在注入到连接字符串中
新行为
当传递给 EF 的连接字符串中不包含 Application Name 时,EF 现在会在其中插入一个 Application Name,该对象包含关于所使用 EF 和 SqlClient 版本的匿名信息。 在绝大多数情况下,这不会以任何方式影响应用程序,但在某些情况下可能会影响行为。 例如,如果使用 EF 和另一种非 EF 数据访问技术(例如 Dapper、ADO.NET)连接到同一个数据库,SqlClient 将使用不同的内部连接池,因为 EF 现在将使用不同、更新的连接字符串(其中 "Application Name" 已被注入)。 如果这种混合访问是在 TransactionScope 内部完成的,那么由于使用了两个连接字符串,这可能会导致升级为之前无需的分布式事务,因为 SqlClient 将其识别为两个不同的数据库。
缓解措施
缓解措施是只需在连接字符串中定义一个 Application Name 。 定义一个后,EF 不会覆盖它,原始连接字符串将完全保留 as-is。
Azure SQL 和兼容性级别 170 上默认使用的 SQL Server json 数据类型
旧行为
以前,将基元集合或拥有的类型映射到数据库中的 JSON 时,SQL Server 提供程序将 JSON 数据存储在 nvarchar(max) 列中:
public class Blog
{
// ...
// Primitive collection, mapped to nvarchar(max) JSON column
public string[] Tags { get; set; }
// Owned entity type mapped to nvarchar(max) JSON column
public List<Post> Posts { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().OwnsMany(b => b.Posts, b => b.ToJson());
}
对于上述情况,EF 以前生成了下表:
CREATE TABLE [Blogs] (
...
[Tags] nvarchar(max),
[Posts] nvarchar(max)
);
新行为
使用 EF 10 时,如果使用(UseAzureSql)配置 EF ,或者将 EF 配置为兼容级别为 170 或更高版本(请参阅文档),EF 将改为映射到新的 JSON 数据类型:
CREATE TABLE [Blogs] (
...
[Tags] json
[Posts] json
);
尽管建议使用新的 JSON 数据类型在 SQL Server 中存储 JSON 数据,但在转换 nvarchar(max)时可能存在一些行为差异,某些特定的查询表单可能不受支持。 例如,SQL Server 不支持对 JSON 数组使用 DISTINCT 运算符,尝试这样做的查询将失败。
请注意,如果你有现有表并且正在使用 UseAzureSql,则升级到 EF 10 将导致生成迁移,这会更改所有现有 nvarchar(max) JSON 列 json。 支持此更改作,应该无缝应用,且没有任何问题,但对数据库进行非简单更改。
为什么
SQL Server 引入的新 JSON 数据类型是一种高级、第一类的方法,用于在数据库中存储和与 JSON 数据进行交互;这明显带来了显著的性能改进(请参阅文档)。 建议使用 Azure SQL 数据库或 SQL Server 2025 的所有应用程序迁移到新的 JSON 数据类型。
缓解措施
如果面向 Azure SQL 数据库,但不想立即过渡到新的 JSON 数据类型,则可以使用低于 170 的兼容级别配置 EF:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseAzureSql("<connection string>", o => o.UseCompatibilityLevel(160));
}
如果面向本地 SQL Server,则默认兼容级别 UseSqlServer 当前为 150(SQL Server 2019),因此不使用 JSON 数据类型。
或者,可以显式将特定属性 nvarchar(max)的列类型设置为:
public class Blog
{
public string[] Tags { get; set; }
public List<Post> Posts { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().PrimitiveCollection(b => b.Tags).HasColumnType("nvarchar(max)");
modelBuilder.Entity<Blog>().OwnsMany(b => b.Posts, b => b.ToJson().HasColumnType("nvarchar(max)"));
modelBuilder.Entity<Blog>().ComplexProperty(e => e.Posts, b => b.ToJson());
}
参数化集合现在默认使用多个参数
旧行为
在 EF Core 9 及更早的版本中,LINQ 查询中的参数化集合(例如用于.Contains()中的集合)默认通过使用 JSON 数组参数来转换为 SQL。 请考虑下列查询:
int[] ids = [1, 2, 3];
var blogs = await context.Blogs.Where(b => ids.Contains(b.Id)).ToListAsync();
在 SQL Server 上,这生成了以下 SQL:
@__ids_0='[1,2,3]'
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Id] IN (
SELECT [i].[value]
FROM OPENJSON(@__ids_0) WITH ([value] int '$') AS [i]
)
新行为
从 EF Core 10.0 开始,参数化集合现在默认使用多个标量参数进行转换:
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Id] IN (@ids1, @ids2, @ids3)
为什么
新的默认翻译为查询规划器提供了有关集合的基数信息,这可能导致许多方案中更好的查询计划。 多个参数方法平衡计划缓存效率(通过参数化)和查询优化(通过提供基数)。
但是,不同的工作负荷可能会受益于不同的转换策略,具体取决于集合大小、查询模式和数据库特征。
缓解措施
如果遇到新的默认行为(例如性能回归)的问题,则可以全局配置转换模式:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer("<CONNECTION STRING>",
o => o.UseParameterizedCollectionMode(ParameterTranslationMode.Constant));
可用模式包括:
-
ParameterTranslationMode.MultipleParameters- 新的默认值(多个标量参数) -
ParameterTranslationMode.Constant- 内联值作为常量(EF8 前默认行为) -
ParameterTranslationMode.Parameter- 使用 JSON 数组参数(EF8-9 默认值)
您还可以针对每个查询单独进行翻译设置控制:
// Use constants instead of parameters for this specific query
var blogs = await context.Blogs
.Where(b => EF.Constant(ids).Contains(b.Id))
.ToListAsync();
// Use a single parameter (e.g. JSON parameter with OPENJSON) instead of parameters for this specific query
var blogs = await context.Blogs
.Where(b => EF.Parameter(ids).Contains(b.Id))
.ToListAsync();
// Use multiple scalar parameters for this specific query. This is the default in EF 10, but is useful if the default was changed globally:
var blogs = await context.Blogs
.Where(b => EF.MultipleParameters(ids).Contains(b.Id))
.ToListAsync();
有关参数化集合转换的详细信息, 请参阅文档。
ExecuteUpdateAsync 现接受常规的非表达式 lambda
旧行为
以前,ExecuteUpdate 接受用于列设置器的表达式树参数(Expression<Func<...>>)。
新行为
从 EF Core 10.0 开始,ExecuteUpdate 现在接受用于列设置器的非表达式参数(Func<...>)。 如果您正在构建表达式树以动态创建列设置器参数,则代码将无法编译,但可以替换为更简单的替代方法(请参阅下文)。
为什么
由于列设置器参数是表达式树,因此在动态构建列设置器时变得非常困难,其中某些设置器只有在某些条件下才存在(有关示例,请参见下面的示例部分)。
缓解措施
生成用于动态创建列设置器参数的表达式树的代码需要重写——但重写后的结果会更简单。 例如,假设我们要更新博客的浏览次数,同时在特定条件下更新其名称。 由于 setters 参数是表达式树,因此需要编写以下代码:
// Base setters - update the Views only
Expression<Func<SetPropertyCalls<Blog>, SetPropertyCalls<Blog>>> setters =
s => s.SetProperty(b => b.Views, 8);
// Conditionally add SetProperty(b => b.Name, "foo") to setters, based on the value of nameChanged
if (nameChanged)
{
var blogParameter = Expression.Parameter(typeof(Blog), "b");
setters = Expression.Lambda<Func<SetPropertyCalls<Blog>, SetPropertyCalls<Blog>>>(
Expression.Call(
instance: setters.Body,
methodName: nameof(SetPropertyCalls<Blog>.SetProperty),
typeArguments: [typeof(string)],
arguments:
[
Expression.Lambda<Func<Blog, string>>(Expression.Property(blogParameter, nameof(Blog.Name)), blogParameter),
Expression.Constant("foo")
]),
setters.Parameters);
}
await context.Blogs.ExecuteUpdateAsync(setters);
手动创建表达式树非常复杂且容易出错,并且使此常见方案比应该的要困难得多。 从 EF 10 开始,您现在可以改为书写以下内容:
await context.Blogs.ExecuteUpdateAsync(s =>
{
s.SetProperty(b => b.Views, 8);
if (nameChanged)
{
s.SetProperty(b => b.Name, "foo");
}
});
复杂类型的列名现已唯一化
旧行为
以前,将复杂类型映射到表列时,如果不同复杂类型中的多个属性具有相同的列名,则它们将无提示共享同一列。
新行为
从 EF Core 10.0 开始,如果表中存在同名的另一列,则复杂类型列名称通过在末尾追加一个数字来确保唯一性。
为什么
这可以防止在无意中将多个属性映射到同一列时发生数据损坏。
缓解措施
如果需要多个属性来共享同一列,请显式配置它们:
modelBuilder.Entity<Customer>(b =>
{
b.ComplexProperty(c => c.ShippingAddress, p => p.Property(a => a.Street).HasColumnName("Street"));
b.ComplexProperty(c => c.BillingAddress, p => p.Property(a => a.Street).HasColumnName("Street"));
});
嵌套复杂类型属性在列名称中使用完整路径
旧行为
以前,嵌套复杂类型的属性仅使用声明类型名称映射到列。 例如, EntityType.Complex.NestedComplex.Property 映射到列 NestedComplex_Property。
新行为
从 EF Core 10.0 开始,嵌套复杂类型的属性使用属性的完整路径作为列名称的一部分。 例如, EntityType.Complex.NestedComplex.Property 现在映射到列 Complex_NestedComplex_Property。
为什么
这提供更好的列名称唯一性,并明确哪些属性映射到哪个列。
缓解措施
如果需要维护旧的列名称,请显式配置它们:
modelBuilder.Entity<EntityType>()
.ComplexProperty(e => e.Complex)
.ComplexProperty(o => o.NestedComplex)
.Property(c => c.Property)
.HasColumnName("NestedComplex_Property");
IDiscriminatorPropertySetConvention 签名已更改
旧行为
以前, IDiscriminatorPropertySetConvention.ProcessDiscriminatorPropertySet 用作 IConventionEntityTypeBuilder 参数。
新行为
从 EF Core 10.0 开始,方法签名更改为采用 IConventionTypeBaseBuilder,而不是 IConventionEntityTypeBuilder。
为什么
此更改允许约定同时处理实体类型和复杂类型。
缓解措施
更新自定义约定实现以使用新签名:
public virtual void ProcessDiscriminatorPropertySet(
IConventionTypeBaseBuilder typeBaseBuilder, // Changed from IConventionEntityTypeBuilder
string name,
Type type,
MemberInfo memberInfo,
IConventionContext<IConventionProperty> context)
IRelationalCommandDiagnosticsLogger 的方法添加了 logCommandText 参数
旧行为
以前,IRelationalCommandDiagnosticsLogger上的方法,例如CommandReaderExecuting、CommandReaderExecuted、CommandScalarExecuting等都接受一个参数,该参数表示正在执行的数据库命令command。
新行为
从 EF Core 10.0 开始,这些方法现在需要额外的 logCommandText 参数。 此参数包含将被记录的 SQL 命令文本,如果未启用 EnableSensitiveDataLogging(),其中可能会对敏感数据进行编辑。
为什么
此更改支持新功能,默认从日志记录中隐去内联常量。 当 EF 将参数值内联到 SQL 中(例如使用 EF.Constant() 时),这些值现在会从日志中去除,除非明确启用敏感数据日志记录。 该 logCommandText 参数为日志记录目的提供经过编辑的 SQL,而 command 该参数包含执行的实际 SQL。
缓解措施
如果有自定义实现 IRelationalCommandDiagnosticsLogger,则需要更新方法签名以包含新 logCommandText 参数。 例如:
public InterceptionResult<DbDataReader> CommandReaderExecuting(
IRelationalConnection connection,
DbCommand command,
DbContext context,
Guid commandId,
Guid connectionId,
DateTimeOffset startTime,
string logCommandText) // New parameter
{
// Use logCommandText for logging purposes
// Use command for execution-related logic
}
该 logCommandText 参数包含要记录的 SQL(内联常量可能会删除),而 command.CommandText 包含将对数据库执行的实际 SQL。
Microsoft.Data.Sqlite 重大变更
总结
| 重大变更 | 影响 |
|---|---|
| 使用不带偏移的 GetDateTimeOffset 现在假定 UTC | 高 |
| 将 DateTimeOffset 写入 REAL 列现在以 UTC 格式写入 | 高 |
| 使用带有偏移量的 GetDateTime 现在返回 UTC 时间值 | 高 |
影响较大的更改
使用不带偏移的 GetDateTimeOffset 现在假定 UTC
旧行为
以前,在对一个没有偏移量的文本时间戳(例如 GetDateTimeOffset 或 2014-04-15 10:47:16)使用 Microsoft.Data.Sqlite 时,它会假定该值位于本地时区。 即值被分析为 2014-04-15 10:47:16+02:00 (假设本地时区为 UTC+2)。
新行为
从 Microsoft.Data.Sqlite 10.0 开始,当在没有偏移量的文本时间戳上使用 GetDateTimeOffset 时,Microsoft.Data.Sqlite 将假定该值为 UTC 时间。
为什么
这是为了与 SQLite 的行为保持一致,其中不带时区偏移的时间戳被视为 UTC。
缓解措施
应相应地调整代码。
作为最后或临时的解决方案,可以通过将Microsoft.Data.Sqlite.Pre10TimeZoneHandling AppContext 开关设置为true的方式恢复之前的行为。有关详细信息,请参阅适用于库使用者的 AppContext。
AppContext.SetSwitch("Microsoft.Data.Sqlite.Pre10TimeZoneHandling", isEnabled: true);
将 DateTimeOffset 写入 REAL 列现在以 UTC 格式写入
旧行为
以前,在将值写入 DateTimeOffset REAL 列中时,Microsoft.Data.Sqlite 将写入该值,而不考虑偏移量。
新行为
从 Microsoft.Data.Sqlite 10.0 开始,在将值写入 DateTimeOffset REAL 列时,Microsoft.Data.Sqlite 会在执行转换和写入之前将该值转换为 UTC。
为什么
写入的值不正确,与 SQLite 的行为不一致,其中 REAL 时间戳被求和为 UTC。
缓解措施
应相应地调整代码。
作为最后或临时的解决方案,可以通过将Microsoft.Data.Sqlite.Pre10TimeZoneHandling AppContext 开关设置为true的方式恢复之前的行为。有关详细信息,请参阅适用于库使用者的 AppContext。
AppContext.SetSwitch("Microsoft.Data.Sqlite.Pre10TimeZoneHandling", isEnabled: true);
使用带有偏移量的 GetDateTime 现在返回的是 UTC 值
旧行为
以前,在具有偏移量的文本时间戳(例如 GetDateTime)上使用 2014-04-15 10:47:16+02:00 时,Microsoft.Data.Sqlite 会返回带有 DateTimeKind.Local 的值,即使偏移量不是本地。 正确解析了时间,并考虑了偏移量。
新行为
从 Microsoft.Data.Sqlite 10.0 开始,使用 GetDateTime 处理具有偏移量的文本时间戳时,Microsoft.Data.Sqlite 会将该值转换为 UTC,并通过 DateTimeKind.Utc 返回。
为什么
尽管时间分析正确,但它依赖于计算机配置的本地时区,这可能会导致意外结果。
缓解措施
应相应地调整代码。
作为最后或临时的解决方案,可以通过将Microsoft.Data.Sqlite.Pre10TimeZoneHandling AppContext 开关设置为true的方式恢复之前的行为。有关详细信息,请参阅适用于库使用者的 AppContext。
AppContext.SetSwitch("Microsoft.Data.Sqlite.Pre10TimeZoneHandling", isEnabled: true);