EF Core 7.0 (EF7) 中的中断性变更

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

目标 Framework

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

总结

中断性变更 影响
对于 SQL Server 连接,Encrypt 默认为 true
默认情况下,某些警告将重新引发异常
带有触发器或某些计算列的 SQL Server 表现在需要特殊的 EF Core 配置
具有 AFTER 触发器和虚拟表的 SQLite 表现在需要特殊的 EF Core 配置
不会自动删除可选关系的孤立依赖项
将 TPT 映射与 SQL Server 配合使用时,会在表之间配置级联删除
未使用预写日志记录时,SQLite 上出现忙碌/锁定错误的可能性更高
关键属性可能需要使用提供程序值比较器进行配置
现在在表中配置检查约束和其他表方面
不再修复从新实体到已删除实体的导航
使用错误的提供程序的 FromSqlRaw 和相关方法将引发
搭建的 OnConfiguring 不再调用 IsConfigured

影响较大的更改

对于 SQL Server 连接,Encrypt 默认为 true

跟踪问题:SqlClient #1210

重要

这是 Microsoft.Data.SqlClient 包的严重中断性变更。 无法在 EF Core 中还原或缓解此更改的影响。 如有其他问题或需要帮助,请直接向 Microsoft.Data.SqlClient GitHub 存储库反馈或联系 Microsoft 支持专业人员

旧行为

默认情况下,SqlClient 连接字符串使用 Encrypt=False。 这允许在本地服务器没有有效证书的开发计算机上建立连接。

新行为

默认情况下,SqlClient 连接字符串使用 Encrypt=True。 这表示:

  • 必须使用有效的证书配置该服务器
  • 客户端必须信任此证书

如果未满足这些条件,则会引发 SqlException。 例如:

已成功与服务器建立连接,但在登录过程中发生错误。 (提供程序:SSL 提供程序,错误:0 - 证书链是不受信任的颁发机构颁发的。)

原因

进行此更改是为了确保在默认情况下,或者连接是安全的,或者应用程序无法连接。

缓解措施

有三种继续使用的方法:

  1. 在服务器上安装有效的证书。 请注意这是一个复杂的过程,需要获取证书并确保它由客户端信任的颁发机构签署。
  2. 如果服务器有证书,但客户端不信任它,则 TrustServerCertificate=True 允许绕过正常的信任机制。
  3. Encrypt=False 显式添加到连接字符串。

警告

选项 2 和 3 都会使服务器处于潜在的不安全状态。

默认情况下,某些警告会重新引发异常

跟踪问题 #29069

旧行为

在 EF Core 6.0 中,SQL Server 提供程序中的 bug 导致一些默认配置为引发异常的警告被记录,但未引发异常。 这些警告如下:

EventId 说明
RelationalEventId.AmbientTransactionWarning 应用程序可能期望使用环境事务,但实际上被忽略了。
RelationalEventId.IndexPropertiesBothMappedAndNotMappedToTable 索引指定的属性中,一些映射到表中的列,一些未映射到表中的列。
RelationalEventId.IndexPropertiesMappedToNonOverlappingTables 索引指定了映射到非重叠表上的列的属性。
RelationalEventId.ForeignKeyPropertiesMappedToUnrelatedTables 外键指定了未映射到相关表的属性。

新行为

从 EF Core 7.0 开始,这些警告会在默认情况下重新引发异常。

原因

这些问题很可能表明应用程序代码中存在应修复的错误。

缓解措施

修复导致警告的根本问题。

或者,可以更改警告级别,使其仅被记录或完全取消。 例如:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .ConfigureWarnings(b => b.Ignore(RelationalEventId.AmbientTransactionWarning));

具有触发器或某些计算列的 SQL Server 表现在需要特殊的 EF Core 配置

跟踪问题 #27372

旧行为

先前版本的 SQL Server 提供程序通过一种效率较低但始终有效的技术保存更改。

新行为

默认情况下,EF Core 现在通过更高效的技术保存更改。遗憾的是,如果目标表具有数据库触发器或某些类型的计算列,则 SQL Server 不支持此技术。 有关详细信息,请参阅 SQL Server 文档

原因

新方法的性能改进十分显著,因此务必在默认情况下将其提供给用户。 同时,我们预计 EF Core 应用程序中数据库触发器或受影响的计算列的使用率将足够低,使得性能提升超过中断性变更的负面影响。

缓解措施

从 EF Core 8.0 开始,可以直接配置使用或不使用“OUTPUT”子句。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .ToTable(tb => tb.UseSqlOutputClause(false));
}

在 EF7 或更高版本中,如果目标表具有触发器,则可以让 EF Core 知晓,EF 将恢复使用以前效率较低的技术。 为此,应按如下方式配置相应的实体类型:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .ToTable(tb => tb.HasTrigger("SomeTrigger"));
}

