需求

作为后端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

使用.NET 6开发TodoList应用(4)——引入数据存储的更多相关文章

  1. 使用.NET 6开发TodoList应用(3)——引入第三方日志库

    需求 在我们项目开发的过程中,使用.NET 6自带的日志系统有时是不能满足实际需求的,比如有的时候我们需要将日志输出到第三方平台上,最典型的应用就是在各种云平台上,为了集中管理日志和查询日志,通常会选 ...

  2. iOS开发UI篇—ios应用数据存储方式(XML属性列表-plist)

    iOS开发UI篇—ios应用数据存储方式(XML属性列表-plist) 一.ios应用常用的数据存储方式 1.plist(XML属性列表归档) 2.偏好设置 3.NSKeydeArchiver归档(存 ...

  3. iOS开发UI篇—ios应用数据存储方式(偏好设置)

    iOS开发UI篇—ios应用数据存储方式(偏好设置) 一.简单介绍 很多iOS应用都支持偏好设置,比如保存用户名.密码.字体大小等设置,iOS提供了一套标准的解决方案来为应用加入偏好设置功能 每个应用 ...

  4. iOS开发UI篇—ios应用数据存储方式(归档)

    iOS开发UI篇—ios应用数据存储方式(归档)  一.简单说明 在使用plist进行数据存储和读取,只适用于系统自带的一些常用类型才能用,且必须先获取路径相对麻烦: 偏好设置(将所有的东西都保存在同 ...

  5. iOS开发UI篇—ios应用数据存储方式(归档) :转发

    本文转发至:文顶顶http://www.cnblogs.com/wendingding/p/3775293.html iOS开发UI篇—ios应用数据存储方式(归档)  一.简单说明 在使用plist ...

  6. Android开发之利用SQLite进行数据存储

    Android开发之利用SQLite进行数据存储 Android开发之利用SQLite进行数据存储 SQLite数据库简单介绍 Android中怎样使用SQLite 1 创建SQLiteOpenHel ...

  7. 使用.NET 6开发TodoList应用(17)——实现数据塑形

    系列导航及源代码 使用.NET 6开发TodoList应用文章索引 需求 在查询的场景中,还有一类需求不是很常见,就是在前端请求中指定返回的字段,所以关于搜索的最后一个主题我们就来演示一下关于数据塑形 ...

  8. Microsoft IoT Starter Kit 开发初体验-反馈控制与数据存储

    在上一篇文章<Microsoft IoT Starter Kit 开发初体验>中,讲述了微软中国发布的Microsoft IoT Starter Kit所包含的硬件介绍.开发环境搭建.硬件 ...

  9. Android开发7:简单的数据存储(使用SharedPreferences)和文件操作

    前言 啦啦啦~大家好,又见面啦~ 本篇博文讲和大家一起完成一个需要注册.登录的备忘录的,一起学习 SharedPreferences 的基本使用,学习 Android 中常见的文件操作方法,复习 An ...

随机推荐

  1. 【Azure 应用服务】App Service 无法连接到Azure MySQL服务,报错:com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

    问题描述 App Service使用jdbc连接MySQL服务,出现大量的  Communications link failure: com.mysql.cj.jdbc.exceptions.Com ...

  2. 面试官问我JVM调优,我忍不住了!

    面试官:今天要不来聊聊JVM调优相关的吧? 面试官:你曾经在生产环境下有过调优JVM的经历吗? 候选者:没有 面试官:... 候选者:嗯...是这样的,我们一般优化系统的思路是这样的 候选者:1. 一 ...

  3. BehaviorTree.CPP行为树BT的队列节点(三)

    Sequences(队列) 只要序列的所有子代返回SUCCESS,它便会对其进行Tick. 如果有任何子级返回FAILURE,则序列中止. 当前,该框架提供三种节点: Sequence Sequenc ...

  4. APIO2020 打铁记

    Day (-3) - 2020.8.11 马上要 APIO 了,不管三七二十一先刷一套历年的 APIO 再说. 花了 3h 写了 APIO2019,爆零150左右,然后查看了一下去年的分数线,Cu 1 ...

  5. Git 使用,本地项目上传到GitHub远程库

    Git 使用,本地项目上传到GitHub远程库 环境 GitHub账号 点此进入github官网 git客户端工具 点此进入git下载页 本地项目上传到 GitHub 在GitHub中创建一个仓库(远 ...

  6. Bebug与Release版本

    如果调试过程无调试信息,检查编译选项是否切换到了release下 比如Cfree5等编译器 ms为了方便调试才诞生了DEBUG版. 这也导致了MFC有两个功能一至但版本不同的类库,一个为DEBUG版, ...

  7. 简易kmeans-c++版本

    typedef double dtype; 主要接口: void Kmeans(const vector<vector<dtype> > &d,int k,string ...

  8. Deep Learning(深度学习)整理,RNN,CNN,BP

     申明:本文非笔者原创,原文转载自:http://www.sigvc.org/bbs/thread-2187-1-3.html 4.2.初级(浅层)特征表示 既然像素级的特征表示方法没有作用,那怎 ...

  9. KEPServeEX 6与KepOPC中间件测试

    KEPServeEX 6可以组态服务器端和客户端连接很多PLC以及具有OPC服务器的设备,以下使用KEPServeEX 6建立一个OPC UA服务器,然后使用KepOPC建立客户端来连接服务器做测试. ...

  10. 12 — springboot集成JPA — 更新完毕

    1.什么是jpa? 一堆不想整在这博客里面的理论知识.这些理论玩意儿就应该自行领悟到自己脑海里 1).JPA & Spring Data JPA 1.1).JPA JPA是Java Persi ...