需求

作为后端CRUD程序员(bushi,数据存储是开发后端服务一个非常重要的组件。对我们的TodoList项目来说,自然也需要配置数据存储。目前的需求很简单:

  1. 需要能持久化TodoList对象并对其进行操作;
  2. 需要能持久化TodoItem对象并对其进行操作;

问题是,我们打算如何存储数据?

存储组件的选择非常多:以MSSQL Server/Postgres/MySql/SQLite等为代表的关系型数据库,以MongoDB/ElasticSearch等为代表的非关系型数据库,除此之外,我们还可以在开发阶段选择内存数据库,在云上部署的时候还可以选择类似Azure Cosmos DB/AWS DynamoDB以及云上提供的多种关系型数据库。

应用程序使用数据库服务,一般都是借助成熟的第三方ORM框架,而在.NET后端服务开发的过程中,使用的最多的两个ORM框架应该是:EntityFrameworkCoreDapper,相比之下,EFCore的使用率更高一些。所以我们也选择EFCore来进行演示。

目标

在这篇文章中,我们仅讨论如何实现数据存储基础设施的引入,具体的实体定义和操作后面专门来说。

  • 使用MSSQL Server容器作为数据存储组件(前提是电脑上需要安装Docker环境,下载并安装Docker Desktop即可);

这样选择的理由也很简单,对于使用Mac的小伙伴来说,使用容器来启动MSSQL Server可以避免因为非Windows平台导致的示例无法运行的问题。

原理和思路

因为我们对开发环境和生产环境的配置有差异,那先来看看共性的部分:

  1. 引入EFCore相关的nuget包并进行配置;
  2. 添加DbContext对象并进行依赖注入;
  3. 修改相关appsettings.{environment}.json文件;
  4. 主程序配置。
  5. 本地运行MSSQL Server容器并实现数据持久化;

同上一篇一样,和具体的第三方对接的逻辑我们还是放到Infrastructure里面去,应用程序中只保留对外部服务的抽象操作。

实现

1. 引入Nuget包并进行配置

需要在Infrastructure项目中引入以下Nuget包:

Microsoft.EntityFrameworkCore.SqlServer

# 第二个包是用于使用PowerShell命令(Add-Migration/Update-Database/...)需要的,如果使用eftool,可以不安装这个包。
Microsoft.EntityFrameworkCore.Tools

为了使用eftool,需要在Api项目中引入以下Nuget包:

Microsoft.EntityFrameworkCore.Design

2. 添加DBContext对象并进行配置

在这一步里,我们要添加的是一个具体的DBContext对象,这对于有经验的开发者来说并不是很难的任务。但是在具体实现之前,我们可以花一点时间考虑一下现在的Clean Architecture结构:我们的目的是希望除了Infrastructure知道具体交互的第三方是什么,在Application以及Domain里都要屏蔽底层的具体实现。换言之就是需要在Infrastrcuture之外的项目中使用接口来做具体实现的抽象,那么我们在Application中新建一个Common/Interfaces文件夹用于存放应用程序定义的抽象接口IApplicationDbContext

namespace TodoList.Application.Common.Interfaces;

public interface IApplicationDbContext
{
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}

接下来在Infrastructure项目中新建Persistence文件夹用来存放和数据持久化相关的具体逻辑,我们在其中定义DbContext对象并实现刚才定义的接口。

using Microsoft.EntityFrameworkCore;
using TodoList.Application.Common.Interfaces; namespace TodoList.Infrastructure.Persistence; public class TodoListDbContext : DbContext, IApplicationDbContext
{
public TodoListDbContext(DbContextOptions<TodoListDbContext> options) : base(options)
{
}
}

这里的处理方式可能会引起困惑,这个IApplicationDbContext存在的意义是什么。这里的疑问和关于要不要使用Repository模式有关,国外多位大佬讨论过这个问题,即Repository是不是必须的。可以简单理解大家达成的共识是:不是必须的,如果不是有某些特别的数据库访问逻辑,或者有足够的理由需要使用Repository模式,那就保持架构上的简洁,在Application层的多个CQRS Handlers中直接注入该IApplicationDbContext去访问数据库并进行操作。如果需要使用Repository模式,那在这里就没有必要定义这个接口来使用了,Application中只需要定义IRepository<T>,在Infrastructure中实现的BaseRepository中访问DbContext即可。

我们后面是需要使用Repository的,是因为希望演示最常用的开发模式,但是在这一篇中我保留IApplicationDbConetxt的原因是也希望展示一种不同的实现风格,后面我们还是会专注到Repository上的。

需要的对象添加好了,下一步是配置DbContext,我们还是遵循当前的架构风格,在Infrastructure项目中添加DependencyInjection.cs文件用于添加所有第三方的依赖:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using TodoList.Application.Common.Interfaces;
using TodoList.Infrastructure.Persistence; namespace TodoList.Infrastructure; public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<TodoListDbContext>(options =>
options.UseSqlServer(
configuration.GetConnectionString("SqlServerConnection"),
b => b.MigrationsAssembly(typeof(TodoListDbContext).Assembly.FullName))); services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<TodoListDbContext>()); return services;
}
}

