ASP.NET Swashbuckle – Swagger

Posted April 21st, 2015 by Piotr Zieliński
Categories: Patterns & Practices, Programowanie rozproszone

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.

Posted April 18th, 2015 by Piotr Zieliński
Categories: Patterns & Practices, Programowanie rozproszone

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

Posted April 15th, 2015 by Piotr Zieliński
Categories: ASP .NET

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ń

Posted April 12th, 2015 by Piotr Zieliński
Categories: ASP .NET, Patterns & Practices

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

Posted April 9th, 2015 by Piotr Zieliński
Categories: C#, Patterns & Practices

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

Posted April 6th, 2015 by Piotr Zieliński
Categories: Patterns & Practices, Programowanie rozproszone

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

Posted April 3rd, 2015 by Piotr Zieliński
Categories: C#

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.

Architektura enterprise: Mikro-serwisy

Posted March 24th, 2015 by Piotr Zieliński
Categories: Patterns & Practices

Od jakiegoś czasu wzorzec “microservices” jest popularny  w wielu firmach.  Szczegółowe informacje znajdują się na blogu Martin’a Fowler’a, aczkolwiek na blogu chciałbym naszkicować koncepcję.

Pomysł nie jest nowy, jednak pewne usystematyzowanie moim zdaniem ma sens. Dla mnie osobiście, to nic innego jak przestrzeganie zasady “Single rensponsibility” na poziomie usług. Wzorzec ma zastosowanie w systemach złożonych, SOA, a nie w prostych aplikacjach klient-serwer.

Skrajnie zła sytuacja, to taka, w której projekcie występuje jedna wielka usługa – np. napisana w WCF. Powoduje to wiele problemów, a mianowicie:

  1. Brak jasnej odpowiedzialności. Wyobraźmy sobie, że mamy 20 programistów i tylko jedną, monolitową usługę.
  2. Release jest bardzo czasochłonny. W celu wdrożenia, należy zebrać programistów z różnych zespołów, ponieważ usługa pokrywa tyle różnych dziedzin.
  3. “Zero-downtime deployment” jest bardzo utrudniony.
  4. Ewentualne wycofanie usługi z produkcji (rollback) jest praktycznie niemożliwe ponieważ należałoby przywrócić poprzednią wersję całego systemu.
  5. Jeśli jakiś komponent przestanie działać, całość systemu nie będzie pracować.  W przypadku mikro serwisów, można byłoby wyłączyć jedynie poszczególne węzły.
  6. Rozproszenie jest trudniejsze i mamy mniejszą kontrolę nad tym.
  7. Projekty Visual Studio zwykle są duże i trudne w utrzymaniu, aczkolwiek można oczywiście tworzyć mini projekty i je eksponować za pomocą jednej usługi. Zwykle łatwiej jest utrzymać jednak porządek w przypadku kilku niezależnych usług.
  8. Ciężko wdrożyć kulturę DevOps ze względu na brak jasnej odpowiedzialności.
  9. Usługa jest wspierana wyłącznie przez kilka osób. Z tego względu, łatwiej utrzymać standardy oraz wizję w jaką stronę kod powinien pójść, niż w przypadku dużych usług modyfikowanych przez wiele zespołów (często z wielu krajów).

Mikro-serwisy, jak sama nazwa sugeruje, polega na tworzeniu wielu usług, które są niezależne od siebie.  Zamiast tworzyć jedną usługę, która ma metody typu GetCustomerInfo i GetCustomerAddress, tworzymy kompletnie dwa inne serwisy.

Przez wspomnianą niezależność mam na myśli:

  1. Osobne repozytorium dla każdego mikro serwisu.
  2. Osobny projekt\solucja.
  3. Osobny proces budowania i wdrażania (CI pipelines).
  4. Możliwość hostowania na osobnych maszynach.

Ostatni punkt wymagana pewnego sprostowania. Bardzo często mikro serwisy hostuje się na tym samym serwerze, ale ze względu, że są to osobne usługi to nic nie stoi na przeszkodzie aby było inaczej.

Pojedyncza usługa powinna być na tyle mała, że jeden zespół (tzn. np. 5 programistów) potrafi utrzymać kilka z nich. Oczywiście istnieją pewne wyzwania i problemy, które nie istnieją w przypadku jednej, wielkiej usługi:

  1. Cross-cutting concerns – czyli wszelki kod występujący jednocześnie w wielu usługach. Przykładami są autoryzacja, audyt, DAL czy po prostu logi. Nie chcemy w końcu za każdym razem pisać od nowa DAL. Rozwiązaniem jest po prostu wewnętrzne repozytorium NuGet i wszelki kod współdzielony trzymać tam w paczkach.
  2. Kompatybilność – mamy tak wiele usług i należy zaimplementować pewien mechanizm umożliwiający weryfikację tego. Jak to zrobić, napiszę w przyszłych postach, należy jednak mieć na uwadze, że potencjalnie jest więcej miejsc, gdzie może dojść do konfliktu.
  3. Zarządzanie wdrożeniami –jak wspomniałem wcześniej, release moim zdaniem są łatwiejsze, ale w tej sytuacji należy mieć inne podejście niż w tradycyjnym, monolitowym wdrażaniu.
  4. Komunikacja między różnymi procesami jest wolniejsza niż lokalne wywołania metod. Z tego względu, należy zwrócić uwagę na przepływ informacji, aby nie pisać metod, które niewiele robią (“chatty interface”). Jeśli piszemy mikro serwisy od nowa, taki problem zwykle nie występuje. Sytuacja jednak może być kłopotliwa jeśli próbujemy zrefaktoryzować istniejący już system.

