Dokumentowanie własnego API automatem – co to Swagger?

Dokumentowanie własnego API automatem – co to Swagger?

Wstęp

Któż z nas nie kocha pisania dokumentacji? 😉 No właśnie. Nikt tego nie chce robić, ale każdy chciałby mieć dokumentację do zewnętrznych systemów. Niestety tworzenie takich materiałów jest po prostu upierdliwe… Ale nie musi. W tym artykule pokażę Ci jak szybko i prosto zrobić bardzo funkcjonalną dokumentację dla własnego API.

Co to Swagger?

Swagger to narzędzie, które magicznie skanuje Twoje API i tworzy stronę, na której ładnie opisuje wszystkie końcówki. Co więcej, umożliwia testowanie takiego API na żywym organizmie. To jest dokumentacja w pełni interaktywna.

Wszystko zrobisz w Visual Studio – nie musisz otwierać żadnego innego edytora. Zaczynamy.

Dodawanie Swaggera do projektu

Swagger jest tak fajnym narzędziem, że Microsoft pozwala na dodanie go już podczas tworzenia samego projektu. W oknie konfiguracji możesz wybrać, czy go używać, czy nie.

To oczywiście najprostsza droga do dodania Swaggera. Ale być może jest tak, że masz projekt, w którym nie zaznaczyłeś tej opcji. Tak też się da.

Dodawanie Swaggera ręcznie

Pobierz NuGet:

Install-Package Swashbuckle.AspNetCore.Swagger

Teraz musisz skonfigurować Swaggera.

Dodaj go przy rejestracji serwisów:

builder.Services.AddControllers();
builder.Services.AddSwaggerGen();

A podczas konfiguracji pipeline dodaj:

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

Tutaj mała uwaga. Być może pracujesz nad API, które będzie wystawiane zewnętrznie dla klientów. W takim przypadku prawdopodobnie nie powinieneś dodawać Swaggera tylko w środowisku deweloperskim ale produkcyjnie też.

I teraz małe wyjaśnienie:

  • builder.Services.AddSwaggerGen(); – rejestruje serwisy potrzebne do obsługi Swaggera
  • app.UseSwagger(); – to podstawowa obsługa
  • app.UseSwaggerUI(); – dodaje do Twojego API specjalną stronę, na której wszystko jest ładnie opisane, a w dodatku można testować.

To tyle, jeśli chodzi o podstawową konfigurację.

Przykładowy projekt

Swaggera najlepiej pokazać na przykładzie. W związku z tym przygotowałem prostą solucję, składającą się z dwóch projektów. SwaggerDemo to jest nasze API, SwagerDemo.Models to projekt przechowujący modele aplikacji. Specjalnie są zrobione dwa projekty, żeby Ci pokazać coś więcej. Cały gotowy kod możesz sobie sklonować z GitHuba: https://github.com/AdamJachocki/SwaggerDemo

Jeśli nie chcesz korzystać z mojego projektu, po prostu dodaj Swaggera do swojego (tak jak to opisane wyżej).

Możesz teraz uruchomić projekt API. Ważne, żeby API otwierało przeglądarkę. Jeśli Twoje nie otwiera, możesz zmodyfikować plik Properties/launchSettings.json, zmieniając wartość zmiennej launchBrowser na true. Możesz też automatem otworzyć stronę Swaggera, dodając do lauchSettings.json zmienną launchUrl:

"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7090;http://localhost:5090",
"environmentVariables": {
  "ASPNETCORE_ENVIRONMENT": "Development"

Jeśli dodawałeś Swaggera automatycznie (lub zmodyfikowałeś launchSettings.json jak wyżej), prawdopodobnie od razu pokazuje Ci się jego strona. Jeśli nie, doklep w przeglądarce końcówkę swagger. Przykładowo, jeśli adres Twojego API to http://localhost:5001, przejdź na: http://localhost:5001/swagger.

Tak mniej więcej wygląda podstawowa dokumentacja wygenerowana Swaggerem. Osobno widzisz każdy kontroler, w kontrolerze kolekcję endpointów, każdy rodzaj endpointa (POST, GET, DELETE) ma swój kolor. Jeśli rozwiniesz endpoint, zobaczysz dokładnie jakie przyjmuje dane, co zwraca i będziesz mógł go wywołać (przycisk Try it out z prawego, górnego narożnika). Swagger automatycznie rozpoznaje dane wchodzące:

Niemniej jednak, zgodzisz się że to słaba dokumentacja i właściwie niczego nie mówi. Poza tym, że pozwala Ci wysłać żądanie po kliknięciu przycisku Try it out. Ale spokojnie. Zaraz się tym zajmiemy.

Dokumentacja generowana z komentarzy

Każdy endpoint możesz dokładnie opisać za pomocą komentarzy dokumentujących, np:

/// <summary>
/// Pobiera użytkownika po przekazanym id
/// </summary>
/// <param name="id">Id użytkownika</param>
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
    User testUser = new User
    {
        Email = "test@example.com",
        Id = 1,
        Name = "Test"
    };

    return Ok(testUser);
}

Tekst jaki wpisałeś w <summary> pojawi się jako opis konkretnego endpointa. Natomiast opisy parametrów <param> pojawią się przy parametrach. Jednak żeby to zadziałało, musisz dokonfigurować projekt API.

Konfiguracja projektu

W pliku projektu dodaj:

<GenerateDocumentationFile>True</GenerateDocumentationFile>

Możesz też zrobić to z poziomu ustawień projektu: Build -> Output -> Documentation file:

To ustawienie sprawi, że VisualStudio podczas budowania aplikacji, utworzy specjalny plik XML z metadanymi dokumentacji. Plik będzie nazywał się tak jak projekt, np: SwaggerDemo.xml. I domyślnie tworzy się w katalogu wynikowym.

To ustawienie jednak spowoduje również mały efekt uboczny. Podczas budowania aplikacji otrzymasz warningi CS1591, mówiące o tym, że są publiczne metody, które nie mają komentarzy dokumentujących. My tutaj dokumentujemy tylko metody w kontrolerach, aby Swagger mógł zadziałać. Jeśli nie dokumentujesz wszystkich metod publicznych, możesz ten warning wyłączyć, dodając do pliku projektu:

<NoWarn>$(NoWarn);1591</NoWarn>

Mój plik projektu API wygląda teraz tak:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>disable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <GenerateDocumentationFile>True</GenerateDocumentationFile>
	<NoWarn>$(NoWarn);1591</NoWarn>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\SwaggerDemo.Models\SwaggerDemo.Models.csproj" />
  </ItemGroup>

</Project>

Konfiguracja Swaggera

Swagger odczytuje opisy właśnie z tego pliku XML. Trzeba mu to tylko powiedzieć. Robisz to, podczas konfigurowania Swaggera w kodzie przy konfiguracji serwisów:

builder.Services.AddSwaggerGen(o =>
{
    var assemblyName = Assembly.GetExecutingAssembly().GetName().Name + ".xml";
    var docFile = Path.Combine(AppContext.BaseDirectory, assemblyName);
    o.IncludeXmlComments(docFile);
});

Tutaj nie ma żadnej magii. Kluczową instrukcją jest IncludeXmlComments, gdzie w parametrze podaję pełną ścieżkę do utworzonej automatycznie dokumentacji xml. Czyli pobieram ścieżkę wykonywanego pliku, pobieram nazwę projektu i łączę to.

Teraz dokumentacja Swaggerowa wygląda już tak:

Opisywanie odpowiedzi

Swaggerowi możesz powiedzieć jeszcze, jakie endpoint generuje odpowiedzi i kiedy:

/// <summary>
/// Pobiera użytkownika po id
/// </summary>
/// <param name="id">Id użytkownika</param>
/// <response code="200">Zwraca znalezionego użytkownika</response>
/// <response code="404">Nie znaleziono takiego użytkownika</response>
/// <response code="500">Wewnętrzny błąd serwera</response>
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
    User testUser = new User
    {
        Email = "test@example.com",
        Id = 1,
        Name = "Test"
    };

    return Ok(testUser);
}