请注意,这样做实际上不会使 EF Core 以任何方式创建或管理触发器 - 目前它只会通知 EF Core 表中存在触发器。 因此,可以使用任何触发器名称。 指定触发器可用于恢复使用旧行为 ,即使表 中实际上没有触发器也是如此。

如果大多数表或所有表都有触发器,则可以使用以下模型生成约定,选择不再对所有模型表使用较新的高效技术:

public class BlankTriggerAddingConvention : IModelFinalizingConvention
{
    public virtual void ProcessModelFinalizing(
        IConventionModelBuilder modelBuilder,
        IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
        {
            var table = StoreObjectIdentifier.Create(entityType, StoreObjectType.Table);
            if (table != null
                && entityType.GetDeclaredTriggers().All(t => t.GetDatabaseName(table.Value) == null)
                && (entityType.BaseType == null
                    || entityType.GetMappingStrategy() != RelationalAnnotationNames.TphMappingStrategy))
            {
                entityType.Builder.HasTrigger(table.Value.Name + "_Trigger");
            }

            foreach (var fragment in entityType.GetMappingFragments(StoreObjectType.Table))
            {
                if (entityType.GetDeclaredTriggers().All(t => t.GetDatabaseName(fragment.StoreObject) == null))
                {
                    entityType.Builder.HasTrigger(fragment.StoreObject.Name + "_Trigger");
                }
            }
        }
    }
}

通过重写 ConfigureConventionsDbContext项使用约定:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Add(_ => new BlankTriggerAddingConvention());
}

这可以有效地对所有模型的表调用 HasTrigger,而无需为每个表手动执行此操作。

具有 AFTER 触发器和虚拟表的 SQLite 表现在需要特殊的 EF Core 配置

跟踪问题 #29916

旧行为

先前版本的 SQLite 提供程序通过一种效率较低但始终有效的技术保存更改。

新行为

默认情况下,EF Core 现在使用 RETURNING 子句通过更高效的技术保存更改。 遗憾的是,如果目标表具有数据库 AFTER 触发器、是虚拟表或正在使用旧版 SQLite,则 SQLite 不支持此方法。 有关详细信息,请参阅 SQLite 文档

原因

新方法的简化和性能改进十分显著,因此务必在默认情况下将其提供给用户。 同时,我们预计 EF Core 应用程序中数据库触发器或虚拟表的使用率将足够低,使得性能提升超过中断性变更的负面影响。

缓解措施

在 EF Core 8.0 中,引入了 UseSqlReturningClause 方法,以直接恢复使用较旧的、效率较低的 SQL。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .ToTable(tb => tb.UseSqlReturningClause(false));
}

如果你仍在使用 EF Core 7.0,则可以通过在上下文配置中插入以下代码,在整个应用程序中恢复使用旧机制:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlite(...)
        .ReplaceService<IUpdateSqlGenerator, SqliteLegacyUpdateSqlGenerator>();

影响中等的更改

不会自动删除可选关系的孤立依赖项

跟踪问题 #27217

旧行为

如果外键可以为 null,则关系是可选的。 将外键设置为 null,可让依赖实体在没有任何相关的主体实体的情况下存在。 可选关系可配置为使用级联删除,尽管这不是默认设置。

通过将可选依赖项的外键设置为 null 或清除指向/来自可选依赖项的导航,可以将其从主体分离。 在 EF Core 6.0 中,这会导致在为级联删除配置关系时删除该依赖项。

新行为

从 EF Core 7.0 开始,从属关系不再会被删除。 请注意,如果主体被删除,则依赖项仍将被删除,因为已为该关系配置了级联删除。

原因

依赖项可以在与主体没有任何关系的情况下存在,因此切断关系不应导致删除实体。

缓解措施

可以显式删除依赖项:

context.Remove(blog);

也可以重写或截获 SaveChanges 以删除没有主体引用的依赖项。 例如:

context.SavingChanges += (c, _) =>
    {
        foreach (var entry in ((DbContext)c!).ChangeTracker
            .Entries<Blog>()
            .Where(e => e.State == EntityState.Modified))
        {
            if (entry.Reference(e => e.Author).CurrentValue == null)
            {
                entry.State = EntityState.Deleted;
            }
        }
    };

将 TPT 映射与 SQL Server 配合使用时,会在表之间配置级联删除

跟踪问题 #28532

旧行为

使用 TPT 策略映射继承层次结构时,基表必须包含保存的每个实体的行,无论该实体的实际类型如何。 删除基表中的行会删除所有其他表中的行。 EF Core 为此配置了级联删除

