iT邦幫忙

2024 iThome 鐵人賽

DAY 20
0

前言

今天就繼續完成 Todo Service 的 ORM,到時候所有資料都會落在 Database 中。

實作 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 之間的操作必須互不影響。
);

結果

https://ithelp.ithome.com.tw/upload/images/20241002/20168953fv5BD9Skyv.png

安裝相依套件

    <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" />

建立 DbContext

因為先前已經在 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;
        }
    }
}

建立 EntityTypeConfigurations

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"
  }
}

置換 ORM

把本來在記憶體的 _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));
    }
}

Unit Of Work

記得對 Repository 的操作結束後都要完成 Transaction。

await _todoListRepository.UnitOfWork.SaveEntitiesAsync();
await _todoItemRepository.UnitOfWork.SaveEntitiesAsync();

DI TodoContext

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 內

Todo List Create / Remove

https://ithelp.ithome.com.tw/upload/images/20241003/2016895348nDHwWciK.png

Todo Item Create / Finish / Remove

https://ithelp.ithome.com.tw/upload/images/20241003/20168953MZH6VGXTjl.png

總結

跟 Account 和 Todo Services 之間高度重複的 Coding 終於告一段落,下一章節要開始重點介紹 Event 的運作了,附上到目前為止的專案結構圖。

https://ithelp.ithome.com.tw/upload/images/20241003/20168953oQbjcMd9tb.png


上一篇
Day 19 - Todo Infrastructure 實作:Repository Pattern 介紹
下一篇
Day 21 - Mediator Pattern - MediatR 和 Domain Event 實作
系列文
DDD? Clean Architecture? Microservices? 帶你用.NET實作打造一個現代化微服務!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言