NserviceBus unterstützt mit dem Konzept der Saga langlaufende Transaktionen. Dieses Konzept soll hier anhand des Beispiels Ansuchen und Bearbeiten von Anforderungen, welches hier zum Download bereit steht, beschrieben werden. Im ersten Schritt sucht ein Mitarbeiter um ein Produkt an. Der jeweilige Vorgesetzte erhält eine Nachricht und hat nun die Aufgabe, das Ansuchen zu bearbeiten, also zu genehmigen oder abzulehnen.
Dabei ergeben sich zwei technische Aufgaben: Zum einen muss die Nachricht mit der Entscheidung des Vorgesetzten zu einer der aktiven Sagas zugeordnet werden, damit das System den richtigen Bedarfsträger über diese Entscheidung informiert bzw. die richtigen Produkte bestellt. Zum anderen müssen Zustandsinformationen über die Saga bei jedem mit ihr assoziierten Methodenaufruf geladen bzw. danach wieder gesichert werden. Diese Zustandsinformationen könnten im betrachteten Beispiel die Id, den Namen sowie die Email-Adresse des Mitarbeiters sowie Informationen über die im Falle einer Genehmigung zu bestellenden Produkte beinhalten. Glücklicherweise kümmert sich NServiceBus um diese beiden Aufgaben.
Um mit NServiceBus eine Saga zu implementieren, sind zunächst Klassen für die Nachrichten sowie eine Klasse, welche den Zustand einer Saga repräsentiert, zu erstellen. Die Nachrichtenklassen implementieren per Definition IMessage; die Klasse mit den Zustandsinformationen ISageEntity (siehe Listing 1). Bei IMessage handelt es sich um ein Marker-Interface; ISageEntity gibt hingegen die Eigenschaften Id, OriginalMessageId und Originator vor, welche NServiceBus-intern Verwendung finden.
Listing 1: Nachrichtenklassen und Saga-Entität
1: public class Request : IMessage
2: {
3: public Request()
4: {
5: Id = Guid.NewGuid().ToString();
6: }
7:
8: public String Id { get; set; }
9: public String ProductName { get; set; }
10: public double Price { get; set; }
11: }
12:
13: public class Decision : IMessage
14: {
15: public String RequestId { get; set; }
16: public bool Approved { get; set; }
17: }
18:
19: public class RequestSagaData : ISagaEntity
20: {
21: public virtual String RequestId { get; set; }
22: public virtual DateTime? RequestDate { get; set; }
23: public virtual bool? Approved { get; set; }
24: public virtual DateTime? DecisionDate { get; set; }
25:
26: public virtual String ProductName { get; set; }
27: public virtual double Price { get; set; }
28:
29:
30: public virtual Guid Id
31: {
32: get;
33: set;
34: }
35:
36: public virtual string OriginalMessageId
37: {
38: get;
39: set;
40: }
41:
42: public virtual string Originator
43: {
44: get;
45: set;
46: }
47: }
Zur Behandlung der einzelnen innerhalb der Saga erwarteten Nachrichten, wird eine Subklasse von Saga bereitgestellt (Listing 2). Dabei ist Saga mit der zu verwendeten SagaEntity-Implementierung zu typisieren. Über die von dieser Klasse geerbte Methode ConfigureHowToFindSaga wird festgelegt, wie Folgenachrichten zu bereits laufenden Sagas zugewiesen werden sollen. Im betrachteten Beispiel wird festgelegt, dass zu diesem Zwecke die RequestId von Decision mit der in den Zustandsinformationen der Saga gespeicherten RequestId zu vergleichen ist. Bei Gleichheit wird die Nachricht mit der jeweiligen Saga assoziiert.
Für jede zu behandelnde Nachrichten ist darüber hinaus ISagaStartedBy<T> oder IHandleMessage<T> zu implementieren, wobei diese mit der jeweiligen Nachrichtenklasse zu typisieren sind. Mit ISagaStartedBy wird angezeigt, dass eine neue Saga bei Empfang einer durch den Typparameter repräsentierten Nachricht gestartet wird. Im Gegensatz dazu muss bei Verwendung von IHandleMessage die jeweilige Saga bereits aktiv sein. Beide Interfaces geben die Methode Handle(T) vor, deren Aufgabe das Behandeln der jeweiligen Nachrichten ist. Über die Eigenschaft Data, welche den Typ der festgelegten SagaEntity aufweist, kann auf die Zustandsinformationen der Saga zugegriffen werden. Zum Laden und Speichern dieser Informationen verwendet NServiceBus den O/R-Mapper NHibernate. Ein Zutun des Entwicklers ist hierbei nicht von Nöten. Beendet eine Handle-Implementierung die Saga, ruft diese per Definition die Methode MarkAsComplete auf. Dies führt dazu, dass die Zustandsinformationen aus der mit NHibernate eingebundenen Datenbank gelöscht werden.
Listing 2: Message-Handler
1: class RequestSaga:
2: Saga<RequestSagaData>,
3: ISagaStartedBy<Request>,
4: IHandleMessages<Decision>
5: {
6: public override void ConfigureHowToFindSaga()
7: {
8: this.ConfigureMapping<Decision>(
9: saga => saga.RequestId,
10: decision => decision.RequestId);
11:
12: }
13:
14: public void Handle(Request message)
15: {
16: Console.WriteLine("GOT REQUEST:");
17: Console.WriteLine("Message Id: " + message.Id);
18: Console.WriteLine("Product Name:" + message.ProductName);
19: Console.WriteLine("Price: " + message.Price);
20: Console.WriteLine();
21:
22: this.Data.ProductName = message.ProductName;
23: this.Data.Price = message.Price;
24:
25: Data.RequestDate = DateTime.Now;
26: Data.RequestId = message.Id;
27:
28:
29: }
30:
31: private static void SaveHotel()
32: {
33: using (var s = HibernateHelper.OpenSession())
34: {
35: using (var t = new TransactionScope())
36: {
37: Hotel h;
38: h = new Hotel
39: {
40: Bezeichnung = "Hotel zum großen Test",
41: Sterne = 4
42: };
43: s.Save(h);
44: t.Complete();
45: }
46: }
47: }
48:
49: public void Handle(Decision message)
50: {
51: Console.WriteLine("GOT DECISION:");
52: Console.WriteLine("Message Id: " + message.RequestId);
53: Console.WriteLine("Product Name:" + Data.ProductName );
54: Console.WriteLine("Price: " + Data.Price);
55: Console.WriteLine("Approved: " + message.Approved);
56: Console.WriteLine();
57:
58: Data.Approved = message.Approved;
59: Data.DecisionDate = DateTime.Now;
60:
61: SaveHotel();
62:
63:
64:
65: this.MarkAsComplete();
66:
67: }
68: }
Damit die gezeigten Sagas zur Ausführung gebracht werden können, wurde NServiceBus mit den Methoden Sagas und NHibernateSagaPersister konfiguriert (Listing 3). Erstere Aktiviert die Unterstützung für Sagas; letztere aktiviert den SagaPersister, welche die Zustände mit Hilfe von NHibernate in einer Datenbank speichert.
Listing 3: Konfigurieren von NServiceBus
1: static void Main(string[] args)
2: {
3: Configure
4: .With()
5: .Log4Net()
6: .DefaultBuilder()
7: .XmlSerializer()
8: .MsmqTransport()
9: .IsTransactional(true)
10: .PurgeOnStartup(false)
11: .UnicastBus()
12: .LoadMessageHandlers()
13: .ImpersonateSender(false)
14: .Sagas()
15: .NHibernateSagaPersister()
16: .CreateBus()
17: .Start();
18:
19: Console.ReadLine();
20: }
21:
22: [...]
23:
Darüber hinaus wurden Konfigurationsdaten für den SagaPersister in der Konfiguration abgelegt (Listing 4).
Listing 4: Konfiguration des SagaPersisters
1: <NHibernateSagaPersisterConfig>
2: <NHibernateProperties>
3:
4: <add Key="connection.provider"
5: Value="NHibernate.Connection.DriverConnectionProvider"/>
6: <add Key="connection.driver_class"
7: Value="NHibernate.Driver.SqlClientDriver"/>
8: <add Key="connection.connection_string"
9: Value="Server=[?];initial catalog=[?];Integrated Security=SSPI"/>
10: <add Key="dialect"
11: Value="NHibernate.Dialect.MsSql2005Dialect"/>
12: </NHibernateProperties>
13: </NHibernateSagaPersisterConfig>