惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

云风的 BLOG
云风的 BLOG
V2EX - 技术
V2EX - 技术
T
Troy Hunt's Blog
TaoSecurity Blog
TaoSecurity Blog
Attack and Defense Labs
Attack and Defense Labs
SecWiki News
SecWiki News
M
MIT News - Artificial intelligence
N
News and Events Feed by Topic
Help Net Security
Help Net Security
IT之家
IT之家
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 聂微东
The GitHub Blog
The GitHub Blog
The Last Watchdog
The Last Watchdog
Martin Fowler
Martin Fowler
Hacker News: Ask HN
Hacker News: Ask HN
酷 壳 – CoolShell
酷 壳 – CoolShell
人人都是产品经理
人人都是产品经理
H
Heimdal Security Blog
B
Blog
Blog — PlanetScale
Blog — PlanetScale
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Threat Research - Cisco Blogs
I
InfoQ
腾讯CDC
L
LangChain Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Know Your Adversary
Know Your Adversary
Cloudbric
Cloudbric
Project Zero
Project Zero
T
Tor Project blog
小众软件
小众软件
博客园 - 司徒正美
www.infosecurity-magazine.com
www.infosecurity-magazine.com
H
Help Net Security
Webroot Blog
Webroot Blog
量子位
NISL@THU
NISL@THU
Schneier on Security
Schneier on Security
Google Online Security Blog
Google Online Security Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
月光博客
月光博客
宝玉的分享
宝玉的分享
V
V2EX
T
Tailwind CSS Blog
Spread Privacy
Spread Privacy
G
Google Developers Blog
K
Kaspersky official blog

博客园 - 东邪独孤

【ASP.NET Core】在 Windows Forms 中运行 Web 服务器 【EF Core】级联删除行为 【EF Core】继承策略——TPC 【EF Core】继承策略——TPT 【EF Core】继承策略——TPH 【EF Core】使用自定义的值比较器 【EF Core】值转换器 【EF Core】直接更新数据 【EF Core】实体追踪——Entry中记录的数据 【EF Core】实体状态与变更追踪 【EF Core】将一个实体映射到多个表的正确方法 【EF Core】“Code First”方案下以编程方式生成迁移 【EF Core】“DB First”方案下用编程方式生成数据库模型代码 【EF Core】三种方法记录生成的 SQL 语句 【EF Core】未定义实体类的数据库模型 【EF Core】“多对多”关系与跳跃导航 【EF Core】FromExpression 方法有什么用? 【EF Core】通过 DbContext 选项扩展框架 【EF Core】框架底层的数据库连接管理 【EF Core】再谈普通实体关系与 Owned 关系的区别 【EF Core】实体类的依赖注入 【EF Core】优化后的模型 【EF Core】使用外部 Model 【EF Core】聊聊“复合”属性 【EF Core】带主键实体与无主键实体 【EF Core】框架是如何识别实体类的属性和主键的
【EF Core】为 DatabaseFacade 扩展“创建”与“删除”数据表功能
东邪独孤 · 2025-07-27 · via 博客园 - 东邪独孤

对于玩 EF 的大伙伴来说,对 DatabaseFacade 类的 EnsureCreated 和 EnsureDeleted 方法应该很熟悉。这对方法可在运行阶段创建或删除数据库。创建数据库时,会连同数据表一起创建;至于说删除数据库时嘛……库都没了,哪还有表呢。

不过,有些时候,不,不是有些时候,很多时候我们其实只想删除数据表。比如要初始化应用程序;或者数据库已存在的情况下,咱们只考虑判断数据表是否存在,不存在的话就创建表。

最简单的方法是直接发送 SQL 语句——如 DROP TABLE、DROP DATABASE 等。这个不在本文的讨论范围内。老周这次讨论的是运用 EF Core 自身的功能去实现。

咱们来热热身——先学点理论知识,有了这些知识,后面实战起来会容易很多。老掉牙的话:EF Core 也是基于服务容器。即支持依赖注入,这个东西好用着呢,也方便定制和扩展。微软在 .NET 上基本贯彻这个路线了。

咱们都知道,为了方便我们用面向对象的方式操作数据库,EF Core 内部实现了将相关操作以及 LINQ 转译为 SQL 的功能。尽管不能够完全覆盖 SQL 所有功能,但常用的一个不落。

下面介绍几个重量级角色:

NO.1:IMigrationsSqlGenerator 接口。

这个接口的功能很关键,看名字就知道,用来生成 SQL 语句的。这个接口只有一个 Generate 方法,它的签名比较长:

public IReadOnlyList<MigrationCommand> Generate(IReadOnlyList<MigrationOperation> operations, IModel? model = default, MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default);

