Category Archives: Programowanie rozproszone

Kilka porad na temat usług REST

O usługach REST, które dzisiaj są wszechobecne pisałem już wiele razy np. tutaj.  Dzisiaj chciałbym napisać krótkie podsumowanie w formie porad i antywzorców. Zaczynamy:

1. Nigdy nie używaj czasowników w URI.

Przykład błędnych linków:

GET: localhost\persons\1\UpdateEmail?email=’…’

Jedynym dozwolonym czasownikiem w adresie to HTTP verb. Całość linku to nic innego jak hierarchia zasobów. Poprawna aktualizacja adresu email może wyglądać zatem następująco:

PUT: localhost\persons\1\email

Metoda PUT oznacza, że mamy do czynienia z modyfikacją danych. UpdateEmail to nazwa metody, a nie zasobu co jest sprzeczne z REST. Adres email’a w przypadku PUT można umieścić w ciele zapytania.

Analogicznie jeśli chcemy odczytać dane osoby, nie powinniśmy:

GET localhost\persons\1\getdata

Poprawny adres to:

GET localhost\persons\1\data

2. REST != RPC.  

Osoby zaczynające z REST, często próbują korzystać z tego typu usług tak samo jak np. z WCF. Klasyczne RPC (Remote-Procedure-Call) polega na wykonaniu konkretnej metody na usłudze. REST to hierarchia zasobów  więc powinniśmy tworzyć linki w postaci:

localhost/persons/1/address/town

Widzimy, że operujemy tutaj na kilku poziomach. Niepoprawną z kolei postacią jest:

localhost/persons/gettown?id=1

Wynika to nie tylko z użycia czasownika w nazwie, ale również w operowaniu bezpośrednio na korzeniu zasobów, zamiast na wykorzystaniu całej dostępnej hierarchii.

3. Korzystaj z dostępnych metod  HTTP.

Jeśli chcemy usunąć zasób wtedy:

DELETE local/host/persons/1

Początkujące osoby zbyt często korzystają wyłącznie z GET. Jeśli usługa ma realizować zadania CRUD, wtedy zdecydowanie potrzebujemy POST,GET,PUT,DELETE.

4. Korzystaj z dostępnych statusów HTTP.

Status 200 (OK) zdecydowanie nie jest jedynym z którego możemy korzystać. Jeśli operacja nie uda się, wtedy oprócz zwrócenia błędu w ciele HTTP, należy zwrócić również kod np. 500 (Internal Error) albo 400 (Bad Request) w zależności od kontekstu. Kody od 400 to błędy spowodowane przez klienta (np. nieprawidłowe zapytanie), z kolei od 500 to błędy spowodowane przez serwer.

Jeśli tworzymy nową encję za pomocą HTTP POST, można zwrócić 201 (Created) zamiast po prostu 200 (OK), który bardziej nadaję się na zapytania HTTP GET.

Inny przykład to kod 404 (Not found). Jeśli użytkownik wywoła localhost/persons/5, a osoba o identyfikatorze 5 nie istnieje, wtedy zamiast 200 należy zwrócić kod 404 .

5. QueryString

Klasyczne parametry QueyString wciąż są dozwolone, ale nie powinny określać one zasobów. Innymi słowy, złą praktyką jest:

localhost/persons?id=1

Identyfikator w tym przypadku jest ściśle powiązany z zasobem. Dopuszczalne jest za to używanie QueryString do określenia np. sortowania:

lcoalhost/persons?sort=desc

Podobnie sprawa wygląda z filtrowaniem czy z innymi parametrami, które nie określają zasobów a po prostu np. ich sposób wyświetlania.

6. Zwrócenie stanu obiektu po aktualizacji.

Utworzenie zasobu lub jego aktualizacja (POST, PUT) powinny zwracać jego reprezentacje albo link, który umożliwia odczytanie pełnego stanu. Innymi słowy, po utworzeniu zasobu, ciało odpowiedzi nie może być puste. Zwykle zwraca się mapę linków\operacji, które można wykonać na nowo utworzonym zasobie.

7.  Usługa powinna być bezstanowa.

Umożliwia to łatwiejsze skalowanie jak i cachowanie. Oznacza to, że wywołanie danej akcji, nie powinno zależeć od wykonania wcześniejszych operacji. Oczywiście mowa o stanie usługi, a nie aplikacji, gdzie stan stanowi zestaw reguł biznesowych. Z tego względu, autoryzacja nie powinna bazować na ciasteczkach czy sesjach ponieważ łamią one tą zasadę. Lepiej przesyłać dany token w każdym zapytaniu. Innymi słowy, każde zapytanie powinno dostarczyć wszystkie niezbędne dane do wykonania akcji.

