Event Sourcing¶
Event Sourcing, uygulamanın durumunu olaylar üzerinden saklayarak tam bir değişiklik geçmişi sunar; yanlış uygulamalar karmaşık sorgulara ve performans sorunlarına yol açar.
1. Mutable State ile Event Sourcing Karışımı¶
❌ Yanlış Kullanım: Hem event store hem de doğrudan state güncellemesi yapmak.
public class BankAccount
{
public decimal Balance { get; set; }
public void Deposit(decimal amount)
{
Balance += amount; // Doğrudan state değişikliği
_events.Add(new MoneyDeposited(amount)); // Event de ekleniyor
_context.SaveChanges(); // State kaydediliyor
}
}
✅ İdeal Kullanım: State’i yalnızca event’lerden türetin.
public class BankAccount
{
private readonly List<IDomainEvent> _uncommittedEvents = new();
public decimal Balance { get; private set; }
public void Deposit(decimal amount)
{
if (amount <= 0) throw new DomainException("Tutar pozitif olmalıdır.");
Apply(new MoneyDeposited(Id, amount, DateTime.UtcNow));
}
private void Apply(IDomainEvent @event)
{
When(@event);
_uncommittedEvents.Add(@event);
}
private void When(IDomainEvent @event)
{
switch (@event)
{
case MoneyDeposited e: Balance += e.Amount; break;
case MoneyWithdrawn e: Balance -= e.Amount; break;
}
}
}
2. Event Versiyonlama Yapmamak¶
❌ Yanlış Kullanım: Event yapısını değiştirip eski event’leri kırmak.
// v1
public record OrderCreated(int OrderId, int CustomerId, decimal Total);
// v2 - breaking change, eski event'ler deserialize edilemez
public record OrderCreated(int OrderId, int CustomerId, decimal Total, string Currency, DateTime CreatedAt);
✅ İdeal Kullanım: Event versiyonlama ve upcasting ile geriye uyumluluk sağlayın.
public record OrderCreatedV1(int OrderId, int CustomerId, decimal Total);
public record OrderCreatedV2(int OrderId, int CustomerId, decimal Total, string Currency, DateTime CreatedAt);
public class OrderCreatedUpcaster : IEventUpcaster
{
public IDomainEvent Upcast(IDomainEvent @event)
{
if (@event is OrderCreatedV1 v1)
{
return new OrderCreatedV2(v1.OrderId, v1.CustomerId, v1.Total, "TRY", DateTime.UtcNow);
}
return @event;
}
}
3. Snapshot Kullanmamak¶
❌ Yanlış Kullanım: Her okumada tüm event geçmişini tekrar oynatmak.
public async Task<BankAccount> GetAccountAsync(Guid accountId)
{
var events = await _eventStore.GetEventsAsync(accountId); // 10.000+ event olabilir
var account = new BankAccount();
foreach (var @event in events)
{
account.Apply(@event); // Her okumada tüm geçmiş tekrar oynatılır
}
return account;
}
✅ İdeal Kullanım: Belirli aralıklarla snapshot alarak performansı artırın.
public async Task<BankAccount> GetAccountAsync(Guid accountId)
{
var snapshot = await _snapshotStore.GetLatestAsync<BankAccount>(accountId);
var fromVersion = snapshot?.Version ?? 0;
var events = await _eventStore.GetEventsAsync(accountId, fromVersion);
var account = snapshot ?? new BankAccount();
foreach (var @event in events)
{
account.Apply(@event);
}
if (events.Count > 100) // Her 100 event'te snapshot al
{
await _snapshotStore.SaveAsync(accountId, account, account.Version);
}
return account;
}
4. Projection’ları Senkron Güncellemek¶
❌ Yanlış Kullanım: Event kaydederken projection’ı aynı transaction’da güncellemek.
public async Task HandleAsync(OrderCreated @event)
{
await _eventStore.AppendAsync(@event);
// Aynı transaction'da - event store ve read model sıkı bağlı
var summary = await _readDb.OrderSummaries.FindAsync(@event.OrderId);
summary.Status = "Created";
summary.Total = @event.Total;
await _readDb.SaveChangesAsync();
}
✅ İdeal Kullanım: Projection’ları asenkron olarak güncelleyin.
// Event kaydı
public async Task HandleAsync(OrderCreated @event)
{
await _eventStore.AppendAsync(@event);
}
// Ayrı projection worker
public class OrderSummaryProjection : IEventHandler<OrderCreated>
{
private readonly ReadDbContext _readDb;
public async Task HandleAsync(OrderCreated @event, CancellationToken ct)
{
var summary = new OrderSummary
{
OrderId = @event.OrderId,
CustomerId = @event.CustomerId,
Total = @event.Total,
Status = "Created"
};
await _readDb.OrderSummaries.AddAsync(summary, ct);
await _readDb.SaveChangesAsync(ct);
}
}
5. Idempotency Sağlamamak¶
❌ Yanlış Kullanım: Aynı event’in tekrar işlenmesine karşı koruma yapmamak.
public class InventoryProjection : IEventHandler<OrderCreated>
{
public async Task HandleAsync(OrderCreated @event, CancellationToken ct)
{
var product = await _context.Products.FindAsync(@event.ProductId);
product.Stock -= @event.Quantity; // Event tekrar işlenirse stok yanlış azalır
await _context.SaveChangesAsync(ct);
}
}
✅ İdeal Kullanım: Event position tracking ile idempotency sağlayın.
public class InventoryProjection : IEventHandler<OrderCreated>
{
public async Task HandleAsync(OrderCreated @event, CancellationToken ct)
{
var processed = await _context.ProcessedEvents
.AnyAsync(e => e.EventId == @event.EventId, ct);
if (processed) return; // Zaten işlenmiş
var product = await _context.Products.FindAsync(@event.ProductId);
product.Stock -= @event.Quantity;
_context.ProcessedEvents.Add(new ProcessedEvent { EventId = @event.EventId });
await _context.SaveChangesAsync(ct);
}
}