基架(反向工程)

反向工程是基于数据库架构搭建实体类型类和 DbContext 类基架的过程。 可使用 EF Core 包管理器控制台 (PMC) 工具的 Scaffold-DbContext 命令或 .NET 命令行接口 (CLI) 工具的 dotnet ef dbcontext scaffold 命令执行这一过程。

注意

此处所述的 DbContext 和实体类型的基架与使用 Visual Studio 的 ASP.NET Core 中控制器的基架不同,后者在这里不作介绍。

提示

如果使用 Visual Studio,请尝试使用 EF Core Power Tools 社区扩展。 这些工具提供了一个图形工具,它在 EF Core 命令行工具的基础上构建,可以提供额外的工作流和自定义选项。

先决条件

  • 在搭建基架之前,需要安装 PMC 工具(仅适用于 Visual Studio)或 .NET CLI 工具(适用于 .NET 支持的所有平台)。
  • 在要搭建基架的项目中安装 Microsoft.EntityFrameworkCore.Design 的 NuGet 包。
  • 为面向要从中搭建基架的数据库架构的数据库提供程序安装 NuGet 包。

必需参数

PMC 和 .NET CLI 命令都有两个必需的参数:数据库的连接字符串,以及要使用的 EF Core 数据库提供程序。

连接字符串

该命令的第一个参数是指向数据库的连接字符串。 工具将使用此连接字符串来读取数据库架构。

引用和转义连接字符串的方式取决于用来执行命令的 shell;有关详细信息,请参阅相关 shell 的文档。 例如,PowerShell 要求转义 $ 字符,而不是 \

以下示例使用 Microsoft.EntityFrameworkCore.SqlServer 数据库提供程序,从位于计算机的 SQL Server LocalDB 实例上的 Chinook 数据库中搭建实体类型和 DbContext 的基架。

dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Chinook" Microsoft.EntityFrameworkCore.SqlServer

连接字符串的用户机密

如果 .NET 应用程序使用托管模型和配置系统(例如 ASP.NET Core项目),则可以使用 Name=<connection-string> 语法从配置中读取连接字符串。

例如,来看看具有以下配置文件的 ASP.NET Core 应用程序:

{
  "ConnectionStrings": {
    "Chinook": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=Chinook"
  }
}

有了配置文件中的此连接字符串,就可以使用以下工具从数据库搭建基架:

dotnet ef dbcontext scaffold "Name=ConnectionStrings:Chinook" Microsoft.EntityFrameworkCore.SqlServer

不过,在配置文件中存储连接字符串不是一个好办法,因为很容易意外公开它们,例如,通过推送到源代码管理的方式。 应该以安全的方式存储连接字符串,例如使用 Azure 密钥保管库,或者在本地工作时,使用机密管理器工具,也称为“用户机密”。

例如,若要使用“用户机密”,请先从 ASP.NET Core 配置文件中删除连接字符串。 接下来,在与 ASP.NET Core 项目相同的目录中执行以下命令,初始化用户机密:

dotnet user-secrets init

此命令会设置计算机上独立于源代码的存储,并将此存储的密钥添加到项目中。

接下来,将连接字符串存储在用户机密中。 例如:

dotnet user-secrets set ConnectionStrings:Chinook "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Chinook"

现在,先前使用配置文件中命名连接字符串的同一命令将改用存储在用户机密中的连接字符串。 例如:

dotnet ef dbcontext scaffold "Name=ConnectionStrings:Chinook" Microsoft.EntityFrameworkCore.SqlServer

已搭建基架的代码中的连接字符串

默认情况下,基架将在已搭建基架的代码中包含连接字符串,但带有警告。 例如:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#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.UseSqlServer("Data Source=(LocalDb)\\MSSQLLocalDB;Database=AllTogetherNow");

这样做是为了使生成的代码在首次使用时不会出现故障,否则将会是一种非常糟糕的学习体验。 但是,如警告所示,连接字符串不应存在于生产代码中。 有关可以管理连接字符串的各种方法,请参阅 DbContext 生存期、配置和初始化

提示

可以传递 -NoOnConfiguring (Visual Studio PMC) 或 --no-onconfiguring (.NET CLI) 选项来禁止创建包含连接字符串的 OnConfiguring 方法。

提供程序名称

第二个参数是提供程序名称。 提供程序名称通常与提供程序的 NuGet 包名称相同。 例如,对于 SQL Server 或 Azure SQL,请使用 Microsoft.EntityFrameworkCore.SqlServer

命令行选项

可以通过各种命令行选项来控制基架过程。

指定表和视图

默认情况下,数据库架构中的所有表和视图的基架都搭建到实体类型中。 可通过指定架构和表来限制对哪些表和视图搭建基架。

-Schemas (Visual Studio PMC) 或 --schema (.NET CLI) 参数指定将为其生成实体类型的表和视图的架构。 如果省略此参数,则包含所有架构。 如果使用此选项,架构中的所有表和视图都将包含在模型中,即使未使用 -Tables--table 显式包含它们也是如此。