ASP.NET WebAPI a JSONP

W poprzednim wpisie pokazałem jak korzystać z JSONP w JQuery. Wiemy, że usługa musi rozpoznawać parametr callback i zwrócić treść w odpowiedniej formie tzn. “callback(dane)”. Załóżmy, że mamy następujący kontroler:

public class DataController : ApiController { public string[] Get() { return new[] {"Hello", "World", "!!!"}; } }

Domyślnie WebAPI nie wspiera JSONP. Możemy to potwierdzić wysyłając następujące zapytanie:

GET http://localhost:5081/api/data?callback='myfunction' HTTP/1.1 User-Agent: Fiddler Host: localhost:5081

Odpowiedź będzie wyglądać tak:

HTTP/1.1 200 OK Cache-Control: no-cache Pragma: no-cache Content-Type: application/json; charset=utf-8 Expires: -1 Server: Microsoft-IIS/10.0 X-AspNet-Version: 4.0.30319 X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcUGlvdHJcRG93bmxvYWRzXFNwb3RsaWdodERldmVsb3BlclRlc3RcTGltZXJpY2tcV2ViQXBwbGljYXRpb240XFdlYkFwcGxpY2F0aW9uNFxhcGlcZGF0YQ==?= X-Powered-By: ASP.NET Date: Fri, 12 Jun 2015 18:30:34 GMT Content-Length: 23 ["Hello","World","!!!"]

Widzimy, że nasz callback nie został rozpoznany.

Na szczęście możemy skorzystać z gotowego pakietu “WebApiContrib.Formatting.Jsonp”. Najpierw zatem należy zainstalować go za pomocą:

Install-Package WebApiContrib.Formatting.Jsonp

 

W pliku Global.asax.cs następnie wystarczy, że umieścimy:

GlobalConfiguration.Configuration.AddJsonpFormatter();

Instrukcja po prostu dodaje formater JSONP, który rozpozna przychodzące zapytania JSONP i umieści odpowiednio callback. Wysyłając teraz takie same zapytanie, serwis odpowie:

HTTP/1.1 200 OK Cache-Control: no-cache Pragma: no-cache Content-Type: text/javascript; charset=utf-8 Expires: -1 Server: Microsoft-IIS/10.0 X-AspNet-Version: 4.0.30319 X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcUGlvdHJcRG93bmxvYWRzXFNwb3RsaWdodERldmVsb3BlclRlc3RcTGltZXJpY2tcV2ViQXBwbGljYXRpb240XFdlYkFwcGxpY2F0aW9uNFxhcGlcZGF0YQ==?= X-Powered-By: ASP.NET Date: Fri, 12 Jun 2015 18:39:22 GMT Content-Length: 36 myfunction(["Hello","World","!!!"]);

Powyższą odpowiedź można załadować jako skrypt JavaScript i wykonać.  Jeśli mamy tylko zaimplementowaną myfunction, przekazane parametry będą stanowić dane, które chcemy pobrać z serwisu.

Jeśli pominiemy parametr callback, wtedy serwis zwróci standardową odpowiedź czyli czyste dane.

JSONP – wywoływanie zewnętrznych usług z JavaScript

Bardzo często tworzymy osobne usługi, które dostarczają jakieś dane. Pisząc aplikacje ASP.NET Web nierzadko chcemy korzystać z zewnętrznych usług, zamiast hostować dane w tym samym projekcie. Niestety może to spowodować problemy, jeśli chcemy skonsumować usługę w JavaScript, a należy ona do innej domeny.

Załóżmy, że mamy jakąś usługę REST. Dla testów posłużyłem się http://www.mocky.io. Polecam tą stronę, można generować tam własne “mocki”.  Dla tego wpisu stworzyłem mock, który zwraca następującą treść np.:

{Text:Hello world!}

Następnie na stronie, mam następujący kod JavaScript, który po prostu próbuje połączyć się z usługą i odczytać dane:

