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.

REST API: Dokumentacja w ASP.NET Web API

Jakiś czas temu, pisałem o Swagger, jako sposobie na dokumentacje REST API. Dzisiaj chciałbym pokazać kolejny mechanizm na generowanie dokumentacji, tym razem napisany przez Microsoft i dostępny od razu w ASP.NET. Od kilku lat jest on już dostępny bez żadnych dodatkowych instalacji.

Jeśli uruchomimy przykładową aplikację WebAPI, zobaczymy w prawym górnym rogu link do API:

image

Po kliknięciu w link API, zobaczymy wygenerowaną dokumentację:

image

Analogicznie do Swagger, możemy kliknąć na danej operacji i zobaczyć szczegóły:

image

UI możemy modyfikować. Jeśli przełączymy się do solucji, to zobaczymy, że całość jest umieszczona w Areas:

image

Najlepsze w tej dokumentacji jest to, że jest generowana na podstawie komentarzy. Nie musimy zatem edytować żadnych zewnętrznych plików JSON a dokumentacja sama jest bardzo blisko kodu (w formie komentarzy).

Przejdźmy zatem do kontrolera i dodajmy jakiś komentarz:

// GET api/Account/ManageInfo?returnUrl=%2F&generateState=true /// <summary> /// Przykladowy komentarz /// </summary> /// <param name="returnUrl">Parameter 1 blabla</param> /// <param name="generateState">Paramter 2 blablabla</param> /// <returns></returns> [Route("ManageInfo")] public async Task<ManageInfoViewModel> GetManageInfo(string returnUrl, bool generateState = false)

Jeśli skompilujemy teraz kod i przejdziemy do dokumentacji niestety nic nie zobaczymy. Domyślnie nie są analizowane komentarze XML. Musimy przejść do pliku HelpPageConfig i odkomentować następującą linie kodu:

config.SetDocumentationProvider(new XmlDocumentationProvider(HttpContext.Current.Server.MapPath("~/App_Data/XmlDocument.xml")));

Następnie otwieramy właściwości projektu i w zakładce Build włączamy generowanie komentarzy XML do App_Data/XmlDocument.xml:

image

Po odpaleniu strony zobaczymy komentarze:

image

Jeśli kogoś interesuje jak działa powyższy mechanizm generowania dokumentacji zachęcam do poczytania o klasie ApiExplorer.

W każdym razie, dzięki temu mechanizmowi (ASP.NET HelpPages) nie rozsynchronizujemy pliku dokumentacji jak to możliwe było w przypadku Swagger.

Do Swagger jeszcze chcę powrócić w przyszłym poście, ponieważ istnieje wsparcie dla ASP.NET WebAPI i również większość rzeczy może być automatycznie generowane.

REST Batching: Ograniczanie liczby zapytań

Kilka wpisów wcześniej zacząłem tematykę micro-serwisów oraz wzorca bramki. Jednym z wyzwań podczas rozłupywania monolitu jest zbyt wysoka liczba zapytań do innych serwisów, co powoduje utratę wydajności. 

Jeśli w monolicie była klasa np. CustomersRepository to teraz będzie to kompletnie nowa usługa. Wysłanie wiadomości do takiej usługo odbywa się przez jakiś protokół – w przypadku REST zwykle jest to HTTP. W monolicie nie było ważne to, że wywołaliśmy np. GetCustomerById(1), potem GetCustomerById(2) itp. Mam na myśli, że wywołania do repozytorium były bardzo tanie ponieważ odbywały się w pamięci, w tym samym procesie.

W przypadku HTTP, wiąże to się z ogromnym obciążeniem ze względu np. na potrzebę wysyłania HTTP header za każdym razem. Dlaczego więc nie wysyłać kilku zapytań w jednym pakiecie?

Taki mechanizm nazywa się po prostu batching. Załóżmy, że mamy następujący kontroler REST API:

public class CustomersController : ApiController { public int GetCustomerById(int id) { Random random = new Random(id); return random.Next(100); } }

Jeśli chcemy dane klientów o identyfikatorach 2 i 3, możemy w batchu wysłać je w następujący sposób:

