使用.NET 6開發TodoList應用之引入數據存儲的思路詳解
需求
作為後端CRUD程序員(bushi,數據存儲是開發後端服務一個非常重要的組件。對我們的TodoList
項目來說,自然也需要配置數據存儲。目前的需求很簡單:
- 需要能持久化
TodoList
對象並對其進行操作; - 需要能持久化
TodoItem
對象並對其進行操作;
問題是,我們打算如何存儲數據?
存儲組件的選擇非常多:以MSSQL Server/Postgres/MySql/SQLite等為代表的關系型數據庫,以MongoDB/ElasticSearch等為代表的非關系型數據庫,除此之外,我們還可以在開發階段選擇內存數據庫,在雲上部署的時候還可以選擇類似Azure Cosmos DB
/AWS DynamoDB
以及雲上提供的多種關系型數據庫。
應用程序使用數據庫服務,一般都是借助成熟的第三方ORM框架,而在.NET後端服務開發的過程中,使用的最多的兩個ORM框架應該是:EntityFrameworkCore和Dapper,相比之下,EFCore的使用率更高一些。所以我們也選擇EFCore來進行演示。
目標
在這篇文章中,我們僅討論如何實現數據存儲基礎設施的引入,具體的實體定義和操作後面專門來說。
- 使用MSSQL Server容器作為數據存儲組件(前提是電腦上需要安裝Docker環境,下載並安裝Docker Desktop即可);
這樣選擇的理由也很簡單,對於使用Mac的小夥伴來說,使用容器來啟動MSSQL Server可以避免因為非Windows平臺導致的示例無法運行的問題。
原理和思路
因為我們對開發環境和生產環境的配置有差異,那先來看看共性的部分:
- 引入EFCore相關的nuget包並進行配置;
- 添加DbContext對象並進行依賴註入;
- 修改相關appsettings.{environment}.json文件;
- 主程序配置。
- 本地運行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等情況,擴展閱讀如下,在這裡就不做進一步的演示瞭,也許以後有機會可以單獨寫篇實踐指南:
使用多個提供程序進行遷移
使用單獨的遷移項目
參考資料
EntityFrameworkCore
使用多個提供程序進行遷移
使用單獨的遷移項目
到此這篇關於使用.NET 6開發TodoList應用之引入數據存儲的文章就介紹到這瞭,更多相關.NET 6開發TodoList引入數據存儲內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- .NET 6開發TodoList應用引入數據存儲
- Docker部署SQL Server及最佳應用小結
- .NET 6開發TodoList應用實現結構搭建
- 使用docker創建和運行跨平臺的容器化mssql數據庫
- .NET 6開發TodoList應用引入第三方日志庫