第三个参数一般保留默认,第一个参数是一系列 MigrationOperation 对象;第二个参数是模型对象,这个可以从 DbContext.Model 属性返回。MigrationOperation 是一个抽象类,表示一个数据库迁移操作(对的,dotnet ef migration 命令调用的正是迁移 API 来生成 SQL,再将其发送到数据库执行)。它的每一个派生类都代表一种操作。比如,DropColumnOperation 表示从数据表中删除一列,AddUniqueConstraintOperation 用于向数据表添加唯一约束。

注意,这些 Operation 类型其实是“参数封装”器,各个属性用于收集此操作所需要的信息,各个数据库提供者都应该实现具体的操作。比如,在 SQL Server 提供者中,用于创建数据库的 SqlServerCreateDatabaseOperation 类,Name 属性用于设置数据库的名字;FileName 属性设置数据库文件的路径,比如连接字符串中使用了 AttachDbFile 字段。

现在说回 IMigrationsSqlGenerator 接口,它有个基础实现类—— MigrationsSqlGenerator。数据库提供者通常不必直接实现接口,而是从该类派生。例如官方默认实现的 SqliteMigrationsSqlGenerator 类和 SqlServerMigrationsSqlGenerator 类。Generate 方法返回一个 MigrationCommand 列表,这个列表传递给 IMigrationCommandExecutor 接口的 ExecuteNonQuery 或 ExecuteNonQueryAsync 方法就会执行 SQL 命令了。

NO.2:IMigrationCommandExecutor 接口

这个流程很明确,XXXSqlGenerator 负责生成 SQL,XXXCommandExecutor 负责执行命令。这个逻辑相信大伙伴们都能理解。这还不够,咱们还不知道怎么连接数据库呢,执行 SQL 前肯定得连接数据库的哟。于是,有请第三位。

NO.3:IRelationalConnection 接口

这个也不必过多介绍,就是用来连接数据库的。默认的基础类是 RelationalConnection。不用意外,这货一定是抽象类的。毕竟,不同数据库的连接方式是不同的,所以,这个得让数据库提供者们自己去实现。比如面向 SQL Server 的 SqlServerConnection 类,面向 SQLite 的 SqliteRelationalConnection 类。

当然了,IRelationalConnection 对象只是套个壳添加必要的逻辑,真正完成连接数据库任务的是 DbConnection 类(通过 DbConnection 属性引用)。如连接 Sqlite 数据库的 SqliteConnection 类。

NO.4:IDatabaseCreator 接口

这个接口从名字上也能猜到,它是用来创建或删除数据库的。这个接口包含了咱们非常熟悉的 EnsureCreated 和 EnsureDeleted 方法。通常,咱们在用的时候不会从服务容器中获取此接口,而是它的派生接口—— IRelationalDatabaseCreator。重点来了,这

再明显不过了,HasTables 返回 bool 值,表示数据表是否已存在;CreateTables 就是创建数据表。不过,DatabaseFacade 类没有这些方法,它只能创建、删除数据库。所以,咱们要做的,就是给它加扩展方法,把创建、删除表的方法封装出来。

于是,咱们在实战部分,要分两个阶段。

=====================================================================================

理论课上完了,下面咱们动手实践。由于 IRelationalDatabaseCreator 已经实现了判断数据表存在性和创建数据表的功能,那咱们直接用它。先要定义一个静态类,比如,叫 MyDatabaseFacadeExtension。

public static class MyDatabaseFacadeExtension
{
     ……
}

为了尽量少写重复代码,可以先搞个私有的扩展方法,用来获取 IRelationalDatabaseCreator。

 private static IRelationalDatabaseCreator GetDBCreator(this DatabaseFacade db)
 {
     return db.GetService<IRelationalDatabaseCreator>();
 }

这个很简单,DatabaseFacade 对象就有一个 GetService 方法,可直接从服务容器中取对象。

接着实现数据表的存在性判断—— HasTables。

 /// <summary>
 /// 判断表是否存在
 /// </summary>
 public static bool HasTables(this DatabaseFacade db)
 {
     IRelationalDatabaseCreator creator = db.GetDBCreator();
     return creator.HasTables();
 }
 public static Task<bool> HasTablesAsync(this DatabaseFacade db)
 {
     IRelationalDatabaseCreator creator = db.GetDBCreator();
     return creator.HasTablesAsync();
 }

