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

456 lines
16 KiB
Markdown

---
name: event-sourcing
description: 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.
compatibility: ".NET 10+, EventStoreDB, Marten, EF Core"
metadata:
author: Velik Ho
version: "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
```csharp
/// <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
```csharp
/// <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
```csharp
/// <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
```csharp
/// <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
```csharp
// ❌ 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
```csharp
// ❌ 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
```csharp
// ❌ 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
- [Detailed Examples](./references/REFERENCE.md) - Full code examples
- [CQRS MediatR](../cqrs-mediatr/SKILL.md) - CQRS patterns
- [Outbox Pattern](../outbox-pattern/SKILL.md) - Reliable event publishing
- [Repository Pattern](../repository-pattern/SKILL.md) - Data access patterns
- [Domain Driven Design](../domain-driven-design/SKILL.md) - DDD patterns