NServiceBus - przykład 1: request/response
Dlaczego w ogóle omawiam ten przykład? Przecież wszystkie moje dotychczasowe notki dotyczące NServiceBus przekonywały Was, że
ten schemat komunikacji jest zły. Otóż czasem jest on nieunikniony. Najlepszym przykładem zastosowania request/reposnse są wszelkiego rodzaju funkcje autoryzujące. Logiki związanej z autoryzacją (niezależnie od tego, co autoryzujemy — czy to użytkownika, czy transakcję, czy cokolwiek innego) nie chcielibyśmy rozpraszać w wielu elementach systemu. Dobrą praktyką związaną z zapewnieniem wysokiego poziomu bezpieczeństwa jest centralizacja logiki autoryzacji, co wiąże się niestety z koniecznością każdorazowego „odpytywania” usługi autoryzującej.
Schemat request/response omówię na przykładzie sample-a
FullDuplex (aka RequestResponse) dostarczanego wraz z NServiceBus. Przed uruchomieniem przykładu polecam do app.config w projekcie „MyClient” skopiować sekcje
Common.Logging oraz
log4net z projektu „LoggingFromAppConfig” z sample-a GenericHost. Możecie także skorzystać ze zmodyfikowanej wersji pliku app.config
dołączonej do tego posta. Dzięki temu komunikaty wypisywane na ekran będą czytelne.
Komunikacja request/response w NSB jest zwykle asynchroniczna. Co to oznacza? Ano to, że preferowanym sposobem postępowania jest przetwarzanie odpowiedzi w odpowiednim momencie w tle, bez blokowania wysyłającego żądanie (który może zająć się użyteczną pracą). Istnieją dwa sposoby rejestracji metody, która przetworzy odpowiedź i oba są zademonstrowane w sampe-u:
Sposób preferowany to implementacja interfejsu
IHandleMessages:
1 class DataResponseMessageHandler : IHandleMessages<DataResponseMessage>
2 {
3 public void Handle(DataResponseMessage message)
4 {
5 Console.WriteLine("Response received with description: {0}\nSecret answer: {1}",
6 message.String, message.SecretAnswer.Value);
7 }
8 }
Jak już pisałem wcześniej, zawsze wszystkie zarejestrowane w danej instancji NSB handler-y mają szansę przetworzyć pasujące do nich komunikaty. Klasy implementujące interfejs IHandleMessages są automatycznie wykrywane przy starcie NServiceBus (który skanuje katalog z binariami naszej aplikacji). Tworzona jest wtedy mapa odwzorowująca znane typy komunikatów na zbiory handler-ów mogących je obsłużyć. W aplikacji przykładowej handler wypisuje na ekran fragmenty informacji zawarte w komunikacie.
Inny sposób to wykorzystanie interfejsu ICallback zwracanego przez wywołanie Send:
1 Bus.Send<RequestDataMessage>(m =>
2 {
3 m.DataId = g;
4 m.String = "<node>it's my \"node\" & i like it<node>";
5 m.SecretQuestion = "What's your favorite color?";
6 })
7 .Register(i => Console.Out.WriteLine(
8 "Response with header 'Test' = {0}, 1 = {1}, 2 = {2}.",
9 Bus.CurrentMessageContext.Headers["Test"],
10 Bus.CurrentMessageContext.Headers["1"],
11 Bus.CurrentMessageContext.Headers["2"]));
Pozwala on zarejestrować callback (metoda Register), który zostanie wykorzystany do przetworzenia konkretnie jednego komunikatu, który zostanie wysłany w odpowiedzi na ten, którego dotyczy dana instancja ICallback. Kojarzenie komunikatu i callback-u odbywa się za pomocą identyfikatora Correlation Id przenoszonego przez wiadomości. Dlaczego sposób ten nie jest preferowany? Jest on problematyczny w wypadku komunikacji transakcyjnej. Kiedy klient "padnie" zaraz po wysłaniu żądania (i rejestracji callback-u) zarejestrowana procedura obsługi zostanie utracona i komunikat odpowiedzi nie zostanie poprawnie przetworzony po ponownym uruchomieniu klienta. Dobra rada jest więc taka: wykorzystujcie funkcjonalność ICallback tylko dla komunikacji nietransakcyjnej lub do realizacji dodatkowej (nie krytycznej) funkcjonalności (jak logowanie).
W API jest także dostępna jedna metoda, która pozwala na realizację synchronicznej komunikacji (jeśli komuś bardzo zależy). Jedna z przeładowanych wersji ICallback.Register zwraca jako wynik IAsynchResult, z którego możemy pobrać obiekt WaitHandle, który z kolei oferuję metodę WaitOne pozwalającą efektywnie zablokować wątek wywołujący Send do czasu otrzymania komunikatu odpowiedzi. Zmodyfikowany kod wygląda tak:
1 .Register(i => Console.Out.WriteLine(
2 "Response with header 'Test' = {0}, 1 = {1}, 2 = {2}.",
3 Bus.CurrentMessageContext.Headers["Test"],
4 Bus.CurrentMessageContext.Headers["1"],
5 Bus.CurrentMessageContext.Headers["2"]), null).AsyncWaitHandle.WaitOne();
Jest to jednak feature dedykowany do specjalnych zastosowań i w większości przypadków nie powinien być wykorzystywany (jest po prostu SZKODLIWY).
Przykład FullDuplex ma jeszcze dwie ciekawe cechy. Pierwszą z nich jest demonstracja mechanizmu szyfrowania za pomocą klucza prywatnego. Obie instancje NSB (klient i serwer) mają w konfiguracji określony współdzielony klucz szyfrujący algorytmu AES (Rijndael). Konfiguracja ta jest banalnie prosta:
<RijndaelEncryptionServiceConfig Key="gdDbqRpqdRbTs3mhdZh9qCaDaxJXl+e7"/>
Drugą ciekawostką jest sposób uruchamiania (hostowana). Jak zapewne zauważyliście,
w solution nie ma żadnego projektu wykonywalnego — tylko biblioteki klas. Otóż NServiceBus począwszy od wersji 2.0 zawiera aplikację generycznego hosta (dla zainteresowanych: NSB wykorzystuje bibliotekę
TopShelf), która może być wykorzystana do hostowana usług NSB. Ta generyczna aplikacja pozwala na uruchamiania zarówno w trybie konsolowym (w Visual Studio, przez F5), jak i trybie usługi Windows (opcja „install”). Jest to
wielkie ułatwienie w procesie debugowania i
deployment-u.
Na koniec pozostaje mi jedynie zachęcić do ściągnięcia NServiceBus i zobaczenia sample-a na własne oczy.