# 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");
```