Lektion 4.11.1 hat den ereignisgesteuerten Stil als einen von sechs vorgestellt, Lektion 4.11.2 hat als zweiten Schritt auf dem Weg vom Modul zum Dienst empfohlen, „asynchron, wo es geht“. Diese Lektion nimmt den Stil auseinander. Das Wort Ereignis steht in der Kladde inzwischen für mindestens drei verschiedene Dinge, und wer sie verwechselt, baut entweder Abhängigkeiten, die er vermeiden wollte, oder Kopien, die niemand pflegt. Danach geht es um die Frage, wer einen Ablauf über mehrere Kontexte steuert, um die drei Dienste, mit denen Azure Ereignisse verteilt, und um Serverless, das in Azure fast immer mit Ereignissen zusammen auftritt.
Am Ende skizzieren Sie den Fluss von der Ausschreibung über das Angebot bis zum Deal zweimal, einmal ohne und einmal mit einer steuernden Stelle, spielen die Fehlerfälle durch und begründen eine der beiden Formen. An der Kladde selbst ändert sich nichts; die Entscheidung wird in Modul 4.12 gebaut.
Fünf frühere Lektionen tragen diese hier. Lektion 4.3.4 hat Domain Events von Integrationsereignissen getrennt. Modul 4.4 hat den Vertrieb mit Event Sourcing gebaut, Ereignisse als Speicher. Lektion 4.6.2 hat Integrationsereignisse mit der Outbox sicher hinausgebracht, Lektion 4.6.3 sie mit der Inbox idempotent empfangen. Und Lektion 4.6.4 hat Choreografie und Orchestrierung gegenübergestellt und den Prozessmanager für „Angebot angenommen“ als Grain gebaut. Serverless kennen Sie aus Modul 3.13 in K3.
Drei Dinge, die „Ereignis“ heißen
Ein Ereignis ist etwas, das geschehen ist, in der Vergangenheitsform benannt. So weit sind sich alle einig. Was ein Ereignis tut, ist in der Kladde aber auf drei verschiedene Arten gemeint:
| Ereignisbenachrichtigung | Ereignis mit Zustand | Event Sourcing | |
|---|---|---|---|
| Was es sagt | „Ausschreibung ted-4711 ist als passend markiert.“ | „… markiert, Titel …, Abgabefrist 28.03.“ | „Deal qualifiziert“, als ein Eintrag im Strom des Deals |
| Wozu | andere aufmerksam machen | anderen geben, was sie brauchen | den eigenen Zustand speichern |
| Wer liest | andere Kontexte; sie fragen nach, wenn sie mehr brauchen | andere Kontexte; sie merken sich eine Kopie | das Aggregat selbst und seine Projektionen |
| Kopplung zur Laufzeit | hoch: Wer nachfragt, braucht die Quelle | niedrig: Die Kopie reicht | keine nach außen |
| Preis | Last und Ausfall bei der Quelle | größere Nachrichten, Kopien, die kurz veralten, Versionierung | alles aus Modul 4.4 |
| In der Kladde | QuoteAcceptedEvent (nur Kennungen, Lektion 4.6.2) |
noch keines | der Vertrieb (Modul 4.4) |
Die ersten beiden sind Arten der Kommunikation zwischen Kontexten. Microsoft beschreibt sie beim ereignisgesteuerten Stil als zwei Wege, eine Nachricht zu füllen: alle Attribute mitschicken, die ein Verbraucher braucht, oder nur die Schlüssel. Alle Attribute ersparen dem Verbraucher die Rückfrage, kosten aber Bandbreite, machen Verträge und Versionen schwerer und schaffen mehrere Stellen, an denen derselbe Wert steht. Nur Schlüssel halten die Daten an einer Stelle, kosten aber Rückfragen bei der Quelle.
Event Sourcing ist etwas anderes: eine Art, Daten zu speichern. Die Ereignisse im Strom eines Deals sind das Innere des Vertriebs. Sie
verlassen ihn nie direkt, sondern nur übersetzt, wie der Relay in Lektion 4.6.2 aus dem gespeicherten DealWon ein Integrationsereignis
macht. Wer Event Sourcing mit ereignisgesteuerter Architektur verwechselt, macht sein Speicherformat zum Vertrag mit anderen, und Microsoft
zählt genau das beim Microservices-Stil zu den typischen Fehlern: Datenbankinhalte als Ereignisse veröffentlichen.
Was der Unterschied zwischen den ersten beiden im Betrieb bedeutet, zeigt ein kleines Programm. Der Vertrieb merkt sich jede Ausschreibung, die
als passend markiert ist, mit ihrer Abgabefrist. Einmal bekommt er eine Benachrichtigung und fragt nach, einmal ein Ereignis mit Zustand. Legen
Sie es in Ihrem Übungsordner als ereignisarten.cs an:
// ereignisarten.cs
// Der Vertrieb merkt sich jede Ausschreibung, die als passend markiert ist, mit ihrer Abgabefrist; daraus kann ein Deal werden.
// Einmal kommt nur eine Benachrichtigung (der Vertrieb fragt nach), einmal ein Ereignis mit Zustand (der Vertrieb hat eine Kopie).
#:property PublishAot=false
using System.Globalization;
var tenders = new Tenders();
tenders.Add("ted-4711", "Netzwerk Grundschule Erlengrund", new DateOnly(2025, 3, 28));
// Die Ausschreibungen markieren ted-4711 als passend und veröffentlichen das Ereignis, hier in beiden Fassungen.
var notification = new TenderMarkedRelevant("ted-4711");
var withState = new TenderMarkedRelevantV2("ted-4711", "Netzwerk Grundschule Erlengrund", new DateOnly(2025, 3, 28));
var notified = new SalesWithNotifications(tenders);
var carried = new SalesWithStateTransfer();
Console.WriteLine($"A1 Benachrichtigung, Quelle läuft: {notified.On(notification)}");
tenders.Running = false;
Console.WriteLine($"A2 Benachrichtigung, Quelle gestoppt: {notified.On(notification)}");
Console.WriteLine($"B1 Ereignis mit Zustand, Quelle gestoppt: {carried.On(withState)}");
// Die Frist wird verschoben. Das Ereignis darüber ist noch unterwegs: Für diese Zeit ist die Kopie veraltet.
tenders.Running = true;
tenders.MoveDeadline("ted-4711", new DateOnly(2025, 4, 4));
Console.WriteLine($"B2 Frist verschoben, Ereignis unterwegs: Kopie {Day(carried.DeadlineOf("ted-4711"))}, Quelle {Day(tenders.AskDeadline("ted-4711"))}");
carried.On(new TenderDeadlineMoved("ted-4711", new DateOnly(2025, 4, 4)));
Console.WriteLine($"B3 Ereignis angekommen: Kopie {Day(carried.DeadlineOf("ted-4711"))}, Quelle {Day(tenders.AskDeadline("ted-4711"))}");
static string Day(DateOnly? day) => day?.ToString("dd.MM.", CultureInfo.InvariantCulture) ?? "keine";
// Die Verträge der Ausschreibungen: zwei Fassungen desselben Ereignisses und ein zweites Ereignis für die Änderung.
sealed record TenderMarkedRelevant(string TenderId);
sealed record TenderMarkedRelevantV2(string TenderId, string Title, DateOnly SubmissionDeadline);
sealed record TenderDeadlineMoved(string TenderId, DateOnly SubmissionDeadline);
// Der Kontext der Ausschreibungen, mit einem Schalter für „läuft gerade nicht“.
sealed class Tenders
{
private readonly Dictionary<string, (string Title, DateOnly Deadline)> tenders = [];
public bool Running { get; set; } = true;
public void Add(string id, string title, DateOnly deadline) => tenders[id] = (title, deadline);
public void MoveDeadline(string id, DateOnly deadline) => tenders[id] = tenders[id] with { Deadline = deadline };
// Die Abfrage, die andere Kontexte stellen. Läuft der Kontext nicht, gibt es keine Antwort.
public DateOnly? AskDeadline(string id) => Running ? tenders[id].Deadline : null;
}
// Der Vertrieb mit Benachrichtigungen: ein kleines Ereignis, aber zur Laufzeit von den Ausschreibungen abhängig.
sealed class SalesWithNotifications(Tenders tenders)
{
public string On(TenderMarkedRelevant marked) =>
tenders.AskDeadline(marked.TenderId) is { } deadline
? $"vorgemerkt, Abgabefrist {deadline.ToString("dd.MM.", CultureInfo.InvariantCulture)}"
: "nicht vorgemerkt, Frist nicht abrufbar";
}
// Der Vertrieb mit Ereignissen mit Zustand: zur Laufzeit unabhängig, dafür mit einer Kopie, die kurz veralten kann.
sealed class SalesWithStateTransfer
{
private readonly Dictionary<string, DateOnly> deadlines = [];
public string On(TenderMarkedRelevantV2 marked)
{
deadlines[marked.TenderId] = marked.SubmissionDeadline;
return $"vorgemerkt, Abgabefrist {marked.SubmissionDeadline.ToString("dd.MM.", CultureInfo.InvariantCulture)}";
}
public void On(TenderDeadlineMoved moved) => deadlines[moved.TenderId] = moved.SubmissionDeadline;
public DateOnly? DeadlineOf(string tenderId) => deadlines.TryGetValue(tenderId, out var deadline) ? deadline : null;
}
dotnet run ereignisarten.cs
A1 Benachrichtigung, Quelle läuft: vorgemerkt, Abgabefrist 28.03.
A2 Benachrichtigung, Quelle gestoppt: nicht vorgemerkt, Frist nicht abrufbar
B1 Ereignis mit Zustand, Quelle gestoppt: vorgemerkt, Abgabefrist 28.03.
B2 Frist verschoben, Ereignis unterwegs: Kopie 28.03., Quelle 04.04.
B3 Ereignis angekommen: Kopie 04.04., Quelle 04.04.
- A2 zeigt den Preis der Benachrichtigung. Das Ereignis ist angekommen, aber ohne die Quelle nützt es nichts. Wer so arbeitet, hat die Kopplung, die er mit dem Ereignis loswerden wollte, nur in den Verbraucher verschoben. Ein Empfänger, der hier scheitert, muss die Nachricht später noch einmal verarbeiten, und die Inbox aus Lektion 4.6.3 muss erlauben, dass er es tut.
- B1 zeigt den Gewinn des Ereignisses mit Zustand: Der Vertrieb arbeitet, obwohl die Ausschreibungen stehen.
- B2 zeigt seinen Preis: Für die Zeit, in der das Ereignis über die Änderung unterwegs ist, kennt der Vertrieb die alte Frist. Wer Zustand überträgt, muss auch jede Änderung übertragen, und jeder Verbraucher sieht eine Weile einen veralteten Stand. Lektion 4.11.4 macht daraus eine Konsistenzfrage mit Namen.
Für die Kladde kommt eine Regel aus der Hausordnung dazu: Personenbezogene Daten stehen nie in Ereignissen. Ein Ereignis mit Zustand darf die Abgabefrist und den Titel einer öffentlichen Ausschreibung tragen, aber nicht den Namen und die Adresse einer Ansprechperson. Dort bleibt es bei der Kennung, und wer den Namen braucht, fragt über den Vertrag der Kontakte.