$.ajax({ url: http://www.mocky.io/v2/55772bf754441acd1b6697d0 }).done(function(data) { $(#testLabel).text(data.Text); });

Szybko przekonamy się, że wykonanie zakończy się błędem:

XMLHttpRequest cannot load http://www.mocky.io/v2/55772bf754441acd1b6697d0. No Access-Control-Allow-Origin header is present on the requested resource. Origin http://localhost:35447 is therefore not allowed access.

Przeglądarka zawsze blokuje wywołania Ajax, które próbują wywołać usługę z innej domeny (w tym przypadku localhost –> mocky.io).

Jeśli usługa zwraca dane JSON, jednym z rozwiązań może być JSONP, czyli “JavaScript Object Notation with Padding”.

Rozwiązanie wykorzystuje lukę w przeglądarkach i jest bardzo często stosowane. JSON jak wiemy, może stanowić poprawny kod JavaScript. Przeglądarki nie dopuszczają wywołań do innych domen, ale dozwolone jest załadowanie kodu, które znajduje się w innej domenie, np. poprzez <script src=’adres do zewnetrznej domeny’./>.  Z tego względu, jeśli potraktujemy zawartość zwróconą przez naszą usługę jako kod, wtedy będziemy w stanie połączyć się z nią.

Wyobraźmy sobie, że usługa zamiast zwracać {“Text”:”Hello world”}, zwraca callback({“Text:”HelloWorld”}). Dla przeglądarki jest to jak najbardziej poprawny kod i można go wykonać.

Technika jest na tyle popularna, że istnieje wiele narzędzi, które wspierają ją. Na przykład, dla powyższego wywołania ajaxowego możemy:

$.ajax({ url: http://www.mocky.io/v2/55772bf754441acd1b6697d0, dataType:jsonp }).done(function(data) { $(#testLabel).text(data.Text); });

Wystarczy dodać dataType:’jsonp’ i zobaczymy na ekranie załadowany tekst. Warto przeanalizować, jakie zapytanie jest wysłane oraz jaka odpowiedź konkretnie przychodzi. Zaglądać do logów zobaczymy, że wysyłamy:

“http://www.mocky.io/v2/55772bf754441acd1b6697d0?callback=jQuery110208627150375396013_1433874507041&_=1433874507042”.

Nie trudno spostrzec, że został dodany parametr callback, wraz z nazwą funkcji. Usługa (w tym przypadku mocky.io) rozpozna parametr callback i zwróci odpowiedź w postaci:

jQuery110208627150375396013_1433874507041({“Text”:”Hello world!”});

Następnie jQuery wykona daną funkcję, która tak naprawdę odczyta parametr wejściowy i przekaże ją do funkcji done. Innymi słowy, kod zwrócony przez usługę zostanie wykonany w formie funkcji callback’a, który wywołuje “done” przekazując dostarczony parametr, który stanowi dane zwrócone przez usługę.

Warto zwrócić uwagę, że usługa musi rozpoznawać parametr callback i zwracać zawartość w formie callback(dane) zamiast po prostu czyste dane. Domyślnie WebAPI nie zrobi tego za nas, ale w przyszłym wpisie pokażę w jaki sposób to osigągnąć. Czyste dane JSON zwracane przez usługi, stanowią poprawny kod JavaScript, ale nic z nimi nie możemy zrobić. Jeśli opleciemy je w funkcję, umożliwi nam to przechwycenie tych danych poprzez implementację danej funkcji i odczytanie parametrów wejściowych.

Circuit Breaker: Implementacja za pomocą Polly

W poprzednim poście wyjaśniłem na czym polega działanie wzorca i kiedy z niego korzystać. Dzisiaj przyszedł czas na implementację. Nie będziemy jednak pisać wszystkiego od początku, ponieważ jest to dość skomplikowane i prawidłowa implementacja zajęłaby sporo czasu. Kilka postów wcześniej, pisałem o Polly, jako mechanizmie do powtarzania nieudanych operacji. Zachęcam do przeczytania tego wpisu przed dalszą lekturą. Dla przypomnienia tytko, wkleję najprostszy przykład:

class Program { static void Main(string[] args) { var policy = Polly.Policy.Handle<DivideByZeroException>().Retry(); policy.Execute(DoSomething); Console.ReadLine(); } public static void DoSomething() { Console.WriteLine("Do something"); throw new DivideByZeroException(); } }

Dzięki Polly,  w analogiczny sposób możemy skonfigurować nasz “obwód”.:

Policy policy = Policy.Handle<Exception>(). CircuitBreaker(3, TimeSpan.FromMinutes(1));

Pierwszy parametr to liczba dopuszczalnych niepowodzeń, a drugi to na ile czasu obwód zostanie otwarty. Całość naszego eksperymentu może wyglądać następująco:

class Program { static void Main(string[] args) { Policy policy = Policy.Handle<Exception>(). CircuitBreaker(3, TimeSpan.FromMinutes(1)); Run(policy); Run(policy); Run(policy); Run(policy); Console.ReadLine(); } private static void Run(Policy policy) { try { policy.Execute(() => DoSomething()); } catch (TimeoutException e) { Console.WriteLine("Nieudana proba."); } } public static void DoSomething() { Console.WriteLine("Test"); throw new TimeoutException(); } }

Uruchamiamy metodę 4 razy, zatem za czwartym dostaniemy wyjątek:

image

Wystarczy, że odczekamy minutę przed czwartym wywołaniem i jak spodziewamy się, obwód pozostanie zamknięty. Kod:

Policy policy = Policy.Handle<Exception>(). CircuitBreaker(3, TimeSpan.FromMinutes(1)); Run(policy); Run(policy); Run(policy); Thread.Sleep(TimeSpan.FromMinutes(1)); Run(policy);

image

Obsługa zdalnych wywołań: wzorzec Circuit Breaker

Sporo ostatnio o SOA i mikroserwisach. Jednym z wyzwań podczas rozłupywania monolitu na serwisy jest wydajność. Wywołania in-memory są zastępowane np. HTTP lub innym zdalnym protokołem. Niesie to ze sobą kilka niedogodności m.in.:

1. Wydajność jest dużo mniejsza – serializacja, deserializacja, nawiązanie połączenia, transmisja danych.

2. Serwis może być nieaktywny.

3. Może wystąpić timeout.

Powyższe punkty mogą być wyjątkowo niebezpieczne, gdy wiele usług próbuje wywołać serwisy, które aktualnie nie odpowiadają. Każde wywołanie niesie ze sobą zużycie zasobów. Jeśli zbyt długa kolejka nieudanych wywołan zostanie skumulowana, kolejna usługa może przestać odpowiadać. Przypomina to efekt domina, gdzie jedna usługa próbuje nieustannie połączyć się z drugą, która aktualnie nie odpowiada. Przez zbyt dużą liczbę nieudanych połączeń, taka usługa sama przestaje odpowiadać, powodując takie same efekty na innych usługach.

Wzorzec Circuit Breaker, ma na celu zminimalizowanie efektu, gdy część usług nie odpowiada, a wiele klientów chce do nich uzyskać dostęp. Nie ma sensu stosować wzorca, gdy wiemy, że nie mamy dużej ilości wywołań na sekundę. W takiej sytuacji, timeout nie zaszkodzi i nie spowoduje wyczerpania zasobów.

W przypadku, gdy mamy setki wywołań, wtedy lepiej, gdy np. pierwszy klient ostrzeże innych o tym, że i tak nie ma sensu łączyć się z daną usługą, ponieważ ona nie odpowiada. I to jest właśnie cała istota wzorca. Stanowi on punkt między konsumentami, a usługą. Jeśli wszystko jest OK to obwód jest zamknięty (circuit). W momencie, gdy nastąpi timeout lub inna zdefiniowana awaria, wtedy obwód jest otwierany tak, że inni klienci dostaną natychmiastową odpowiedź, że nie można połączyć się z daną usługą. Nie będziemy musieli czekać na timeout, co pochłania ogromne ilości zasobów (w przypadku akumulacji połączeń).

Kolejny problemy jaki należy rozwiązać to ponowne zamknięcie obwodu po naprawie awarii. W przypadku programów, bardziej racjonalne jest automatyczne zamknięcie niż czekanie na manualną interwencję. Sposobów jest wiele, ale najprostszym może być okresowe zamykanie obwodu. Jeśli po automatycznym zamknięciu, znów okaże się, że zewnętrzne zasoby są niedostępne, wtedy możemy go natychmiast otworzyć i czekać na kolejny moment, kiedy możemy spróbować i przetestować połączenie poprzez zamykanie obwodu.

Wzorzec zatem, to nic innego jak proxy, który ma następujące stany:

1. Obwód zamknięty – normalny tryb pracy. Wszystkie zapytania są przesyłane do danych usług\dostawców. W tym stanie, proxy powinien również śledzić liczbę nieudanych połączeń. Jeśli przekroczy ona z definiowany próg w danym okresie (np. 5 nieudanych połączeń w ciągu 10 sekund), wtedy obwód jest otwierany. Warto podkreślić, że chcemy resetować licznik niepowodzeń okresowo. Mam na myśli, że nie powinniśmy po prostu śledzić całkowitej liczby błędów. Chcemy uniknąć sytuacji, gdzie tymczasowe awarie po paru dniach pracy mogą doprowadzić do otwarcia obwodu. Dzięki prostej zasadzie, że awarie muszą nastąpić z określoną częstotliwością, unikniemy otwarcia obwodu w przypadku krótkotrwałych awarii albo po prostu chwilowego przeciążenia sieci.

2. Obwód otwarty – jeśli proxy jest w tym stanie, to znaczy, że dopuszczalna liczba nieudanych połączeń została przekroczona i ze względu bezpieczeństwa, obwód został otwarty. Wszystkie wywołania kończą się natychmiastowym wyjątkiem, bez pochłaniania zasobów (jeszcze raz podkreślam, w celu uniknięcia kaskadowego wyczerpania zasobów).

3.  Powyższe dwa stany są obowiązkowe w każdej implementacji. Zwykle jednak dodaje się kolejny, który jest pomiędzy nimi (half-open, półotwarty). Wspomniałem wyżej, że nie chcemy manualnie zamykać obwodu po awarii. Prostym rozwiązaniem jest dopuszczanie ograniczonej liczby zapytań, które będą stanowić tak naprawdę test usług. Jeśli  zakończą się powodzeniem, wtedy możemy przypuszczać, że awaria została naprawiona i możemy dopuścić już wszystkie zapytania poprzez całkowite zamknięcie obwodu.

W przyszłym wpisie pokażę prostą implementację wzorca w C#.  Wszystko zależy od potrzeb i w zależności od tego, implementacja wzorca może być bardziej lub mniej skomplikowana. Zdecydowanie należy wziąć pod uwagę wielowątkowość i wynikające z tego zagrożenia typu stampede czy lock convoy.

Kolejnym ważnym aspektem jest wykonywanie logów. W końcu jest to doskonały punkt na wszelkiego typu monitoring, audyt, logging itp. Zdecydowanie stanowi to dobry punkt do czerpania informacji o sposobie wykorzystania naszego systemu.

Innym wyzwaniem jest łapanie konkretnych wyjątków. Nie chcemy wszystkich traktować jednakowo bo choćby po statusie HTTP, możemy domyślić się czy awaria potrwa kilka minut czy godzin. Bardzo możliwe, że same wysłanie zapytania zakończyło się błędem i w tym przypadku, jakakolwiek próba wysłania kolejnego zapytania nie ma sensu.

Istnieje wiele strategii zamykania obwodu po awarii. Może być to np. okresowe zamykanie, ping’owanie usługi w celu sprawdzenia jej stanu czy wspomniany mechanizm half-open.

Kompatybilność usług: consumer-driven contracts

Ostatnio na blogu sporo o SOA. W poprzednim wpisie, poruszyłem temat kompatybilności, teraz czas na coś, co ma na celu zminimalizowanie ilości różnych wersji usług. Najprostszym sposobem na uniknięcie problemów z kompatybilnością jest po prostu nie wprowadzenie niekompatybilnych zmian.

Consumer-driven contracts to prosty sposób, aby mieć pod kontrolą śledzenie zmian oraz ich wpływ  na konsumentów naszej usługi.

Zwykle usługi posiadają pewien schemat (schema) czyli po prostu kontrakt. Najbardziej restrykcyjną polityką jest dopasowywanie kontraktu posiadanego przez klienta z tym dostępnym aktualnie po stronie serwera. Jeśli nie są one takie same, wtedy uznawane jest, że klient jest niekompatybilny z serwerem.

Powyższa strategia jest nieco zbyt restrykcyjna. W końcu dodanie nowej metody do API nie powinno mieć wpływu na klientów. Dlatego zwykle uznaje się, że schemat (schema) można rozszerzać o nowe wpisy czy pola. Jeśli np. encja Person zawiera FirstName oraz LastName, wtedy dodanie nowej właściwości do niej (np. MiddleName) nie powinno mieć znaczenia. W końcu taka zmiana, nie powinna zaszkodzić aktualnym klientom.

Co jednak z usunięciem pola? Taka zmiana w standardowym SOA jest uznawana za łamiącą kompatybilność i w przypadku semantycznego wersjonowania, powinno zwiększyć się segment “Major”.  Jest to oczywiście naturalne i logiczne ponieważ istnieje ryzyko, że ktoś korzysta np. z FirstName i usunięcie tego pola jest niedopuszczalne.

Consumer-driven contracts wychodzą  z założenia, że często nawet usunięcie pola nie powinno wiązać się ze zmianą “major”. Jeśli nikt z niego aktualnie nie korzysta, to po co tym martwić się? Wystarczy, że będziemy wiedzieć jak nasz serwis jest używany. Konsumenci kształtują nasze API, stąd nazwa consumer driven contracts.

Klienci definiują zatem ich wymagania za pomocą testów jednostkowych. Test jednostkowy może sprawdzać czy dane pole występuje w schema.

Następnie serwis bierze napisane testy przez konsumentów i wykonuje je (jako część CI) na realnym serwisie. Jeśli jakieś pole zostało usunięte, a było wykorzystywane przez jednego z konsumentów, test zakończy się oczywiście błędem. Po stronie serwisu możemy zatem dokonać jakichkolwiek zmian (usunięcie, dodanie nowych pól itp) dopóki żaden z testów “konsumenckich” nie zostanie popsuty.

Konsumenci zwykle piszą klasyczne testy jednostkowe, które wykonywane są na mock’u, a nie realnym serwisie. Serwis z kolei, zbiera wszystkie testy konsumentów i wykonuje normalny, integracyjny test. Jakakolwiek modyfikacja API jest teraz bezpieczna i przewidywalna jeśli chodzi o rezultat.

W jaki sposób piszemy testy to już zależy od konkretnego API. Możemy sprawdzać XML\JSON czy dane pole istnieje. W zależności od konkretnego protokołu inaczej to będzie wyglądać. Cel jest zawsze ten sam – wyrazić jakie pola i elementy są wymagane dla danego konsumenta i ich zmiana musi zakończyć się wydaniem nowej wersji ze zmianą “major”.

Kompatybilność usług

Kompatybilność usług jest problemem w każdej architekturze SOA, ale w przypadku mikro-usług staje się jeszcze bardziej widoczna. W przyszłości chce napisać post o tzw. consumer-driven contracts, które znacząco mogą zminimalizować potrzebę wersjonowania usług. W każdym razie, bardzo prawdopodobne, że w pewnym momencie zajdzie potrzeba wprowadzenia zmiany, która nie jest kompatybilna wstecz.

Jeśli nasz system składa się np. z 20 usług to musimy mieć mechanizm, który zagwarantuje nam, że nie wprowadzimy zmiany w którejś z usług, która spowoduje, że całość przestanie działać. Jednym z rozwiązań jest tzw. semantyczne wersjonowanie (semantic versioning).

Idea jest bardzo prosta. Każda z usług jest opisana wersją w następującym formacie:

Major.Minor.Patch (przykład 3.5.15)

Major to najbardziej dotkliwa zmiana wersji i dotyczy funkcjonalności, która nie jest kompatybilna z poprzednimi wersjami. Usługa 3.5.16 nie jest kompatybilna z 2.5.16.

Minor oznacza nową funkcjonalność, ale jest ona kompatybilna z poprzednimi wersjami. Może to dotyczyć zatem dodania nowej metody do usługi, co naturalnie nie powinno mieć znaczenia dla starszych klientów.

Patch dotyczy drobnych zmian typu naprawienie błędu, refaktoryzacja itp.  Naturalnie jest to najbezpieczniejsza zmiana ponieważ nie zmienia nawet API\kontraktu.

Proszę zauważyć, że nie możemy polegać na standardowym wersjonowaniu, gdzie każdy commit zmienia patch. Zwykle, w wiadomości każdego commit’a podaje się numer user story albo błędu. Następnie CI może na podstawie tego, zdecydować, który segment wersji należy zmienić (major, minor, patch).

Możliwe jest również doczepianie tagu, np. 3.5.15-prealpha albo 3.5.16-release. Dla dzisiejszego postu nie ma to jednak znaczenia.

Wiemy zatem, że każda usługa jak i klient powinien mieć wersję. Konsument takiej usługi powinien mieć test jednostkowy sprawdzający jego wersje klienta z aktualnie dostępną wersją usługi. Jeśli wersja major konsumenta i usługi są różne, to wykonanie takiego testu powinno zakończyć się błędem. Dzięki temu, mamy pewność, że usługi w systemie są ze sobą kompatybilne i w momencie niekontrolowanej zmiany wersji, zostanie to wykryte w CI. Bez takich testów jednostkowych i semantycznego wersjonowania byłoby ciężko mieć kontrolę nad wszystkimi usługami i każda zmiana niosła by za sobą ryzyko, że coś zostanie popsute.

Richardson Maturity Model, warstwa 3 – HATEOAS

Dzisiaj ostatnia warstwa modelu, która zdecydowanie często jest pomijana w implementacjach REST. Moim zdaniem, w przypadku publicznych API jest bardzo ważna, szczególnie w środowisku mikro-serwisów, gdzie nawigacja jest utrudniona ze względu na liczbę usług.

HATEOAS to skrót od Hypertext As The Engine Of Application State. Mechanizm dostarcza możliwość nawigacji przez zasoby bez wiedzy o konkretnych adresach URL.  Załóżmy, że mamy bazę klientów w systemie i możemy w niej:

  1. Wylistować listę klientów.
  2. Zwrócić dane konkretnego klienta.
  3. Aktualizować dane.
  4. Usuwać klienta z bazy.
  5. Zwracać poszczególne dane takie jak dane kontaktowe, adres itp.

Projektując serwis zgodnie z poprzednimi zasadami, otrzymalibyśmy następujące linki:

  1. GET /customers
  2. GET /customers/{id}
  3. PUT /customers i w ciele dane klienta
  4. DELETE /customers/{id}
  5. GET customers/{id}/email, customers/{id}/address itd.

Bez warstwy trzeciej, klient musi znać te linki. Innymi słowy, klient musi znać implementację wewnętrzną serwera i w przypadku jakiejkolwiek zmiany, wszyscy klienci muszą zaktualizować kod. Klient tak naprawdę powinien tylko wiedzieć co chce zrobić, a nie jak to ma zrobić.

W przypadku hateoas, konsument musi znać dane tylko korzenia. Załóżmy, że wykonujemy zapytanie HTTP GET /customers, aby wylistować wszystkie osoby w bazie. Jako odpowiedz przyjdzie coś w rodzaju:

HTTP/1.1 200 OK <Customers> <Customer FirstName="Piotr" Last="Zielinski" Id="1"> <link rel = "details" uri = "/customers/1"/> <link rel = "address" uri = "/customers/1/address"/> </Customer> <Customer FirstName="afaf" Last="sfsagsa" Id="2"> <link rel = "details" uri = "/customers/2"/> <link rel = "address" uri = "/customers/1/address"/> </Customer> </Customers>

Analogicznie, dodając nową osobę do bazy czyli HTTP POST /customers/, dostaniemy w odpowiedzi również mapę linków:

HTTP/1.1 201 Created <Customer FirstName="Piotr" Last="Zielinski" Id="1"> <link rel = "details" uri = "/customers/1"/> <link rel = "address" uri = "/customers/address"/> </Customer>

Jak widzimy, to serwer decyduje, co klient może zrobić. Odpowiedź określa również aktualny stan aplikacji, czyli co możemy ze zwróconymi zasobami, w danym momencie zrobić.  API zaprojektowane w ten sposób ma zatem wbudowany mechanizm discovery – przekazujemy konsumentowi tylko korzeń, a on sam już jest w stanie przeglądać i wykonywać dowolne operacje na zasobach.

ASP.NET Swashbuckle – Swagger

W dwóch postach poruszałem już temat dokumentacji usług REST. Ręczne tworzenie plików JSON dla swagger jest dosyć czasochłonne i łatwo potem zapomnieć przy jakiś modyfikacjach o aktualizacji dokumentacji.

Dla ASP.NET MVC WebAPI na szczęście jest Swashbuckle. Zacznijmy od instalacji odpowiedniego pakietu:

Install-Package Swashbuckle

Zobaczymy, że został dodany m.in. plik SwaggerConfig.cs. Wpisując teraz adres /swagger np. “http://localhost:35447/swagger” ujrzymy automatycznie wygenerowaną dokumentację:

image

Polecam również włączenie komentarzy XML, jak to było w przypadku ASP.NET HelpPages. Przechodzimy do właściwości projektu i zaznaczamy XML Documentation File:

image6.png (1024×632)

Następnie możemy odkomentować następującą linię:

c.IncludeXmlComments(GetXmlCommentsPath());

Gdzie GetXmlCommentsPath to:

private static string GetXmlCommentsPath() { return System.String.Format(@"{0}\App_Data\XmlDocument.xml", System.AppDomain.CurrentDomain.BaseDirectory); }

Teraz w komentarzach możemy określić parametry, zwracane kody itp.:

public class SampleController : ApiController { /// <summary> /// Any method description /// </summary> /// <param name="id">Id of something</param> /// <remarks>Any remarks</remarks> /// <response code="400">Bad request</response> /// <response code="500">Internal Server Error</response> public int Test(int id) { return 0; } }

Przechodząc do Swagger, zobaczymy:

image

Co to jest usługa REST? Richardson maturity model oraz poziomy 0,1,2.

Martin Fowler, kilka lat temu pisał o tzw. Richardson maturity model, którego autorem jest tak naprawdę Leonard Richardson. Groźnie brzmiąca nazwa, jak zwykle nie opisuje nic bardzo skomplikowanego. Nie mniej jednak, model ten doskonale opisuje założenia usług RESTful. W zasadzie nie ma framework’ów, które wymuszałyby poprawną implementację REST, stąd niezbędne jest zrozumienie jakie są założenia tych usług. Programiści zbyt często luźno interpretują pojęcie REST. Moim zdaniem, w momencie, gdy REST wchodził, nie było jasnej definicji wymagań i założeń.

Model jest podzielony na cztery poziomy (Level 0, Level 1, Level 2, Level 3). Jeśli zaimplementujemy wszystkie z nich, wtedy możemy mówić, że w pełni implementujemy model. Nie znaczy to jednak, że nasza usługa będzie automatycznie REST, ponieważ jest to dużo bardziej złożone niż przedstawiany tutaj model. Moim zdaniem są to po prostu fundamenty usługi. Nie znaczy to również, że bez poziomu trzeciego, nasza usługa jest zła – wszystko zależy od wymagań.

Poziom 0

Zacznijmy od zerowego poziomu. Mówi on po prostu to, że usługa REST to nie nowy protokół, a całość komunikacji jest już oparta na istniejącym protokole. Najczęściej jest to po prostu tunelowanie HTTP, czyli wykorzystanie HTTP z odpowiednim nagłówkiem i ciałem. Przykładowo, aby dodać nowego klienta do bazy, możemy wysłać:

POST /customerService HTTP/1.1 <AddCustomer FirstName="Piotr" LastName="Zielinski"/>

Jako odpowiedź dostaniemy wtedy HTTP 200. Na poziomie zerowym, będzie zawsze to 200. W przypadku niepowodzenia operacji, po prostu błąd będzie dołączony jako opis (np. w formancie  XML), ale status zawsze będzie 200.

Jeśli chcemy zwrócić informacje o konkretnym kliencie, również wysyłamy zapytanie za pomocą HTTP:

POST /customerService HTTP/1.1 <GetCustomer id=1/>

Odpowiedź:

HTTP/1.1 200 OK <Customer FirstName="Piotr" LastName="Zielinski"/>

Jak widzimy, HTTP służy nam do komunikacji. W ciele wstawiamy nasze zapytanie. Nie jest to nic innego jak RPC, czyli zdalne wywoływanie procedur. Można to porównać do klasycznych usług, gdzie wywołujemy po prostu konkretne metody na danej usłudze.

Poziom 1

Poziom pierwszy wprowadza definicje zasobów. Nie będziemy już wysyłać zapytań do jednego punktu, ale usługa będzie tworzyć hierarchie zasobów. Innymi słowy, aby zwrócić dane klienta, wystarczy:

POST /customerServices/customers/1 HTTP/1.1 <GetCustomer/>

Proszę zwrócić uwagę na adres. Dane uzyskujemy wysyłając zapytanie do konkretnego adresu, a nie głównego, jak to było  w warstwie zerowej. Innymi słowy, aby np. zwrócić numer telefonu klienta o identyfikatorze 1, wysyłamy:

POST /customerServices/customers/1/phone HTTP/1.1 <GetPhoneNumber/>

W przypadku poziomu zerowego, byłoby to:

POST /customerServices HTTP/1.1 <GetCustomerPhone id="1"/>

Kluczami do zrozumienia poziomu 1, są słowa “hierarchia” oraz “zasoby”. Nie odwołujemy się do głównego adresu, ale konkretne zapytania tworzą po prostu drzewo zasobów.

Poziom 2

Proszę zauważyć, że wszystkie powyższe zapytania korzystały wyłącznie z HTTP Post. Poziom drugi rozpoznaje typ zapytania, tzn. POST, PUT, GET, DELETE. W celu zwrócenia danych konkretnego klienta wystarczy:

GET /customerServices/customers/1 HTTP/1.1

GET służy zatem do zwracania danych i nie trzeba już tego określać w HTTP body za pomocą nazwy komendy, jak to było w poprzednich warstwach. HTTP PUT zwykle służy do aktualizacji danych, POST do wstawiania nowych wierszy, a DELETE naturalnie do ich usuwania.

Ponadto jest różnica w jaki sposób zwracamy odpowiedź. W poprzednich warstwach było to po prostu HTTP 200 OK plus ewentualnie jakiś opis. Teraz chcemy dostarczyć bardziej dokładną odpowiedź. Na przykład, po dodaniu klienta dostaniemy:

HTTP/1.1 201 Created Location: /customers/1

Jak widać, operujemy tutaj na statusie HTTP i nie zwracamy tylko 200, a np. 201, gdy nowy zasób został dodany. Ponadto dobrze zwrócić również lokalizacje właśnie dodanego zasobu (Location). Zamiast zwracania treści błędu, jak to miało miejsce na poziomach 01,2, zwracamy konkretny status HTTP, który ma już dobrze znaną definicję ze standardów webowych.

Do opisania pozostał jeszcze poziom 3, ale tym zajmiemy się w kolejnym poście bo jest on nieco bardziej skomplikowany.

Z dzisiejszego postu powinno jasno wynikać, że usługi REST to kompletnie inna idea niż usługi SOAP i klasyczny RPC. Tutaj nie chodzi po prostu o wywołanie jakieś metody na usłudze.

Podsumowując:

1. Poziom  0 – REST powinien być oparty na istniejącym protokole np. HTTP.

2. Poziom 1 – Zasoby oraz ich hierarchia.

3. Poziom 2 – HTTP verbs oraz HTTP status codes.