Myślę, że warto jeszcze raz podkreślić, że usługi powinny być niezależne od siebie. Jeśli któryś zespół chce korzystać z Dapper zamiast EntiyFramework, nic nie powinno stać na przeszkodzie.

Dokumentacja REST API – Swagger

Posted March 21st, 2015 by Piotr Zieliński
Categories: Ogólne, Patterns & Practices

Dzisiaj chciałbym pokazać Swagger, doskonałe narzędzie służące do dokumentacji REST API.

Swagger, korzysta z plików JSON, którymi można opisać nasze API. W przypadku RESTful api m.in. możemy określić:

  1. Nazwę zasobu
  2. Typ zwracanych danych
  3. Opis obiektów
  4. Wersję API
  5. Zwracane kody statusu HTTP
  6. Parametry (np. query lub HTTP body)
  7. HTTP verbs (PUT, GET, POST etc)

Gotowy, przykładowy plik opisujący API możemy znaleźć tutaj:

http://petstore.swagger.io/v2/swagger.json

Oczywiście sam plik nie jest zbyt łatwy w odczycie dlatego zwykle korzysta się z Swagger-UI. Jest to prosta aplikacja internetowa, która wizualizuje wspomniany plik JSON. Dzięki niej możemy odczytywać dokumentację, jak i testować różne zapytania. Przykład:

http://petstore.swagger.io

Screenshot:

image

Pozostaje wyjaśnić jak stworzyć wspomniane pliki JSON. Ręczne edytowanie w pliku tekstowego jest dość mozolne.  Do dyspozycji mamy edytor online:

http://editor.swagger.io/#/

Screenshot:

image

Modyfikując plik, na bieżąco widać po prawej strony rezultat (podgląd).

Plik json opisujący API powinien być hostowany razem z usługą. Z kolei w przypadku Swagger-UI  jest to niekonieczne. Można np. mieć jedno repozytorium na całą firmę i tam trzymać swagger-ui, który wskazuje do poszczególnych usług. Inna możliwość to trzymanie swagger-ui razem z usługą i JSON.

Istnieje wiele frameworków, które integrują  Swagger z konkretną technologią np. ASP.NET MVC WebAPI. Wtedy framework wygeneruje JSON na podstawie np. komentarzy i analizy kodu. Bardzo wygodne i dzięki temu unikniemy rozbieżności między dokumentacją, a faktycznym stanem usługi.

Niestety nie znalazłem (na dziej dzisiejszy) dobrego narzędzia dla Nancy. Istnieje pewien projekt (wciąż w wersji PreAlph), ale nie jest zbyt stabilny:

https://github.com/khellang/Nancy.Swagger

Większość usług nad którymi pracuje jest napisana w Nancy, dlatego wciąż niestety edytuję plik ręcznie za pomocą przedstawionego wcześniej edytora online.

NSubstitute–dobra alternatywa dla Moq

Posted February 28th, 2015 by Piotr Zieliński
Categories: C#, Patterns & Practices

Zwykle używałem Moq w swoich projektach, ale ostatnio miałem do czynienia z NSubstitute i jego składnia dużo bardziej podoba mi się.

Standardowo instalujemy pakiet NuGet:

image

Załóżmy, że mamy interfejs:

interface IPersonRepository { Person GetById(int personId); void Add(Person person); }

Konfiguracja metod jest bardzo prosta i naturalna:

IPersonRepository personRepositoryMock = Substitute.For<IPersonRepository>(); personRepositoryMock.GetById(1).Returns(new Person() { FirstName = "test" }); Person person = personRepositoryMock.GetById(1);

W przypadku Moq, mamy do czynienia z wyrażeniami lambda, które nie są aż tak bardzo przejrzyste jak powyższy kod.

Podobnie sprawa wygląda z weryfikacją, czy jakaś metoda została wykonana:

personRepositoryMock.Received(5).GetById(Arg.Any<int>());

Analogiczną metodą jest DidNotReceived:

personRepositoryMock.DidNotReceive().GetById(Arg.Any<int>());

Możliwe jest również odpalenie zdarzenia za pomocą Raise.EventWith:

IPersonRepository personRepositoryMock = Substitute.For<IPersonRepository>(); bool wasCalled = false; personRepositoryMock.SampleEvent += (sender, args) => wasCalled = true; personRepositoryMock.SampleEvent += Raise.EventWith(new object(), new EventArgs());

Ze względu, że nie mamy do czynienia z lambda, wsparcie ze strony resharper jest lepsze i moim zdaniem łatwiej robić TDD.