POST http://piotr-pc:8289/api/$batch HTTP/1.1 Content-Type: multipart/mixed; boundary="8530da6b-1778-487a-a058-e78640a096e0" Host: piotr-pc:8289 Content-Length: 336 Expect: 100-continue Connection: Keep-Alive --8530da6b-1778-487a-a058-e78640a096e0 Content-Type: application/http; msgtype=request GET /api/customers/1 HTTP/1.1 Host: piotr-pc:8289 --8530da6b-1778-487a-a058-e78640a096e0 Content-Type: application/http; msgtype=request GET /api/customers/2 HTTP/1.1 Host: piotr-pc:8289 --8530da6b-1778-487a-a058-e78640a096e0--

Widzimy, że mamy jeden nagłówek, a potem w HTTP body przekazujemy konkretne zapytania, w tym przypadku /api/customers/1 oraz /api/customers/2.

Odpowiedz z kolei również przyjdzie w jednej paczce i wygląda następująco:

HTTP/1.1 200 OK Cache-Control: no-cache Pragma: no-cache Content-Length: 366 Content-Type: multipart/mixed; boundary="996affb8-9317-4fbd-8899-8cce1223b023" Expires: -1 Server: Microsoft-IIS/8.0 X-AspNet-Version: 4.0.30319 X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcUGlvdHJcRG93bmxvYWRzXFNwb3RsaWdodERldmVsb3BlclRlc3RcTGltZXJpY2tcV2ViQXBwbGljYXRpb24xXFdlYkFwcGxpY2F0aW9uMVxhcGlcJGJhdGNo?= X-Powered-By: ASP.NET Date: Sun, 12 Apr 2015 19:24:25 GMT --996affb8-9317-4fbd-8899-8cce1223b023 Content-Type: application/http; msgtype=response HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 24 --996affb8-9317-4fbd-8899-8cce1223b023 Content-Type: application/http; msgtype=response HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 77 --996affb8-9317-4fbd-8899-8cce1223b023--

W tym przypadku są to liczby 24 i 77.

ASP.NET MVC wspiera powyższy mechanizm i wystarczy w pliku konfiguracyjnym określić routing:

config.Routes.MapHttpBatchRoute( routeName: "WebApiBatch", routeTemplate: "api/$batch", batchHandler: new DefaultHttpBatchHandler(GlobalConfiguration.DefaultServer));

I to wszystko! Nie musimy męczyć się z żadną własną implementacją. W przypadku klienta, może to wyglądać następująco:

const string baseAddress = "http://piotr-pc:8289"; HttpClient client = new HttpClient(); var batchRequest = new HttpRequestMessage(HttpMethod.Post, baseAddress + "/api/$batch") { Content = new MultipartContent() { new HttpMessageContent(new HttpRequestMessage(HttpMethod.Get, baseAddress + "/api/customers/1")), new HttpMessageContent(new HttpRequestMessage(HttpMethod.Get, baseAddress + "/api/customers/2")) }}; HttpResponseMessage batchResponse = client.SendAsync(batchRequest).Result; MultipartStreamProvider streamProvider = batchResponse.Content.ReadAsMultipartAsync().Result; foreach (var content in streamProvider.Contents) { HttpResponseMessage response = content.ReadAsHttpResponseMessageAsync().Result; Console.WriteLine(response.Content.ReadAsStringAsync().Result); }

Domyślnie każde zapytanie z batch’a jest wykonywane sekwencyjne. W przypadku powyższego scenariusza nie ma to sensu. Nic nie stoi na przeszkodzie, aby jednocześnie, z dwóch różnych wątków pobierać dane. Wystarczy zmienić konfigurację na:

aultHttpBatchHandler(GlobalConfiguration.DefaultServer) { ExecutionOrder = BatchExecutionOrder.NonSequential }; config.Routes.MapHttpBatchRoute( routeName: "WebApiBatch", routeTemplate: "api/$batch", batchHandler: batchHandler);

Jeśli powyższy batching z jakich względów nie odpowiada nam, możemy zawsze napisać własny, na przykład dziedzicząc po  DefaultHttpBatchHandler.

