本文来自『.NET大牛之路』星球的免费分享
在前面的文章中,书大师网站展示的书籍信息是硬编码的。虽然满足了基本需求,但不便于后面的信息更新。更新书籍信息还得修改代码,而且以往发布的书籍信息没有记录。所以我们需要用数据库来做数据的持久化,把数据和代码分离。
为使操作数据库的编程更加高效,我们的项目将使用 EF Core 作为 ORM 框架。
1升级为 .NET 6 正式版
上周 .NET 6 已经正式发布了,VS 2022 也同步发布了,相信很多人已经更新了。只要你升级 VS 2022 为最新版本,就会自动安装或升级 .NET 为 6.0。如果你用的是 MacOS 或 Linux 系统,则可能需要去官网手动下载 SDK。
同样,我们的生产环境也做相应的升级,由原来的 RC2 版本升级到正式版。升级很简单,直接按照官网文档操作就行。我的服务器是 CentOS 8,对应的 .NET 6 安装文档:
https://docs.microsoft.com/en-us/dotnet/core/install/linux-centos
注意,如果你生产环境和我一样是 CentOS 8,请按 CentOS 7 的方式安装 ASP.NET Core 运行时,因为:
官网表示 .NET 6 不支持 CentOS 8 了,但 CentOS 8 包的安装是兼容 CentOS 7 的,所以按 CentOS 7 的方式安装即可。执行文档中的两个命令即可:
$ sudo rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm
$ sudo yum install aspnetcore-runtime-6.0
安装完成后,需要修改一下我们的 bookist.service
服务文件:
然后使用 systemctl daemon-reload
重新加载服务。
2安装 MySQL
我们这个项目使用的是 MySQL 数据库,版本是 8.x。由于我们使用的是 EF Core ORM 框架,对于同一套代码,不同数据库,API 都是一样的,所以你也可以使用任何其它你熟悉的数据库。
为了简单起见,开发和生产环境我直接使用同一数据库,加上大多数个人没有条件为一个小网站设一个专门的数据库服务器,所以我把 MySQL 数据库直接安装在应用生产环境。
CentOS 8 安装 MySQL 8 很简单,直接运行下面命令:
$ dnf -y install @mysql
稍等片刻即可完成安装,使用下面命令验证一下版本:
$ mysql -V
mysql Ver 8.0.26 for Linux on x86_64 (Source distribution)
安装完成后,我们还需要把 MySQL 服务开启,并设为开机启动,运行如下两行命令:
# 开启 MySQL 服务
$ systemctl start mysqld
# 使 MySQL 开机启动
$ systemctl enable mysqld
Created symlink /etc/systemd/system/multi-user.target.wants/mysqld.service → /usr/lib/systemd/system/mysqld.service.
我们还需要设置一下数据库用户 root 的访问密码,以便我们程序能够连接。使用如下命令进入 MySQL 的安全向导:
$ mysql_secure_installation
运行该脚本后,会让你选择配置安全选项,比如密码强度设置,这个根据自己需要选择。其中,有两个选项需要注意,一是允许 root 远程访问,二是配置 root 用户密码:
为了更方便远程管理数据库,我们需要开户 MySQL 的远程登录,所以建议设置高强度的密码。如果你使用的是云服务器,需要先到安全组配置中添加 3306
端口的入站规则。以腾讯云为例:
注意,如果是重要项目,请关闭 root 远程登录。如果需要远程登录,则创建权限受限的数据库帐号。
最后,需要配置一下允许远程访问数据库的 IP 地址,这里配置为所有 IP(因为家庭宽带 IP 一般是动态的):
$ mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 14
Server version: 8.0.26 Source distribution
mysql> CREATE USER 'root'@'%' IDENTIFIED BY '这里是你的密码';
Query OK, 0 rows affected (0.01 sec)
mysql> GRANT ALL ON *.* TO 'root'@'%';
Query OK, 0 rows affected (0.01 sec)
mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)
mysql> exit
使用任一 MySQL 客户端(我用的是 Navicat),测试一下在本地是否可以正常连接数据库。
3添加 EF Core 包
Entity Framework Core 是微软提供的一个非常强大的 ORM(Object Relational Mapper )框架。下面在我们的项目中使用该框架。
切换到 Bookist.Web
目录,使用如下命令安装两个 Nuget 包:
$ dotnet add package Microsoft.EntityFrameworkCore.Design
$ dotnet add package Pomelo.EntityFrameworkCore.MySql
第一个是 EF Core 的设计库,包含 EF Core 基本库和工具库;第二个是一个第三方的 MySql 支持库,不同的数据库需要使用对应的支持库,它们实现了使用 EF Core 用统一的接口访问数据库。安装完后会在 Bookist.Web.csproj
文件中生成如下代码:
...
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.0" />
</ItemGroup>
...
如果你用的是 VS,可以用 Nuget 包管理器搜索 EntityFrameworkCore
安装这两个包。
4设计实体类
要使 ORM 框架能把数据表映射为 C# 对象,则需要把 C# 类设计成和数据库实体一致,EF Core 会按照约定俗成的规则(比如默认类的属性应对数据表的字段)自动处理。
我们把之前的 BookVM
类重命名为 Book
,属性基本保持不变,只是加上了主键和一些用于描述字段定义的特性:
using System.ComponentModel.DataAnnotations;
namespace Bookist.Web.Models;
public class Book
{
public int Id { get; set; }
[Required, MaxLength(100)]
public string Title { get; set; }
[Required, MaxLength(255)]
public string Cover { get; set; }
[MaxLength(100)]
public string Author { get; set; }
public DateOnly PubDate { get; set; }
[MaxLength(2000)]
public string Description { get; set; }
[Required, MaxLength(50)]
public string Format { get; set; }
[Required, MaxLength(100)]
public string FetchUrl { get; set; }
[MaxLength(10)]
public string FetchCode { get; set; }
}
这里的 MaxLength
将告诉 EF Core 为字符串类型的字段指定最大长度,比如 Title
是 varchar(100)
。
我们不需要编写任何 SQL 脚本,通过使用 EF 工具可以自动为我们生成脚本,并可自动执行到数据库,创建对应的 Book
数据表,下文将介绍如何操作。
5编写 DbContext
对于 EF Core 来说,一般一个数据库对应一个 DbContext
类,它对应数据库操作的上下文,所有的数据库操作都是在这个上下文中进行。
在 Bookist.Web/Models
目录中添加一个类,名为 BookistDbContext
,代码如下:
using Microsoft.EntityFrameworkCore;
namespace Bookist.Web.Models;
public class BookistDbContext : DbContext
{
public BookistDbContext(DbContextOptions<BookistDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Book>();
}
}
注意,自定义的 DbContext
类一定要和上面这样写一个带有 DbContextOptions<T>
参数的构造函数。
在 BookistDbContext
类中覆盖 OnModelCreating()
方法,在该方法中通过 builder.Entity<T>()
方法指定要映射的实体类。
DbContext
很强大,基本上所有的数据库定义和操作都可以通过它来完成,比如定义数据修改的勾子,后面我们还会经常接触这个类。
6配置 EF Core
要使我们的应用程序能连上数据库,自然少不了数据库连接。在 appsettings.json
中添加数据库连接字符串,如下:
{
"ConnectionStrings": {
"BookistConnection": "server=你的服务器IP;port=3306;database=bookist;uid=root;pwd=你的数据库密码"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
然后我们还需要注册 EF Core 服务,编辑 Program.cs
文件如下:
using Bookist.Web.Models;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;
// 注册服务
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<BookistDbContext>(opt =>
{
var conStr = config.GetConnectionString("BookistConnection");
opt.UseMySql(conStr, ServerVersion.Parse("8.0"));
});
var app = builder.Build();
// 注册中间件
app.UseStaticFiles();
app.MapDefaultControllerRoute();
app.Run();
这里通过 AddDbContext
方法注册了相关服务并指定了 BookistDbContext
的数据库连接字符串。
7使用 EF 工具构建数据库
.NET CLI 提供了 EF 相关的工具,代号为 dotnet-ef
。它可以帮助我们自动生成脚本、更新数据库结构等。第一次使用需要使用如下命令安装:
$ dotnet tool install --global dotnet-ef
可使用以下命令调用工具: dotnet-ef
已成功安装工具“dotnet-ef”(版本“6.0.0”)。
如果你已经安装过,也可以执行该命令将 dotnet-ef
升级到最新版本。
在 Bookist.Web
目录下,使用如下命令给当前实体类创建一个数据库变更版本:
$ dotnet ef migrations add 001
为了简单起见,我这里直接使用数字序号代表每次数据库的变更,你也可以使用更友好的描述性的词。执行该命令后,会在目录下生成一个 Migrations
文件夹和对应的数据库修改描述文件,*.cs
和 *.Designer.cs
。
第一次创建数据库,我们可以直接使用如下命令(不推荐这种方式):
$ dotnet ef database update
这个命令会自动生成数据库升级脚本,并自动执行。此时,我们的数据库已经创建好了,包括一个 Books
表。
但是直接自动执行升级脚本是非常不安全的。稍大点的开发团队是有专门的 DBA 来修改数据库结构的,一般我们需要把数据库升级脚本给到 DBA 去执行。而且,有时候数据库升级不仅仅是结构发生变化,也有可能数据也需要调整,比如删除一个字段的同时把这个字段的值移到另一个表,这时候我们需要增加手写脚本了。
所以推荐的做法是,使用如下命令先生成数据库升级脚本:
$ dotnet ef migrations script -o ./Scripts/001.sql
检查生成的脚本文件 001.sql
,确认无误后再把脚本复制到服务器上执行。
由于我们还没有做后台书籍管理的功能,所以暂时需要手动添加数据,执行如下脚本创建初始数据:
INSERT INTO `bookist`.`Book` (`Title`, `Cover`, `Author`, `PubDate`, `Description`, `Format`, `FetchUrl`, `FetchCode`)
VALUES ('CLR via C# (4th Edition)', 'https://img.geekgist.com/Foc1d-NbacAQ6D1WSQ_3UndhaOuR-w2h3', 'Jeffrey Richter', '2012-10-01', 'Dig deep and master the intricacies of the CLR, C#, and .NET development. You’ll gain pragmatic insights for building robust, reliable, and responsive apps and components.', 'PDF', 'https://url19.ctfile.com/f/15677019-228693113-e89578', 'bookist');
8从数据库读取数据
数据和 EF Core 配置都准备好了,现在只需要使用 EF Core 把数据从数据库读取出来展示到页面上就行。
原来我们是把数据硬编码到 Controller
中返回给 View
的,这次改为从数据库中读取,视图代码不动。这里就体现了 MVC 分层的优点,业务代码修改了不会影响到视图。
修改 HomeController
代码如下:
using Bookist.Web.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Bookist.Web.Controllers;
public class HomeController : Controller
{
private readonly BookistDbContext _context;
public HomeController(BookistDbContext context)
{
_context = context;
}
public async Task<ViewResult> Index()
{
var book = await _context.Set<Book>()
.OrderBy(x => x.Id).LastOrDefaultAsync();
return View(book);
}
}
运行起来,浏览器查看一下确保程序没有问题。到这,我们就成功使用 EF Core + MySQL 实现了数据持久化。
完整代码已同步更新到 GitHub:
https://github.com/liamwang/bookist.cc
9保护敏感信息
前文我们在 appsettings.json
中配置了数据库连接字符串,请千万不要把这种敏感信息提交到公开的 Git 仓库。
在 .NET 中,我们可以使用 user-secrets
工具来管理敏感信息,这个工具可以将敏感信息保存在一个 secrets.json
文件中,它不在项目文件夹下,而是存放在另外的地方。对于三种操作系统,它的位置是:
Windows: %APPDATA%/Microsoft/UserSecrets/<UserSecretsId>/secrets.json
Linux : ~/.microsoft/usersecrets/<UserSecretsId>/secrets.json
Mac : ~/.microsoft/usersecrets/<UserSecretsId>/secrets.json
一个 .NET 应用对应一个唯一的 UserSecretsId
,一般是一个 GUID。
现在我们要把数据库链接字符串存在项目之外的 secrets.json
文件中,避免它随项目代码一起提交到 Git 仓库。
首先使用如下命令初给我们的项目始化一个 UserSecretsId
:
$ dotnet user-secrets init
Set UserSecretsId to 'adfa2b88-1fe4-438e-b567-03296ac46b2d' for MSBuild project 'D:\bookist\Bookist.Web\Bookist.Web.csproj'.
打开 Bookist.Web.csproj
文件,可以看到生成的 UserSecretsId
:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>adfa2b88-1fe4-438e-b567-03296ac46b2d</UserSecretsId>
</PropertyGroup>
...
然后使用如下命令在 secrets.json
中设置我们想要保护的敏感信息,比如数据库连接字符串:
$ dotnet user-secrets set "ConnectionStrings:BookistConnection" "server=******;port=3306;database=bookist;uid=root;pwd=******"
Successfully saved ConnectionStrings:BookistConnection = server=******;port=3306;database=bookist;uid=root;pwd=****** to the secret store.
你可以使用 dotnet user-secrets list
命令查看已经存储的敏感信息列表。
如果你使用的是 VS,则操作 user-secrets
更简单。直接右击项目,在菜单中选择 Manage User Secrets
选项:
它会直接帮你创建好 secrets.json
文件,并自动打开,你直接编辑该文件就行,比使用命令行方便很多。
如果本机 secrets.json
和 appsettings.json
有相同的配置项,程序会优先读取前者的值。
对于 secrets.json
的所有配置项,最好也在 appsettings.json
保留相应的占位,比如数据库连接字符串:
{
"ConnectionStrings": {
"BookistConnection": "<here is your database connection string>"
}
}
在 appsettings.json
中的使用占位可以告诉其它开发者有这样一个配置,这对于开源项目十分有用。
10发布
和前两篇文章一样,执行发布命令生成发布文件并复制到服务器上,然后重启服务即可,这里不再赘述。
不一样的是,这次我们生成发布文件后,配置文件中并没有包含真实的连接字符串。为了使生产环境拥有独立的配置项(比如生产环境的连接字符串和本机不一样),避免每次替换把生产环境的配置文件覆盖掉,我们在生产环境中创建一个名为 appsettings.Production.json
的文件,其内容和 appsettings.josn
文件一致,只修改其中数据库连接字符串:
{
"ConnectionStrings": {
"BookistConnection": "server=localhost;database=bookist;uid=root;pwd=***"
}
}
生产环境运行的是 Release
模式,程序会优先读取 *.Production.json
配置文件。这个 appsettings.Production.json
文件只在生产服务器上有,所以即使我们复制全部文件替换到服务器也没有关系。
点击下方卡片关注DotNet NB
一起交流学习
▲ 点击上方卡片关注DotNet NB,一起交流学习
请在公众号后台