Teraz strona Swaggera wygląda tak:

Patrząc na taką dokumentację nadal nie wiesz, jakie dane zwróci endpoint, jeśli zapytanie zakończy się sukcesem (kod 200). Możesz oczywiście wywołać tę końcówkę z poziomu Swaggera i otrzymasz wszystkie dane pobrane z API:

Jednak jest pewien sposób…

Opisywanie zwracanego modelu

Możesz pokazać Swaggerowi zwracany model:

[Route("api/[controller]")]
[ApiController]
[Produces("application/json")]
public class UsersController : ControllerBase
{
    /// <summary>
    /// Pobiera użytkownika po id
    /// </summary>
    /// <param name="id">Id użytkownika</param>
    /// <response code="200">Zwraca znalezionego użytkownika</response>
    /// <response code="404">Nie znaleziono takiego użytkownika</response>
    /// <response code="500">Wewnętrzny błąd serwera</response>
    [HttpGet("{id}")]
    [ProducesResponseType(typeof(User), 200)]
    public IActionResult GetById(int id)
    {
        User testUser = new User
        {
            Email = "test@example.com",
            Id = 1,
            Name = "Test"
        };

        return Ok(testUser);
    }
}

Tutaj zrobiłem dwie rzeczy. Na poziomie kontrolera powiedziałem, jaką odpowiedź kontroler zwraca (json). Co nie jest wymagane, ale lepiej wygląda w Swaggerze. No i oczywiście klienci Twojego API nie mają żadnych wątpliwości co do rodzaju zwrotki. W innym przypadku Swagger pokaże combobox z możliwością wyboru typu zwrotki.

Ważniejsza jednak rzecz jest na poziomie samego endpointa – atrybut ProducesResponseType. W parametrach pokazuję jaki typ jest zwracany przy jakim kodzie. Różne kody mogą zwracać różne typy modeli. Teraz Swagger wygląda tak:

Jak widzisz Swagger pokazuje teraz szablon zwracanego modelu.

Opisywanie pól modelu

W rzeczywistości modele bywają bardziej skomplikowane niż ten powyżej. A ich pola nie opisują się tak ładnie. Możemy Swaggerowi opisać dokładnie każde pole modelu. Jak? Również za pomocą komentarzy dokumentujących. Tym razem na poziomie konkretnego modelu:

public class User
{
    /// <summary>
    /// Id użytkownika
    /// </summary>
    public int Id { get; set; }
    /// <summary>
    /// Imię i nazwisko użytkownika. Uwaga! Pole może być puste
    /// </summary>
    public string Name { get; set; }
    /// <summary>
    /// E-mail użytkownika
    /// </summary>
    public string Email { get; set; }
    /// <summary>
    /// Hasło użytkownika. Zawsze puste, gdy pobieramy rekord.
    /// </summary>
    public string Password { get; set; }
}

Co się stanie, gdy odpalimy teraz Swaggera? Zupełnie nic 🙂

Dlatego też stworzyłem dwa projekty – jeden api, drugi dla modeli.

Przypominam, że Swagger opisy odczytuje z pliku dokumentacji (xml) tworzonego przez Visual Studio. O ile projekt API został ładnie ustawiony, to projekt z modelami nie ma takiej konfiguracji. Musimy ją więc dodać. Do projektu z modelami dodaj znane już elementy:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>disable</Nullable>
	<GenerateDocumentationFile>True</GenerateDocumentationFile>
	<NoWarn>$(NoWarn);1591</NoWarn>
  </PropertyGroup>

</Project>

Teraz jeszcze tylko musisz Swaggerowi powiedzieć, skąd ma ten dokument zaczytać. To też już robiliśmy. Podczas konfiguracji Swaggera trzeba tylko dodać kolejny plik:

builder.Services.AddSwaggerGen(o =>
{
    var assemblyName = Assembly.GetExecutingAssembly().GetName().Name + ".xml";
    var docFile = Path.Combine(AppContext.BaseDirectory, assemblyName);
    o.IncludeXmlComments(docFile);

    var modelsAssemblyName = typeof(User).Assembly.GetName().Name + ".xml";
    var modelsDocFile = Path.Combine(AppContext.BaseDirectory, modelsAssemblyName);
    o.IncludeXmlComments(modelsDocFile);
});

Tutaj, żeby nie wpisywać na sztywno nazwy projektu, posłużyłem się jakąś klasą, która występuje w projekcie z modelami. Traf chciał, że padło na klasę User. Generalnie wybrałem pierwszą lepszą. Chodziło o to, żeby refleksja zwróciła nazwę projektu. Reszta jest taka sama jak wyżej: IncludeXmlComments i wio.

Teraz Swagger wygląda tak:

Pamiętaj, że żeby zobaczyć opisy pól modelu, musisz kliknąć na Schema.

Swagger i wersjonowanie API

Często nasze API są wersjonowane. Swagger niestety nie ogarnia tego domyślnie. Jest kilka sposobów, żeby to zadziałało. Ja Ci pokażę jeden z nich – moim zdaniem najbardziej prawilny.

Jeśli jesteś ciekawy w jaki sposób możesz wersjonować swoje API, koniecznie przeczytaj ten artykuł.

Jak to działa?

Słowem wstępu, Swagger działa tak, że używa mechanizmu dostarczanego przez Microsoft: EndpointsApiExplorer. Nie musisz tego dodawać ręcznie, to już dodaje Swagger podczas rejestrowania swoich serwisów.

ApiExplorer skanuje Twoje API i zwraca informacje o nim, a Swagger za jego pomocą buduje swoje pliki „map”.

Przy tym podejściu musisz zapewnić, że wersjonujesz API tak jak napisałem tutaj. Głównie chodzi o trzymanie kontrolerów dla różnych wersji w różnych namespace.

Krok 1 – stworzenie konwencji

Na początek musimy utworzyć konwencję, która odpowiednio pogrupuje kontrolery. Stwórz taką klasę:

public class GroupingByNamespaceConvention : IControllerModelConvention
{
    public void Apply(ControllerModel controller)
    {
        var controllerNamespace = controller.ControllerType.Namespace;
        var apiVersion = controllerNamespace.Split(".").Last().ToLower();
        if (!apiVersion.StartsWith("v")) 
            apiVersion = "v1";

        controller.ApiExplorer.GroupName = apiVersion;
    }
}

Zadaniem tej klasy jest odpowiednie zgrupowanie kontrolera (dodanie atrybutu GroupName). To grupowanie jest używane tylko przez ApiExplorer, czyli nie ma żadnego znaczenia dla działającego kodu. Zapamiętaj – tylko dla dokumentacji. Teraz trzeba tą konwencję zarejestrować podczas rejestracji serwisów:

builder.Services.AddControllers(o =>
{
    o.Conventions.Add(new GroupingByNamespaceConvention());
});

Krok 2 – konfiguracja dokumentacji Swaggera

Teraz musimy skonfigurować dokumentację dla każdej wersji. Robimy to podczas konfiguracji Swaggera:

builder.Services.AddSwaggerGen(o =>
{
    o.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "Wersja 1",
        Version = "v1"
    });

    o.SwaggerDoc("v2", new OpenApiInfo
    {
        Title = "Wersja 2",
        Version = "v2"
    });
});

Dodałem tutaj dwie wersje. Na koniec trzeba jeszcze je dodać podczas konfiguracji middleware:

app.UseSwagger();
app.UseSwaggerUI(o =>
{
    o.SwaggerEndpoint("/swagger/v1/swagger.json", "Wersja 1");
    o.SwaggerEndpoint("/swagger/v2/swagger.json", "Wersja 2");
});

Ważne, żeby nazwa przekazana w SwaggerEnpoint (Wersja1, Wersja2) była spójna z tytułem skonfigurowanym w SwaggerDoc.

Krok trzeci – aktualizacja kontrolerów