Noda Time: testy jednostkowe oraz obsługa czasu

W dzisiejszym poście pokażę bibliotekę Noda Time. Generalnie jest ona stworzona, aby zastąpić DateTime, który często powoduje problemy. Dzisiaj jednak, chciałbym pokazać Noda Time na przykładzie testów jednostkowym, bo to jest miejsce, gdzie DateTime po prostu nie nadaje się do użycia (przynajmniej bezpośrednio).

Załóżmy, że mamy w kodzie taką metodę:

class Sample { public void DoSomething(DateTime dateTime) { DateTime now = DateTime.Now; if(dateTime>now) Console.WriteLine("{0}>{1}",dateTime,now); else Console.WriteLine("{0}<={1}", dateTime, now); } }

Przykład, jak to zwykle na tym blogu bez sensu, ale chodzi mi o odwołanie do DateTime.Now. Jak teraz taką metodę można przetestować? Jednym rozwiązaniem (brzydkim) jest użycie Microsoft Fakes – http://www.pzielinski.com/?p=2066.

W praktyce, odradzam Shims i zawsze powinniśmy preferować stub’y\mock’i. Zainstalujmy NodaTime i zobaczymy jak można to naprawić:

class Sample { private readonly IClock _clock; public Sample(IClock clock) { _clock = clock; } public void DoSomething(Instant dateTime) { Instant now = _clock.Now; if(dateTime>now) Console.WriteLine("{0}>{1}",dateTime,now); else Console.WriteLine("{0}<={1}", dateTime, now); } }

Widzimy, że nie mamy żadnych statycznych wywołań. IClock reprezentuje abstrakcję czasu. Możemy wstrzyknąć zegar systemowy lub mock, co zrobimy zaraz w teście:

var fakeClock = Substitute.For<IClock>(); fakeClock.Now.Returns(Instant.FromUtc(2013, 1, 2, 0, 0)); Sample sample = new Sample(fakeClock); sample.DoSomething(Instant.FromUtc(2013,1,1,0,0));

Możemy również użyć czasu systemowego:

Sample sample = new Sample(SystemClock.Instance); sample.DoSomething(Instant.FromUtc(2013,1,1,0,0));

Myślę, że każdy z nas pisał wrapper’y na DateTime w celu implementacji testów jednostkowych. Dzięki Noda Time nie musimy tego robić i dostajemy ponadto kilka innych możliwości manipulowaniem czasem, którego brakowało w standardowym DateTime.

Mikro-serwisy: wzorzec gateway

Największa zaleta mikro-serwisów, a mianowicie pojedyncza odpowiedzialność, często bywa również problemem, a raczej wyzwaniem. Załóżmy, że nasz system ma następujący mikro-usługi:

  1. CustomerService – podstawowe informacje o klientach
  2. AddressService – wyszukiwarka adresów
  3. CreditCardDetails – dane o kartach

Nie chce wymieniać tutaj długiej listy, ale wyobraźmy sobie, że mamy 10 usług co jest normą w przypadku mikro-serwisów. Często w celu wykonania jednej operacji (np. dokonanie płatności w sklepie internetowym), musimy zgromadzić informacje z różnych usług – np. w celu wystawienia faktury.

Oznacza to, że nasz klient będzie musiał łączyć się z wszystkimi nimi, w celu uzyskania danych. Rozwiązanie jest nie tylko wolne (liczba połączeń), ale i brzydkie – klient musi znać wewnętrzna architekturę naszych usług. Często kod po stronie klienta może przypominać spaghetti ze względu, że odwołuje się do tylu różnych usług

Innymi słowy, rozdrobnienie usług, a sposób w jaki klient je konsumuje zwykle jest różny. Dane o kliencie (ConsumerService) oraz o adresach zamieszkania (AddressService) zwykle konsumowane są jednocześnie i z punktu widzenia klienta, jest to nienaturalne, że pochodzą z dwóch różnych usług.

Z tego względu, warto rozważyć wzorzec gateway, który jest po prostu punktem centralnym i dostępowym do naszych mikro usług, o których klient nie powinien często nawet wiedzieć. Idea mikro-serwisów polega na możliwości ich wprowadzenia w każdym momencie, bez wielkich zmian po stronie klienta.

