16 KiB
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 |
|
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
- Detailed Examples - Full code examples
- CQRS MediatR - CQRS patterns
- Outbox Pattern - Reliable event publishing
- Repository Pattern - Data access patterns
- Domain Driven Design - DDD patterns