【ASP.NET Core】调用 Web API 备份数据库
<p>老周不经意间翻了一下博客列表,上一篇水文竟然在 一个月前。啊,大海啊,全是水;时间啊,你跑得真快!过了一个月老周竟没感觉出来,可是这一个月里,好像啥事也没干成,就改了一下几个旧项目。也许是现在大环境真的不好,新项目不好找。新的活有是有,比较简单,却很奇怪,比那货难的项目都做过,偏偏这货没头绪。这东西需求就是画图——是用程序来画图,类似甘特图。莫名其妙的是,这活会卡在窗口排版上。按照需求,这货窗口特多。点一下这里,弹一个对话框出来可以修改;点一下那里,又要弹一个出来允许修改;右键单击一下,弹出上百个选项的菜单(Check 模式),简直离谱,那么多指标项,我都怀疑他们老板根本不会去看的,但他们非要做全面。更闹心的是主窗口,想想现在的显示屏分辨率又高又大,一个窗口全屏放在那里,可上面又没几个控件,70% 的地方就是画进度图,每几秒刷新一下。虽然简单易用最好,但这窗口是看着空洞了一些。免得那些不懂编程的人说老周这项目根本没干啥活,老周还打算给它弄个高清《美少女战士》或《刘姥姥进大观园》当背景图,这样看起来就不空洞了。</p><p>今天咱们聊一个很单的主题,写一个 Web API,客户端可以调用它来备份 SQL Server 数据库。不知道大伙伴们有没有做过这活。相信做过的人会比老周更明白,毕竟,老熟人都知道,老周有两大弱项:SQL、汇编。汇编呢,是学生时代没好好学,想当年很轻松地就拿下了二级C++,偏就没学会汇编;而 SQL 呢,本来就学得一般,再加上用得少,忘得差不多了,所以别人给老周安排的项目基本不包括写 SQL 的,最终导致 SQL 方面越来越弱。</p>
<p>EF Core 不仅能用 LINQ 和实体类型配合操作,确实能让你在80%的情况下不用写SQL语句,但,为了灵活,EF Core 和早期 EF 和 ADO.NET 一样,是可以直接执行 SQL 语句的。这意味着,备份和还原数据库不在话下。</p>
<p>这个功能的实现并不难,但有两小坑,老周接下来会慢慢讲。</p>
<p>备份数据库用的是 BACKUP DATABASE 语句,比如这样</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">BACKUP</span> <span style="color: rgba(0, 0, 255, 1)">DATABASE</span> <span style="color: rgba(128, 128, 128, 1)"><</span>你的数据库名<span style="color: rgba(128, 128, 128, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)">TO</span> <span style="color: rgba(0, 0, 255, 1)">DISK</span> <span style="color: rgba(128, 128, 128, 1)">=</span> <span style="color: rgba(255, 0, 0, 1)">'</span><span style="color: rgba(255, 0, 0, 1)">D:\backups\dbs.bak</span><span style="color: rgba(255, 0, 0, 1)">'</span></pre>
</div>
<p>DISK 后的表达式不一定指定磁盘,更多时候是备份的文件名。如果还要备份日志,可以接着执行 BACKUP LOG 语句。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">BACKUP</span> <span style="color: rgba(255, 0, 255, 1)">LOG</span> <span style="color: rgba(128, 128, 128, 1)"><</span>数据库名<span style="color: rgba(128, 128, 128, 1)">></span> <span style="color: rgba(0, 0, 255, 1)">TO</span> <span style="color: rgba(0, 0, 255, 1)">DISK</span> <span style="color: rgba(128, 128, 128, 1)">=</span> <span style="color: rgba(255, 0, 0, 1)">'</span><span style="color: rgba(255, 0, 0, 1)">E:\WrtMsn\oodo-log.bak</span><span style="color: rgba(255, 0, 0, 1)">'</span></pre>
</div>
<p>如果要把备份放到另一台服务器上,可以用共享路径。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">BACKUP</span> <span style="color: rgba(0, 0, 255, 1)">DATABASE</span> <span style="color: rgba(128, 128, 128, 1)"><</span>数据库名<span style="color: rgba(128, 128, 128, 1)">></span> <span style="color: rgba(0, 0, 255, 1)">TO</span> <span style="color: rgba(0, 0, 255, 1)">DISK</span> <span style="color: rgba(128, 128, 128, 1)">=</span> <span style="color: rgba(255, 0, 0, 1)">'</span><span style="color: rgba(255, 0, 0, 1)">\\GaoXServer\shares\team\abc.bak</span><span style="color: rgba(255, 0, 0, 1)">'</span></pre>
</div>
<p>这里就有第一个坑——权限,不管是你的程序进程还是 MSSQL 进程,有些目录是没有写入的权限的。比较懒的做法是在放置备份的目录上给个 everyone 的完全控制权限。老周不推荐这样,太不厚道了。若是不太好确定用哪个用户(毕竟有时候不一定是某个用户,而是系统服务),Windows 里面有一个好用的名称,叫 Authenticated Users,看字面意思就已经过验证的用户,就不是单指某个用户了。这个比 everyone 靠谱多了,起码匿名的不允许。</p>
<p>简单说说操作,在目录上右击,打开“属性”窗口,切换到“安全”页。点击下面的“高级”按钮。</p>
<p><img src="https://img2024.cnblogs.com/blog/367389/202506/367389-20250602165332029-135338705.png" alt="" width="341" height="442" loading="lazy"></p>
<p>点击“添加”按钮。</p>
<p><img src="https://img2024.cnblogs.com/blog/367389/202506/367389-20250602165431324-344405886.png" alt="" width="472" height="414" loading="lazy"></p>
<p> 点击“选择主体”,找到 Authenticated Users 并添加。最起码给予写入和修改权限。</p>
<p><img src="https://img2024.cnblogs.com/blog/367389/202506/367389-20250602165645663-891005679.png" alt="" width="351" height="257" loading="lazy"></p>
<p>然后一路确定、应用即可。</p>
<p>============================================================================================</p>
<p>好了,理论知识完毕,下面可以动手了。</p>
<p>先建个库用来测试。这里老周建了个实体,名为 Movie,表示一部大片。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> Movie
{
</span><span style="color: rgba(128, 128, 128, 1)">///</span> <span style="color: rgba(128, 128, 128, 1)"><summary></span>
<span style="color: rgba(128, 128, 128, 1)">///</span><span style="color: rgba(0, 128, 0, 1)"> 编号
</span><span style="color: rgba(128, 128, 128, 1)">///</span> <span style="color: rgba(128, 128, 128, 1)"></summary></span>
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">int</span> Id { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span><span style="color: rgba(0, 0, 0, 1)">; }
</span><span style="color: rgba(128, 128, 128, 1)">///</span> <span style="color: rgba(128, 128, 128, 1)"><summary></span>
<span style="color: rgba(128, 128, 128, 1)">///</span><span style="color: rgba(0, 128, 0, 1)"> 电影标题
</span><span style="color: rgba(128, 128, 128, 1)">///</span> <span style="color: rgba(128, 128, 128, 1)"></summary></span>
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">string</span>? Subject {<span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span><span style="color: rgba(0, 0, 0, 1)">; }
</span><span style="color: rgba(128, 128, 128, 1)">///</span> <span style="color: rgba(128, 128, 128, 1)"><summary></span>
<span style="color: rgba(128, 128, 128, 1)">///</span><span style="color: rgba(0, 128, 0, 1)"> 导演是谁
</span><span style="color: rgba(128, 128, 128, 1)">///</span> <span style="color: rgba(128, 128, 128, 1)"></summary></span>
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">string</span>? Director {<span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span><span style="color: rgba(0, 0, 0, 1)">; }
</span><span style="color: rgba(128, 128, 128, 1)">///</span> <span style="color: rgba(128, 128, 128, 1)"><summary></span>
<span style="color: rgba(128, 128, 128, 1)">///</span><span style="color: rgba(0, 128, 0, 1)"> 哪年上映的
</span><span style="color: rgba(128, 128, 128, 1)">///</span> <span style="color: rgba(128, 128, 128, 1)"></summary></span>
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">int</span>? Year { <span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span><span style="color: rgba(0, 0, 0, 1)">; }
</span><span style="color: rgba(128, 128, 128, 1)">///</span> <span style="color: rgba(128, 128, 128, 1)"><summary></span>
<span style="color: rgba(128, 128, 128, 1)">///</span><span style="color: rgba(0, 128, 0, 1)"> 讲了啥故事
</span><span style="color: rgba(128, 128, 128, 1)">///</span> <span style="color: rgba(128, 128, 128, 1)"></summary></span>
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">string</span>? Desc {<span style="color: rgba(0, 0, 255, 1)">get</span>; <span style="color: rgba(0, 0, 255, 1)">set</span><span style="color: rgba(0, 0, 0, 1)">; }
}</span></pre>
</div>
<p>接下来写 DbContext。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> TestDBContext:DbContext
{
</span><span style="color: rgba(0, 0, 255, 1)">public</span> TestDBContext(DbContextOptions<TestDBContext><span style="color: rgba(0, 0, 0, 1)"> options)
: </span><span style="color: rgba(0, 0, 255, 1)">base</span><span style="color: rgba(0, 0, 0, 1)">(options)
{ }
</span><span style="color: rgba(0, 0, 255, 1)">protected</span> <span style="color: rgba(0, 0, 255, 1)">override</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> OnModelCreating(ModelBuilder modelBuilder)
{
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 主键</span>
modelBuilder.Entity<Movie>().HasKey(x =><span style="color: rgba(0, 0, 0, 1)"> x.Id);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 影片名为必填</span>
modelBuilder.Entity<Movie>().Property(x =><span style="color: rgba(0, 0, 0, 1)"> x.Subject).IsRequired();
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 填充初始数据</span>
modelBuilder.Entity<Movie>().ToTable(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">t_movies</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">).<span style="background-color: rgba(255, 255, 0, 1)">HasData(
</span></span> <span style="background-color: rgba(255, 255, 0, 1)">new Movie { Id = 1, Subject = "狗二总裁", Year = 2026, Director = "大头苏", Desc = "二五仔二次创业的励志故事" },
</span>new Movie { Id = 2, Subject = "子夜实验室", Year = 2025, Director = "丁小丁", Desc = "某大学的实验室总是在子夜时分莫名发生火灾,校方怀疑有人恶意纵火,于是,学校成立专项小组进行调查……" }
);
// 下面这行可以省略,Id 属性默认是自增长标识
//modelBuilder.Entity<Movie>().Property(c => c.Id).ValueGeneratedOnAdd();
}
public DbSet<Movie> Movies { get; set; }
}</pre>
</div>
<p>重写 OnModelCreating 方法是对模型做一些自定义设置,如果你只需保留默认,可以不重写此方法。由于表示主键的属性名为 Id,EF Core 会自动认为是主键,且启用自增长标识。上面代码老周用 ToTable 方法映射到的数据表名 t_movies。如果你要求表名和实体类名一样,那可以忽略。</p>
<p>另外,代码还调用了 HasData 方法,其用途是插入一些初始数据。如果你的数据表的初始状态允许空白,可以忽略。如果要插入种子数据(即初始数据),这里有第二个坑,一定要显式地指定 Id 属性的值,这里比较特殊。因为这是种子数据,必须保证每个字段的值是静态的,也就是说,不管你的程序在哪台机器上运行,得保证插入的数据是相同的。Id 字段虽然是自增长的,可你无法保证实际使用时数据库会从1开始增长,也不保证步长值一定是1。说不定人家是 100、101、102 呢。正因为如此,如果你忽略 Id 的值就会抛出异常。有一种方案就是使用负值,这个不如硬编码一个整数值保险一些。</p>
<p>完工后,要在 ASP.NET Core 应用程序的服务容器中注册。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">var</span> builder =<span style="color: rgba(0, 0, 0, 1)"> WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 配置数据库</span>
<span style="color: rgba(0, 0, 255, 1)">string</span>? connectionStr = builder.Configuration.GetConnectionString(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">prj_cns</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">);
builder.Services.<span style="background-color: rgba(255, 255, 0, 1)">AddDbContext</span></span><span style="background-color: rgba(255, 255, 0, 1)"><TestDBContext></span>(ob =><span style="color: rgba(0, 0, 0, 1)">
{
ob.<span style="background-color: rgba(255, 255, 0, 1)">UseSqlServer(connectionStr)</span>;
});
</span><span style="color: rgba(0, 0, 255, 1)">var</span> app = builder.Build();</pre>
</div>
<p>数据库的连接字符串在配置文件中(appsettings.json)。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">{
</span>"Logging"<span style="color: rgba(0, 0, 0, 1)">: {
……
}
},
</span>"AllowedHosts": "*"<span style="color: rgba(0, 0, 0, 1)">,
</span>"<span style="background-color: rgba(204, 255, 204, 1)"><em><strong>ConnectionStrings</strong></em></span>"<span style="color: rgba(0, 0, 0, 1)">: {
</span>"<span style="background-color: rgba(255, 255, 0, 1)">prj_cns</span>": "Data Source=.\\SQLTEST;Initial Catalog=Demo;Integrated Security=True;Persist Security Info=False;Pooling=False;Encrypt=True;Trust Server Certificate=True"<span style="color: rgba(0, 0, 0, 1)">
}
}</span></pre>
</div>
<p>在配置文件中使用 ConnectionStrings 节点有特殊含义,把数据库连接字符串放在此节点下,代码中获取时可以简化一些。</p>
<div class="cnblogs_code">
<pre>builder.Configuration.GetConnectionString(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">prj_cns</span><span style="color: rgba(128, 0, 0, 1)">"</span>);</pre>
</div>
<p>就是 <span style="color: rgba(0, 0, 255, 1)"><em><strong>GetSection("ConnectionStrings")["prj_cns"]</strong></em></span> 的简化版。</p>
<p>======================================================================================</p>
<p>最后要实现的是 Web API。对于有明确的模块功能的代码我们尽量用 MVC Controller 去实现,Mini-API 只适合没啥模块化的简单代码,也避免把 Main 方法的代码搞得又长又臭。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> TestController : Controller
{
</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">readonly</span><span style="color: rgba(0, 0, 0, 1)"> TestDBContext _db;
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> TestController(TestDBContext db)
{
_db </span>=<span style="color: rgba(0, 0, 0, 1)"> db;
}
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> ActionResult Index()
{
_db.Database.EnsureCreated();
</span><span style="color: rgba(0, 0, 255, 1)">return</span> View(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">~/Views/home.cshtml</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">, _db.Movies.ToArray());
}
</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">async</span> Task<JsonResult><span style="color: rgba(0, 0, 0, 1)"> NewData(Movie mvi)
{
</span><span style="color: rgba(0, 0, 255, 1)">bool</span> hasany = <span style="color: rgba(0, 0, 255, 1)">await</span> _db.Movies.AnyAsync(m => m.Subject!<span style="color: rgba(0, 0, 0, 1)">.Equals(mvi.Subject));
</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)">(hasany)
{
</span><span style="color: rgba(0, 0, 255, 1)">return</span> Json( <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)">
{
Code </span>= <span style="color: rgba(128, 0, 128, 1)">2</span><span style="color: rgba(0, 0, 0, 1)">,
Message </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">此电影已存在</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">
} );
}
</span><span style="color: rgba(0, 0, 255, 1)">await</span><span style="color: rgba(0, 0, 0, 1)"> _db.Movies.AddAsync(mvi);
</span><span style="color: rgba(0, 0, 255, 1)">int</span> i = <span style="color: rgba(0, 0, 255, 1)">await</span><span style="color: rgba(0, 0, 0, 1)"> _db.SaveChangesAsync();
</span><span style="color: rgba(0, 0, 255, 1)">if</span>(i <= <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">)
{
</span><span style="color: rgba(0, 0, 255, 1)">return</span> Json(<span style="color: rgba(0, 0, 255, 1)">new</span> { Code = <span style="color: rgba(128, 0, 128, 1)">3</span>, Message = <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">添加失败</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)"> });
}
</span><span style="color: rgba(0, 0, 255, 1)">return</span> Json(<span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)">
{
Code </span>= <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">,
Message </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">OK</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">
});
}
</span><span style="color: rgba(0, 0, 255, 1)">public</span> IEnumerable<Movie><span style="color: rgba(0, 0, 0, 1)"> GetAllMovies()
{
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> _db.Movies.ToArray();
}
<strong>
</span><span style="color: rgba(0, 0, 255, 1)">public</span> ActionResult Backup(<span style="color: rgba(0, 0, 255, 1)">string</span><span style="color: rgba(0, 0, 0, 1)"> filename)
{
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 取得当前目录</span>
IWebHostEnvironment env = HttpContext.RequestServices.GetRequiredService<IWebHostEnvironment><span style="color: rgba(0, 0, 0, 1)">();
</span><span style="color: rgba(0, 0, 255, 1)">string</span> curDir =<span style="color: rgba(0, 0, 0, 1)"> env.ContentRootPath;
</span><span style="color: rgba(0, 0, 255, 1)">string</span> bkfilePath =<span style="color: rgba(0, 0, 0, 1)"> Path.Combine(curDir, filename);
</span><span style="color: rgba(0, 0, 255, 1)">string</span> dbname =<span style="color: rgba(0, 0, 0, 1)"> _db.Database.GetDbConnection().Database;
</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (System.IO.File.Exists(bkfilePath))
{
System.IO.File.Delete(bkfilePath);
}
</span><span style="color: rgba(0, 0, 255, 1)">string</span> sql = $<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">BACKUP DATABASE {dbname} TO DISK = '{bkfilePath}'</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)">
{
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 不必理会返回值</span>
_ =<span style="color: rgba(0, 0, 0, 1)"> _db.Database.ExecuteSqlRaw(sql);
}
</span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)">
{
</span><span style="color: rgba(0, 0, 255, 1)">return</span> Json(<span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)">
{
Code </span>= <span style="color: rgba(128, 0, 128, 1)">1</span><span style="color: rgba(0, 0, 0, 1)">,
Message </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">备份失败</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">
});
}
</span><span style="color: rgba(0, 0, 255, 1)">return</span> Json(<span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)">
{
Code </span>= <span style="color: rgba(128, 0, 128, 1)">0</span><span style="color: rgba(0, 0, 0, 1)">,
Message </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">OK</span><span style="color: rgba(128, 0, 0, 1)">"</span></strong><span style="color: rgba(0, 0, 0, 1)"><strong>
});
}</strong>
}</span></pre>
</div>
<p>其他代码你可以不看,重点关注 Backup 方法即可,这才是核心。数据上下文(我们从 DbContext 类派生了)有一个 Database 属性,可以对数据库进行额外的操作,比如 EnsureCreated 方法可以检查数据库是否存在,不存在就自动创建;EnsureDeleted 方法可以检查数据库是否已存在,若是,删除它;你还可以用 EnsureDeleted 方法启动一个事务。咱们用到的是 ExecuteSqlRaw 方法,它的功能是执行原始 SQL 语句。何谓“原始”?就是你给啥SQL就是啥,不做参数化处理,也不过滤SQL注入攻击。所以,这个方法你要在安全的情况下调用。本例的代码调用是安全的,因为只是执行 BACKUP 语句,客户端不能传入其他无关的数据——仅仅需要提供备份的文件名。出于安全考虑,你甚至可以不用客户端提供文件名,而是直接生成(用日期时间 + GUID 方式就行)。 当然了,在实际使用中,你应该对 API 的调用做一下验证,比如,客户端必须传一个有效的 token 表明身份等。</p>
<p>由于执行 BACKUP 语句不属于常规的“增删改查”操作,无论执行成功还是失败,返回的整数值都是负值。因此,咱们不能依据返回的数值来判断是否备份成功。不过,倒可以用 try 语句块来简单判断。因为如果备份出错会抛异常。把 ExecuteSqlRaw 方法调用写在 try 语句块中,若抛异常,说明备份过程不顺利。</p>
<p>为了让客户端能方便管理数据维护,你还可以加一个 API,让客户端下载备份的文件。不过,如果操作的环境不够安全,或没有有效的安全措施,有泄露数据库的风险。</p>
<p> </p>
<p>============================================================================================</p>
<p>本示例备份的文件是放在应用程序目录中的,所以,你得事先给程序目录设置一下权限,允许写入文件(参考前文)。这是为了程序部署方便,如果服务器环境允许,可以在服务器上选择一个专用的目录来存放备份。可以把路径配置到 appsettings.json 文件中,程序中直接读取,这样你在部署项目后可以手动在 appsettings.json 文件中编辑正确的目录。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">{
……
</span>"AllowedHosts": "*"<span style="color: rgba(0, 0, 0, 1)">,
</span>"ConnectionStrings"<span style="color: rgba(0, 0, 0, 1)">: {
……
},
</span><span style="background-color: rgba(255, 255, 0, 1)">"backup_dir": "D:\\backups"</span><span style="color: rgba(0, 0, 0, 1)">
}</span></pre>
</div>
<p>修改程序代码,从 backup_dir 配置项读取目录。</p>
<div class="cnblogs_code">
<pre> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 取得当前目录</span>
IWebHostEnvironment env = HttpContext.RequestServices.GetRequiredService<IWebHostEnvironment><span style="color: rgba(0, 0, 0, 1)">();
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 读取配置</span>
IConfiguration config = HttpContext.RequestServices.GetRequiredService<IConfiguration><span style="color: rgba(0, 0, 0, 1)">();
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 如果找不到配置的目录,那就放在应用程序目录下</span>
<span style="color: rgba(0, 0, 255, 1)">string</span> bkfilePath =<span style="color: rgba(0, 0, 0, 1)"> Path.Combine(bkDir, filename);
</span><span style="color: rgba(0, 0, 255, 1)">string</span> dbname =<span style="color: rgba(0, 0, 0, 1)"> _db.Database.GetDbConnection().Database;
</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (System.IO.File.Exists(bkfilePath))
{
System.IO.File.Delete(bkfilePath);
}
</span><span style="color: rgba(0, 0, 255, 1)">string</span> sql = $<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">BACKUP DATABASE {dbname} TO DISK = '{bkfilePath}'</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
……</span></pre>
</div>
<p>你不能保证配置 backup_dir 一定有效,如果没有配置,就把备份文件放在应用程序目录下。</p>
<p>在运行程序后,你要访问一次根 URL(/),目的是让 EF Core 自动创建数据库。如果在 VS 中调试运行,默认会自动打浏览器的。</p>
<p><img src="https://img2024.cnblogs.com/blog/367389/202506/367389-20250602183736696-1639902785.png" alt="" width="264" height="199" loading="lazy"></p>
<p>创建的数据库如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/367389/202506/367389-20250602183932227-1436186209.png" alt="" width="255" height="281" loading="lazy"></p>
<p>现在,你可以用任何方式,任何 API 测试工具来验证一下,能不能备份。不想用第三方工具,可以用.NET 官方的 httprepl 工具,安装命令:</p>
<div class="cnblogs_code">
<pre>dotnet tool install -g microsoft.dotnet-httprepl</pre>
</div>
<p>安装好后,在 cmd 中直接输入 httprepl 就能进入对话状态。是的,交互式的。</p>
<p>为了后面在 POST JSON 数据时方便编辑,咱们设置一个默认的文本编译器。免得折腾,你可以直接用 VS Code。</p>
<div class="cnblogs_code">
<pre> pref <span style="color: rgba(0, 0, 255, 1)">set</span> editor.command.<span style="color: rgba(0, 0, 255, 1)">default</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">C:\Users\Admin\Softs\VSCode\Code.exe</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">
pref </span><span style="color: rgba(0, 0, 255, 1)">set</span> editor.command.<span style="color: rgba(0, 0, 255, 1)">default</span>.argument <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">-w</span><span style="color: rgba(128, 0, 0, 1)">"</span></pre>
</div>
<p>第一条命令是设置 VS Code 的路径,注意,是 VSC 的可执行文件路径。</p>
<p>第二条命令是设置命令行参数 -w,此参数是让 VSC 等待打开的文件顺利保存并关闭才退出进程。因为 httprepl 工具在启动 VSC 时会创建一个临 时文件,并让 VSC 打开来编辑。</p>
<p>好,现在咱们测试。</p>
<p>1、运行示例程序。</p>
<p>2、CMD中进入 httprepl 会话。</p>
<p>3、连接根URL。</p>
<div class="cnblogs_code">
<pre>connect http:<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">localhost:5276</span></pre>
</div>
<p>4、调用 /demo/backup 接口。</p>
<div class="cnblogs_code">
<pre>post /demo/backup -h content-type=application/json</pre>
</div>
<p>-h 指定HTTP头。</p>
<p>5、此时,VSC 会启动,我们编辑一下。</p>
<p><img src="https://img2024.cnblogs.com/blog/367389/202506/367389-20250602190537264-1578441187.png" alt="" width="253" height="83" loading="lazy"></p>
<p>注意这里,不用输入大括号,直接输入 Javascript 字符串就行,因为 filename 参数不是复杂对象,而是一个基础类型。</p>
<p>6、保存,关闭 VSC。</p>
<p>7、回到 CMD 窗口,就会看到备份成功的响应。</p>
<p><img src="https://img2024.cnblogs.com/blog/367389/202506/367389-20250602190945616-45215752.png" alt="" width="350" height="144" loading="lazy"></p>
<p>到备份目录下,就能看到刚备份的文件了。大小约 3.46 MB 。</p>
<p>要验证备份是否真有效,可以把这个文件还原回 SQL Server,如果还原后,看到数据,说明备份是没有问题的。</p>
<p><img src="https://img2024.cnblogs.com/blog/367389/202506/367389-20250602192407115-1575396025.png" alt="" width="447" height="390" loading="lazy"></p>
<p><img src="https://img2024.cnblogs.com/blog/367389/202506/367389-20250602192533394-86793248.png" alt="" width="528" height="264" loading="lazy"></p>
<p> 好了,今天就水到这里了。</p><br><br>
来源:https://www.cnblogs.com/tcjiaan/p/18907380
頁:
[1]