Na koniec już tylko musisz zaktualizować kontrolery, żeby powiedzieć im, którą wersję API wspierają. Prawdopodobnie będą sytuacje, że pomiędzy pierwszą i drugą wersją API zmieni Ci się tylko część kontrolerów. Wystarczy, że dodasz do nich atrybuty ApiVersion:

[Route("api/[controller]")]
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class ItemsController : ControllerBase
{

}

Ułatwienia

Istnieje NuGet, który ułatwia konfigurowanie wersji w Swaggerze. Jednak komentarz autora (który przytaczam fragmentami niżej) mi daje taką myśl: „Wstrzymaj konie i poczekaj na nową wersję”. Ten NuGet to: Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer, jednak nie daj się zmylić – nie ma za wiele wspólnego aktualnie z Microsoftem:

Projekt rozpoczął się jako eksperyment myślowy jak wziąć pomysły stojące za wersjonowaniem API (zwłaszcza RESTowego) i zaimplementować je w praktyczny sposób (…). Rozwój (…) różnych projektów w Microsoft zajął około dwóch lat, ale w końcu powstał ogólny wzorzec i framework. 6 lat temu (2016 – przyp. tłumacz) przeniosłem to do społeczności open source, żeby rozwijać dalej i szczerze – dla mojego własnego egoistycznego użytku do projektów poza Microsoftem. To, że projekt stał się taki popularny, przerosło moje najśmielsze oczekiwania.

Decyzja, żeby przenieść projekt na Microsoftowy GitHub była głównie podyktowana open source’ową polityką firmy. Pomimo powszechnego przekonania, nie jestem i nigdy nie byłem częścią teamu od AspNetCore (…). Ten projekt nigdy w żaden sposób nie był oficjalnie wspierany. Pomimo, że pojawiło się kilku zewnętrznych kontrybutorów, głównie utrzymywałem go sam. W 2021 (…) zdecydowałem się opuścić Microsoft (…). Próbowałem zachować projekt i przekazać go, jednak pojawiło się wiele wyzwań. Zajęło to kilka miesięcy, ale ostatecznie uznałem, że najlepszym będzie przeniesienie projektu z organizacji Microsoft do .NET Foundation (…).

Pojawiło się kilka nowych problemów, m.in. nazwa, która wskazuje, że projekt jest zarządzany przez Microsoft (…). Chciałem zrobić fork projektu i rozpocząć nowy, jednak mogłoby to wprowadzić zamieszanie w społeczności (…).

Drugi problem to identyfikatory pakietów NuGet. Zasugerowano, że po prostu wyślę zawiadomienie, że identyfikator się zmieni. Jednak po 100 milionach pobrań stwierdziłem, że jest to niedopuszczalne. Zajęło to wiele miesięcy aby wyśledzić odpowiednich interesariuszy NuGet, aby rozpocząć proces, ale identyfikatory pakietów zostały teraz przeniesione do zespołu api-versioning z dotnetfoundation. Jeśli zastanawiasz się, dlaczego nie było żadnych aktualizacji od dłuższego czasu, to właśnie dlatego. Teraz mam trochę więcej kontroli nad pakietem i aktualizacje mogą znów się pojawiać. Jednak są z tym związane limity. Nowe funkcje nie mogą pojawiać się pod szyldem Microsoft(…). Zacząłem nawet prace nad nową wersją, która zaczynałaby się prefixem Asp.Versioning.* (…).

Krótko mówiąc – projekt powinien mieć jakieś aktualizacje do wersji 5.*. Jednak niczego więcej po nim nie można się spodziewać. A jego klon gdzieś kiedyś się pojawi.


To tyle, jeśli chodzi o dokumentowanie API. Jak widzisz, nie musi to być tak nudne, jak się wydaje. A i musisz przyznać, że dla klienta taka interaktywna dokumentacja ma dużo większą wartość niż tabelka w Wordzie. Spróbuj sam i zobacz, jak to jest. Co więcej, Swagger posługuje się standardem OpenAPI 3.0, więc możesz to sobie zaimportować nawet do PostMana! 🙂

