--- 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 /// /// EN: Base interface for all domain events. /// VI: Interface cơ sở cho tất cả domain events. /// public interface IDomainEvent { Guid EventId { get; } DateTime OccurredOn { get; } int Version { get; } } /// /// EN: Order created event. /// VI: Event tạo order. /// 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!; } /// /// EN: Order item added event. /// VI: Event thêm item vào order. /// 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 /// /// EN: Base class for event-sourced aggregates. /// VI: Lớp cơ sở cho aggregate event-sourced. /// public abstract class EventSourcedAggregate { private readonly List _uncommittedEvents = new(); public Guid Id { get; protected set; } public int Version { get; protected set; } = -1; public IReadOnlyList UncommittedEvents => _uncommittedEvents.AsReadOnly(); protected void Apply(IDomainEvent @event) { When(@event); _uncommittedEvents.Add(@event); Version++; } protected abstract void When(IDomainEvent @event); public void Load(IEnumerable history) { foreach (var @event in history) { When(@event); Version++; } } public void ClearUncommittedEvents() => _uncommittedEvents.Clear(); } /// /// EN: Order aggregate with event sourcing. /// VI: Order aggregate với event sourcing. /// 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 _items = new(); public IReadOnlyList 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 /// /// EN: Repository for event-sourced aggregates. /// VI: Repository cho aggregate event-sourced. /// public interface IEventSourcedRepository where T : EventSourcedAggregate { Task GetByIdAsync(Guid id, CancellationToken ct = default); Task SaveAsync(T aggregate, CancellationToken ct = default); } /// /// EN: EF Core implementation of event store. /// VI: Event store triển khai với EF Core. /// public class EfCoreEventStore : IEventSourcedRepository where T : EventSourcedAggregate, new() { private readonly EventStoreDbContext _context; private readonly ILogger> _logger; public EfCoreEventStore( EventStoreDbContext context, ILogger> logger) { _context = context; _logger = logger; } public async Task 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 /// /// EN: Projection handler for order read models. /// VI: Projection handler cho order read models. /// public class OrderProjection : IEventHandler, IEventHandler { 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 GetByIdAsync(Guid id, CancellationToken ct) { // Load from snapshot if exists var snapshot = await _snapshotStore.GetLatestAsync(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