第7章 配置非关系型属性

发布时间 2024-01-05 16:08:18作者: 生活的倒影

本章涵盖

  • 以三种方式配置 EF Core
  • 专注于非关系属性
  • 定义数据库结构
  • 引入值转换器、阴影属性和支持字段
  • 决定哪种类型的配置在不同情况下最有效

本章介绍配置 EF Core 的一般内容,但重点介绍如何配置实体类中的非关系属性;这些属性称为标量(Scalar)属性。第 8 章介绍如何配置关系属性,第 10 章介绍如何配置更高级的功能,如 DbFunctions、计算列等。

本章首先概述了首次使用应用程序的 DbContext 时 EF Core 运行的配置过程。然后将学习如何配置 .NET 类与其关联的数据库表之间的映射,以及设置表中列的名称、SQL 类型和可为 null 性等功能。

本章还介绍了三个 EF Core 功能(值转换器、影子属性和支持字段),这些功能使你能够控制数据的存储方式,以及其余非 EF Core 代码对数据的存储和控制方式。例如,值转换器允许您在从数据库写入/读取数据时转换数据,从而使数据库表示更易于理解和调试;影子属性和后备字段允许您在软件级别“隐藏”或控制对数据库数据的访问。这些功能可以帮助您编写更好、更不脆弱的应用程序,这些应用程序更易于调试和重构。

7.1 配置 EF Core 的三种方式

第 1 章介绍了 EF Core 如何对数据库进行建模,并提供了一个图表来显示 EF Core 正在执行的操作,重点是数据库。图 7.1 更详细地描述了首次使用应用程序的 DbContext 时发生的配置过程。下图显示了整个过程,包括三种配置方法:按约定、数据注释和 Fluent API。此示例重点介绍标量属性的配置,但对于 EF Core 的所有配置,该过程都是相同的。

图 7.1 首次使用应用程序的 DbContext 时,EF Core 会启动一个程序来配置自身并生成它应该访问的数据库的模型。可以使用三种方法来配置 EF Core:按约定、数据批注和 Fluent API。大多数实际应用程序需要混合使用所有三种方法,以完全按照应用程序需要的方式配置 EF Core。

此列表总结了配置 EF Core 的三种方法:

  • 按约定 - 当您遵循有关属性类型和名称的简单规则时,EF Core 将自动配置许多软件和数据库功能。按约定方法快速简便,但它无法处理所有可能发生的情况。
  • 数据批注 - 可以将一系列称为“数据批注”的 .NET 属性添加到实体类和/或属性中,以提供额外的配置信息。这些属性对于数据验证也很有用,如第 4 章所述。
  • Fluent API - EF Core 具有一个名为 OnModelCreating 的方法,该方法在首次使用 EF 上下文时运行。可以重写此方法并添加命令(称为 Fluent API),以便在建模阶段向 EF Core 提供额外信息。Fluent API 是最全面的配置信息形式,某些功能只能通过该 API 使用。

注意:大多数实际应用程序都需要使用所有三种方法,以完全按照所需的方式配置 EF Core 和数据库。某些配置功能可通过两种甚至全部三种方法获得(例如在实体类中定义主键)。第 7.16 节为您提供了我关于使用哪种方法用于某些功能的建议,以及一种自动化某些配置的方法。

7.2 配置 EF Core 的工作示例

对于使用 EF Core 的 Hello World 版本之外的任何内容,您可能需要某种形式的数据注释或 Fluent API 配置。在第 1 部分中,您需要为多对多链接表设置密钥。在本章中,您将看到一个应用第 7.1 节中介绍的三种配置方法的示例,以更好地将数据库与我们的图书应用程序的需求相匹配。

在此示例中,您将重新建模第 2-5 章中使用的 Book 实体类,并通过 EF Core 迁移更改 EF Core 使用的默认值的某些列的大小和类型。这些更改使您的数据库更小,使某些列的排序或搜索速度更快,并检查某些列是否不为空。根据业务需求为数据库列定义正确的大小、类型和可为空性始终是一种很好的做法。

为此,您将结合使用所有三种配置方法。 By Convention 配置发挥着重要作用,因为它定义了表和列名称,但您将添加特定的数据注释和 Fluent API 配置方法来更改默认 By Convention 设置中的一些列。图 7.2 显示了每种配置方法如何影响 EF Core 数据库表结构的内部模型。由于空间限制,图中没有显示应用于表的所有数据注释和 Fluent API 配置方法,但您可以分别在清单 7.1 和清单 7.2 中看到它们。

注意 图 7.2 使用箭头将不同的 EF Core 配置代码链接到数据库表列的各个部分。需要明确的是,更改 EF Core 配置不会神奇地更改数据库。第 9 章介绍了更改数据库结构(称为架构)的几种方式,其中 EF Core 配置更改数据库或数据库更改代码中的 EF Core 配置。

图 7.2 要以所需的确切格式配置 Books 表,必须使用所有三种配置方法。很大一部分是按约定完成的(所有部分都不是粗体),但随后使用数据批注来设置 Title 列的大小和可为 null 性,并使用 Fluent API 来更改 PublishedOn 和 ImageUrl 列的类型。

在阅读本章时,您将看到有关这些设置的更详细说明,但本部分将全面了解配置应用程序的 DbContext 的不同方法。考虑其中一些配置如何在您自己的项目中有用也很有趣。下面是我在处理的大多数项目中使用的一些 EF Core 配置:

  • [Required] 属性 - 此属性告知 EF Core 标题列不能为 SQL NULL,这意味着如果尝试插入/更新具有 null 标题属性的书籍,数据库将返回错误。
  • [MaxLength(256)] 属性 - 此属性告知 EF Core,数据库中存储的字符数应为 256,而不是默认为数据库的最大大小(在 SQL Server 中为 2 GB)。使用正确类型的固定长度字符串(2 字节 Unicode 或 1 字节 ASCII)可以使数据库访问效率略高,并允许将 SQL 索引应用于这些固定大小的列。

定义:SQL 索引是一种提高排序和搜索性能的功能。第 7.10 节更详细地介绍了此主题。

  • HasColumnType(date) Fluent API - 通过使 PublishedOn 列仅保存日期(这是您所需要的)而不是默认的 datetime2,可以将列大小从 8 个字节减少到 3 个字节,从而更快地对 PublishedOn 列进行搜索和排序。
  • IsUnicode(false) Fluent API - ImageUrl 属性仅包含 8 位 ASCII 字符,因此您可以告知 EF Core,这意味着字符串将以这种方式存储。因此,如果 ImageUrl 属性具有 [MaxLength(512)] 属性(如清单 7.1 所示),则 IsUnicode(false) 方法会将 ImageUrl 列的大小从 1024 字节(Unicode 每个字符占用 2 个字节)减小到 512 个字节(ASCII 每个字符占用 1 个字节)。