在 EF Core 6.0 中,SQL Server 数据库提供程序中的 bug 导致不会创建这些级联删除。

新行为

从 EF Core 7.0 开始,现在会像对其他数据库一样为 SQL Server 创建级联删除。

原因

从基表到 TPT 中的子表的级联删除允许通过删除基表中的行来删除实体。

缓解措施

在大多数情况下,此更改不会导致任何问题。 但当表之间配置了多个级联行为时,SQL Server 会受到很大限制。 这意味着如果 TPT 映射中的表之间已存在级联关系,那么 SQL Server 可能会生成以下错误:

Microsoft.Data.SqlClient.SqlException:DELETE 语句与 REFERENCE 约束“FK_Blogs_People_OwnerId”发生冲突。 数据库 "Scratch"、表 "dbo.Blogs"、列 "OwnerId" 中发生冲突。 语句已终止。

例如,此模型创建了级联关系的循环:

[Table("FeaturedPosts")]
public class FeaturedPost : Post
{
    public int ReferencePostId { get; set; }
    public Post ReferencePost { get; set; } = null!;
}

[Table("Posts")]
public class Post
{
    public int Id { get; set; }
    public string? Title { get; set; }
    public string? Content { get; set; }
}

其中一项需要配置为不在服务器上使用级联删除。 例如,若要更改显式关系:

modelBuilder
    .Entity<FeaturedPost>()
    .HasOne(e => e.ReferencePost)
    .WithMany()
    .OnDelete(DeleteBehavior.ClientCascade);

或要更改为 TPT 映射创建的隐式关系:

modelBuilder
    .Entity<FeaturedPost>()
    .HasOne<Post>()
    .WithOne()
    .HasForeignKey<FeaturedPost>(e => e.Id)
    .OnDelete(DeleteBehavior.ClientCascade);

未使用预写日志记录时,SQLite 上出现忙碌/锁定错误的可能性更高

旧行为

以前版本的 SQLite 提供程序通过效率较低的技术保存更改,该操作在表锁定/繁忙且未启用预写日志记录 (WAL) 时可自动重试。

新行为

默认情况下,EF Core 现在使用 RETURNING 子句通过更高效的技术保存更改。 遗憾的是,此方法在表忙碌/锁定时无法自动重试。 在不使用预写日志记录的多线程应用程序(如 Web 应用)中,通常会发生这些错误。

为什么

新方法的简化和性能改进十分显著,因此务必在默认情况下将其提供给用户。 EF Core 创建的数据库也默认启用预写日志记录。 SQLite 团队还建议默认启用预写日志记录。

缓解措施

如果可能,应在数据库上启用预写日志记录。 如果数据库是由 EF 创建的,应已启用。 如未启用,可通过执行以下命令启用预写日志记录。

PRAGMA journal_mode = 'wal';

如果因某种原因无法启用预写日志记录,可以通过在上下文配置中插入以下代码将整个应用程序还原到旧机制:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlite(...)
        .ReplaceService<IUpdateSqlGenerator, SqliteLegacyUpdateSqlGenerator>();

影响较小的更改

关键属性可能需要使用提供程序值比较器进行配置

跟踪问题 #27738

旧行为

在 EF Core 6.0 中,直接从实体类型属性提取的关键值用于在保存更改时比较关键值。 这将利用在这些属性上配置的任何自定义值比较器

新行为

从 EF Core 7.0 开始,这些比较将使用数据库值。 这“只适用于”绝大多数案例。 但是,如果属性使用的是自定义比较器,并且该比较器无法应用于数据库值,则可能需要如下所示的“提供程序值比较器”。

原因

各种实体拆分和表拆分可能会导致多个属性映射到同一数据库列,反之亦然。 这需要将值转换到要在数据库中使用的值之后进行比较。

缓解措施

配置提供程序值比较器。 例如,假设将值对象用作键,而该键的比较器使用不区分大小写的字符串比较:

var blogKeyComparer = new ValueComparer<BlogKey>(
    (l, r) => string.Equals(l.Id, r.Id, StringComparison.OrdinalIgnoreCase),
    v => v.Id.ToUpper().GetHashCode(),
    v => v);

var blogKeyConverter = new ValueConverter<BlogKey, string>(
    v => v.Id,
    v => new BlogKey(v));

modelBuilder.Entity<Blog>()
    .Property(e => e.Id).HasConversion(
        blogKeyConverter, blogKeyComparer);

数据库值(字符串)不能直接使用为 BlogKey 类型定义的比较器。 因此,必须为不区分大小写的字符串比较配置提供程序比较器:

var caseInsensitiveComparer = new ValueComparer<string>(
    (l, r) => string.Equals(l, r, StringComparison.OrdinalIgnoreCase),
    v => v.ToUpper().GetHashCode(),
    v => v);