Dzięki za przeczytanie artykułu. Jeśli czegoś nie zrozumiałeś lub znalazłeś jakiś błąd, koniecznie podziel się w komentarzu. A jeśli znasz kogoś, komu ten artykuł się zdecydowanie przyda, udostępnij mu go 🙂

Podziel się artykułem na:
Wersjonowanie API

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.

  1. Dla .NET < 6 Pobierz NuGet: Microsoft.AspNetCore.Mvc.Versioning
    Dla .NET >= 6 Pobierz NuGet: Asp.Versioning.Mvc
  2. Zarejestruj serwisy:
builder.Services.AddApiVersioning(o =>
{
    o.DefaultApiVersion = new ApiVersion(1, 0);
    o.AssumeDefaultVersionWhenUnspecified = true;
    o.ReportApiVersions = true;
});
  1. 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");
    }
}

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");
    }
}
  1. Upewnij się, że nie masz informacji o ścieżce w URLach (atrybut Route kontrolerów)
  2. 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");
});

Zarejestruj dokładnie tak, jakbyś chciał wersjonować kontrolery, a potem:

var app = builder.Build();

var versionSet = app.NewApiVersionSet()
    .HasApiVersion(1)
    .HasApiVersion(2) //<-- dodajemy przykładowo dwie wersje API
    .Build();

//a teraz do endpointów dodajemy te wersje
app.MapGet("hello", () => "Hello")
    .WithApiVersionSet(versionSet)
    .MapToApiVersion(1); //oznaczamy numerem wersji

app.MapGet("hello", () => "Hello from version 2")
    .WithApiVersionSet(versionSet)
    .MapToApiVersion(2); //oznaczamy numerem wersji

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 wersjami
  • MINOR – gdy dodajesz nowe funkcje, które są KOMPATYBILNE z API
  • PATCH – 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: Asp.Versioning.Mvc (dla wersji .NET poniżej 6 możesz pobrać 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:
Przykład odpowiedzi z PostMan

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} albo piesek-{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} to x-1 zadziała i y-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:

Zrzut z PostMana

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.

Minimal API

W przypadku Minimal Api sprawa wygląda bardzo podobnie. Też musisz zarejestrować sobie wersjonowanie dokładnie tak jak przy kontrolerach:

builder.Services.AddApiVersioning(o =>
{
    o.DefaultApiVersion = new ApiVersion(1, 0);
    o.AssumeDefaultVersionWhenUnspecified = true;
    o.ReportApiVersions = true;
    o.ApiVersionReader = new HeaderApiVersionReader("api-version"); //jeśli chcesz posługiwać się nagłówkiem
});

A następnie musisz stworzyć „zbiór” posiadanych wersji i zmapować swoje endpointy pod konkretne zbiory, np.:

var app = builder.Build();

var versionSet = app.NewApiVersionSet()
    .HasApiVersion(1)
    .HasApiVersion(2) //&lt;-- dodajemy przykładowo dwie wersje API
    .Build();

//a teraz do endpointów dodajemy te wersje
app.MapGet("hello", () => "Siema")
    .WithApiVersionSet(versionSet)
    .MapToApiVersion(1); //oznaczamy numerem wersji

app.MapGet("hello", () => "Siema z wersji 2")
    .WithApiVersionSet(versionSet)
    .MapToApiVersion(2); //oznaczamy numerem wersji

Jeśli nie stworzysz żadnego ApiVersionReadera (jak w konfiguracji wyżej), to domyślnie mechanizm informacji o wersji będzie szukał w parametrze query o nazwie api-version, np.:

https://localhost/hello?api-version=1

Poza tym wszystko (np. wersja w ścieżce endpointu) działa dokładnie tak samo jak przy kontrolerach.

Dokumentowanie wersji API

Używając narzędzi dokumentujących API (np. Swaggera) możesz te wersje też uwzględnić. Opisałem to w artykule o dokumentowaniu swojego API.


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

Podziel się artykułem na: