Files
pos-system/microservices/.agent/skills/event-sourcing/SKILL.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

16 KiB

name, description, compatibility, metadata
name description compatibility metadata
event-sourcing Event Sourcing pattern - lưu trữ thay đổi trạng thái dưới dạng chuỗi sự kiện. Use for audit trails, temporal queries, CQRS integration, và event replay. .NET 10+, EventStoreDB, Marten, EF Core
author version
Velik Ho 1.0

Event Sourcing / Event Sourcing Pattern

Event Sourcing pattern cho GoodGo microservices - lưu trữ mọi thay đổi trạng thái dưới dạng sự kiện bất biến.

When to Use This Skill / Khi Nào Sử Dụng

Use this skill when:

  • Building audit trails / Xây dựng audit trails
  • Implementing temporal queries (time travel) / Truy vấn theo thời gian
  • Debugging production issues / Debug lỗi production
  • CQRS with event-driven projections / CQRS với projections
  • Ensuring data consistency / Đảm bảo tính nhất quán dữ liệu

Core Concepts / Khái Niệm Cốt Lõi

Traditional vs Event Sourcing / Truyền Thống vs Event Sourcing

┌─────────────────────────────────────────────────────────────┐
│                   TRADITIONAL (State-Based)                  │
├─────────────────────────────────────────────────────────────┤
│  Order Table                                                 │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ Id: 1, Status: Shipped, Amount: 500, Updated: 10:30 │    │
│  └─────────────────────────────────────────────────────┘    │
│  ❌ Lost: Why was it shipped? What was previous status?    │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                   EVENT SOURCING (Event-Based)               │
├─────────────────────────────────────────────────────────────┤
│  Event Stream: Order-1                                       │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ 1. OrderCreated { Amount: 500, UserId: "u1" } @10:00│    │
│  │ 2. OrderPaid { PaymentId: "p1" } @10:15             │    │
│  │ 3. OrderShipped { TrackingNo: "TN123" } @10:30      │    │
│  └─────────────────────────────────────────────────────┘    │
│  ✅ Full history preserved                                  │
└─────────────────────────────────────────────────────────────┘

Key Components / Thành Phần Chính

Component Purpose Description
Event Immutable fact Sự kiện đã xảy ra, không thể thay đổi
Event Stream Ordered sequence Chuỗi events theo thời gian cho mỗi aggregate
Event Store Append-only log Database lưu trữ events
Projection Read model View được xây dựng từ events
Snapshot State cache Cache trạng thái để tối ưu replay

Event Sourcing Flow / Luồng Event Sourcing

┌─────────┐    ┌─────────────┐    ┌─────────────┐
│ Command │───▶│  Aggregate  │───▶│   Events    │
└─────────┘    │  (Domain)   │    │  (Facts)    │
               └─────────────┘    └──────┬──────┘
                                         │
               ┌─────────────────────────┼─────────────────────────┐
               │                         │                         │
               ▼                         ▼                         ▼
        ┌─────────────┐          ┌─────────────┐          ┌─────────────┐
        │ Event Store │          │  Projection │          │  Snapshot   │
        │ (Write)     │          │  (Read)     │          │  (Cache)    │
        └─────────────┘          └─────────────┘          └─────────────┘

Key Patterns / Mẫu Chính

Domain Event Definition / Định Nghĩa Domain Event

/// <summary>
/// EN: Base interface for all domain events.
/// VI: Interface cơ sở cho tất cả domain events.
/// </summary>
public interface IDomainEvent
{
    Guid EventId { get; }
    DateTime OccurredOn { get; }
    int Version { get; }
}

/// <summary>
/// EN: Order created event.
/// VI: Event tạo order.
/// </summary>
public record OrderCreated : IDomainEvent
{
    public Guid EventId { get; init; } = Guid.NewGuid();
    public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
    public int Version { get; init; }
    
    public Guid OrderId { get; init; }
    public string UserId { get; init; } = default!;
    public decimal TotalAmount { get; init; }
    public Address ShippingAddress { get; init; } = default!;
}

/// <summary>
/// EN: Order item added event.
/// VI: Event thêm item vào order.
/// </summary>
public record OrderItemAdded : IDomainEvent
{
    public Guid EventId { get; init; } = Guid.NewGuid();
    public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
    public int Version { get; init; }
    