随后就是创建表的封装,也很简单。

 /// <summary>
 /// 创建表
 /// </summary>
 public static void CreateTables(this DatabaseFacade db)
 {
     IRelationalDatabaseCreator creator = db.GetDBCreator();
     creator.CreateTables();
 }
 public static async Task CreateTablesAsync(this DatabaseFacade db)
 {
     IRelationalDatabaseCreator creator = db.GetDBCreator();
     await creator.CreateTablesAsync();
 }

上面的都好弄,最难的来了!IRelationalDatabaseCreator 只实现了创建表,可没有删除表啊。所以,只好自己动手了。

回想一下前文的理论热身,咱们是不是需要三个服务接口?

1、IRelationalConnection:负责连接数据库;

2、IMigrationsSqlGenerator:负责生成 SQL 命令;

3、IMigrationCommandExecutor:负责执行命令。

由于在调用服务方法时咱们需要 IModel,而它一般可从 DbContext 对象的 Model 属性获取。所以咱们还要想办法从 DatabaseFacade 对象中获取当前 DbContext 对象的引用(其实是从 DbContext 派生的类实例)。仔细观察,可发现 DatabaseFacade 类显示实现了 IDatabaseFacadeDependenciesAccessor 接口,而这个接口有个 Context 属性,正好能获取到 DbContext 实例。

好了,现在,所有难题都解决了,可以开干了。

/// <summary>
/// 删除表
/// </summary>
public static void RemoveTables(this DatabaseFacade db)
{
    // 获取服务
    IRelationalConnection conn = db.GetService<IRelationalConnection>();
    IMigrationsSqlGenerator generator = db.GetService<IMigrationsSqlGenerator>();
    IMigrationCommandExecutor executor = db.GetService<IMigrationCommandExecutor>
();
    // 获取 DbContext 实例
    DbContext context = ((IDatabaseFacadeDependenciesAccessor)db).Context;
    // 获取 Model 实例
    IModel model = context.Model;
    List<MigrationOperation> operations = new();
    // 看看要删除哪些表
    foreach (var entity in model.GetEntityTypes())
    {
        DropTableOperation drpopr = new()
        {
            // 被删除表的架构名
            Schema = entity.GetSchema(),
            // 被删除表的表名
            Name = entity.GetTableName()!
        };
        operations.Add(drpopr);
    }
    // 构建命令
    var commands = generator.Generate(operations, model);
    // 执行命令
    executor.ExecuteNonQuery(commands, conn);
}

删除数据表,对应的迁移操作是 DropTable,所以实例化一个 DropTableOperation 对象,并设置要删除的表名(可能还有架构名)。接着生成 SQL 命令,最后执行它。完事。

为了兼容,还可以实现异步版本。

public static async Task RemoveTablesAsync(this DatabaseFacade db)
{
    // 获取DbContext实例
    DbContext context = ((IDatabaseFacadeDependenciesAccessor)db).Context;
    // 获取服务
    IMigrationCommandExecutor executor = context.
GetService<IMigrationCommandExecutor>();
    IMigrationsSqlGenerator generator = context.
GetService<IMigrationsSqlGenerator>();
    IRelationalConnection conn = context.GetService<IRelationalConnection>();
    // 构建operation列表
    List<MigrationOperation> operations = new();
    // 删除所有表
    foreach (var ent in context.Model.GetEntityTypes())
    {
        operations.Add(new DropTableOperation()
        {
            Schema = ent.GetSchema(),
            Name = ent.GetTableName()!
        });
    }
    // 生成命令
    var cmds = generator.Generate(operations, context.Model);
    // 执行命令
    await executor.ExecuteNonQueryAsync(cmds, conn);
}

这里注意,Model 可能不只一个实体类,即不只一个表,所以要 foreach 逐个访问 IModel.GetEntityTypes 方法返回的实体类型集合。把实体对应的表都删除。

可以测试一下。假设用 SQL Server ,事先建一个数据库,里面没有表。

public class Student
{
    public int ID { get; set; }
    public string Name { get; set; } = string.Empty;
    public string? Major { get; set; }
    public int Age { get; set; }
    public string? Email { get; set; }
}


public class MyDbContext : DbContext
{

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("server=(localdb)\\MSSQLLOCALDB; database=Demo");
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Student>(et =>
        {
            et.HasKey(x => x.ID).HasName("PK_Student");
            et.Property(x => x.ID).HasColumnName("sid")
                .UseIdentityColumn(100, 1);
        });

    }
}

模型配置时,让主键例的名字为 sid。

运行程序时,检查有没有数据表,没有就创建。

var dc = new MyDbContext();

if (!dc.Database.HasTables())
{
    dc.Database.CreateTables();
}

若需要删除数据表,可以这样写:

dc.Database.RemoveTables();

怎么样,这样封装后是不是就和官方 API 那样方便?