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.

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: