EF Core 10 中的重大变更(EF10)

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

总结

注释

如果使用 Microsoft.Data.Sqlite,请参阅 以下关于 Microsoft.Data.Sqlite 中断性变更的单独部分

重大变更 影响
EF 工具现在要求为多目标项目指定框架 中等
应用程序名称现在注入到连接字符串中
Azure SQL 和兼容性级别 170 上默认使用的 SQL Server json 数据类型
参数化集合现在默认使用多个参数
ExecuteUpdateAsync 现接受常规的非表达式 lambda
复杂类型列名称现在做到唯一
嵌套复杂类型属性在列名称中使用完整路径
IDiscriminatorPropertySetConvention 签名已更改
IRelationalCommandDiagnosticsLogger 方法添加 logCommandText 参数

影响中等的更改

EF 工具现在要求为多目标项目指定框架

跟踪问题 #37230

旧行为

以前,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)。

影响较低的更改

应用程序名称现在注入到连接字符串中

跟踪问题 #35730

新行为

当传递给 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 数据类型

跟踪问题 #36372

旧行为

以前,将基元集合或拥有的类型映射到数据库中的 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());
}

参数化集合现在默认使用多个参数

跟踪问题 #34346

旧行为

在 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

跟踪问题 #32018

旧行为

以前,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");
    }
});

复杂类型的列名现已唯一化

跟踪问题 #4970

旧行为

以前,将复杂类型映射到表列时,如果不同复杂类型中的多个属性具有相同的列名,则它们将无提示共享同一列。

新行为

从 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 参数

跟踪问题 #35757

旧行为

以前,IRelationalCommandDiagnosticsLogger上的方法,例如CommandReaderExecutingCommandReaderExecutedCommandScalarExecuting等都接受一个参数,该参数表示正在执行的数据库命令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

跟踪问题 #36195

旧行为

以前,在对一个没有偏移量的文本时间戳(例如 GetDateTimeOffset2014-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 格式写入

跟踪问题 #36195

旧行为

以前,在将值写入 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 值

跟踪问题 #36195

旧行为

以前,在具有偏移量的文本时间戳(例如 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);