# Event Sourcing - Reference Examples ## Complete Implementation Examples ### 1. Event Store DbContext ```csharp /// /// EN: DbContext for storing events. /// VI: DbContext để lưu events. /// public class EventStoreDbContext : DbContext { public DbSet Events => Set(); public DbSet Snapshots => Set(); public EventStoreDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.ToTable("EventStore"); entity.HasKey(e => e.Id); entity.Property(e => e.StreamId) .HasMaxLength(200) .IsRequired(); entity.Property(e => e.EventType) .HasMaxLength(500) .IsRequired(); entity.Property(e => e.Data) .HasColumnType("jsonb") .IsRequired(); entity.HasIndex(e => new { e.StreamId, e.Version }) .IsUnique(); }); modelBuilder.Entity(entity => { entity.ToTable("Snapshots"); entity.HasKey(e => e.Id); entity.Property(e => e.AggregateId) .IsRequired(); entity.Property(e => e.Data) .HasColumnType("jsonb") .IsRequired(); entity.HasIndex(e => new { e.AggregateType, e.AggregateId, e.Version }) .IsUnique(); }); } } /// /// EN: Stored event entity. /// VI: Entity lưu event. /// public class StoredEvent { public Guid Id { get; set; } public string StreamId { get; set; } = default!; public string EventType { get; set; } = default!; public string Data { get; set; } = default!; public int Version { get; set; } public DateTime OccurredOn { get; set; } } /// /// EN: Snapshot entity. /// VI: Entity snapshot. /// public class Snapshot { public Guid Id { get; set; } public string AggregateType { get; set; } = default!; public Guid AggregateId { get; set; } public string Data { get; set; } = default!; public int Version { get; set; } public DateTime CreatedAt { get; set; } } ``` ### 2. Complete Order Aggregate ```csharp /// /// EN: Complete order aggregate with all events. /// VI: Order aggregate hoàn chỉnh với tất cả events. /// public class Order : EventSourcedAggregate { public string UserId { get; private set; } = default!; public OrderStatus Status { get; private set; } public decimal TotalAmount { get; private set; } public Address? ShippingAddress { get; private set; } public string? PaymentId { get; private set; } public string? TrackingNumber { get; private set; } private readonly List _items = new(); public IReadOnlyList Items => _items.AsReadOnly(); private Order() { } public static Order Create(Guid orderId, string userId, Address shippingAddress) { if (string.IsNullOrEmpty(userId)) throw new ArgumentException("UserId is required"); 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) { if (Status != OrderStatus.Created) throw new InvalidOperationException("Cannot add items to non-draft order"); if (quantity <= 0) throw new ArgumentException("Quantity must be positive"); Apply(new OrderItemAdded { OrderId = Id, ProductId = productId, Quantity = quantity, UnitPrice = unitPrice, Version = Version + 1 }); } public void MarkAsPaid(string paymentId) { if (Status != OrderStatus.Created) throw new InvalidOperationException("Order is not in Created status"); Apply(new OrderPaid { OrderId = Id, PaymentId = paymentId, Version = Version + 1 }); } public void Ship(string trackingNumber) { if (Status != OrderStatus.Paid) throw new InvalidOperationException("Order must be paid before shipping"); Apply(new OrderShipped { OrderId = Id, TrackingNumber = trackingNumber, Version = Version + 1 }); } public void Cancel(string reason) { if (Status is OrderStatus.Shipped or OrderStatus.Delivered) throw new InvalidOperationException("Cannot cancel shipped/delivered order"); Apply(new OrderCancelled { OrderId = Id, Reason = reason, Version = Version + 1 }); } protected override void When(IDomainEvent @event) { switch (@event) { case OrderCreated e: Id = e.OrderId; UserId = e.UserId; ShippingAddress = e.ShippingAddress; 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 e: Status = OrderStatus.Paid; PaymentId = e.PaymentId; break; case OrderShipped e: Status = OrderStatus.Shipped; TrackingNumber = e.TrackingNumber; break; case OrderCancelled: Status = OrderStatus.Cancelled; break; } } } ``` ### 3. Snapshot Service ```csharp /// /// EN: Service for managing aggregate snapshots. /// VI: Service quản lý snapshots của aggregates. /// public interface ISnapshotService { Task GetLatestSnapshotAsync(Guid aggregateId, CancellationToken ct = default) where T : EventSourcedAggregate; Task SaveSnapshotAsync(T aggregate, CancellationToken ct = default) where T : EventSourcedAggregate; } public class SnapshotService : ISnapshotService { private readonly EventStoreDbContext _context; private readonly ILogger _logger; private const int SnapshotThreshold = 100; public SnapshotService( EventStoreDbContext context, ILogger logger) { _context = context; _logger = logger; } public async Task GetLatestSnapshotAsync( Guid aggregateId, CancellationToken ct = default) where T : EventSourcedAggregate { var snapshot = await _context.Snapshots .Where(s => s.AggregateType == typeof(T).Name && s.AggregateId == aggregateId) .OrderByDescending(s => s.Version) .FirstOrDefaultAsync(ct); if (snapshot == null) return null; return JsonSerializer.Deserialize(snapshot.Data); } public async Task SaveSnapshotAsync( T aggregate, CancellationToken ct = default) where T : EventSourcedAggregate { // EN: Only snapshot if threshold reached // VI: Chỉ snapshot nếu đạt ngưỡng if (aggregate.Version % SnapshotThreshold != 0) return; var snapshot = new Snapshot { Id = Guid.NewGuid(), AggregateType = typeof(T).Name, AggregateId = aggregate.Id, Data = JsonSerializer.Serialize(aggregate), Version = aggregate.Version, CreatedAt = DateTime.UtcNow }; _context.Snapshots.Add(snapshot); await _context.SaveChangesAsync(ct); _logger.LogInformation( "Created snapshot for {AggregateType} {AggregateId} at version {Version}", typeof(T).Name, aggregate.Id, aggregate.Version); } } ``` ### 4. Projection Dispatcher ```csharp /// /// EN: Dispatches events to projections. /// VI: Dispatch events đến projections. /// public interface IProjectionDispatcher { Task DispatchAsync(IDomainEvent @event, CancellationToken ct = default); } public class ProjectionDispatcher : IProjectionDispatcher { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; public ProjectionDispatcher( IServiceProvider serviceProvider, ILogger logger) { _serviceProvider = serviceProvider; _logger = logger; } public async Task DispatchAsync(IDomainEvent @event, CancellationToken ct = default) { var eventType = @event.GetType(); var handlerType = typeof(IEventHandler<>).MakeGenericType(eventType); var handlers = _serviceProvider.GetServices(handlerType); foreach (var handler in handlers) { try { var method = handlerType.GetMethod("HandleAsync"); await (Task)method!.Invoke(handler, new object[] { @event, ct })!; _logger.LogDebug( "Projected {EventType} to {HandlerType}", eventType.Name, handler!.GetType().Name); } catch (Exception ex) { _logger.LogError(ex, "Failed to project {EventType} to {HandlerType}", eventType.Name, handler!.GetType().Name); throw; } } } } ``` ### 5. Command Handler with Event Sourcing ```csharp /// /// EN: Command handler using event sourcing. /// VI: Command handler sử dụng event sourcing. /// public class CreateOrderCommandHandler : IRequestHandler { private readonly IEventSourcedRepository _repository; private readonly IProjectionDispatcher _projections; private readonly ISnapshotService _snapshots; private readonly ILogger _logger; public CreateOrderCommandHandler( IEventSourcedRepository repository, IProjectionDispatcher projections, ISnapshotService snapshots, ILogger logger) { _repository = repository; _projections = projections; _snapshots = snapshots; _logger = logger; } public async Task Handle( CreateOrderCommand request, CancellationToken ct) { var orderId = Guid.NewGuid(); var order = Order.Create(orderId, request.UserId, request.ShippingAddress); foreach (var item in request.Items) { order.AddItem(item.ProductId, item.Quantity, item.UnitPrice); } // EN: Save events to event store // VI: Lưu events vào event store await _repository.SaveAsync(order, ct); // EN: Update projections // VI: Cập nhật projections foreach (var @event in order.UncommittedEvents) { await _projections.DispatchAsync(@event, ct); } // EN: Save snapshot if threshold reached // VI: Tạo snapshot nếu đạt ngưỡng await _snapshots.SaveSnapshotAsync(order, ct); _logger.LogInformation("Order created: {OrderId}", orderId); return new OrderResult(orderId); } } ``` ### 6. DI Registration ```csharp /// /// EN: Register event sourcing services. /// VI: Đăng ký event sourcing services. /// public static class EventSourcingServiceExtensions { public static IServiceCollection AddEventSourcing( this IServiceCollection services, IConfiguration configuration) { // EN: Register Event Store DbContext services.AddDbContext(options => options.UseNpgsql(configuration.GetConnectionString("EventStore"))); // EN: Register repositories services.AddScoped(typeof(IEventSourcedRepository<>), typeof(EfCoreEventStore<>)); // EN: Register snapshot service services.AddScoped(); // EN: Register projection dispatcher services.AddScoped(); // EN: Register projections services.AddScoped, OrderProjection>(); services.AddScoped, OrderProjection>(); services.AddScoped, OrderProjection>(); return services; } } ``` ### 7. Temporal Query Example ```csharp /// /// EN: Get order state at specific point in time. /// VI: Lấy trạng thái order tại thời điểm cụ thể. /// public class GetOrderAtTimeQueryHandler : IRequestHandler { private readonly EventStoreDbContext _context; public GetOrderAtTimeQueryHandler(EventStoreDbContext context) { _context = context; } public async Task Handle( GetOrderAtTimeQuery request, CancellationToken ct) { var streamId = $"Order-{request.OrderId}"; // EN: Get events up to the specified time // VI: Lấy events đến thời điểm chỉ định var events = await _context.Events .Where(e => e.StreamId == streamId && e.OccurredOn <= request.AsOfTime) .OrderBy(e => e.Version) .ToListAsync(ct); if (!events.Any()) return null; // EN: Replay events to reconstruct state // VI: Phát lại events để khôi phục trạng thái var order = new Order(); order.Load(events.Select(DeserializeEvent)); return new OrderStateDto { OrderId = order.Id, Status = order.Status.ToString(), TotalAmount = order.TotalAmount, ItemCount = order.Items.Count, AsOfTime = request.AsOfTime }; } } ``` ## Database Migration ```sql -- EN: Create EventStore table / VI: Tạo bảng EventStore CREATE TABLE "EventStore" ( "Id" uuid PRIMARY KEY, "StreamId" varchar(200) NOT NULL, "EventType" varchar(500) NOT NULL, "Data" jsonb NOT NULL, "Version" integer NOT NULL, "OccurredOn" timestamp with time zone NOT NULL ); CREATE UNIQUE INDEX "IX_EventStore_StreamId_Version" ON "EventStore" ("StreamId", "Version"); -- EN: Create Snapshots table / VI: Tạo bảng Snapshots CREATE TABLE "Snapshots" ( "Id" uuid PRIMARY KEY, "AggregateType" varchar(200) NOT NULL, "AggregateId" uuid NOT NULL, "Data" jsonb NOT NULL, "Version" integer NOT NULL, "CreatedAt" timestamp with time zone NOT NULL ); CREATE UNIQUE INDEX "IX_Snapshots_Type_Id_Version" ON "Snapshots" ("AggregateType", "AggregateId", "Version"); ```