今天就繼續完成 Todo Service 的 ORM,到時候所有資料都會落在 Database 中。
這裡用到的還是 Day 06 - DDD Tactical Design 所設計的 SQL Script,需要注意的是這裡有一些小改動,我們這裡不能使用 FOREIGN KEY。
先用 Database Client 連線到 1434
port 的 SQL Server,執行下面的 Script:
-- 創建 db_todo 資料庫
CREATE DATABASE db_todo;
-- 切換到 db_todo 資料庫
USE db_todo;
-- 創建 tb_todo_list 資料表
CREATE TABLE [dbo].[tb_todo_list] (
[id] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
[name] NVARCHAR(100) NOT NULL,
[description] NVARCHAR(255) NOT NULL,
[status] NVARCHAR(50) NOT NULL CHECK (status IN ('active', 'removed')),
[user_id] UNIQUEIDENTIFIER NOT NULL,
[created_date_time] DATETIME2 NOT NULL,
[updated_date_time] DATETIME2 NOT NULL,
-- FOREIGN KEY (user_id) REFERENCES [dbo].[tb_user](id) --因為我們拆分成微服務 記得拿掉 Domain 以外的 FK
);
-- 創建 tb_todo_item 資料表
CREATE TABLE [dbo].[tb_todo_item] (
[id] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
[content] NVARCHAR(255) NOT NULL,
[status] NVARCHAR(50) NOT NULL CHECK (status IN ('todo', 'finished', 'removed')),
-- [color] NVARCHAR(7) NOT NULL, -- 因為先前設計 Color 就在 Application Layer 自動跟 Status 轉換,故這裡不存
[list_id] UNIQUEIDENTIFIER NOT NULL,
[created_date_time] DATETIME2 NOT NULL,
[updated_date_time] DATETIME2 NOT NULL,
-- FOREIGN KEY (list_id) REFERENCES [dbo].[tb_todo_list](id) -- 這裡的 FK 必須拿掉,因為我們把 List 和 Item 當作兩個 Aggregates 來使用,Aggregates 之間的操作必須互不影響。
);
結果
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
因為先前已經在 Day 16 - Account Service 實作 Infrastructure
介紹過 DbContext 和 Configuration 的實作,這邊就加快速度給結果吧!
using System.Reflection;
using Common.Library.Seedwork;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Todo.Domain.Aggregates;
namespace Todo.Infrastructure;
public class TodoContext : DbContext, IUnitOfWork
{
private const string DEFAULT_CONNECTION_SECTION = "DefaultConnection";
private readonly IConfiguration _configuration;
public DbSet<TodoList> TodoLists { get; set; }
public DbSet<TodoItem> TodoItems{ get; set; }
public TodoContext(DbContextOptions<TodoContext> options, IConfiguration configuration) : base(options)
{
_configuration = configuration;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer(_configuration.GetConnectionString(DEFAULT_CONNECTION_SECTION));
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
}
public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default)
{
try
{
await base.SaveChangesAsync(cancellationToken);
return true;
}
catch (Exception)
{
return false;
}
}
}
TodoListConfiguration
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Todo.Domain.Aggregates;
using Todo.Domain.ValueObjects;
using Todo.Domain.ValueObjects.Enums;
namespace Todo.Infrastructure;
public class TodoListConfiguration : IEntityTypeConfiguration<TodoList>
{
public void Configure(EntityTypeBuilder<TodoList> builder)
{
builder.ToTable("tb_todo_list");
builder.HasKey(t => t.Id);
builder.Property(t => t.Id)
.HasColumnName("id")
.ValueGeneratedNever()
.HasConversion(
id => id.Value,
value => TodoListId.Create(value))
.IsRequired();
builder.Property(t => t.Name)
.HasColumnName("name")
.HasMaxLength(100)
.IsRequired();
builder.Property(t => t.Description)
.HasColumnName("description")
.HasMaxLength(255)
.IsRequired();
builder.Property(t => t.Status)
.HasColumnName("status")
.HasConversion(
s => s.ToString().ToLower(), // Enum 轉字串
s => (TodoListStatus)Enum.Parse(typeof(TodoListStatus), s, true)) // 字串轉 Enum
.IsRequired();
builder.Property(t => t.UserId)
.HasColumnName("user_id")
.IsRequired();
builder.Property(t => t.CreatedDateTime)
.HasColumnName("created_date_time")
.IsRequired();
builder.Property(t => t.UpdatedDateTime)
.HasColumnName("updated_date_time")
.IsRequired();
builder.Ignore(t => t.TodoItemIds);
}
}
TodoItemConfiguration
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Todo.Domain.Aggregates;
using Todo.Domain.ValueObjects;
using Todo.Domain.ValueObjects.Enums;
namespace Todo.Infrastructure;
public class TodoItemConfiguration : IEntityTypeConfiguration<TodoItem>
{
public void Configure(EntityTypeBuilder<TodoItem> builder)
{
builder.ToTable("tb_todo_item");
builder.HasKey(t => t.Id);
builder.Property(t => t.Id)
.HasColumnName("id")
.ValueGeneratedNever()
.HasConversion(
id => id.Value,
value => TodoItemId.Create(value))
.IsRequired();
builder.Property(t => t.Content)
.HasColumnName("content")
.HasMaxLength(255)
.IsRequired();
builder.Property(t => t.Status)
.HasColumnName("status")
.HasConversion(
v => v.State.ToString().ToLower(), // Enum 狀態轉字串
v => new TodoItemStatus((TodoItemState)Enum.Parse(typeof(TodoItemState), v, true))) // 字串轉 Enum 狀態
.IsRequired();
builder.Property(t => t.ListId)
.HasColumnName("list_id")
.IsRequired();
builder.Property(t => t.CreatedDateTime)
.HasColumnName("created_date_time")
.IsRequired();
builder.Property(t => t.UpdatedDateTime)
.HasColumnName("updated_date_time")
.IsRequired();
}
}
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost,1434;Database=db_todo;User Id=sa;Password=Passw0rd!;TrustServerCertificate=True"
}
}
把本來在記憶體的 _todoLists
和 _todoItems
置換成我們 EF 的 DbSet,並回傳 UnitOfWork。
這邊要記得 GetByGuid 的時候要使用 ObjectValue 來做比較哦!
using Common.Library.Seedwork;
using Todo.Application;
using Todo.Domain.Aggregates;
namespace Todo.Infrastructure;
public class TodoListRepository : ITodoListRepository
{
private readonly TodoContext _todoContext;
public TodoListRepository(TodoContext todoContext)
{
this._todoContext = todoContext;
}
public IUnitOfWork UnitOfWork => _todoContext;
public void Add(TodoList list)
{
_todoContext.TodoLists.Add(list);
}
public TodoList? GetByGuid(Guid guid)
{
return _todoContext.TodoLists.SingleOrDefault(x => x.Id == TodoListId.Create(guid));
}
}
using Common.Library.Seedwork;
using Todo.Application;
using Todo.Domain.Aggregates;
namespace Todo.Infrastructure;
public class TodoItemRepository : ITodoItemRepository
{
private readonly TodoContext _todoContext;
public TodoItemRepository(TodoContext todoContext)
{
this._todoContext = todoContext;
}
public IUnitOfWork UnitOfWork => _todoContext;
public void Add(TodoItem item)
{
_todoContext.TodoItems.Add(item);
}
public TodoItem? GetByGuid(Guid guid)
{
return _todoContext.TodoItems.SingleOrDefault(x => x.Id == TodoItemId.Create(guid));
}
}
記得對 Repository 的操作結束後都要完成 Transaction。
await _todoListRepository.UnitOfWork.SaveEntitiesAsync();
await _todoItemRepository.UnitOfWork.SaveEntitiesAsync();
using Microsoft.Extensions.DependencyInjection;
namespace Todo.Infrastructure;
public static class TodoInfrastructureRegister
{
public static IServiceCollection AddTodoInfrastructure(this IServiceCollection services)
{
services.AddScoped<ITodoListRepository, TodoListRepository>();
services.AddScoped<ITodoItemRepository, TodoItemRepository>();
services.AddDbContext<TodoContext>();
return services;
}
}
確認真的有作用在 Database 內
跟 Account 和 Todo Services 之間高度重複的 Coding 終於告一段落,下一章節要開始重點介紹 Event 的運作了,附上到目前為止的專案結構圖。