image

 

Jak widać z rysunku, implementacja gateway może sprowadzać się do zwykłego proxy. Klient wysyła zapytanie tylko do jednego punktu, a potem jest to już odpowiedzialność bramki, aby odpowiednio komunikować się z mikro-usługami.

Oczywiście nie możemy mieć logiki biznesowej w bramce. Jedynie co gateway zawiera to proxy, caching, autoryzacja i inne “cross-cutting concerns”, czyli problemy niezależne od usługi.

Gdybyśmy jakąś logikę wrzucili do gateway, to skończylibyśmy na starym monolitycznym SOA.

W następnych wpisach zajmiemy się implementacją bramki ponieważ nie jest to takie oczywiste. Często, szczególnie w przypadku REST, mamy do dyspozycji z różnymi typami klientów. Na przykład, aplikacja mobilna może nie potrzebować wszystkich danych na raz. Z tego względu, bramka powinna być zaimplementowana w sposób generyczny, eksponując różne API, w zależności od kontekstu.

Kolejnym zagadnieniem, którym  musimy się zając jest agregacja danych, nierzadko z różnych usług. Jeśli mamy 10 usług, to klient może zażądać dowolnej kombinacji (np. CustomerService,CreditCardDetails lub CreditCardsService, AddressService).

Wynika z tego, że będziemy musieli opracować elastyczne rozwiązanie bo oczywiście nie jesteśmy w stanie zaimplementować sami wszystkim metod, biorąc pod uwagę, że w każdej chwili infrastruktura mikro-usług może zmienić się.

Dzięki takiemu rozwiązaniu, zachowamy łatwość w utrzymaniu i wdrażaniu usług, a jednocześnie nie będziemy mieli spaghetti po stronie klienta.

Polly: przydatna biblioteka do obsługi błędów

Czasami zachodzi potrzeba ponownego wykonania jakiegoś kodu, w przypadku np. wyrzucenia błędu. Można samemu zaimplementować to za pomocą np. pętli, kontynuować daną operację w kolejnych iteracjach.

Problem w tym, że taki mechanizm można dość znacząco rozbudowywać. Zwykle, chcemy poczekać przed następną iteracją ponieważ szanse, że ponowna próba, natychmiast po pierwszej próbie zakończy się sukcesem jest niska. Ponadto, zdefiniowanie “niepowodzenia” też jest dość skomplikowane.

Polly to mała, ale dość rozbudowana biblioteka rozwiązująca powyższy problem. Zaczynamy od instalacji NuGet:

Install-Package Polly
W najprostszej postaci, konfiguracja może wyglądać następująco:
static void Main(string[] args) { var policy = Polly.Policy.Handle<DivideByZeroException>().Retry(); policy.Execute(DoSomething); Console.ReadLine(); } private static void DoSomething() { Console.WriteLine(DateTime.Now); throw new DivideByZeroException(); }

Na ekranie wtedy zobaczymy dwie próby:

image

Możemy również próbować w nieskończoność:

var policy = Polly.Policy.Handle<DivideByZeroException>().RetryForever(); policy.Execute(DoSomething);

Zwykle jest to zły pomysł i lepiej określić maksymalną liczbę prób:

var policy = Polly.Policy.Handle<DivideByZeroException>().Retry(5);

Jeśli chcemy czekać pomiędzy kolejnymi próbami, wtedy należy przekazać kolekcję TimeSpan:

var policy = Polly.Policy.Handle<DivideByZeroException>().WaitAndRetry(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3) });

WaitAndRetry przyjmuje jako parametr również metodę, określającą czas czekania dla poszczególnej iteracji:

var policy = Polly.Policy. Handle<DivideByZeroException>(). WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) );

Definiowane wyjątki można również filtrować:

Policy .Handle<SqlException>(ex => ex.Number == 1205)

Do dyspozycji jest metodą Or, która umożliwia łączenie warunków:

Policy .Handle<DivideByZeroException>() .Or<ArgumentException>()

Polly wspiera asynchroniczne wywołania za pomocą np. ExecuteAsync.