3. 配置文件修改

我们对appsettings.Development.json文件进行配置:

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"UseFileToLog": true,
"ConnectionStrings": {
"SqlServerConnection": "Server=localhost,1433;Database=TodoListDb;User Id=sa;Password=StrongPwd123;"
}
}

这里需要说明的是如果是使用MSSQL Server默认端口1433的话,连接字符串里是可以不写的,但是为了展示如果使用的不是默认端口应该如何配置,还是显式写在这里了供大家参考。

4. 主程序配置

Api项目中,我们只需要调用上面写好的扩展方法,就可以完成配置。

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.ConfigureLog(); builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); // 添加基础设施配置
builder.Services.AddInfrastructure(builder.Configuration); // 省略以下...

5. 本地运行MSSQL Server容器及数据持久化

在保证本地Docker环境正常启动之后,运行以下命令:

# 拉取mssql镜像
$ docker pull mcr.microsoft.com/mssql/server:2019-latest
2019-latest: Pulling from mssql/server
7b1a6ab2e44d: Already exists
4ffe416cf537: Pull complete
fff1d174f64f: Pull complete
3588fd79aff7: Pull complete
c8203457909f: Pull complete
Digest: sha256:a098c9ff6fbb8e1c9608ad7511fa42dba8d22e0d50b48302761717840ccc26af
Status: Downloaded newer image for mcr.microsoft.com/mssql/server:2019-latest
mcr.microsoft.com/mssql/server:2019-latest # 创建持久化存储
$ docker create -v /var/opt/mssql --name mssqldata mcr.microsoft.com/mssql/server:2019-latest /bin/true
3c144419db7fba26398aa45f77891b00a3253c23e9a1d03e193a3cf523c66ce1 # 运行mssql容器,挂载持久化存储卷
$ docker run -d --volumes-from mssqldata --name mssql -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=StrongPwd123' -p 1433:1433 mcr.microsoft.com/mssql/server:2019-latest
d99d774f70229f688d71fd13e90165f15abc492aacec48de287d348e047a055e # 确认容器运行状态
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d99d774f7022 mcr.microsoft.com/mssql/server:2019-latest "/opt/mssql/bin/perm…" 24 seconds ago Up 22 seconds 0.0.0.0:1433->1433/tcp mssql

验证

为了验证我们是否可以顺利连接到数据库,我们采用添加Migration并在程序启动时自动进行数据库的Migration方式进行:

首先安装工具:

dotnet tool install --global dotnet-ef
# dotnet tool update --global dotnet-ef # 生成Migration
$ dotnet ef migrations add SetupDb -p src/TodoList.Infrastructure/TodoList.Infrastructure.csproj -s src/TodoList.Api/TodoList.Api.csproj
Build started...
Build succeeded.
[17:29:15 INF] Entity Framework Core 6.0.1 initialized 'TodoListDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.1' with options: MigrationsAssembly=TodoList.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Done. To undo this action, use 'ef migrations remove'

为了在程序启动时进行自动Migration,我们向Infrastructure项目中增加一个文件ApplicationStartupExtensions.cs并实现扩展方法:

using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TodoList.Infrastructure.Persistence; namespace TodoList.Infrastructure; public static class ApplicationStartupExtensions
{
public static void MigrateDatabase(this WebApplication app)
{
using var scope = app.Services.CreateScope();
var services = scope.ServiceProvider; try
{
var context = services.GetRequiredService<TodoListDbContext>();
context.Database.Migrate();
}
catch (Exception ex)
{
throw new Exception($"An error occurred migrating the DB: {ex.Message}");
}
}
}

并在Api项目的Program.cs中调用扩展方法:

// 省略以上...
app.MapControllers(); // 调用扩展方法
app.MigrateDatabase(); app.Run();

最后运行主程序:

$ dotnet run --project src/TodoList.Api
Building...
[17:32:32 INF] Entity Framework Core 6.0.1 initialized 'TodoListDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.1' with options: MigrationsAssembly=TodoList.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
[17:32:32 INF] Executed DbCommand (22ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT 1
[17:32:32 INF] Executed DbCommand (19ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT OBJECT_ID(N'[__EFMigrationsHistory]');
[17:32:32 INF] Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT 1
[17:32:32 INF] Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT OBJECT_ID(N'[__EFMigrationsHistory]');
[17:32:33 INF] Executed DbCommand (4ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [MigrationId], [ProductVersion]
FROM [__EFMigrationsHistory]
ORDER BY [MigrationId];
[17:32:33 INF] Applying migration '20211220092915_SetupDb'.
[17:32:33 INF] Executed DbCommand (4ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20211220092915_SetupDb', N'6.0.1');
[17:32:33 INF] Now listening on: https://localhost:7039
[17:32:33 INF] Now listening on: http://localhost:5050
[17:32:33 INF] Application started. Press Ctrl+C to shut down.
[17:32:33 INF] Hosting environment: Development
[17:32:33 INF] Content root path: /Users/yu.li1/Projects/asinta/blogs/cnblogs/TodoList/src/TodoList.Api/

使用数据库工具连接容器数据库,可以看到Migration已经成功地写入数据库表__EFMigrationsHistory了:

本篇文章仅完成了数据存储服务的配置工作,目前还没有添加任何实体对象和数据库表定义,所以暂时没有可视化的验证,仅我们可以运行程序看我们的配置是否成功:

总结

在本文中,我们探讨并实现了如何给.NET 6 Web API项目添加数据存储服务并进行配置,下一篇开始将会深入数据存储部分,定义实体,构建Repository模式和SeedData等操作。

除了本文演示的最基础的使用方式意外,在实际使用的过程中,我们可能会遇到类似:为多个DbContext分别生成Migrations或者为同一个DbContext根据不同的环境生成不同Database Provider适用的Migrations等情况,扩展阅读如下,在这里就不做进一步的演示了,也许以后有机会可以单独写篇实践指南:

参考资料

  1. EntityFrameworkCore
  2. 使用多个提供程序进行迁移
  3. 使用单独的迁移项目

系列导航

  • 使用.NET 6开发TodoList应用(1)——系列背景
  • 使用.NET 6开发TodoList应用(2)——项目结构搭建
  • 使用.NET 6开发TodoList应用(3)——引入第三方日志
  • 使用.NET 6开发TodoList应用(4)——引入数据存储
  • 使用.NET 6开发TodoList应用(5.0)——领域实体创建和配置
  • 使用.NET 6开发TodoList应用(5.1)——实现CQRS模式
  • 使用.NET 6开发TodoList应用(5.2)——实现AutoMapper
  • 使用.NET 6开发TodoList应用(6)——实现POST请求
  • 使用.NET 6开发TodoList应用(7)——实现GET请求
  • 使用.NET 6开发TodoList应用(8)——实现全局异常处理
  • 使用.NET 6开发TodoList应用(9)——实现PUT请求
  • 使用.NET 6开发TodoList应用(10)——实现PATCH请求
  • 使用.NET 6开发TodoList应用(11)——HTTP请求幂等性的考虑
  • 使用.NET 6开发TodoList应用(12)——实现接口请求验证
  • 使用.NET 6开发TodoList应用(13)——实现ActionFilter
  • 使用.NET 6开发TodoList应用(14)——实现查询分页
  • 使用.NET 6开发TodoList应用(15)——实现查询过滤
  • 使用.NET 6开发TodoList应用(16)——实现查询搜索
  • 使用.NET 6开发TodoList应用(17)——实现查询排序
  • 使用.NET 6开发TodoList应用(18)——实现数据塑形
  • 使用.NET 6开发TodoList应用(19)——实现HATEAOS支持
  • 使用.NET 6开发TodoList应用(20)——处理OPTION和HEAD请求
  • 使用.NET 6开发TodoList应用(21)——实现Root Document
  • 使用.NET 6开发TodoList应用(22)——实现API版本控制
  • 使用.NET 6开发TodoList应用(23)——实现缓存
  • 使用.NET 6开发TodoList应用(24)——实现请求限流和阈值控制
  • 使用.NET 6开发TodoList应用(25)——实现基于JWT的Identity功能
  • 使用.NET 6开发TodoList应用(26)——实现RefreshToken
  • 使用.NET 6开发TodoList应用(27)——实现Configuration和Option的强类型绑定
  • 使用.NET 6开发TodoList应用(28)——实现API的Swagger文档化
  • 使用.NET 6开发TodoList应用(29)——实现应用程序健康检查
  • 使用.NET 6开发TodoList应用(30)——实现本地化功能
  • 使用.NET 6开发TodoList应用(31)——实现Docker打包和部署
  • 使用.NET 6开发TodoList应用(32)——实现基于Github Actions和ACI的CI/CD

最新文章

  1. IOS开发之显示微博表情
  2. NTDLL未文档化函数RtlGetNtVersionNumbers获取操作系统版本
  3. el captain设置环境变量
  4. 【BZOJ-1040】骑士 树形DP + 环套树 + DFS
  5. bug:clang: error: no input files
  6. C#泛型-小心使用静态成员变量
  7. Java中ArrayDeque,栈与队列
  8. bt 介绍以及 bt 种子的hash值(特征值)计算
  9. WEB编码事项
  10. 2014最热门、最具争议的10个Java话题
  11. C#入门经典-第15章ListBox,CheckedListBox
  12. JSP页面中&lt;%!%&gt;与&lt;%%&gt;与&lt;%=%&gt;
  13. 微信中通过页面(H5)直接打开本地app的解决方案
  14. LINUX下分区命令Parted详解
  15. Django自定义模板标签和过滤器
  16. vue2.0之axios使用详解
  17. idea 去除xml文件sql语句背景色
  18. Django分页(二)
  19. SpringMVC框架01——使用IDEA搭建SpringMVC环境
  20. Centos7更改yum镜像源

热门文章

  1. Study Blazor .NET(二)安装
  2. 同时在多个 Git 分支上工作,老板要榨干我
  3. CF1539F Strange Array
  4. MYSQL5.8-----3
  5. URI和URL的区别(转)
  6. Excel-vlookup内部能不能用函数?(即内部嵌套函数)
  7. Leetcode中的SQL题目练习(一)
  8. Mybatis相关知识点(二)
  9. docker之镜像制作
  10. When does compiler create default and copy constructors in C++?