    public Guid OrderId { get; init; }
    public Guid ProductId { get; init; }
    public int Quantity { get; init; }
    public decimal UnitPrice { get; init; }
}

Event-Sourced Aggregate / Aggregate Event-Sourced

/// <summary>
/// EN: Base class for event-sourced aggregates.
/// VI: Lớp cơ sở cho aggregate event-sourced.
/// </summary>
public abstract class EventSourcedAggregate
{
    private readonly List<IDomainEvent> _uncommittedEvents = new();
    
    public Guid Id { get; protected set; }
    public int Version { get; protected set; } = -1;
    
    public IReadOnlyList<IDomainEvent> UncommittedEvents => _uncommittedEvents.AsReadOnly();
    
    protected void Apply(IDomainEvent @event)
    {
        When(@event);
        _uncommittedEvents.Add(@event);
        Version++;
    }
    
    protected abstract void When(IDomainEvent @event);
    
    public void Load(IEnumerable<IDomainEvent> history)
    {
        foreach (var @event in history)
        {
            When(@event);
            Version++;
        }
    }
    
    public void ClearUncommittedEvents() => _uncommittedEvents.Clear();
}

/// <summary>
/// EN: Order aggregate with event sourcing.
/// VI: Order aggregate với event sourcing.
/// </summary>
public class Order : EventSourcedAggregate
{
    public string UserId { get; private set; } = default!;
    public OrderStatus Status { get; private set; }
    public decimal TotalAmount { get; private set; }
    private readonly List<OrderItem> _items = new();
    public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();

    // EN: For rehydration / VI: Để khôi phục từ events
    private Order() { }

    // EN: Factory method for creation / VI: Factory method để tạo mới
    public static Order Create(Guid orderId, string userId, Address shippingAddress)
    {
        var order = new Order();
        order.Apply(new OrderCreated
        {
            OrderId = orderId,
            UserId = userId,
            ShippingAddress = shippingAddress,
            Version = 0
        });
        return order;
    }

    public void AddItem(Guid productId, int quantity, decimal unitPrice)
    {
        Apply(new OrderItemAdded
        {
            OrderId = Id,
            ProductId = productId,
            Quantity = quantity,
            UnitPrice = unitPrice,
            Version = Version + 1
        });
    }

    protected override void When(IDomainEvent @event)
    {
        switch (@event)
        {
            case OrderCreated e:
                Id = e.OrderId;
                UserId = e.UserId;
                Status = OrderStatus.Created;
                break;
                
            case OrderItemAdded e:
                _items.Add(new OrderItem(e.ProductId, e.Quantity, e.UnitPrice));
                TotalAmount += e.Quantity * e.UnitPrice;
                break;
                
            case OrderPaid:
                Status = OrderStatus.Paid;
                break;
        }
    }
}

Event Store Repository / Repository Event Store

/// <summary>
/// EN: Repository for event-sourced aggregates.
/// VI: Repository cho aggregate event-sourced.
/// </summary>
public interface IEventSourcedRepository<T> where T : EventSourcedAggregate
{
    Task<T?> GetByIdAsync(Guid id, CancellationToken ct = default);
    Task SaveAsync(T aggregate, CancellationToken ct = default);
}

/// <summary>
/// EN: EF Core implementation of event store.
/// VI: Event store triển khai với EF Core.
/// </summary>
public class EfCoreEventStore<T> : IEventSourcedRepository<T> 
    where T : EventSourcedAggregate, new()
{
    private readonly EventStoreDbContext _context;
    private readonly ILogger<EfCoreEventStore<T>> _logger;

    public EfCoreEventStore(
        EventStoreDbContext context,
        ILogger<EfCoreEventStore<T>> logger)
    {
        _context = context;
        _logger = logger;
    }

    public async Task<T?> GetByIdAsync(Guid id, CancellationToken ct = default)
    {
        var streamName = $"{typeof(T).Name}-{id}";
        
        var events = await _context.Events
            .Where(e => e.StreamId == streamName)
            .OrderBy(e => e.Version)
            .ToListAsync(ct);

        if (!events.Any())
            return null;

        var aggregate = new T();
        aggregate.Load(events.Select(e => DeserializeEvent(e)));
        
        return aggregate;
    }

    public async Task SaveAsync(T aggregate, CancellationToken ct = default)
    {
        var streamName = $"{typeof(T).Name}-{aggregate.Id}";
        var expectedVersion = aggregate.Version - aggregate.UncommittedEvents.Count;

        // EN: Optimistic concurrency check
        // VI: Kiểm tra optimistic concurrency
        var currentVersion = await _context.Events
            .Where(e => e.StreamId == streamName)
            .MaxAsync(e => (int?)e.Version, ct) ?? -1;

        if (currentVersion != expectedVersion)
            throw new ConcurrencyException(
                $"Expected version {expectedVersion} but found {currentVersion}");

        foreach (var @event in aggregate.UncommittedEvents)
        {
            _context.Events.Add(new StoredEvent
            {
                Id = @event.EventId,
                StreamId = streamName,
                EventType = @event.GetType().AssemblyQualifiedName!,
                Data = JsonSerializer.Serialize(@event, @event.GetType()),
                Version = @event.Version,
                OccurredOn = @event.OccurredOn
            });
        }

        await _context.SaveChangesAsync(ct);
        aggregate.ClearUncommittedEvents();
        
        _logger.LogInformation(
            "Saved {Count} events to stream {Stream}",
            aggregate.UncommittedEvents.Count,
            streamName);
    }
}