-Tables (Visual Studio PMC) 或 --table (.NET CLI) 参数指定了将为其生成实体类型的表和视图。 可以使用“schema.table”或“schema.view”格式包含特定架构中的表或视图。 如果省略此选项,则包含所有表和视图。 |

例如,仅搭建 ArtistsAlbums 表的基架:

dotnet ef dbcontext scaffold ... --table Artist --table Album

CustomerContractor 架构搭建所有表和视图的基架:

dotnet ef dbcontext scaffold ... --schema Customer --schema Contractor

例如,若要从 Customer 架构搭建 Purchases 表的基架,从 Contractor 架构搭建 AccountsContracts 表的基架:

dotnet ef dbcontext scaffold ... --table Customer.Purchases --table Contractor.Accounts --table Contractor.Contracts

保留数据库名称

默认情况下,表和列名已修正,以便更好地匹配类型和属性的 .NET 命名约定。 指定 -UseDatabaseNames (Visual Studio PMC) 或 --use-database-names (.NET CLI) 将禁用此行为,尽可能保留原始数据库名称。 无效的 .NET 标识符仍将被修正,而合成名称(如导航属性)仍将符合 .NET 命名约定。

例如,来看看下表:

CREATE TABLE [BLOGS] (
    [ID] int NOT NULL IDENTITY,
    [Blog_Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Blogs] PRIMARY KEY ([ID]));

CREATE TABLE [posts] (
    [id] int NOT NULL IDENTITY,
    [postTitle] nvarchar(max) NOT NULL,
    [post content] nvarchar(max) NOT NULL,
    [1 PublishedON] datetime2 NOT NULL,
    [2 DeletedON] datetime2 NULL,
    [BlogID] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogID]) REFERENCES [Blogs] ([ID]) ON DELETE CASCADE);

默认情况下,将从这些表中搭建以下实体类型的基架:

public partial class Blog
{
    public int Id { get; set; }
    public string BlogName { get; set; } = null!;
    public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
}

public partial class Post
{
    public int Id { get; set; }
    public string PostTitle { get; set; } = null!;
    public string PostContent { get; set; } = null!;
    public DateTime _1PublishedOn { get; set; }
    public DateTime? _2DeletedOn { get; set; }
    public int BlogId { get; set; }
    public virtual Blog Blog { get; set; } = null!;
    public virtual ICollection<Tag> Tags { get; set; } = new List<Tag>();
}

但是,使用 -UseDatabaseNames--use-database-names 会生成以下实体类型:

public partial class BLOG
{
    public int ID { get; set; }
    public string Blog_Name { get; set; } = null!;
    public virtual ICollection<post> posts { get; set; } = new List<post>();
}

public partial class post
{
    public int id { get; set; }
    public string postTitle { get; set; } = null!;
    public string post_content { get; set; } = null!;
    public DateTime _1_PublishedON { get; set; }
    public DateTime? _2_DeletedON { get; set; }
    public int BlogID { get; set; }
    public virtual BLOG Blog { get; set; } = null!;
}

使用映射属性(也称为“数据注释”)

默认情况下,使用 OnModelCreating 中的 ModelBuilder API 配置实体类型。 指定 -DataAnnotations (PMC) 或 --data-annotations (.NET Core CLI) 以改为使用映射属性(如果可能)。

例如,使用 Fluent API 将搭建以下项的基架:

entity.Property(e => e.Title)
    .IsRequired()
    .HasMaxLength(160);

而使用数据注释则将搭建以下项的基架:

[Required]
[StringLength(160)]
public string Title { get; set; }

提示

模型的某些方面不能使用映射属性进行配置。 基架仍将使用模型生成 API 来处理这些情况。

DbContext 名称

默认情况下,已搭建基架的 DbContext 类名将是后缀为 Context 的数据库名称。 若要指定不同名称,请在 PMC 中使用 -Context,在 .NET Core CLI 中使用 --context

目标目录和命名空间

实体类和 DbContext 类将搭建到项目的根目录中,并使用项目的默认命名空间。

可使用 --output-dir 指定在其中为类搭建基架的目录,并且可使用 --context-dir 将 DbContext 类搭建到与实体类型类不同的目录中:

dotnet ef dbcontext scaffold ... --context-dir Data --output-dir Models

默认情况下,命名空间将是根命名空间加上项目根目录下任何子目录的名称。 但是,可使用 --namespace 覆盖所有输出类的命名空间。 还可使用 --context-namespace 仅覆盖 DbContext 类的命名空间:

dotnet ef dbcontext scaffold ... --namespace Your.Namespace --context-namespace Your.DbContext.Namespace

已搭建基架的代码

从现有数据库搭建基架的结果是:

  • 一个包含继承自 DbContext 的类的文件
  • 每个实体类型的一个文件

提示

从 EF7 开始,还可以使用 T4 文本模板自定义生成的代码。 有关更多详细信息,请参阅自定义反向工程模板

C# 可为空引用类型

基架可以创建使用 C# 可为空引用类型 (NRT) 的 EF 模型和实体类型。 在要搭建代码的 C# 项目中,如果启用 NRT 支持,则将自动搭建 NRT 用法。

例如,以下 Tags 表包含可为 null 和不可为 null 的字符串列:

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

