Event Sourcing und CQRS in .NET 9: Wann sich die Komplexität lohnt
Ein praxisnaher Leitfaden zu Event Sourcing und CQRS in .NET 9 — wann das Muster seine Komplexität rechtfertigt, Implementierung mit Marten und Produktionserfahrungen.
Event Sourcing und CQRS sind leistungsstarke Muster. Sie sind aber auch die am häufigsten übermäßig angewendeten Muster in Enterprise-Software. Teams übernehmen sie, weil sie elegant klingen, und verbringen dann Monate damit, gegen Infrastrukturkomplexität zu kämpfen, die eine einfache CRUD-Anwendung vermieden hätte.
Dieser Beitrag ist kein Evangelisierungs-Artikel. Er erklärt, wann Event Sourcing seinen Aufwand wirklich rechtfertigt, wie man es in .NET 9 mit Marten implementiert und welche Produktionsprobleme auftreten, die Blog-Tutorials nie erwähnen.
Wann sich Event Sourcing lohnt
Sie brauchen Event Sourcing wahrscheinlich wenn:
Vollständiger Audit Trail ist regulatorische Pflicht — Finanzdienste, Gesundheitswesen und Rechtssysteme müssen nachweisen, was genau wann und in welcher Reihenfolge passiert ist. Ein Append-only Event Log ist rechtlich belastbarer als veränderbare Datenbankzeilen.
Ihre Domäne hat komplexe Zustandsmaschinen — Ein Versicherungsanspruch, der durch Created → UnderReview → AdditionalInfoRequested → Approved → Paid → Disputed → Resolved wandert, hat Übergänge, die am besten als Events modelliert werden.
Sie brauchen temporale Abfragen — „Was war der Kontostand am 15. März?" Event Replay zu einem Zeitpunkt beantwortet das natürlich.
Mehrere Read-Models aus denselben Daten — Ein einzelner Event Stream kann gleichzeitig in eine relationale Berichtsdatenbank, einen Suchindex und ein Echtzeit-Dashboard projizieren.
Sie brauchen Event Sourcing wahrscheinlich nicht wenn:
- Ihre Domäne einfaches CRUD ohne komplexe Zustandsübergänge ist
- Sie starke Konsistenz für Lesevorgänge unmittelbar nach Schreibvorgängen brauchen
- Ihr Team keine Erfahrung mit dem Muster hat und keine Zeit zum Lernen
- Ihre Audit-Anforderungen mit einer einfachen Änderungsprotokoll-Tabelle erfüllt werden können
Architekturübersicht
Implementierung mit Marten in .NET 9
Marten verwendet PostgreSQL sowohl als Event Store als auch als Dokumentendatenbank.
Das Aggregate
public sealed class ShoppingCart
{
public Guid Id { get; private set; }
public CartStatus Status { get; private set; }
public List<CartItem> Items { get; private set; } = new();
public decimal TotalAmount => Items.Sum(i => i.Price * i.Quantity);
public void Apply(CartCreated @event)
{
Id = @event.CartId;
Status = CartStatus.Active;
}
public void Apply(ItemAdded @event)
{
Items.Add(new CartItem(@event.ProductId, @event.ProductName, @event.Price, @event.Quantity));
}
public void Apply(CartCheckedOut @event)
{
Status = CartStatus.CheckedOut;
}
}Command Handler
public sealed class AddItemHandler
{
private readonly IDocumentSession _session;
public async Task Handle(AddItemCommand command, CancellationToken ct)
{
var stream = await _session.Events.FetchForWriting<ShoppingCart>(command.CartId, ct);
var cart = stream.Aggregate;
if (cart.Status != CartStatus.Active)
throw new InvalidOperationException("Artikel können nicht zu einem ausgecheckten Warenkorb hinzugefügt werden.");
stream.AppendOne(new ItemAdded(
command.CartId, command.ProductId, command.ProductName, command.Price, command.Quantity
));
await _session.SaveChangesAsync(ct);
}
}Projektionen (Read Models)
public sealed class CartSummaryProjection : SingleStreamProjection<CartSummary>
{
public CartSummary Create(CartCreated @event) => new()
{
Id = @event.CartId,
CustomerId = @event.CustomerId,
CreatedAt = @event.Timestamp,
ItemCount = 0,
TotalAmount = 0
};
public CartSummary Apply(ItemAdded @event, CartSummary current) => current with
{
ItemCount = current.ItemCount + @event.Quantity,
TotalAmount = current.TotalAmount + (@event.Price * @event.Quantity)
};
}Event-Lebenszyklus: Vom Command zum Read Model
Produktions-Herausforderungen
Snapshotting
10.000 Events zu replaying zum Aggregate-Rebuild ist langsam:
opts.Events.UseAggregateSnapshots<ShoppingCart>(snapshotting =>
{
snapshotting.SnapshotEvery(50);
});Event-Versionierung und Upcasting
public sealed class OrderCreatedV1ToV2Upcaster : EventUpcaster<OrderCreatedV1, OrderCreatedV2>
{
protected override OrderCreatedV2 Upcast(OrderCreatedV1 old) => new(
old.OrderId, old.CustomerId, Currency: "EUR"
);
}Eventual Consistency in Read Models
Asynchrone Projektionen bedeuten, dass Abfragen veraltete Daten zurückgeben können. Lösungen:
- Event-Version mit Schreibvorgängen zurückgeben, Client pollt bis Read Model aufholt
- Inline-Projektionen für kritische Read Models verwenden
- UI optimistisch gestalten
Kosten-Nutzen-Zusammenfassung
| Nutzen | Kosten |
|---|---|
| Vollständiger Audit Trail | Event-Versionierung-Komplexität |
| Temporale Abfragen | Projektions-Rebuild-Zeit |
| Mehrere Read Models | Eventual Consistency |
| Natürliche Domänenmodellierung | Team-Lernkurve |
| Produktionsprobleme debuggen | Speicherwachstum (Append-only) |
Die Komplexität ist real. Aber für die richtigen Domänen — Finanztransaktionen, Versicherungsansprüche, Logistik-Workflows, Compliance-schwere Systeme — transformiert Event Sourcing unmögliches Debugging in triviales Event Replay.
Erwägen Sie Event Sourcing für Ihr Enterprise-System? Kontaktieren Sie uns — wir helfen Teams, das Muster dort einzusetzen, wo es echten Mehrwert bringt.
Themen