此列表显示更新后的 Book 实体类代码,其中新的数据注释以粗体显示。(第 7.5 节中介绍了 Fluent API 命令。

清单 7.1 添加了数据注释的 Book 实体类

public class Book
{
    public int BookId { get; set; }
    [Required]   // 告诉 EF Core 该字符串不可空
    [MaxLength(256)]  // 定义数据库中字符串列的大小
    public string Title { get; set; }
    public string Description { get; set; } 
    public DateTime PublishedOn { get; set; } 
    [MaxLength(64)]
    public string Publisher { get; set; } 
    public decimal Price { get; set; }
    [MaxLength(512)]
    public string ImageUrl { get; set; } 
    public bool SoftDeleted { get; set; }
    //relationships
    public PriceOffer Promotion { get; set; } 
    public IList<Review> Reviews { get; set; }
    public IList<BookAuthor> AuthorsLink { get; set; }
}

提示:您通常会使用常量在 [MaxLength(nn)] 属性中设置 size 参数,以便在创建 DTO 时使用相同的常量。如果更改一个属性的大小,则会更改所有关联的属性。

现在,您已经看到了使用所有三种配置方法的示例,让我们详细探讨每种方法。

7.3 按约定配置

按约定是默认配置,可以由其他两种方法(数据注释和 Fluent API)覆盖。按约定方法依赖于开发人员使用按约定命名标准和类型映射,这允许 EF Core 查找和配置实体类及其关系,以及定义大部分数据库模型。此方法提供了一种快速配置大部分数据库映射的方法,因此值得学习。

7.3.1 实体类的约定

EF Core 映射到数据库的类称为实体类。如第 2 章所述,实体类是普通的 .NET 类,有时称为 POCO(普通的旧 CLR 对象)。EF Core 要求实体类具有以下功能:

  • 该类必须是公共访问的:关键字 public 应位于类之前。
  • 该类不能是静态类,因为 EF Core 必须能够创建该类的新实例。
  • 该类必须具有 EF Core 可以使用的构造函数。默认的无参数构造函数可以工作,其他带有参数的构造函数也可以工作。有关 EF Core 如何使用构造函数的详细规则,请参阅第 6.1.10 节。

7.3.2 实体类中参数的约定

按照约定,EF Core 将在实体类中查找具有公共 getter 和任何访问模式(public、internal、protected 或 re-vate)的 setter 的公共属性。典型的全公共属性是

public int MyProp { get; set; }

尽管全公共属性是常态,但在某些地方,使用具有更本地化访问设置的属性(例如 public int MyProp { get; private set; })可以更好地控制其设置方式。一个示例是实体类中的方法,该方法在设置属性之前也会执行一些检查;详见第13章。

注意:EF Core 可以处理只读属性,即仅具有 getter 的属性,例如 public int MyProp { get; }。但在这种情况下,“按约定”方法将行不通;需要使用 Fluent API 告知 EF Core 这些属性已映射到数据库。

7.3.3 名称、类型和大小的约定

以下是关系列的名称、类型和大小的规则:

  • 属性的名称用作表中列的名称。
  • .NET 类型由数据库提供程序转换为相应的 SQL 类型。许多基本 .NET 类型都具有与相应数据库类型的一对一映射。这些基本 .NET 类型大多是 .NET 基元类型(int、bool 等),但也有一些特殊情况(如 string、DateTime 和 Guid)。
  • 大小由 .NET 类型定义;例如,32 位 INT 类型存储在相应的 SQL 的 32 位 INT 类型中。String 和 byte[] 类型的大小为 max,每个数据库类型的大小都不同。

EF6:默认映射约定的一个变化是,EF Core 将 .NET DateTime 类型映射到 SQL datetime2(7),而 EF6 将 .NET DateTime 映射到 SQL datetime。Microsoft 建议使用 datetime2(7),因为它低于 ANSI 和 ISO SQL 标准。此外, datetime2(7) 更准确: SQL datetime 的分辨率约为 0.004 秒, 而 datetime2(7) 的分辨率为 100 纳秒。

7.3.4 按照约定,属性的可为 null 性基于 .NET 类型

在关系数据库中,NULL 表示缺失或未知数据。列是否可以为 NULL 由 .NET 类型定义:

  • 如果类型为字符串,则列可以为 NULL,因为字符串可以为 null。
  • 默认情况下,基元类型(如 int)或结构类型(如 DateTime)为非 null。
  • 基元或结构类型可以通过使用 ?后缀(如 int?)或泛型 Nullable<T>(如 <int>Nullable)。在这些情况下,该列可以为 NULL。

图 7.3 显示了应用于属性的名称、类型、大小和可为 null 性约定。

图 7.3 应用 By Convention 规则定义 SQL 列。数据库提供程序将属性的类型转换为等效的 SQL 类型,而属性的名称用于列的名称。

7.3.5 EF Core 命名约定标识主键

另一条规则是关于定义数据库表的主键。用于指定主键的 EF Core 设置如下:

  • EF Core 需要一个主键属性。(按约定方法不处理由多个属性/列组成的键,称为复合键。
  • 该属性称为 Id 或 id(例如 BookId)。
  • 属性的类型定义了为键分配唯一值的内容。第 8 章介绍密钥生成。

图 7.4 显示了一个数据库生成的主键示例,该主键具有 Book 的 BookId 属性和 Books 表的 SQL 列 BookId 的 By 约定映射。

图 7.4 .NET 类属性 BookId 和 SQL 主列 BookId 之间的映射,使用按约定方法。该属性的名称告知 EF Core 此属性是主键。此外,数据库提供程序知道 int 类型意味着它应该为添加到表中的每行创建一个唯一值。

提示:虽然您可以选择使用短名称 Id 作为主键,但我建议您使用较长的名称:后跟 Id(例如 BookId)。如果使用 Where(p => BookId == 1) 而不是较短的 Where(p => Id == 1),则更容易理解代码中发生的情况,尤其是当您有很多实体类时。

7.4 通过数据注解进行配置

数据批注是用于验证和数据库功能的特定类型的 .NET 属性。这些特性可以应用于实体类或属性,并向 EF Core 提供配置信息。本部分介绍在哪里可以找到它们以及它们通常如何应用。与 EF Core 配置相关的数据批注属性来自两个命名空间。

7.4.1 使用 System.ComponentModel.DataAnnotations 中的批注

System.ComponentModel.DataAnnotations 命名空间中的属性主要用于前端的数据验证,例如 ASP.NET,但 EF Core 使用其中一些属性来创建映射模型。[Required] 和 [Max- Length] 等属性是主要属性,许多其他数据批注对 EF Core 没有影响。图 7.5 显示了主要属性 [Required] 和 [MaxLength] 如何影响数据库列定义。

图 7.5 [Required] 和 [MaxLength] 属性会影响到数据库列的映射。[Required] 属性指示列不应为 null,而 [MaxLength] 属性设置 nvarchar 的大小。

7.4.2 使用 System.ComponentModel.DataAnnotations.Schema 中的批注

System.ComponentModel.DataAnnotations.Schema 命名空间中的属性更特定于数据库配置。此命名空间是在 NET Framework 4.5 中添加的,远远早于编写 EF Core,但 EF Core 使用其属性(如 [Table]、[Column] 等)来设置表名和列名/类型,如第 7.11 节中所述。

7.5 通过 Fluent API 进行配置

配置 EF Core 的第三种方法称为 Fluent API,是一组适用于模型构建器类的方法,该类在应用程序的 DbContext 中的 OnModelCreating 方法中可用。正如您将看到的,Fluent API 通过扩展方法工作,这些方法可以链接在一起,就像 LINQ 命令链接在一起一样,用于设置配置设置。Fluent API 提供了最全面的配置命令行表,其中许多配置只能通过该 API 使用。

但在定义 Fluent API 关系命令之前,我想介绍一种不同的方法,将 Fluent API 命令分离到每个实体类大小的组中。此方法非常有用,因为随着应用程序的增长,将所有 Fluent API 命令放在 OnModelCreating 方法中(如图 2.6 所示)会使查找特定的 Fluent API 变得困难。解决方案是将实体类的 Fluent API 移动到单独的配置类中,然后从 OnModelCreating 方法调用该配置类。

EF Core 提供了一种以 IEntity-TypeConfiguration 接口的形式促进此过程的方法<T>。清单 7.2 显示了您的新应用程序 DbContext EfCoreContext,您可以在其中将各种类的 Fluent API 设置移动到单独的配置类中。这种方法的好处是,实体类的 Fluent API 全部集中在一个位置,而不是与其他实体类的 Fluent API 命令混合使用。

EF6:EF6.x 具有一个 EntityTypeConfiguration 类,<T>您可以继承该类来封装给定实体类的 Fluent API 配置。 EF Core 的实现实现实现相同的结果,但使用应用于配置类的 IEntityType- Configuration<T> 接口。

清单 7.2 应用程序的 DbContext 用于具有关系的数据库

public class EfCoreContext : DbContext  // 购买了某些图书的用户名
{
    // 使用注册 DbContext 时设置的选项创建 DbContext
    public EfCoreContext(DbContextOptions<EfCoreContext> options) : base(options) { }
    // 您的代码将访问的实体类 
    public DbSet<Book> Books { get; set; }
    public DbSet<Author> Authors { get; set; }
    public DbSet<PriceOffer> PriceOffers { get; set; } 
    public DbSet<Order> Orders { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)  //您的 Fluent API 命令运行的方法
    {
        // 为每个需要配置的实体类运行每个单独的配置。 
        modelBuilder.ApplyConfiguration(new  BookConfig());
        modelBuilder.ApplyConfiguration(new BookAuthorConfig()); 
        modelBuilder.ApplyConfiguration(new PriceOfferConfig()); 
        modelBuilder.ApplyConfiguration(new LineItemConfig());
    }
}

让我们看一下清单 7.2 中使用的 BookConfig 类,看看如何构造一个按类型配置的类。 清单 7.3 显示了一个配置类,它实现了 IEntityTypeConfiguration 接口,并包含 Book 实体类的 Fluent API 方法。

注意:我没有在清单 7.3 中描述 Fluent API,因为它是对 IEntityTypeConfiguration 接口使用情况的检查。第 7.7 节(数据库类型)和第 7.10 节(索引)介绍了 Fluent API。

清单 7.3 BookConfig 扩展类配置 Book 实体类

internal class BookConfig : IEntityTypeConfiguration<Book>
{
    public void Configure (EntityTypeBuilder<Book> entity)
    {
        // .NET DateTime 的基于约定的映射是 SQL datetime2。此命令将 SQL 列类型更改为 date,该类型仅保存日期,而不保存时间。
        entity.Property(p => p.PublishedOn).HasColumnType("date");
        // 精度 (9,2) 设置的最大价格为 9,999,999.99(9 位,小数点后 2),这在数据库中占用最小大小。
        entity.Property(p => p.Price).HasPrecision(9,2);
        // .NET 字符串的基于约定的映射是 SQL nvarchar(16 位 Unicode)。此命令将 SQL 列类型更改为 varchar(8 位 ASCII)。
        entity.Property(x => x.ImageUrl).IsUnicode(false);  
        // 向 PublishedOn 属性添加索引,因为您可以对此属性进行排序和筛选
        entity.HasIndex(x => x.PublishedOn);
    }
}

在清单 7.2 中,我列出了每个单独的 modelBuilder.ApplyConfiguration 调用,以便您可以看到它们的实际效果。但是,一个名为 ApplyConfigurations- FromAssembly 的省时方法可以找到继承 IEntityType- Configuration 的所有配置类,并为你运行它们。请参阅以下代码片段,该代码片段在与 DbContext 相同的程序集中查找并运行所有配置类:

modelBuilder.ApplyConfigurationsFromAssembly( Assembly.GetExecutingAssembly());

清单 7.3 显示了 Fluent API 的典型用法,但请记住,API 的流畅特性允许链接多个命令,如以下代码片段所示:

modelBuilder.Entity<Book>()
  .Property(x => x.ImageUrl)
  .IsUnicode(false)
  .HasColumnName("DifferentName")
  .HasMaxLength(123)
  .IsRequired(false);

EF6:Fluent API 在 EF6.x 中的工作方式相同,但具有许多新功能和设置关系的重大更改(在第 8 章中介绍)和数据类型的细微变化。

当应用程序首次访问应用程序的 DbContext 时,将调用 OnModelCreation。在此阶段,EF Core 使用所有三种方法进行自我配置:按约定、数据批注以及在 OnModel- Creating 方法中添加的任何 Fluent API。

如果数据注释和 Fluent API 说不同的话怎么办?

数据注释和 Fluent API 建模方法始终覆盖基于约定的建模。但是,如果数据注释和 Fluent API 都提供了相同属性和设置的映射,会发生什么情况?

我尝试通过数据注释和 Fluent API 将 WebUrl 属性的 SQL 类型和长度设置为不同的值。使用了 Fluent API 值。该测试不是最终的测试,但 Fluent API 是最终仲裁者是有道理的。

现在,您已经了解了数据注释和 Fluent API 配置方法,让我们详细介绍数据库模型特定部分的配置。

7.6 从数据库中排除属性和类

第 7.3.2 节介绍了 EF Core 如何查找属性。但有时,您需要将实体类中的数据排除在数据库中。例如,您可能希望在类实例的生存期内使用用于计算的本地数据,但不希望将其保存到数据库中。您可以通过两种方式排除类或属性:通过数据注释或通过 Fluent API。

7.6.1 通过数据注释排除类或属性

EF Core 将排除应用了 [NotMapped] 数据特性的属性或类。下面的清单显示了 [NotMapped] 数据归因于属性和类的应用。

清单 7.4 排除 3 个属性,其中 2 个使用 [NotMapped]

public class MyEntityClass
{
    public int MyEntityClassId { get; set; } 
    public string NormalProp{ get; set; }  // 包括:一个普通的公共财产,有公共的 getter 和 setter
    [NotMapped]  // 排除:放置 [NotMapped] 属性会告知 EF Core 不要将此属性映射到数据库中的列。
    public string LocalString { get; set; }
    public ExcludeClass LocalClass { get; set; }  // 已排除:此类不会包含在数据库中,因为类定义具有 [NotMapped] 属性。
}

[NotMapped]  // 已排除:将排除此类,因为类定义上具有 [NotMapped] 属性。
public class ExcludeClass
{
    public int LocalInt { get; set; }
}

7.6.2 通过 Fluent API 排除类或属性

此外,还可以使用 Fluent API 配置命令 Ignore 来排除属性和类,如清单 7.5 所示。

注意:为简单起见,我在 OnModelCreating 方法中显示 Fluent API,而不是在单独的配置类中显示。

清单 7.5 使用 Fluent API 排除属性和类

public class ExcludeDbContext : DbContext
{
    public DbSet<MyEntityClass> MyEntities { get; set; }

    protected override void OnModelCreating (ModelBuilder modelBuilder)
    {
        // Ignore 方法用于排除将实体类 MyEntityClass 中的 LocalString 属性添加到数据库中。
        modelBuilder.Entity<MyEntityClass>().Ignore(b => b.LocalString);
        // 不同的 Ignore 方法可以排除某个类,这样,如果 Ignored 类型的实体类中有一个属性,则不会将该属性添加到数据库中。
        modelBuilder.Ignore<ExcludeClass>();
    }
}

正如我在第 7.3.2 节中所说,默认情况下,EF Core 将忽略只读属性,即只有一个 getter 的属性(例如 public int MyProp { get; })。

7.7 设置数据库列类型、大小和可为 null 性

如前所述,基于约定的建模对基于 .NET 类型的 SQL 类型、大小/精度和可为 null 性使用默认值。一个常见的要求是手动设置这些属性中的一个或多个,因为您使用的是现有数据库,或者因为您有性能或业务原因需要这样做。

在配置简介(第 7.3 节)中,您完成了一个更改了各种列的类型和大小的示例。表 7.1 提供了可用于执行此任务的命令的完整列表。

表 7.1 设置列的可为 null 性和 SQL 类型/大小

Setting Data Annotations Fluent API
Set not null (Default is nullable.) [Required]
public string MyProp { get; set; }
modelBuilder.Entity<MyClass>().Property(p => p.MyProp).IsRequired();
Set size (string) (Default is MAX length.) [MaxLength(123)] 
public string MyProp { get; set; }
modelBuilder.Entity<MyClass>().Property(p => p.MyProp).HasMaxLength(123);
Set SQL type/size (Each type has a default precision and size.) [Column(TypeName = "date")]
public DateTime PublishedOn { get; set; }
modelBuilder.Entity<MyClass>(.Property(p => p.PublishedOn).HasColumnType("date");

一些特定的 SQL 类型有自己的 Fluent API 命令,如下列表所示。您可以在清单 7.3 中看到使用的第一个 Fluent API 命令:

  • IsUnicode(false) - 将 SQL 类型设置为 varchar(nnn)(1 字节字符,称为 ASCII),而不是默认的 nvarchar(nnn)(2 字节字符,称为 Unicode)。
  • HasPrecision( precision,scale)—设置位数(精度参数)以及小数点后有多少位(比例参数)。此 Fluent 命令是 EF Core 5 中的新命令。SQL 小数的默认设置为 (18,2)。
  • HasCollat​​ion(“collat​​ion name”) - 另一个 EF Core 5 功能,允许您定义属性的排序规则 - 即 char 和 string 类型的排序规则、大小写和重音敏感度属性。 (有关排序规则的更多信息,请参阅第 2.8.3 节。)

我建议使用 IsUnicode(false) 方法告诉 EF Core 字符串属性仅包含单字节 ASCII 格式字符,因为使用 IsUnicode 方法允许您单独设置字符串大小。

EF6:EF Core 设置列的 SQL 数据类型的方法略有不同。如果提供数据类型,则需要给出整个定义,包括类型和长度/精度,如 [Column(TypeName = varchar(nnn))] 中所示,其中 nnn 是整数。在 EF6 中,您可以使用 [Column(TypeName = varchar)],然后使用 [MaxLength(nnn)] 定义长度,但该技术在 EF Core 中不起作用。有关更多信息,请参阅 https://github.com/dotnet/efcore/issues/3985。

7.8 价值转换:将数据更改为数据库或从数据库更改数据

EF Core 的值转换功能允许您在读取属性并将其写入数据库时更改数据。典型用途包括

  • 将枚举类型属性保存为字符串(而不是数字),以便在查看数据库中的数据时更容易理解
  • 修复了从数据库读回时 DateTime 丢失其 UTC(协调世界时)设置的问题
  • (高级)对写入数据库的属性进行加密,并在读回时解密

价值转换分为两部分:

  • 在数据写出到数据库时转换数据的代码
  • 在回读时将数据库列转换回原始类型的代码

值转换的第一个示例涉及 SQL 数据库在存储 DateTime 类型方面的限制,因为它不保存 DateTime 结构的 DateTimeKind 部分,该部分告诉我们 DateTime 是本地时间还是 UTC。这种情况可能会导致问题。例如,如果使用 JSON 将该 DateTime 发送到前端,则 DateTime 将不包含告知 JavaScript 时间是 UTC 的 Z 后缀字符,因此前端代码可能会显示错误的时间。下面的列表演示如何将属性配置为具有值转换,该值转换在从数据库返回时设置 DateTimeKind。

清单 7.6 配置 DateTime 属性以替换丢失的 DateTimeKind 设置

protected override void OnModelCreating (ModelBuilder modelBuilder)
{
    // 创建从 DateTime 到 DateTime 的 ValueConverter 
    var utcConverter = new ValueConverter<DateTime, DateTime>( toDb => toDb,  // 以正常方式(如不转换)将 DateTime 保存到数据库
        fromDb => DateTime.SpecifyKind(fromDb, DateTimeKind.Utc));  // 从数据库读取时,将 UTC 设置添加到 DateTime 中。

    modelBuilder.Entity<ValueConversionExample>()
        .Property(e => e.DateTimeUtcUtcOnReturn)  // 选择您要配置的属性
        .HasConversion(utcConverter);  // 将 utcConverter 添加到该属性

    //… other configurations left out
}

在这种情况下,您必须创建自己的值转换器,但大约有 20 个内置值转换器可用。(见 http://mng.bz/mgYP)。事实上,一个值转换器非常流行,以至于它有一个预定义的 Fluent API 方法或属性,即在数据库中将枚举存储为字符串的转换。让我解释一下。

枚举通常以数字的形式存储在数据库中,这是一种有效的格式,但如果您需要深入研究数据库以弄清楚发生了什么,这确实会使事情变得更加困难。因此,一些开发人员喜欢将数据库中的枚举保存为字符串。可以使用 HasConversion()  命令配置枚举类型到字符串的转换,如以下代码片段所示:

modelBuilder.Entity<ValueConversionExample>()
  .Property(e => e.Stage)
  .HasConversion<string>();

以下是使用价值转换的一些规则和限制:

  • 永远不会将空值传递给值转换器。您需要编写一个值转换器来仅处理非 null 值,因为仅当该值不是 null 时才会调用转换器。
  • 注意包含对转换值排序的查询。例如,如果将枚举转换为字符串,则排序将按枚举名称排序,而不是按枚举值排序。
  • 转换器只能将单个属性映射到数据库中的单个列。
  • 您可以创建一些复杂的值转换器,例如将 int 列表串行化为 JSON 字符串。此时,EF Core 无法将 List 属性与数据库中的 JSON 进行比较,因此它不会更新数据库。要解决这个问题,你需要添加一个所谓的值比较器。有关此主题的详细信息,请参阅 http://mng.bz/5j5z 上的 EF Core 文档。

稍后,在第 7.16.4 节中,您将学习一种自动将值转换器应用于某些属性类型/名称的方法,以使您的生活更轻松。

7.9 配置主键的不同方式

您已经了解了设置实体主键的 By Convention 方法。本部分介绍常规主键设置,即 .NET 属性为其定义名称和类型的一个键。在两种情况下,您需要显式配置主键:

  • 当密钥名称不符合按约定命名规则时
  • 当主键由多个属性/列组成时,称为复合键

多对多关系链接表是“按约定”方法不起作用的一个示例。您可以使用两种替代方法来定义主键

注意:第 8 章介绍如何配置外键,因为外键定义关系,即使它们是标量类型。

7.9.1 通过数据注解配置主键

[Key] 特性允许您将一个属性指定为类中的主键。如果不使用 By 约定主键名称,请使用此批注,如以下清单所示。此代码很简单,可以清楚地标记主键。

清单 7.7 使用 [Key] 注解将属性定义为主键 bu

private class SomeEntity
{
    [Key]  // [key] 属性告诉 EF Core 该属性是主键。
    public int NonStandardKeyName { get; set; }
    public string MyString { get; set; }
}

请注意,[Key] 属性不能用于复合键。在早期版本的 EF Core 中,可以使用 [Key] 和 [Column] 属性定义复合键,但该功能已被删除。

7.9.2 通过 Fluent API 配置主键

您还可以通过 Fluent API 配置主键,这对于不符合 By Convention 模式的主键非常有用。以下清单显示了 Fluent API 的 HasKey 方法配置的两个主键。第一个主键是 SomeEntity 实体类中具有非标准名称的单个主键,第二个主键是 BookAuthor 链接表中的复合主键,由两列组成。

清单 7.8 使用 Fluent API 在两个实体类上配置主键

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<SomeEntity>()
        .HasKey(x => x.NonStandardKeyName);  // 定义一个普通的单列主键。当密钥名称与“按约定”默认值不匹配时,请使用 HasKey。

    modelBuilder.Entity<BookAuthor>()
        .HasKey(x => new {x.BookId, x.AuthorId});  //使用匿名对象定义两个(或多个)属性以形成复合键。属性在匿名对象中的显示顺序定义了它们的顺序。

    //… other configuration settings removed
}

组合键没有按约定版本,因此必须使用 Fluent API 的 HasKey 方法。

7.9.3 将实体配置为只读

在某些高级情况下,实体类可能没有主键。这里有三个例子:

  • 您希望将实体类定义为只读。如果实体类没有主键,则 EF Core 会将其视为只读。
  • 您希望将实体类映射到只读 SQL 视图。SQL 视图是类似于 SQL 表的 SQL 查询。有关详细信息,请参阅此文章:http://mng.bz/6g6y
  • 您希望使用 ToSqlQuery Fluent API 命令将实体类映射到 SQL 查询。ToSqlQuery 方法允许您定义一个 SQL 命令字符串,该字符串将在您读取该实体类时执行。

若要将实体类显式设置为只读,可以使用 Fluent API HasNoKey() 命令或将属性 [Keyless] 应用于实体类。如果实体类没有主键,则必须使用这两种方法之一将其标记为只读。任何通过没有主键的实体类更改数据库的尝试都将失败,并出现异常。EF Core 之所以这样做,是因为它无法在没有键的情况下执行更新,这是将实体类定义为只读的一种方法。将实体标记为只读的另一种方法是使用 Fluent API 方法 ToView(ViewNameString) 命令将实体映射到 SQL 视图,如以下代码片段所示:

  modelBuilder.Entity<MyEntityClass>().ToView("MyView");

如果尝试通过映射到 View 的实体类更改数据库,EF Core 将引发异常。如果要将实体类映射到可更新的视图(可更新的 SQL 视图),则应改用 ToTable 命令。

7.10 向数据库列添加索引

关系数据库具有一种称为索引的功能,它可以根据索引中的一列或多列更快地搜索和排序行。此外,索引可能具有约束,该约束可确保索引中的每个条目都是唯一的。例如,为主键指定一个唯一索引,以确保表中每一行的主键都不同。

您可以通过 Fluent API 和属性向列添加索引,如表 7.2 所示。索引将加快快速搜索和排序速度,如果添加唯一约束,数据库将确保每行中的列值不同。

表 7.2 向列添加索引

Action Fluent API
Add index, Fluent modelBuilder.Entity<MyClass>().HasIndex(p => p.MyProp);
Add index, Attribute [Index(nameof(MyProp))]
public class MyClass …
Add index, multiple columns modelBuilder.Entity<Person>().HasIndex(p => new {p.First, p.Surname});
Add index, multiple columns, Attribute [Index(nameof(First), nameof(Surname)] 
public class MyClass …
Add unique index, Fluent modelBuilder.Entity<MyClass>().HasIndex(p => p.BookISBN).IsUnique();
Add unique index, Attribute [Index(nameof(MyProp), IsUnique = true)] 
public class MyClass …
Add named index, Fluent modelBuilder.Entity<MyClass>().HasIndex(p => p.MyProp).HasDatabaseName("Index_MyProp");

提示:不要忘记,您可以将 Fluent API 命令链接在一起,以混合和匹配这些方法。

某些数据库允许您使用 WHERE 子句指定筛选索引或部分索引以忽略某些情况。例如,可以设置忽略任何软删除项的唯一筛选索引。若要设置筛选索引,请使用包含 SQL 表达式的 HasFilter Fluent API 方法来定义是否应使用该值更新索引。以下代码片段提供了一个示例,该示例强制属性 MyProp 将包含唯一值,除非表的 SoftDeleted 列为 true:

modelBuilder.Entity<MyClass>()
  .HasIndex(p => p.MyProp)
  .IsUnique()
  .HasFilter(“NOT SoftDeleted");

注意:当您使用 SQL Server 提供程序时,EF 会为属于唯一索引的所有可空列添加 IS NOT NULL 筛选器。您可以通过向 HasFilter 参数提供 null 来覆盖此约定,即 HasFilter(null)。

7.11 配置数据库端命名

如果您正在构建新的数据库,则可以使用数据库各个部分的默认名称。但是,如果您有一个现有的数据库,或者您的数据库需要由您无法更改的现有系统访问,那么您很可能需要对数据库的架构名称、表名称和列名称使用特定的名称。

定义:模式是指数据库内部数据的组织方式,即数据组织为表、列、约束等的方式。在某些数据库(例如 SQL Server)中,架构还用于为特定数据分组提供命名空间,数据库设计者使用该命名空间将数据库划分为逻辑组。

7.11.1 配置表名

按照约定,表的名称由应用程序的 DbContext 中的 DbSet 属性的名称设置,或者如果未定义 DbSet 属性,则表使用类名称。例如,在 Book App 的应用程序 DbContext 中,您定义了 DbSet Books 属性,因此数据库表名称设置为 Books。相反,您尚未在应用程序的 DbContext 中为 Review 实体类定义 DbSet 属性,因此其表名使用类名,因此为 Review。

如果您的数据库具有不符合约定命名规则的特定表名称(例如,如果表名称由于其中包含空格而无法转换为有效的 .NET 变量名称),您可以使用 Data注释或 Fluent API 来专门设置表名称。表 7.3 总结了设置表名称的两种方法。

表7.3 为实体类显式配置表名的两种方法

Configuration method Example: Setting the table name of the Book class to "XXX"
Data Annotations [Table("XXX")]
public class Book … etc.

7.11.2 配置模式名称和模式分组

某些数据库(如 SQL Server)允许您使用所谓的架构名称对表进行分组。可以有两个名称相同但架构名称不同的表:例如,架构名称为 Display 的名为 Books 的表与架构名称为 Order 的名为 Books 的表不同。

按照惯例,架构名称由数据库提供程序设置,因为某些数据库(如 SQLite 和 MySQL)不支持架构。对于支持架构的 SQL Server,默认架构名称为 dbo,这是 SQL Server 的默认名称。只能通过 Fluent API 更改默认架构名称,方法是在应用程序的 DbContext 的 OnModelCreating 方法中使用以下代码片段:

modelBuilder.HasDefaultSchema(NewSchemaName);

表 7.4 显示了如何设置表的架构名称。如果将数据库拆分为逻辑组(如销售组、生产组、帐户组等),并且需要将表专门分配给架构,则可以使用此方法。

表 7.4  在特定表上设置模式名称

Configuration method Example: Setting the schema name "sales" on a table
Data Annotations [Table("SpecialOrder", Schema = "sales")] 
class MyClass … etc.
Fluent API modelBuilder.Entity<MyClass>().ToTable("SpecialOrder", schema: "sales");

7.11.3 配置表中的数据库列名

按照约定,表中的列与属性名称同名。如果数据库的名称无法表示为有效的 .NET 变量名称或不适合软件使用,则可以使用数据批注或 Fluent API 设置列名称。表 7.5 显示了这两种方法。

表 7.5 配置列名的两种方式

Configuration method Setting the column name of the BookId property to SpecialCol
Data Annotations [Column("SpecialCol")]
public int BookId { get; set; }
Fluent API modelBuilder.Entity<MyClass>().Property(b => b.BookId).HasColumnName("SpecialCol");

7.12 配置全局查询过滤器

许多应用程序(例如 ASP.NET Core)都具有控制用户可以访问哪些视图和控件的安全功能。 EF Core 有一个类似的安全功能,称为全局查询过滤器(简称查询过滤器)。您可以使用查询过滤器构建多租户应用程序。此类应用程序将不同用户的数据保存在一个数据库中,但每个用户只能看到他们有权访问的数据。另一个用途是实现软删除功能;您可以使用查询过滤器使软删除的行消失,而不是删除数据库中的数据,但如果稍后需要取消删除,数据仍然存在。

我发现查询过滤器在许多客户端作业中都很有用,因此我在第 6 章(第 6.1.6 节)中添加了名为“在实际情况中使用全局查询过滤器”的详细部分。该部分包含有关如何配置查询过滤器的信息,因此请在那里查找该信息。在本章的 7.16.4 节中,我将展示如何自动配置查询过滤器,这确保您不会忘记将重要的查询过滤器添加到实体类之一。

7.13 根据数据库提供程序类型应用 Fluent API 命令

EF Core 数据库提供程序提供了一种在创建应用程序 DbContext 实例时检测正在使用的数据库提供程序的方法。这种方法对于使用 SQLite 数据库进行单元测试等情况很有用,但生产数据库位于 SQL Server 上,并且您希望更改一些内容以使单元测试正常工作。

例如,SQLite 不完全支持一些 NET 类型,例如小数,因此如果您尝试对 SQLite 数据库中的小数属性进行排序,您将收到一个异常,指出您不会获得正确的结果来自 SQLite 数据库。解决这个问题的一种方法是在使用 SQLite 时将十进制类型转换为双精度类型;它不会准确,但对于一组受控的单元测试来说可能没问题。

每个数据库提供程序都提供一个扩展方法,如果数据库与该提供程序匹配,则返回 true。例如,SQL Server 数据库提供程序有一个名为 IsSqlServer() 的方法; SQLite 数据库提供程序有一个名为 IsSqlite() 的方法;等等。另一种方法是使用 ModelBuilder 类中的 ActiveProvider 属性,该属性返回一个字符串,该字符串是数据库提供程序的 NuGet 包名称,例如 Microsoft.EntityFrameworkCore.SqlServer。

以下清单是将十进制应用到双精度类型更改(如果数据库是 SQLite)的示例。此代码允许图书应用程序的 OrderBooksBy 查询对象方法使用内存 SQLite 数据库。

清单 7.9 使用database-provider命令设置列名

protected override void OnModelCreating (ModelBuilder modelBuilder)
{
    //… 把你的正常配置放在这里
    if (Database.IsSqlite())  // 如果选项中提供的数据库是 SQLite,则 IsSqlite 将返回 true。
    {
        // 将两个十进制值设置为双精度,以便对这些值进行排序的单元测试不会引发异常。
        modelBuilder.Entity<Book>()
            .Property(e => e.Price)
            .HasConversion<double>(); 
        modelBuilder.Entity<PriceOffer>()
            .Property(e => e.NewPrice)
            .HasConversion<double>();
    }
}

EF Core 5 添加了 IsRelational() 方法,该方法对于非关系型数据库提供程序(例如 Cosmos Db)返回 false。您可以在每个数据库提供程序的 EF Core 文档中找到一些特定于数据库的 Fluent API 命令,例如 SQL Server 提供程序方法 IsMemoryOptimized。

注意:虽然您可以使用此方法为不同的生产数据库类型创建迁移,但不建议这样做。 EF Core 团队建议您为每种数据库类型创建一个迁移,并将每个迁移存储在单独的目录中。有关详细信息,请参阅第 9 章。

7.14 影子属性:隐藏 EF Core 内的列数据

EF6:EF6.x 具有影子属性的概念,但它们仅在内部用于处理丢失的外键。在 EF Core 中,影子属性成为您可以使用的一项适当功能。

影子属性允许您访问数据库列,而无需将它们作为属性出现在实体类中。影子属性允许您“隐藏”您认为不属于实体类正常使用一部分的数据。这都是关于良好的软件实践:您让上层仅访问他们需要的数据,并隐藏这些层不需要知道的任何内容。让我举两个例子来说明何时可以使用影子属性:

  • 一个常见的需求是跟踪数据被谁以及何时更改,可能是出于审计目的或了解客户行为。您收到的跟踪数据与该类的主要用途是分开的,因此您可能决定使用影子属性来实现该数据,这些属性可以在实体类外部获取。
  • 当您设置的关系中没有在实体类中定义外键属性时,EF Core 必须添加这些属性以使关系正常工作,并且它通过影子属性来实现这一点。第 8 章介绍了这个主题。

7.14.1 配置阴影属性

配置阴影属性有一种按约定的方法,但由于它仅与关系相关,因此我在第 8 章中对此进行了解释。另一种方法是使用 Fluent API。可以使用 Fluent API 方法 Property 引入新属性。由于要设置影子属性,因此实体类中不会有该名称的属性,因此需要使用 Fluent API 的 Property 方法,该方法采用 .NET 类型和影子属性的名称。下面的列表显示了名为 UpdatedOn 的 DateTime 类型的影子属性的设置。

清单 7.10 使用 Fluent API 创建 UpdatedOn shadow 属性

public class Chapter06DbContext : DbContext
{
    …
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MyEntityClass>()
            .Property<DateTime>("UpdatedOn");  // 使用Property方法来定义shadow属性类型
        …
    }
}

在“按约定”下,shadow 属性映射到的表列的名称与 shadow 属性的名称相同。可以通过将 HasColumnName 方法添加到属性方法的末尾来重写此设置。

警告:如果实体类中已存在该名称的属性,则配置将使用该属性,而不是创建影子属性。

7.14.2 访问影子属性

由于影子属性不映射到类属性,因此需要通过 EF Core 直接访问它们。为此,必须使用 EF Core 命令 Entry(myEntity)。属性 (MyPropertyName) 。CurrentValue,这是一个读/写属性,如下面的清单所示。

清单 7.11 使用 Entry(inst).Property(name) 设置 shadow 属性

var entity = new SomeEntityClass(); 
context.Add(entity);   // 创建一个实体类并将其添加到上下文中,以便现在对其进行跟踪
context.Entry(entity)  // 从跟踪的实体数据中获取 EntityEntry
    .Property("UpdatedOn").CurrentValue  // 使用 Property 方法获取具有读/写访问权限的 shadow 属性
        = DateTime.Now;   // 将该属性设置为您想要的值
context.SaveChanges();    // 调用 SaveChanges 将 MyEntityClass 实例及其正常属性值和影子属性值保存到数据库

如果要读取已加载的实体中的影子属性,请使用上下文。Entry(entityInstance)。Property(属性名称)。CurrentValue 命令。但您必须将实体读取为跟踪的实体;应在查询中不使用 AsNoTracking 方法的情况下读取实体。Entry()。Property 方法使用 EF Core 中跟踪的实体数据来保存该值,因为它未保存在实体类实例中。

在 LINQ 查询中,可以使用另一种技术来访问影子属性:EF。属性命令。可以按 UpdatedOn shadow 属性进行排序,例如,使用以下查询代码段和 EF.粗体属性方法:

context.MyEntities
  .OrderBy(b => EF.Property<DateTime>(b, "UpdatedOn"))
  .ToList();

7.15 支持字段:控制对实体类中数据的访问

EF6:支持字段在 EF6 中不可用。此 EF Core 功能提供了对 EF6.x 用户长期以来一直追求的数据访问的控制级别。

正如您之前所见,数据库表中的列通常会映射到具有普通 getter 和 setter 的实体类属性 — public int MyProp { get ;放; }。但您也可以将私有字段映射到数据库。此功能称为支持字段,它使您可以更好地控制软件读取或设置数据库数据的方式。

与影子属性一样,支持字段隐藏数据,但它们以另一种方式进行隐藏。对于影子属性,数据隐藏在 EF Core 的数据内部,但支持字段将数据隐藏在实体类内部,因此实体类更容易访问类内部的支持字段。以下是您可能使用支持字段的一些情况示例:

  • 隐藏敏感数据——将一个人的出生日期隐藏在私人字段中,并将其年龄(以岁为单位)提供给软件的其余部分。
  • 捕获更改——通过将数据存储在私有字段中并在 setter 中添加代码来检测属性的更新来检测属性的更新。当您使用属性更改来触发事件时,您将在第 12 章中使用此技术。
  • 创建领域驱动设计(DDD)实体类——创建 DDD 实体类,其中所有实体类的属性都需要是只读的。支持字段允许您锁定导航集合属性,如第 8.7 节中所述。

但在进入复杂版本之前,让我们从最简单的支持字段形式开始,其中属性 getter/setter 访问该字段。

7.15.1 创建由读/写属性访问的简单支持字段

下面的代码片段显示了一个名为 MyProperty 的字符串属性,其中字符串数据存储在私有字段中。这种形式的后备字段与使用普通属性没有什么特别的不同,但此示例显示了链接到私有字段的属性的概念:

public class MyClass
{
    private string _myProperty; 
    public string MyProperty
    {
        get { return _myProperty; } 
        set { _myProperty = value; }
    }
}

EF Core 的 By Convention 配置将查找支持字段的类型并将其配置为支持字段(有关支持字段配置选项,请参阅第 7.15.4 节),默认情况下,EF Core 会将数据库数据读取/写入此专用字段。

7.15.2 创建只读列

创建只读列是最明显的用途,尽管它也可以通过私有设置属性来实现(参见第 7.3.2 节)。如果数据库中有一列需要读取,但不希望软件写入,则支持字段是一个很好的解决方案。在这种情况下,您可以创建一个私有字段并使用公共属性(仅包含 getter)来检索值。以下代码片段给出了一个示例:

public class MyClass
{
    private string _readOnlyCol;
    public string ReadOnlyCol => _readOnlyCol;
}

某些东西必须设置列属性,例如在数据库列中设置默认值(在第 9 章中介绍)或通过某种内部数据库方法设置默认值。

7.15.3 隐藏一个人的出生日期:在类中隐藏数据

隐藏一个人的出生日期是支持字段的可能用途。在这种情况下,出于安全原因,您认为可以设置一个人的出生日期,但只能从实体类中读取他们的年龄。下面的清单演示如何在 Person 类中执行此操作,方法是使用私有_dateOfBirth字段,然后提供设置该字段的方法和计算人员年龄的属性。

清单 7.12 使用支持字段隐藏敏感数据,使其无法正常访问

public class Person
{
    private DateTime _dateOfBirth;  // 私有支持字段,无法通过普通 .NET 软件直接访问
 
    public void SetDateOfBirth(DateTime dateOfBirth)  // 允许设置支持字段
    {
        _dateOfBirth = dateOfBirth;
    }
    public int AgeYears => Years(_dateOfBirth, DateTime.Today);  // 您可以访问该人的年龄,但无法访问其确切的出生日期。

    //Thanks to dana on stackoverflow
    //see
    private static int Years(DateTime start, DateTime end)
    {
        return (end.Year start.Year 1) + (((end.Month > start.Month) || ((end.Month == start.Month) && (end.Day >= start.Day))) ? 1 : 0);
    }
}

注意:在前面的示例中,需要使用 Fluent API 创建仅后备字段变量(在第 7.15.2 节中介绍),因为 EF Core 无法使用“按约定”方法找到此后备字段。

从类的角度来看,_dateOfBirth字段是隐藏的,但你仍可以通过各种 EF Core 命令访问表列,其方式与访问影子属性的方式相同:使用 EF.Property(entity, _dateOfBirth) 方法。

_dateOfBirth,支持字段对开发人员来说并不完全安全,但这不是目标。这个想法是从正常属性中删除出生日期数据,这样它就不会无意中显示在任何用户可见的视图中。

7.15.4 配置支持字段

在看到支持字段的运行情况后,你可以按约定、通过 Fluent API 配置它们,现在还可以在 EF Core 5 中通过数据注释配置它们。By 约定方法运行良好,但依赖于类具有按类型和命名约定匹配字段的属性。如果字段与属性名称/类型不匹配,或者没有匹配的属性(如_dateOfBirth示例中所示),则需要使用数据注释或使用 Fluent API 配置支持字段。以下各节介绍各种配置方法。

按约定配置支持字段

如果支持字段链接到有效属性(请参阅第 7.3.2 节),则可以按约定配置该字段。按约定配置的规则规定,私有字段必须具有与同一类中的属性匹配的以下名称之一:

  • _<property name> (for example, _MyProperty)
  • _<camel-cased property name > (for example, _myProperty)
  • m_<property name> (for example, m_MyProperty)
  • m_<camel-cased property name> (for example, m_myProperty)

定义:Camel 大小写是一种约定,其中变量名称以小写字母开头,但使用大写字母作为名称中每个后续单词的开头,如 thisIsCamelCase 所示。

通过数据注释配置支持字段

EF Core 5 中的添加功能是 BackingField 特性,该特性允许您将属性链接到实体类中的私有字段。如果不使用“按约定”后备字段命名样式,则此属性很有用,如以下示例所示:

private string _fieldName; [BackingField(nameof(_fieldName))] public string PropertyName
{
    get { return _fieldName; }
}
public void SetPropertyNameValue(string someString)
{
    _fieldName = someString;
}

通过 FLUENT API 配置支持字段

您可以通过多种方式通过 Fluent API 配置支持字段。我们将从最简单的开始,然后逐步解决更复杂的问题。每个示例都显示了应用程序的 DbContext 中的 OnModelCreating 方法,其中仅配置了字段部分:

  • 设置后备字段的名称 - 如果后备字段名称不符合 EF Core 的约定,则需要通过 Fluent API 指定字段名称。下面是一个示例:
protected override void OnModelCreating (ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .Property(b => b.MyProperty)
        .HasField("_differentName");
    …
}
  • 仅提供字段名称 - 在这种情况下,如果存在具有正确名称的属性,则按照惯例,EF Core 将引用该属性,并且属性名称将用于数据库列。下面是一个示例:
protected override void OnModelCreating (ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .Property("_dateOfBirth")
        .HasColumnName("DateOfBirth");
    …
}

如果未找到属性 getter 或 setter,则该字段仍将使用其名称映射到该列,在此示例中,该名称为 _dateOfBirth,但这很可能不是您想要的列名称。因此,添加 HasColumnName Fluent API 方法以获得更好的列名。缺点是,您仍然需要通过其字段名称(在本例中为 _dateOfBirth)来引用查询中的数据,这不太友好或明显。

高级:配置如何读取/写入支持字段的数据

自 EF Core 3 发布以来,支持字段的默认数据库访问模式是 EF Core 读取和写入字段。此模式几乎在所有情况下都有效,但如果要更改数据库访问模式,可以通过 Fluent API UsePropertyAccessMode 方法执行此操作。以下代码片段指示 EF Core 尝试使用该属性进行读/写,但如果该属性缺少 setter,EF Core 将填写数据库读取的字段:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .Property(b => b.MyProperty)
        .HasField("_differentName")
        .UsePropertyAccessMode(PropertyAccessMode.PreferProperty);
    …
}

提示 若要查看支持字段的各种访问模式,请使用 Visual Studio 的智能感知功能查看每个 PropertyAccessMode 枚举值的注释。

7.16 使用 EF Core 配置的建议

配置 EF Core 的方法有很多种,其中一些方法会相互重复,因此对于配置的每个部分,应使用三种方法中的哪一种并不总是很明显。下面是用于 EF Core 配置的每个部分的建议方法:

  • 尽可能使用“按约定”方法,因为它既快速又简单。
  • 使用“数据注释”方法中的验证属性(MaxLength、Required 等),因为它们对验证很有用。
  • 对于其他所有内容,请使用 Fluent API 方法,因为它具有最全面的命令集。但请考虑编写代码来自动执行常见设置,例如将 DateTime“UTC 修补程序”应用于 Name 以 Utc 结尾的所有 DateTime 属性。

以下部分更详细地介绍了我关于配置 EF Core 的建议。

7.16.1 首先使用约定配置

EF Core 在配置大多数标准属性方面做得相当不错,因此请始终从该方法开始。在第 1 部分中,除了 BookAuthor 多对多链接实体类中的复合键之外,您还使用 By 约定方法构建了此初始数据库的整个数据库。

按约定方法既快速又简单。您将在第 8 章中看到,大多数关系都可以纯粹通过使用 By 约定命名规则来设置,这可以为您节省大量时间。了解 By Convention 可以配置的内容将大大减少需要编写的配置代码量。

7.16.2 尽可能使用验证数据注释

尽管您可以使用数据注释或 Fluent API 执行一些操作,例如限制字符串属性的大小,但我建议使用数据注释,原因如下:

  • 前端验证可以使用它们。尽管 EF Core 在将实体类保存到数据库之前不会对其进行验证,但系统的其他部分可能会使用数据批注进行验证。例如,ASP.NET Core 使用数据注释来验证输入,因此,如果直接输入到实体类中,则验证属性将很有用。或者,如果使用单独的 ViewModel 或 DTO 类 ASP.NET,则可以剪切并粘贴属性及其验证属性。
  • 您可能希望向 EF Core 的 SaveChanges 添加验证。使用数据验证将检查移出业务逻辑可以使业务逻辑更简单。第 4 章介绍了如何在调用 SaveChanges 时添加实体类的验证。
  • 数据注释提供了很好的评论。属性(包括数据注释)是编译时常量;它们很容易看到,也很容易理解。

7.16.3 将 Fluent API 用于其他任何事情

通常,当数据库列映射(列名、列数据类型等)与常规值不同时,我会使用 Fluent API 来设置它。您可以使用架构数据注释来执行此操作,但我尝试将此类内容隐藏在 OnModelCreating 方法中,因为它们是数据库实现问题,而不是软件结构问题。不过,这种做法与其说是一种规则,不如说是一种偏好,所以要自己做决定。第 7.16.4 节描述了如何自动执行某些 Fluent API 配置,这样可以节省您的时间,并确保所有配置规则都应用于每个匹配的类/属性。

7.16.4 通过类/属性签名自动添加 Fluent API 命令

Fluent API 命令的一个有用功能允许您编写代码,以根据类/属性类型、名称等查找和配置某些配置。在实际应用程序中,您可能有数百个 DateTime 属性需要清单 7.6 中使用的 UTC 修复。与其手动添加每个属性的配置,不如找到需要 UTC 修复的每个属性并自动应用它不是很好吗?你要做的正是这样做的。

自动查找/添加配置依赖于名为 IMutableModel 的类型,您可以在 OnModelCreating 方法中访问该类型。此类型允许你访问 EF Core 映射到数据库的所有类,并且每个 IMutableEntityType 都允许你访问属性。大多数配置选项都可以通过这两个接口中的方法应用,但少数配置选项(如查询筛选器)需要更多的工作。

首先,您将构建遍历每个实体类及其属性的代码,并添加一个配置,如清单 7.13 所示。此迭代方法定义了自动执行配置的方法,在后面的示例中,您将添加额外的命令来执行更多配置。

下面的示例向 DateTime 添加一个值转换器,该转换器应用清单 7.6 中所示的 UTC 修复。但在下面的清单中,UTC 修复值转换器应用于 Name 以 Utc 结尾的 DateTime 的每个属性。

清单 7.13 将值转换器应用于任何以 Utc 结尾的 DateTime 属性

protected override void OnModelCreating(ModelBuilder modelBuilder)  // Fluent API 命令应用于 OnModelCreating 方法。
{ 
    // 定义一个值转换器,将 UTC 设置设置为返回的 DateTime
    var utcConverter = new ValueConverter<DateTime, DateTime>( toDb => toDb,
        fromDb => DateTime.SpecifyKind(fromDb, DateTimeKind.Utc));

    foreach (var entityType in modelBuilder.Model.GetEntityTypes())  // 循环遍历 EF Core 当前找到映射到数据库的所有类
    {
        foreach (var entityProperty in entityType.GetProperties())  // 循环访问实体类中映射到数据库的所有属性
        {
            // 将 UTC 值转换器添加到以“Utc”结尾的 DateTime 和 Name 类型的属性中
            if (entityProperty.ClrType == typeof(DateTime) && entityProperty.Name.EndsWith("Utc"))
            {
                entityProperty.SetValueConverter(utcConverter);
            }
            //… other examples left out for clarity
    }
}
//… rest of configration code left out

清单 7.13 只显示了一个 Type/Named 属性的设置,但通常情况下,您会有很多 Fluent API 设置。在此示例中,您将执行以下操作:

  1. 将 UTC 修复值转换器添加到名称以 Utc 结尾的 DateTime 类型的属性中。
  2. 设置属性的 Name 包含 Price 的十进制精度/小数位数。
  3. 将 Name 以 Url 结尾的任何字符串属性设置为存储为 ASCII,即 varchar(nnn)。
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
    foreach (var entityProperty in entityType.GetProperties())
    {
        if (entityProperty.ClrType == typeof(DateTime) && entityProperty.Name.EndsWith("Utc"))
        {
            entityProperty.SetValueConverter(utcConverter);
        }

        if (entityProperty.ClrType == typeof(decimal) && entityProperty.Name.Contains("Price"))
        {
            entityProperty.SetPrecision(9); entityProperty.SetScale(2);
        }

        if (entityProperty.ClrType == typeof(string) && entityProperty.Name.EndsWith("Url"))
        {
            entityProperty.SetIsUnicode(false);
        }
    }
}

以下代码片段显示了 Book App DbContext 中 OnModelCreating 方法中的代码,用于添加以下三个配置设置:

但是,一些 Fluent API 配置需要特定于类的代码。例如,查询筛选器需要访问实体类的查询。在这种情况下,您需要向要添加查询筛选器的实体类添加一个接口,并动态创建正确的筛选器查询。

例如,您将构建代码,该代码允许您自动添加第 3.5.1 节中描述的 SoftDelete 查询过滤器和第 6.1.7 节中所示的 UserId 查询过滤器。在这两个查询筛选器中,UserId 更为复杂,因为它需要获取当前的 UserId,该 User Id 在图书应用程序的 DbContext 的每个实例上都会更改。可以通过几种方式执行此操作,但决定向查询提供 DbContext 的当前实例。下面的列表显示了名为 SoftDeleteQueryExtensions 的扩展类及其 MyQueryFilterTypes 枚举。

清单 7.14 用于在每个兼容类上设置查询过滤器的枚举/类

public enum MyQueryFilterTypes { SoftDelete, UserId }  // 定义不同类型的 LINQ 查询以放入查询筛选器

public static class SoftDeleteQueryExtensions  // 静态扩展类
{
    public static void AddSoftDeleteQueryFilter( this  // 调用此方法设置查询过滤器。
          IMutableEntityType entityData,   // 第一个参数来自 EF Core,可用于添加查询筛选器
          MyQueryFilterTypes queryFilterType,   // 第二个参数允许您选择添加哪种类型的查询过滤器
         IUserId userIdProvider = null)  // 第三个可选属性保存当前 DbContext 实例的副本,以便 UserId 将是当前
    {
        var methodName = $"Get{queryFilterType}Filter";
        var methodToCall = typeof(SoftDeleteQueryExtensions)  // 创建类型正确的方法,以创建要在查询筛选器中使用的 Where LINQ 表达式
            .GetMethod(methodName,BindingFlags.NonPublic | BindingFlags.Static)
            .MakeGenericMethod(entityData.ClrType); var filter = methodToCall
            .Invoke(null, new object[] { userIdProvider }); 
        entityData.SetQueryFilter((LambdaExpression)filter);   // 使用 SetQueryFilter 方法中创建的类型方法返回的筛选器
        if (queryFilterType == MyQueryFilterTypes.SoftDelete)  // 在 SoftDeleted 属性上添加索引以获得更好的性能
            entityData.AddIndex(entityData.FindProperty( nameof(ISoftDelete.SoftDeleted)));
        if (queryFilterType == MyQueryFilterTypes.UserId)   // 在 UserId 属性上添加索引以获得更好的性能
            entityData.AddIndex(entityData.FindProperty(nameof(IUserId.UserId)));
    }
 
    // 仅当_userId与实体类中的 UserID 匹配时,才创建为 true 的查询
    private static LambdaExpression GetUserIdFilter<TEntity>( IUserId userIdProvider) where TEntity : class, IUserId
    {
        Expression<Func<TEntity, bool>> filter = x => x.UserId == userIdProvider.UserId; return filter;
    }

    // 创建只有在 SoftDeleted 属性为 false 时才为 true 的查询
    private static LambdaExpression GetSoftDeleteFilter<TEntity>( IUserId userIdProvider) where TEntity : class, ISoftDelete
    {
        Expression<Func<TEntity, bool>> filter = x => !x.SoftDeleted;
        return filter;
    }
}

由于对具有查询筛选器的实体的每个查询都将包含该属性的筛选器,因此该代码会自动在查询筛选器中使用的每个属性上添加索引。该技术可提高该实体的性能。最后,下面的清单显示了如何在 Book App 的 DbContext 中使用清单 7.14 中所示的代码来自动配置查询过滤器。

清单 7.15 向 DbContext 添加代码以自动设置查询过滤器

public class EfCoreContext : DbContext, IUserId  // 将 IUserId 添加到 DbContext 意味着我们可以将 DbContext 传递给 UserId 查询筛选器。
{
    public Guid UserId { get; private set; }  // 保存 UserId,该 UserId 在使用 IUserId 接口的查询筛选器中使用
 
    // 设置 UserId。如果 userIdService 为 null,或者如果它为 UserId 返回 null,则设置替换 UserId。 
    public EfCoreContext(DbContextOptions<EfCoreContext> options, IUserIdService userIdService = null) : base(options)
    {
        UserId = userIdService?.GetUserId() ?? new ReplacementUserIdService().GetUserId();
    }
 
    //DbSets removed for clarity
    protected override void OnModelCreating(ModelBuilder modelBuilder)  // 自动化代码进入 OnModelCreating 方法。
    {
        //other configration code removed for clarity

        // 循环遍历 EF Core 当前找到映射到数据库的所有类 
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            //other property code removed for clarity
            
            if (typeof(ISoftDelete).IsAssignableFrom(entityType.ClrType))  // 如果该类继承 ISoftDelete 接口,则需要 SoftDelete 查询筛选器。
            {
                entityType.AddSoftDeleteQueryFilter( MyQueryFilterTypes.SoftDelete);  // 向此类添加查询筛选器,其中包含适用于 SoftDelete 的查询
            }
            if (typeof(IUserId).IsAssignableFrom(entityType.ClrType))  // 如果类继承 IUserId 接口,则需要 IUserId 查询筛选器。
            {
                entityType.AddSoftDeleteQueryFilter( MyQueryFilterTypes.UserId, this);  // 将 UserId 查询筛选器添加到此类。传递“this”允许访问当前 UserId。
            }
        }
    }
}

对于图书应用程序来说,所有这些自动化都是矫枉过正,但在更大的应用程序中,它可以为您节省大量时间;更重要的是,它确保您正确设置了所有内容。为了结束本节,以下是如果要使用此方法时应了解的一些建议和限制:

  • 如果在手动编码配置之前运行自动 Fluent API 代码,则手动编码的配置将覆盖任何自动 Fluent API 设置。但请注意,如果存在仅通过手动编写的 Fluent API 注册的实体类,则自动 Fluent API 代码不会看到该实体类。
  • 配置命令每次都必须应用相同的配置,因为 EF Core 仅在首次使用时配置应用程序的 DbContext 一次,然后从缓存版本开始工作。

总结

  • 首次创建应用程序的 DbContext 时,EF Core 会使用三种方法的组合进行自我配置:按约定、数据批注和 Fluent API。
  • 值转换器允许您在从数据库写入和读回时转换软件类型/值。
  • 两个 EF Core 功能,影子属性和支持字段,允许您隐藏更高级别的代码中的数据和/或控制对实体类中数据的访问。使用“按约定”方法尽可能多地进行设置,因为它的编码既简单又快速。
  • 当“按约定”方法不符合您的需求时,数据注释和/或 EF Core 的 Fluent API 可以提供额外的命令来配置 EF Core 将实体类映射到数据库的方式以及 EF Core 处理该数据的方式。
  • 除了手动编写配置代码外,您还可以添加代码以根据类/属性签名自动配置实体类和/或属性。

对于熟悉EF6的读者:

  • EF Core配置的基本过程表面上与EF6的工作方式相似,但有大量的更改或新命令。
  • EF Core可以使用配置类来保存给定实体类的Fluent API命令。Fluent API命令提供类似于EF6.x EntityType-Configuration类的功能,但EF Core使用IEntityTypeConfiguration接口。
  • EF Core引入了许多EF6中没有的额外功能,例如值转换器,shadow属性和后盾字段,所有这些都是EF的受欢迎补充。