前置作業

  • 需安裝以下 Packages

    • Microsoft.EntityFrameworkCore
    • Microsoft.entityFrameworkCore.Design
    • Microsoft.EntityFrameworkCore.Tools
  • 設置好 []DbContext

    • 範例
    public class AppDbContext : DbContext
    {
        public DbSet<Reminder> Reminders { get; set; } = null!;
        public DbSet<User> Users { get; set; } = null!;
    
        public AppDbContext()
        {
        }
    
        protected override void OnConfiguring(DbContextOptionsBuilder options)
        {
            options.UseNpgsql("Host=localhost;Port=5432;Username=********;Password=********;Database=testdb");
        }
    }
    

若是實作的 DbContext 沒有合適的建構子,是無法進行 migration 的。

如果 DbContext 採用依賴注入的方式,且程式的 entry 又不在同一個專案底下,則需要覆寫 OnConfiguring() 來提供 Database 的連線資訊。

  • 建構好對應的 Entity,需包含至少一個帶有 Id 的 property。
    • 由於我們要進行持久化(persistance)的物件通常是聚合根(aggregate root),可透過繼承以下的抽象物件 Entity來統一 Id property。
    • 又聚合根通常又會作為 DomainEvent 的發起者,透過 EventBus 來進行推播。可參考 EventBus Middleware 的實作

migration 指令

進行 Migration 初始化

  • 以下指令會產生將 DbContext 遷移至 Database 的 migration codes。
  • -p 或是 -project 用以指定專案。
dotnet ef migrations add InitialMigration -p src/CleanWebApi.Infrastructure
  • --startup-project 可以指定入口專案,可適用於將 connectionString 以 DependencyInjection 的方式注入於入口專案的情形。
dotnet ef migrations add InitialCreate --startup-project src/CleanWebApi.Api --project src/CleanWebApi.Infrastructure

查詢 Migrations 清單

  • 以下指令可以查詢該專案中有哪些 migration。
dotnet ef migrations list  -p src/CleanWebApi.Infrastructure

執行 Migration

  • 以下指令會執行 Migration,實際在 Database 產生對應的 tables。
dotnet ef database update InitialMigration -p src/CleanWebApi.Infrastructure

移除 Migration

  • 在 Database 已進行 Migration 的情況下無法執行,必要時需要先將 Database 清除。
dotnet ef migrations remove -p src/CleanWebApi.Infrastructure

清除 Database

dotnet ef database update 0 -p src/CleanWebApi.Infrastructure

此指令會刪除 Database,若有重要資料,需先做好備份。

補充

抽象物件 Entity 實作

  • INotification 來自於 MediatR package,透過 Event-EventHandler 的方式來進行方法之間的解耦(decouple)。
public interface IDomainEvent : INotification { }

public abstract class Entity
{
    public Guid Id { get; private init; }

    protected readonly List<IDomainEvent> _domainEvents = new();

    public Entity(Guid id)
    {
        Id = id;
    }

    public List<IDomainEvent> PopDomainEvents()
    {
        var copy = _domainEvents.ToList();
        _domainEvents.Clear();

        return copy;
    }

    protected Entity() { }
}

EventBus Middleware 的實作,用來將聚合根的 DomainEvents 進行推播。

public class EventualConsistencyMiddleware
{
    public const string DomainEventsKey = "DomainEventsKey";

    private readonly RequestDelegate _next; 

    public EventualConsistencyMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, IPublisher publisher, AppDbContext dbContext)
    {
        var transaction = await dbContext.Database.BeginTransactionAsync();
        context.Response.OnCompleted(async () =>
        {
            try
            {
                if (context.Items.TryGetValue(DomainEventsKey, out var value) && value is Queue<IDomainEvent> domainEvents)
                {
                    while (domainEvents.TryDequeue(out var @event))
                    {
                        await publisher.Publish(@event);
                    }
                }

                await transaction.CommitAsync();
            }
            catch (Exception)
            {
            }
            finally
            {
                await transaction.DisposeAsync();
            }
        });

        await _next(context);
    }
}