Wersjonowanie API
Gdy tworzysz własne API, powinieneś od razu pomyśleć o wersjonowaniu. Wprawdzie można je dodać później (podczas powstawania kolejnej wersji), jednak dużo wygodniej jest wszystko mieć zaplanowane od początku. W tym artykule pokażę Ci jak wersjonować WebAPI w .Net.
Na szybko
Jak zarejestrować wersjonowanie
- Pobierz NuGet: Microsoft.AspNetCore.Mvc.Versioning
- Zarejestruj serwisy:
builder.Services.AddApiVersioning(o =>
{
o.DefaultApiVersion = new ApiVersion(1, 0);
o.AssumeDefaultVersionWhenUnspecified = true;
o.ReportApiVersions = true;
});
- Dodaj atrybut ApiVersion do kontrolerów, mówiąc jakie wersje API obsługują:
[Route("api/[controller]")]
[ApiController]
[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("2.0")]
public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult Index()
{
return Content("Wersja 1");
}
[HttpGet]
[MapToApiVersion("2.0")]
public IActionResult Index_V2()
{
return Content("Wersja 2");
}
}
Jak przekazać informacje o wersji w ścieżce?
W taki sposób wywołasz API przez: /api/v1/users
Dodaj znacznik a trybucie Route kontrolerów:
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1.0", Deprecated = true)]
public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult Index()
{
return Content("Wersja 1");
}
}
Jak przekazać informacje o wersji w nagłówku?
- Upewnij się, że nie masz informacji o ścieżce w URLach (atrybut Route kontrolerów)
- Dodaj ustawienie do konfiguracji wersjonowania:
builder.Services.AddApiVersioning(o =>
{
o.DefaultApiVersion = new ApiVersion(2, 0);
o.AssumeDefaultVersionWhenUnspecified = false;
o.ReportApiVersions = true;
o.ApiVersionReader = new HeaderApiVersionReader("api-version");
});
Po co?
Być może masz jakieś wątpliwości, czy faktycznie potrzebujesz wersjonowania. Jeśli tworzysz aplikację dla siebie, na swój własny użytek i będziesz się do niej dobijał swoim własnym klientem – nie potrzebujesz wersjonowania. Ale jeśli tworzysz API, do którego będą dobijać się inne osoby/aplikacje, nawet korzystający z Twojego klienta, to koniecznie pomyśl o wersjonowaniu, bo dość szybko może się okazać, że Twoje API nie jest wstecznie kompatybilne. A to może prowadzić do problemów.
Jeśli tworzysz swoje portfolio to również pomyśl o wersjonowaniu. Pokaż, że znasz te mechanizmy i nie zawahasz się ich użyć.
Jak oznaczać wersje?
Istnieje dokument pokazujący jak stosować semantyczne wersjonowanie (SemVer). Osobiście uważam ten rodzaj wersjonowania za dość naturalne i powiem szczerze, że nawet nie wnikałem czy są inne OFICJALNE sposoby i czym się różnią. Chociaż pewnie są.
W skrócie, wersjonowanie semantyczne polega na tym, że masz 3 lub 4 liczby:
MAJOR.MINOR.PATCH
, np: 1.0.2
. Czasem pojawia się dodatkowo czwarta liczba oznaczana jako BUILD
. Posiada różne zastosowania i w tym artykule się nimi nie zajmujemy.
SemVer
mówi tak:
MAJOR
zmieniaj, gdy wprowadzasz zmiany NIEKOMPATYBILNE z poprzednimi wersjamiMINOR
– gdy dodajesz nowe funkcje, które są KOMPATYBILNE z APIPATCH
– gdy poprawiasz błędy, a poprawki są KOMPATYBILNE z API.
Teraz co to znaczy, że wersja jest kompatybilna lub nie?
W świecie aplikacji desktopowych to może być bardzo duży problem. Tam zmiana chociażby typu danych z int
na long
w modelu może być już niekompatybilna. Natomiast, jeśli chodzi o aplikacje internetowe, można całkiem bezpiecznie założyć, że jeśli nie dodajesz/usuwasz pól do modeli DTO ani nie zmieniasz wywołań endpointów, to wszelkie inne zmiany są kompatybilne. Jeśli uważasz, że jest inaczej – podziel się w komentarzu.
Zapraszam Cię do zapoznania się z dokumentacją SemVer. Jest dość krótka, a wiele wątpliwości może Ci rozjaśnić.
Jeśli chodzi o typowe API restowe, to tutaj raczej używa się wersjonowania MAJOR
lub MAJOR.MINOR
. Czyli przykładowo: /api/v1/endpoint
lub /api/v1.0/endpoint
– więcej informacji raczej nie ma sensu. Chociażby z tego powodu, że na serwerze nikt nie będzie utrzymywał wersji 1.0.5, tylko najczęściej 1, 2, 3…itd.
Której wersji API używa klient?
Do tego jest kilka podejść. Jeśli masz API Restowe lub „restowe” często wersję wkłada się do URL, np:
http://www.example.com/api/v2/user/123
http://www.example.com/api/v3/user/123
Niektórym się to bardzo nie podoba i uznają za złe, inni uważają, że to jest bardzo użyteczne, bo od razu widać do jakiej wersji dobija się klient. Osobiście wolę inne podejście – trzymanie numeru wersji w nagłówku zapytania. Przejdziemy przez oba podejścia.
Konwencja kontrolerów
Wersjonowanie API polega w skrócie na tym, że masz dwa kontrolery, wskazujące na ten sam endpoint, ale dla różnych wersji. Np: ścieżka /api/v1/users/123
powinna uruchomić inny kontroler niż /api/v2/users/123
. Oczywiście to nie jest wymóg, możesz trzymać wszystko w jednym kontrolerze i mieć burdel w kodzie. Po pewnym czasie coś być może pier****nie. Więc dobrą metodą jest tworzenie odpowiednich folderów dla poszczególnych wersji kontrolerów:
Kontrolery nie tylko są w osobnych katalogach, ale i w osobnych namespacach. Dlatego możesz je nazwać tak samo.
Takie rozdzielenie porządkuje Ci kod, ale są sytuacje, w których jest to nieco uciążliwe. Nie przeczę. W najprostszym przypadku jest to tylko porządek dla Ciebie. .Net nie robi z tego użytku. Chyba, że chcesz to automatycznie dokumentować przykładowo za pomocą Swaggera (OpenAPI). Wtedy to już ma znaczenie.
Rejestracja wersjonowania
Teraz musisz dodać rzeczy odpowiedzialne za obsługę wersjonowania. Najpierw pobierz sobie NuGet: Microsoft.AspNetCore.Mvc.Versioning
. Teraz możesz zarejestrować serwisy:
builder.Services.AddApiVersioning(o =>
{
o.DefaultApiVersion = new ApiVersion(1, 0);
o.AssumeDefaultVersionWhenUnspecified = true;
o.ReportApiVersions = true;
});
Co mówią poszczególne opcje:
DefaultApiVersion
– ta wersja ma być używana, jeśli klient nie prześle żadnych informacji o wersji, której chce używaćAssumeDefaultVersionWhenUnspecified
– jeśli klient nie prześle informacji o wersji, której chce używać, wtedy wersją ma być domyślna (DefaultApiVersion
).
Mogłoby się wydawać, że jedno ustawienie bez drugiego nie ma sensu. Ale to nie do końca tak jest. Dojdziemy do tego.
ReportApiVersions
– ustawione na TRUE spowoduje to, że serwer przekaże informacje o obsługiwanych wersjach. Po wywołaniu poprawnego endpointa, w odpowiedzi dostaniesz dodatkowy nagłówek:
Jeśli ReportApiVersions
ustawisz na FALSE, tego nagłówka w odpowiedzi nie będzie.
Konfiguracja kontrolerów
W konfiguracji kontrolerów mówimy do jakiej wersji należy jaki kontroler. Spójrz na moje dwa kontrolery:
//wersja 1
namespace SimpleApiVer.Controllers.V1
{
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1.0")]
public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult Index()
{
return Content("Wersja 1");
}
}
}
//wersja 2
namespace SimpleApiVer.Controllers.V2
{
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("2.0")]
public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult Index()
{
return Content("Wersja 2");
}
}
}
Zauważ tutaj kilka rzeczy:
- każdy kontroler jest w osobnym namespace
- kontrolery w atrybucie
Route
mają zaszytą wersję:/v{version:apiVersion}/
. Najważniejszy jest tutaj znacznik:{version:apiVersion}
. Zwyczajowo dodaje się prefix 'v’. Ale możesz równie dobrze zrobićversion-{version:apiVersion}
albopiesek-{version:apiVersion}
. Pamiętaj tylko, żeby dokładnie tak samo określić to w innych kontrolerach. Jeśli określisz inaczej np. w kontrolerze V1 dasz:/x-{version:apiVersion}
, a w V2:/y-{version:apiVersion}
tox-1
zadziała iy-2
też zadziała. W innych przypadkach dostaniesz błąd. Więc trzymaj się jednego szablonu, żeby nie narobić sobie burdelu - kontrolery mają określoną wersję API, którą obsługują, w atrybucie
[ApiVersion]
. Co ciekawe, jeden kontroler może obsługiwać kilka wersji:
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class ItemsController : ControllerBase
{
}
W takim przypadku poszczególne metody możesz odróżnić za pomocą atrybutu MapToApiVersion
:
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class ItemsController : ControllerBase
{
[HttpGet]
public IActionResult Index()
{
return Content("Item z wersji 2");
}
[HttpGet]
[MapToApiVersion("1.0")]
public IActionResult IndexV1()
{
return Content("Item z wersji 1");
}
}
Przekazywanie informacji o wersji w nagłówku
Wyżej zobaczyłeś jak przekazać informację o wersji w URL (za pomocą atrybutu Route
). W tym przypadku informacja o wersji musi znaleźć się zawsze. Jeśli jej nie dodasz, to dostaniesz błąd 404 – nie znaleziono strony.
Jest też możliwość dodania informacji o wersji w nagłówku żądania. Musisz po prostu dodać taką informację podczas rejestracji wersjonowania:
builder.Services.AddApiVersioning(o =>
{
o.DefaultApiVersion = new ApiVersion(1, 0);
o.AssumeDefaultVersionWhenUnspecified = true;
o.ReportApiVersions = true;
o.ApiVersionReader = new HeaderApiVersionReader("api-version");
});
To, co dasz w konstruktorze HeaderApiVersionReader
to będzie nazwa nagłówka, w którym trzymasz wersję api, której chcesz użyć.
Jeśli po dodaniu ApiVersionReader
uruchomisz program BEZ INNYCH ZMIAN, to on wciąż będzie działał. Wersjonowanie nadal zadziała przez ten specjalny znacznik {version:apiVersion}
w atrybucie Route
kontrolerów. Ale teraz go usuńmy:
//wersja 1
namespace SimpleApiVer.Controllers.V1
{
[Route("api/[controller]")]
[ApiController]
[ApiVersion("1.0")]
public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult Index()
{
return Content("Wersja 1");
}
}
}
//wersja 2
namespace SimpleApiVer.Controllers.V2
{
[Route("api/[controller]")]
[ApiController]
[ApiVersion("2.0")]
public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult Index()
{
return Content("Wersja 2");
}
}
}
Nie masz już informacji o wersji. I tutaj wracamy do ustawień wersjonowania z początku: DefaultApiVersion
i AssumeDefaultVersionWhenUnspecified
. Teraz one mają sens nawet osobno. Jeśli w tym momencie uruchomisz aplikację to nie przekazałeś informacji o wersji, ale z ustawień wynika, że ma być domyślna.
Jeśli jednak opcję AssumeDefaultVersionWhenUnspecified
ustawisz na FALSE i nie przekażesz numeru wersji, dostaniesz błąd 400 z treścią, że musisz przekazać informacje o wersji.
W tym przypadku wystarczy, że dodasz nagłówek o nazwie api-version
i treści odpowiedniej wersji, np:
URL, czy nagłówek?
W zależności od Twoich potrzeb i projektu. Jeśli tworzę API, a do tego klienta, który jest oparty o HttpClient
, bardzo lubię dawać informację o wersji w nagłówku. Wtedy w kliencie mam wszystko w jednym miejscu i nie muszę się martwić o poprawne budowanie ścieżki.
Oznaczanie wersji jako przestarzałej
W pewnym momencie dojdziesz do wniosku, że nie chcesz już utrzymywać wersji 1.0 swojego API. Naturalną koleją rzeczy w takiej sytuacji jest najpierw oznaczenie tej wersji jako przestarzałej, a w kolejnym etapie całkowite jej usunięcie. Żeby oznaczyć wersję jako przestarzałą posłuż się opcją Deprecated
z atrybutu ApiVersion
:
[Route("api/[controller]")]
[ApiController]
[ApiVersion("1.0", Deprecated = true)]
public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult Index()
{
return Content("Wersja 1");
}
}
To oczywiście w żaden magiczny sposób nie powie użytkownikowi: „Hej, używasz starej wersji, my tu ją za chwilę wyrzucimy„. Po prostu zwróci dodatkowy nagłówek w odpowiedzi:
api-deprecated-versions
, z wartością wersji oznaczonej jako przestarzała.
To oczywiście działanie na obszarze kontrolera. Tzn., że jeden kontroler może być oznaczony jako deprecated w wersji 1, ale drugi już nie.
Dzięki za przeczytanie artykułu. To tyle jeśli chodzi o obsługę wersjonowania API w .NET. Jest jeszcze kilka możliwości, ale to już specyficzne przypadki, które być może kiedyś opiszę.
Tymczasem, jeśli masz jakiś problem lub znalazłeś błąd w artykule, koniecznie podziel się w komentarzu 🙂
Obrazek artyułu: Technologia zdjęcie utworzone przez rawpixel.com – pl.freepik.com