这会导致生成的类中有可为 null 和不可为 null 的字符串属性:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

同样,以下 Posts 表包含 Blogs 表所需的关系:

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    [BlogId] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]));

这将导致在博客之间搭建(要求)不可为 null 的关系:

public partial class Blog
{
    public Blog()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;

    public virtual ICollection<Post> Posts { get; set; }
}

在帖子之间也是:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

多对多关系

基架过程可检测简单的联接表,并自动为它们生成多对多映射。 例如,请考虑 PostsTags 表,以及连接这两个表的联接表 PostTag

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]));

CREATE TABLE [PostTag] (
    [PostsId] int NOT NULL,
    [TagsId] int NOT NULL,
    CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostsId], [TagsId]),
    CONSTRAINT [FK_PostTag_Posts_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tags_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE);

搭建基架后,这会为 Post 生成一个类:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

并为 Tag 生成一个类:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

PostTag 表没有任何类。 而是搭建了多对多关系的配置:

entity.HasMany(d => d.Tags)
    .WithMany(p => p.Posts)
    .UsingEntity<Dictionary<string, object>>(
        "PostTag",
        l => l.HasOne<Tag>().WithMany().HasForeignKey("PostsId"),
        r => r.HasOne<Post>().WithMany().HasForeignKey("TagsId"),
        j =>
            {
                j.HasKey("PostsId", "TagsId");
                j.ToTable("PostTag");
                j.HasIndex(new[] { "TagsId" }, "IX_PostTag_TagsId");
            });

其他编程语言

由 Microsoft 基架 C# 代码发布的 EF Core 包。 不过,基础基架系统支持一个插件模型,可以将基架转换为其他语言。 此插件模型由各种社区运行项目使用,例如:

自定义代码

从 EF7 开始,自定义生成的代码的最佳方式之一是自定义用于生成代码的 T4 模板

生成代码后也可以对其进行更改,但最好的方式取决于是否打算在数据库模型更改时重新运行基架过程。

仅搭建一次基架

使用此方法,已搭建基架的代码为今后基于代码的映射提供了起点。 可以根据需要对生成的代码进行任何更改,它就像你项目中的其他代码一样成为正常的代码。

可以通过以下两种方式之一使数据库和 EF 模型保持同步:

  • 切换为使用 EF Core 数据库迁移,并使用实体类型和 EF 模型配置作为事实源,从而使用迁移来驱动架构。
  • 在数据库发生更改时,手动更新实体类型和 EF 配置。 例如,如果将新列添加到表中,则将该列的属性添加到映射的实体类型,并使用 OnModelCreating 中的映射属性和/或代码添加任何必要的配置。 这相对容易,唯一真正的挑战是确保以某种方式记录或检测数据库更改,让负责代码的开发人员能够做出反应。

重复搭建基架

一次搭建基架的另一种方法是在每次数据库发生变化时重新搭建基架。 这将覆盖任何以前已搭建基架的代码,意味着对该代码中的实体类型或 EF 配置所做的任何更改都将丢失。

[提示] 默认情况下,EF 命令不会覆盖任何现有代码,以防止意外丢失代码。 -Force (Visual Studio PMC) 或 --force (.NET CLI) 参数可用于强制覆盖现有文件。

由于已搭建基架的代码将被覆盖,因此最好不要直接修改它,而是依赖于分部类和方法,以及 EF Core 中允许重写配置的机制。 具体而言:

  • DbContext 类和实体类都作为分部类生成。 这允许在运行基架时不会重写的单独文件中引入其他成员和代码。
  • DbContext 类包含一个名为 OnModelCreatingPartial 的分部方法。 可以将此方法的实现添加到 DbContext 的分部类中。 然后,在调用 OnModelCreating 后调用它。
  • 使用 ModelBuilder API 进行的模型配置将替代由约定或映射属性完成的任何配置,以及早期在模型生成器上完成的配置。 这意味着 OnModelCreatingPartial 中的代码可用于替代基架过程生成的配置,而无需删除该配置。

最后,请记住,从 EF7 开始,可以自定义用于生成代码的 T4 模板。 相较于使用默认值搭建基架然后使用分部类和/或方法进行修改,这通常是一种更有效的方法。

工作原理

反向工程从读取数据库架构开始。 它会读取有关表、列、约束和索引的信息。

接下来,它将使用架构信息创建 EF Core 模型。 使用表创建实体类型;使用列创建属性;使用外键创建关系。

最后,使用模型生成代码。 为相应实体类型类、Fluent API 和数据注释搭建基架,以便从应用重新创建相同的模型。

限制

  • 并非模型的所有内容都可使用数据库架构表示。 例如,数据库架构中不存在有关继承层次结构固有类型表拆分的信息。 因此,永远不会对这些构造搭建基架。
  • 此外,EF Core 提供程序可能不支持某些列类型。 这些列不会包含在模型中。
  • 可在 EF Core 模型中定义并发令牌,以防止两个用户同时更新同一实体。 某些数据库使用一种特殊的类型来表示这种类型的列(例如 SQL Server 中的 rowversion),在这种情况下,我们可以对此信息进行反向工程;然而并不会对其他并发令牌搭建基架。