Projections / Projections

/// <summary>
/// EN: Projection handler for order read models.
/// VI: Projection handler cho order read models.
/// </summary>
public class OrderProjection : IEventHandler<OrderCreated>, IEventHandler<OrderPaid>
{
    private readonly ReadDbContext _readDb;

    public OrderProjection(ReadDbContext readDb)
    {
        _readDb = readDb;
    }

    public async Task HandleAsync(OrderCreated @event, CancellationToken ct)
    {
        var readModel = new OrderReadModel
        {
            Id = @event.OrderId,
            UserId = @event.UserId,
            Status = "Created",
            TotalAmount = @event.TotalAmount,
            CreatedAt = @event.OccurredOn
        };

        _readDb.Orders.Add(readModel);
        await _readDb.SaveChangesAsync(ct);
    }

    public async Task HandleAsync(OrderPaid @event, CancellationToken ct)
    {
        var order = await _readDb.Orders.FindAsync(@event.OrderId);
        if (order != null)
        {
            order.Status = "Paid";
            order.PaidAt = @event.OccurredOn;
            await _readDb.SaveChangesAsync(ct);
        }
    }
}

Common Mistakes / Lỗi Thường Gặp

1. Mutable Events

// ❌ BAD: Mutable event
public class OrderCreated
{
    public string Status { get; set; } // Mutable!
}

// ✅ GOOD: Immutable record
public record OrderCreated
{
    public string Status { get; init; } // Immutable
}

2. Storing Derived Data in Events

// ❌ BAD: Storing computed values
public record OrderItemAdded
{
    public decimal UnitPrice { get; init; }
    public int Quantity { get; init; }
    public decimal TotalPrice { get; init; } // Derived!
}

// ✅ GOOD: Store only facts, compute when needed
public record OrderItemAdded
{
    public decimal UnitPrice { get; init; }
    public int Quantity { get; init; }
    // TotalPrice computed in aggregate
}

3. Large Event Streams Without Snapshots

// ❌ BAD: Loading thousands of events
var order = await _repo.GetByIdAsync(orderId); // Slow!

// ✅ GOOD: Use snapshots for performance
public async Task<Order?> GetByIdAsync(Guid id, CancellationToken ct)
{
    // Load from snapshot if exists
    var snapshot = await _snapshotStore.GetLatestAsync<Order>(id, ct);
    var fromVersion = snapshot?.Version ?? 0;
    
    // Load only events after snapshot
    var events = await _eventStore.GetEventsAsync(id, fromVersion, ct);
    
    var aggregate = snapshot ?? new Order();
    aggregate.Load(events);
    return aggregate;
}

Quick Reference / Tham Chiếu Nhanh

When to Use Event Sourcing

Scenario Recommendation
Simple CRUD Overkill
Complex domains Use ES
Audit requirements Use ES
Temporal queries Use ES
High write volume ⚠️ Consider carefully

Event Naming Conventions

Type Convention Example
Create {Entity}Created OrderCreated
Update {Entity}{Property}Changed OrderStatusChanged
Delete {Entity}Deleted OrderDeleted
Action {Entity}{Action} OrderShipped

Snapshot Strategy

Trigger When to Snapshot
Version-based Every N events (e.g., 100)
Time-based Every N minutes
Size-based When aggregate size > threshold

Resources / Tài Nguyên