F# & NServiceBus - praktyczny przewodnik: Wysłanie komendy
Posty z tej serii:
- F# & NServiceBus - praktyczny przewodnik: Wprowadzenie
- F# & NServiceBus - praktyczny przewodnik: Konfiguracja Endpointa
- F# & NServiceBus - praktyczny przewodnik: Wysłanie komendy
- F# & NServiceBus - praktyczny przewodnik: Komunikacja wielu Endpointów
- F# & NServiceBus - praktyczny przewodnik: Publikowanie zdarzeń
- F# & NServiceBus - praktyczny przewodnik: Zarządzanie procesem biznesowym - Saga
- F# & NServiceBus - praktyczny przewodnik: Zakończenie
Z poprzedniego artykułu wiesz, w jaki sposób skonfigurować oraz wystartować Endpoint NServiceBusa. Utworzony kod będzie Ci potrzebny do kontynuowania ćwiczeń z tego artykułu, w którym przejdziemy przez utworzenie, wysyłanie oraz przetworzenie wiadomości.
Źródłem, na którym bazują przykłady, jest druga część tutoriala wprowadzającego do NServiceBusa, który znajduje się na stronie dokumentacji technicznej frameworka.
Utworzenie wiadomości
Wiadomości służą do przesyłania informacji pomiędzy Endpointami. W kodzie wiadomość definiowana jest przez klasę. Poszczególne informacje wiadomości definiowane są przez odpowiednie member klasy. Wiadomość jest kontraktem pomiędzy Endpointami. Powinna być jak najprostsza. Nie powinna zawierać żadnej logiki. Można ją traktować tak samo, jak wzorzec Data Transfer Object.
W celu zminimalizowania zależności pomiędzy Endpointami wiadomości powinny znajdować się w osobnym projekcie, który z kolei posiada zależności tylko do bibliotek systemowych oraz paczki Nuget NServiceBusa.
NServiceBus definiuje trzy typy wiadomości:
- Command - wysyłając Command rozpoczynamy jakąś akcję
- Event - publikując Event oznajmiamy, że jakaś akcja została zrealizowana
- Message - wysyłając Message odpowiadamy nadawcy, który wysłał wiadomość rozpoczynającą akcję
W tym artykule skupiamy się na wiadomościach typu Command. Więcej informacji o typach wiadomości znajdziesz na stronie dokumentacji.
Tak jak pisałem w pierwszym artykule, kodując rozwiązanie w języku obiektowym, dobrą praktyką jest umieszczanie klasy reprezentującej wiadomość w osobnym pliku. Przy większych systemach wynikiem takiego podejścia jest bardzo duża ilość plików, zawierających bardzo małą ilość kodu. W F# klasa jest jednym z wielu typów. Nie ma konieczności trzymania się zasady klasa per osobny plik. Plik możemy potraktować jako kontener na logicznie powiązane ze sobą typy oraz funkcje, operujące na tych typach.
Po tym krótkim wstępie przejdźmy do stworzenia nowej wiadomości. W tym celu w Visual Studio:
- dodaj do solucji RetailDemo nowy projekt Add…New Project…
- wybierz typ projektu Visual F#…NET Core…Class Library (.NET Core)
- w pole Name wpisz nazwę projektu Messages
- zainstaluj paczkę Nuget NServiceBus
- zmień nazwę pliku Library.fs na Commands.fs
- usuń domyślnie utworzony kod
- dodaj poniższy kod
Przejdźmy po kolei przez utworzone konstrukcje:
namespace Commands
- definicja przestrzeni nazwopen NServiceBus
- zaimportowanie elementów z przestrzeni nazw NServiceBustype PlaceOrder(orderId: string) =
- deklaracja oraz definicja klasy reprezentującej wiadomość o nazwie PlaceOrderPlaceOrder(orderId: string)
- definiuje nazwę oraz konstruktor klasy, który zawiera parametr orderIdinterface ICommand
- dziedziczenie po interfejsieICommand
, należącego do przestrzeni nazw NServiceBusmember this.OrderId = orderId
- deklaracja oraz definicja informacji wysyłanej wraz z wiadomością
W F# typy definiowane są przez słowo kluczowe type
. Wcięcia określają początek i koniec definicji typu. Dziedziczenie po interfejsie odbywa się przy pomocy słowa kluczowego interface
. Pola, properties, metody, … definiuje się za pomocą słowa kluczowego member
. Domyślnym modyfikatorem dostępu każdego member
jest private
. Sama klasa posiada domyślny modyfikator dostępu public
.
W powyższym przykładzie wiadomość o nazwie PlaceOrder zawiera prywatne member o nazwie OrderId, któremu przypisywane jest Value o nazwie orderId, pochodzące z konstruktora klasy. Konstruktor zawiera doprecyzowanie typu, ponieważ chcemy, aby id zamówienia zawsze miało wartości typu string
.
Klasa PlaceOrder dziedziczy po interfejsie ICommand
należącego do przestrzeni nazw NServiceBus
. W ten sposób oznaczamy, że jest to wiadomości typu Command. Dzięki temu framework będzie umiał ją wysłać oraz przetworzyć.
Przetwarzanie wiadomości
Mając zdefiniowaną wiadomość, możemy utworzyć funkcjonalność, która będzie ją przetwarzać. W języku NServiceBusa taka funkcjonalność nazwa się Message Handler. W celu utworzenia Handlera dla wiadomości PlaceOrder:
- dodaj do projektu ClientUI referencję do wcześniej utworzonego projektu Messages
- dodaj do projektu ClientUI nowy plik źródłowy F# oraz nadaj mu nazwę Handlers.fs
- usuń domyślnie utworzony kod
- dodaj poniższy kod
PlaceOrderHandler jest klasą, która dziedziczy po interfejsie IHandleMessages<T>
, należącym do przestrzeni nazw NServiceBus, gdzie T jest typem wiadomości do przetworzenia. W przykładzie jest to nasz Command PlaceOrder. Jeśli interfejs, po którym dziedziczy klasa, zawiera jakiekolwiek member, musi ono być jawnie zaimplementowane. Służy do tego słowo kluczowe with
, wstawiane zaraz za nazwą dziedziczonego interfejsu.
Metoda Handle
zawiera implementację logiki, którą wykona NServiceBus, po otrzymaniu Commanda PlaceOrder. Zobacz, że zarówno parametry metody Handle
, jak i jej zwracany typ, nie muszą być doprecyzowane. Kompilator ma wystarczającą ilość informacji, aby wydedukować, że parametr message jest typu PlaceOrder
, a parametr context jest typu IMessageHandlerContext
. Sama metoda zwraca zaś wartość typu Task
.
Aby się o tym przekonać, najedź kursorem w Visual Studio na nazwę metody Handle i sprawdź wyświetloną informację.
F# obsługuje statyczne member. Przykładem tego jest log, reprezentujący mechanizm logowania wbudowany w NServiceBusa. Do elementów statycznych możemy odwoływać się poprzez ich pełną nazwę PlaceOrderHandler.log
. Ostatnią instrukcją member Handle
jest zwrócenie typu zgodnego z sygnaturą.
Powyższy przykład zawiera bardzo prostą logikę przetwarzania wiadomości, wypisującą tekst za pomocą mechanizmu logowania.
Zwróć uwagę na pierwszą linijkę kodu w plikach Commands.fs oraz Handlers.fs. W pierwszym przypadku tworzymy namespace
, a w drugim module
. W F# namespace pozwala na grupowanie typów. Nie pozwala na grupowanie funkcji. Do tego służy module, który pozwala grupować zarówno typy jak funkcje. W ten sposób możemy grupować ze sobą logicznie powiązane elementy. Jeśli plik zawiera tylko definicje typów, lepiej jest używać namespace. Jeśli definiujemy funkcje, używamy module.
W powyższym przykładzie ponownie skorzystaliśmy z tego, że w F#, klasy nie muszą znajdować się w osobnych plikach. Logicznie powiązane ze sobą Message Handlery, mogą być zdefiniowane w jednym pliku. Dzięki wykorzystaniu module, mamy możliwość definiowania funkcji, których możemy używać w dowolnym handlerze należącym do modułu.
Ćwiczenie dla Ciebie. Utwórz nową funkcję w module Handlers zwracającą tekst, a następnie użyj tej funkcji do wyświetlenia tekstu przez member log, klasy PlaceOrderHandler.
W tym momencie NServiceBus posiada pełną informację. W momencie, kiedy wiadomość PlaceOrder zostanie wysłana, framework automatycznie utworzy nową instancję klasy PlaceOrderHandler i wykona logikę, zaimplementowaną w member Handle.
Zanim przejdziemy dalej, przeanalizujmy jeszcze jedną pewną właściwość. W tutorialu z dokumentacji NServiceBusa opisany jest przypadek grupowania Handlerów, poprzez definiowanie kolejnych metod w klasie:
Jeśli spróbujemy zrobić to samo w F#:
Kod nie skompiluje się, a kompilator zwróci przyczynę:
This type implements the same interface at different generic instantiations 'IHandleMessages<DoSomethingElse>' and 'IHandleMessages<DoSomething>'. This is not permitted in this version of F#.
F# w wersji 4.7.0 nie obsługuje takiej konstrukcji. Ponieważ F# rozwijany jest na zasadach Open Source, możemy śledzić jego rozwój. Dzięki temu dowiadujemy się, że istnieje zatwierdzone RFC, którego celem jest dodanie powyższej konstrukcji, w którejś z przyszłych wersji języka.
Ponieważ logicznie ze sobą powiązane Handlery możemy implementować w konkretnym module, to brak powyższej właściwości językowej nie utrudnia nam konstruowania kodu. Będzie to miało większe znaczenie przy definiowaniu funkcjonalności zarządzającej bardziej skomplikowanym procesem biznesowym. Wrócimy do tego zagadnienia w ostatnim artykule wchodzącym w skład tej serii.
Wysłanie wiadomości
Teraz możemy dopisać ostatnią funkcjonalność, która wyśle wiadomość do przetworzenia. W tym celu:
- w projekcie ClientUI, dodaj do pliku Program.fs poniższy kod, tak, aby znajdował się przed funkcją main
- doprowadź kod do stanu kompilacji, dodając brakujące sekcje open
Mechanizm polega na tym, że po naciśnięciu klawisza P, tworzony jest Command PlaceOrder
z unikatowym orderId. Następnie, poprzez asynchroniczne wywołanie metody SendLocal
, Command wysyłany jest na Endpoint ClientUI. Metoda SendLocal
należy do obiektu endpointInstance
, który reprezentowany jest przez interfejs IEndpointInstance
, należący do API NServiceBusa. Obiekt endpointInstance
przekazywany jest w parametrze funkcji RunLoop
. Po naciśnięciu klawisza Q następuje wyjście z funkcji. Po naciśnięciu innego przycisku następuje powrót do ponownego wprowadzenia znaku.
Rozpoznajesz elementy języka F#, których używaliśmy w poprzednich przykładach? Są to między innymi: tworzenie obiektów, tworzenie loga NServiceBusa, wywołanie asynchroniczne, … Nowością są dwie konstrukcje: pętla while oraz Pattern Matching z wykorzystaniem słów kluczowych match...with
.
Pętla while
działa tak samo jak w języku C#. Dopóki spełniony jest warunek pętli, wykonywany będzie kod zawarty w ciele pętli. W naszym przykładzie warunek pętli reprezentuje mutable value
o nazwie continueLooping. W momencie wybrania znaku Q warunek pętli zmienia się na wartość false i następuje wyjście z pętli.
Pattern Matching, w dużym skrócie, polega na dopasowaniu wartości wejściowej do odpowiedniego wzorca, a następnie wykonanie kodu zdefiniowanego w dopasowaniu. W naszym przykładzie wejście to wybrany znak na klawiaturze. Za pomocą wyrażania match
następuje próba dopasowania wejścia do odpowiednio bloku kodu. Bloki definiuje się za pomocą |
. Pierwsze dopasowanie wygrywa. Jeśli wybranym znakiem jest P, następuje wysłanie wiadomości. Jeśli wybranym znakiem jest Q następuje wyjście z pętli. We wszystkich innych przypadkach, wypisywana jest informacja o nieznanym wejściu. Dowolne dopasowanie realizujemy za pomocą _
.
Aby wykorzystać stworzoną funkcję RunLoop
, zastąp w funkcji main
fragment kodu z:
na:
W tym momencie wszystko jest gotowe do uruchomienia i przetestowania. Naciśnij w Visual Studio klawisz F5, a następnie klawisz P, aby wysłać wiadomość.
Czy wszystko działa tak jak powinno?
Na pierwszy rzut oka tak, ale przyjrzyj się, co jest wyświetlane na ekranie, kiedy wiadomość jest przetwarzana. Porównaj wynik działania z zakodowaną logiką w klasie PlaceOrderHandler
.
Immutable Messages
Brak wypisywania id zamówienia spowodowane jest tym, że member OrderId
klasy PlaceOrder
jest immutable. Jego wartość można ustawić jedynie w konstruktorze klasy. NServiceBus wysyłając wiadomość, serializuje dane do zdefiniowanego formatu. Pobierając wiadomość do przetworzenia deserializuje ją do odpowiedniej klasy. Domyślny serializator nie wspiera operacji dla modyfikatora dostępu private
, a taki jest tworzony dla OrderId
w momencie utworzenia obiektu PlaceOrder
. Więcej o immutable messages możesz przeczytać w materiałach:
Ponieważ immutability jest jednym z podstawowych założeń języka F#, użyjmy serializatora wspierającego modyfikator private
. W tym celu zainstaluj paczkę Nuget:
Install-Package NServiceBus.Newtonsoft.Json -Version 2.2.0 -ProjectName ClientUI
Następnie, w funkcji main
, zaraz za utworzeniem transportu, dodaj poniższy wpis:
endpointConfiguration.UseSerialization<NewtonsoftSerializer>() |> ignore
Bonusem tej operacji jest to, że NServiceBus będzie serializował dane do formatu JSON.
Teraz uruchom ponownie program i sprawdź, czy wszystko działa, tak jak powinno:
Na ten moment znasz już podstawowe elementy frameworka NServiceBus, które umożliwiają tworzenie, wysyłanie oraz przetwarzanie wiadomości. Wiesz również, jakie są podstawowe konstrukcje języka F#, pozwalające na ich zakodowanie.
Ja również wyciągam z tej serii coś dla siebie. Mianowicie…zaczynam “zaprzyjaźniać” się z językiem F# :) Ucząc się nowych konstrukcji, kodując je w praktyce, powracając do nich po czasie i analizując, zauważam, że wszystko układa się w jedną całość. Dorzucając do tego kodowanie przykładów NServiceBusa, wyzwala to u mnie efekt “Wow. To działa!” :)
Całe rozwiązanie znajdziesz na GitHubie.
W następnym artykule do naszych funkcjonalności dołożymy nowy Endpoint, a następnie wyślemy do niego wiadomość do przetworzenia.
=