var blogKeyComparer = new ValueComparer<BlogKey>(
    (l, r) => string.Equals(l.Id, r.Id, StringComparison.OrdinalIgnoreCase),
    v => v.Id.ToUpper().GetHashCode(),
    v => v);

var blogKeyConverter = new ValueConverter<BlogKey, string>(
    v => v.Id,
    v => new BlogKey(v));

modelBuilder.Entity<Blog>()
    .Property(e => e.Id).HasConversion(
        blogKeyConverter, blogKeyComparer, caseInsensitiveComparer);

现在在表中配置检查约束和其他表方面

跟踪问题 #28205

旧行为

在 EF Core 6.0 中,HasCheckConstraintHasCommentIsMemoryOptimized 在实体类型生成器上直接调用。 例如:

modelBuilder
    .Entity<Blog>()
    .HasCheckConstraint("CK_Blog_TooFewBits", "Id > 1023");

modelBuilder
    .Entity<Blog>()
    .HasComment("It's my table, and I'll delete it if I want to.");

modelBuilder
    .Entity<Blog>()
    .IsMemoryOptimized();

新行为

从 EF Core 7.0 开始,这些方法改为在表生成器上调用:

modelBuilder
    .Entity<Blog>()
    .ToTable(b => b.HasCheckConstraint("CK_Blog_TooFewBits", "Id > 1023"));

modelBuilder
    .Entity<Blog>()
    .ToTable(b => b.HasComment("It's my table, and I'll delete it if I want to."));

modelBuilder
    .Entity<Blog>()
    .ToTable(b => b.IsMemoryOptimized());

现有方法已标记为 Obsolete. 它们当前的行为与新方法相同,但将在未来的版本中被删除。

原因

这些方面仅适用于表。 它们不会应用于任何映射的视图、函数或存储过程。

缓解措施

如上所示,使用表生成器方法。

跟踪问题 #28249

旧行为

在 EF Core 6.0 中,当通过跟踪查询将其附加到 DbContext 的方式跟踪新实体时,指向和来自 Deleted state 的相关实体的导航会被修复

新行为

从 EF Core 7.0 开始,不再修复指向和来自 Deleted 实体的导航。

原因

只要实体被标记为 Deleted,将其与未删除的实体关联就基本没有意义。

缓解措施

在将实体标记为 Deleted 之前查询或附加实体,或者手动设置指向和来自已删除实体的导航属性。

跟踪问题 #26502

旧行为

在 EF Core 6.0 中,在使用关系提供程序时使用 Azure Cosmos DB FromSqlRaw 扩展方法,或在使用 Azure Cosmos DB 提供程序时使用关系 FromSqlRaw 扩展方法,都可能无提示失败。 同样,在内存中提供程序上使用关系方法是无提示的无操作。

新行为

从 EF Core 7.0 开始,使用针对不同提供程序上的一个提供程序设计的扩展方法将引发异常。

原因

必须使用正确的扩展方法,才能在所有情况下正常运行。

缓解措施

对正在使用的提供程序使用正确的扩展方法。 若引用了多个提供程序,则将扩展方法作为静态方法调用。 例如:

var result = CosmosQueryableExtensions.FromSqlRaw(context.Blogs, "SELECT ...").ToList();

或:

var result = RelationalQueryableExtensions.FromSqlRaw(context.Blogs, "SELECT ...").ToList();

搭建的 OnConfiguring 不再调用 IsConfigured

跟踪问题 #4274

旧行为

在 EF Core 6.0 中,从现有数据库搭建的 DbContext 类型包含对 IsConfigured 的调用。 例如:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    if (!optionsBuilder.IsConfigured)
    {
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
        optionsBuilder.UseNpgsql("MySecretConnectionString");
    }
}

新行为

从 EF Core 7.0 开始,不再包含对 IsConfigured 的调用。

原因

在极少数情况下,某些案例中的数据库提供程序会在 DbContext 内配置,但前提是上下文尚未配置。 而即使有编译时警告,将 OnConfiguring 留在此处也更有可能在代码中留下包含敏感信息的连接字符串。 因此,移除这一点所带来的更加安全、简洁的代码很值得,特别是考虑到 --no-onconfiguring (.NET CLI) 或 -NoOnConfiguring (Visual Studio 包管理器控制台)标志可用于防止搭建 OnConfiguring 方法,并且可通过自定义模板在必要时重新添加 IsConfigured

缓解措施

可以是:

  • 从现有数据库搭建基架时,请使用 --no-onconfiguring (.NET CLI) 或 -NoOnConfiguring (Visual Studio 包管理器控制台)参数。
  • 自定义 T4 模板 以重新添加对 IsConfigured的调用。