Piszemy klienta do WebAPI

Piszemy klienta do WebAPI

Wstęp

Często mówimy o tym, czym jest WebApi, jak je tworzyć, a jak nie. Ale jakoś nie mówimy o tym jak stworzyć dobrze fajnego klienta do tego WebAPI.

Mogłoby się zdawać, że wystarczy utworzyć instancję HttpClient i wywołać odpowiednią końcówkę. I czasem nawet można tak zrobić. Ale jeśli chcesz mieć naprawdę dobrego klienta do większego API niż tylko dwie końcówki, to ten artykuł pokaże Ci jak do tego podejść na konkretnym przykładzie.

Jest NuGetowa paczka – RestSharp. Jest to bardzo popularna darmowa biblioteka, która zdecydowanie ułatwia tworzenie klientów API. Jednak w tym artykule nie posłużymy się nią. Zrobimy coś sami. Potem sam zdecydujesz, czy wolisz tworzyć takie rozwiązania samodzielnie, czy z użyciem RestSharpa.

Przede wszystkim – WebAPI

Żeby klient API miał sens, musi przede wszystkim łączyć się z jakimś API. Dlatego też przygotowałem dość proste rozwiązanie, na którym będziemy pracować. Możesz je pobrać z GitHuba.

Uwaga! Nie zwracaj za bardzo uwagi na kod API – jest bardzo prosty, banalny i nie we wszystkich aspektach super poprawny. Nie zajmujemy się tutaj WebAPI, tylko klientem do API.

To Api trzyma dane w słowniku, to znaczy że po ponownym uruchomieniu, wszystkie dane znikną.

Api ma kilka końcówek, możesz sobie je zobaczyć, uruchamiając swaggera. Z grubsza to:

  • POST – /api/clients/all – pobiera listę klientów (dlaczego POST – o tym niżej)
  • POST – /api/clients – dodaje klienta
  • GET – /api/clients/{id} – pobiera klienta o konkretnym id
  • DELETE – /api/clients/{id} – usuwa klienta o konkretnym id
  • POST – /api/orders/all – pobiera zamówienia (dlaczego POST – o tym niżej)
  • POST – /api/orders – dodaje zamówienie
  • GET – /api/orders/client/{clientId} – pobiera zamówienia dla konkretnego klienta
  • GET – /api/orders/{id} – pobiera zamówienie o konkretnym id

Także mamy kilka końcówek podzielonych na dwa kontrolery.

Zaczynamy pisać klienta

OK, skoro już wiemy jak mniej więcej wygląda API, możemy utworzyć projekt, w którym napiszemy klienta. Niech to będzie zwykły projekt Class Library.

Model DTO

Najpierw musimy utworzyć modele DTO. DTO czyli Data Transfer Object – są to klasy, które przekazują dane między API, a klientem. Modele DTO mogą być jak najgłupsze się da. To po prostu worek na dane. Nic więcej.

Teraz możesz zapytać – po co tworzyć dodatkowy model, skoro mamy już dokładny model bazodanowy? Nie lepiej ten model bazodanowy z projektu WebApi przenieść do jakiegoś współdzielonego?

W tym konkretnym przypadku banalnej aplikacji – pewnie tak. Natomiast przy aplikacjach bardziej rozbudowanych przekazywanie danych za pomocą modeli bazodanowych może okazać się baaaardzo problematyczne. Sam wiele lat temu zrobiłem taki błąd. W pewnym momencie okazało się, że muszę stosować jakieś dziwne haki i czary, żeby to wszystko jakoś działało. Dlatego – stwórz osobny model DTO.

W przykładowej aplikacji są w projekcie Models. Modele DTO wyglądają prawie tak samo jak modele bazodanowe. Specjalnie dodałem do modeli bazodanowych jedną właściwość (IsDeleted), żeby je czymś rozróżnić.

Zwróć uwagę na dwie klasy:

GetClientsRequestDto:

public class GetClientsRequestDto
{
    public int Skip { get; set; }
    public int Take { get; set; }
}

GetClientsResultDto:

public class GetClientsResultDto
{
    public IEnumerable<ClientDto> Data { get; init; }
    public int  Offset { get; init; }

    public GetClientsResultDto(IEnumerable<ClientDto> data, int offset)
    {
        Data = data;
        Offset = offset;
    }
}

W standardowym tutorialu tworzenia WebApi zobaczyłbyś, że gdy żądasz listy klientów, API zwraca po prostu listę klientów, np: IEnumerable<ClientDto>.

Jednak w prawdziwym świecie to może być za mało. Dlatego też stworzyłem dwie dodatkowe klasy:

  • GetClientsRequestDto – obiekt tej klasy będzie wysyłany wraz z żądaniem pobrania listy klientów
  • GetClientsResultDto – obiekt tej klasy będzie zwracany przez API zamiast zwykłej listy klientów.

Jak widzisz, te klasy zawierają w sobie informacje ograniczające ilość pobieranych danych. Jeśli miałbyś bazę z 10000 klientów i z jakiegoś powodu chciałbyś pobrać ich listę, to zupełnie bez sensu byłoby pobieranie wszystkich 10000 rekordów. To naprawdę sporo danych. Zamiast tego możesz pobierać te dane partiami i napisać jakiś prosty mechanizm paginacji. Do tego mogą właśnie służyć te dodatkowe klasy.

Analogicznie zrobiłem dla modelu OrderDto.

Abstrakcja

Skoro już mamy modele DTO, możemy pomyśleć o abstrakcji, która umożliwi nam testowanie klienta API.

Zgodnie z regułą pojedynczej odpowiedzialności (signle responsibility) klient API nie powinien być odpowiedzialny za wszystkie operacje związane z API. Ale powinien dać taką możliwość. Jak to osiągnąć? Poprzez dodatkowe klasy operacji. I tak będziemy mieć klasę odpowiedzialną za operacje na zamówieniach i drugą odpowiedzialną za klientów. Stwórzmy teraz takie abstrakcje:

public interface IClientOperations
{
    public Task<ClientDto> AddClient(ClientDto data);
    public Task<GetClientsResultDto> GetClients(GetClientsRequestDto data);
    public Task<ClientDto> GetClientById(int id);
    public Task<bool> DeleteClient(int id);
}

To jest interfejs, którego implementacja będzie odpowiedzialna za operacje na klientach. Analogicznie stworzymy drugi interfejs – do zamówień:

public interface IOrderOperations
{
    public Task<OrderDto> AddOrder(OrderDto order);
    public Task<GetOrdersResultDto> GetOrdersForClient(int clientId, GetOrdersRequestDto data);
    public Task<GetOrdersResultDto> GetOrders(GetOrdersResultDto data);
    public Task<bool> DeleteOrder(int id);
}

To są bardzo proste interfejsy i na pierwszy rzut oka wszystko jest ok. Ale co jeśli z WebApi otrzymasz jakiś konkretny błąd? Np. podczas dodawania nowego klienta mógłbyś otrzymać błąd w stylu: „Nazwa klienta jest za długa”. W taki sposób tego nie ogarniesz. Dlatego proponuję stworzyć dwie dodatkowe klasy, które będą przechowywały rezultat wywołania końcówki API:

public class BaseResponse
{
    public int StatusCode { get; init; }
    public bool IsSuccess { get { return StatusCode >= 200 && StatusCode <= 299 && string.IsNullOrWhitespace(ErrorMsg); } }
    public string ErrorMsg { get; init; }

    public BaseResponse(int statusCode = 200, string errMsg = "")
    {
        StatusCode = statusCode;
        ErrorMsg = errMsg;
    }
}

public class DataResponse<T> : BaseResponse
{
    public T Data { get; init; }

    public DataResponse(T data, int statusCode = 200, string errMsg = "")
        : base(statusCode, errMsg)
    {
        Data = data;
    }
}

Klasa BaseResponse i operacja zakończona poprawnie

Klasa BaseResponse będzie przechowywała kod odpowiedzi wraz z ewentualnym komunikatem o błędzie. Wg specyfikacji HTTP wszystkie kody od 200 do 299 włącznie oznaczają operację zakończoną poprawnie, dlatego też IsSuccess jest tak skonstruowane.

Teraz pojawia się pytanie – co oznacza „operacja zakończona poprawnie”? W kontekście WebApi zazwyczaj chodzi tutaj o to, że dane przesłane w żądaniu były prawidłowe, na serwerze nic się nie wywaliło, nie było problemu z autoryzacją i serwer odpowiedział prawidłowo. Jednak nie znaczy to, że operacja zakończyła się tak, jak byśmy sobie tego życzyli.

To trochę dziwnie brzmi, zatem pokażę Ci pewien przykład. Załóżmy, że chcesz pobrać klienta o ID = 5. Wg specyfikacji REST Api, jeśli taki klient nie istnieje, powinieneś otrzymać zwrotkę z kodem 404. Jednak błąd 404 oznacza również, że nie znaleziono określonej strony (końcówki API). Jest to pewien znany problem. Czasami się to tak zostawia, czasem można rozróżnić w taki sposób, że z WebAPI zwracamy kod 200 – operacja się powiodła, ale dołączamy informację o błędzie w odpowiedzi np: „Nie ma klienta o takim ID”.

To nam wszystko załatwia klasa BaseResponse.

Klasa DataResponse

Jak widzisz, DataResponse dziedziczy po BaseResponse. Jedyną różnicą jest to, że DataResponse przechowuje dodatkowo dane, które mogły przyjść w odpowiedzi. Teraz, mając takie klasy, możemy zmienić zwracany typ z interfejsów IClientOperations i IOrderOperations. Do tej pory wyglądało to tak:

public interface IClientOperations
{
    public Task<ClientDto> AddClient(ClientDto data);
    public Task<GetClientsResultDto> GetClients(GetClientsRequestDto data);
    public Task<ClientDto> GetClientById(int id);
    public Task<bool> DeleteClient(int id);
}

public interface IOrderOperations
{
    public Task<OrderDto> AddOrder(OrderDto order);
    public Task<GetOrdersResultDto> GetOrdersForClient(int clientId, GetOrdersRequestDto data);
    public Task<GetOrdersResultDto> GetOrders(GetOrdersResultDto data);
    public Task<bool> DeleteOrder(int id);
}

A teraz będziemy mieli coś takiego:

public interface IClientOperations
{
    public Task<DataResponse<ClientDto>> AddClient(ClientDto data);
    public Task<DataResponse<GetClientsResultDto>> GetClients(GetClientsRequestDto data);
    public Task<DataResponse<ClientDto>> GetClientById(int id);
    public Task<BaseResponse> DeleteClient(int id);
}

public interface IOrderOperations
{
    public Task<DataResponse<OrderDto>> AddOrder(OrderDto order);
    public Task<DataResponse<GetOrdersResultDto>> GetOrdersForClient(int clientId, GetOrdersRequestDto data);
    public Task<DataResponse<GetOrdersResultDto>> GetOrders(GetOrdersResultDto data);
    public Task<BaseResponse> DeleteOrder(int id);
}

Interfejs IApiClient

Skoro mamy już interfejsy dla poszczególnych operacji, możemy teraz napisać sobie interfejs do ApiClienta. I tutaj znów – ta abstrakcja nie jest konieczna. Jednak bez niej nie będziesz w stanie testować jednostkowo kodu, który używa klienta API.

Interfejs jest banalny:

public interface IApiClient
{
    IClientOperations ClientOperations { get; }
    IOrderOperations OrderOperations { get; }
}

Jak widzisz, klient API będzie dawał dostęp do poszczególnych operacji. To teraz zajmijmy się implementacją poszczególnych operacji, która zasadniczo będzie prosta.

Implementacja IClientOperations

Do komunikacji z WebApi wykorzystujemy HttpClient – dlatego też on musi znaleźć się w konstruktorze.

internal class ClientOperations : IClientOperations
{
    private readonly HttpClient _httpClient;

    public ClientOperations(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<DataResponse<ClientDto>> AddClient(ClientDto data)
    {
        var response = await _httpClient.PostAsJsonAsync("clients", data);
        return await ResponseFactory.CreateDataResponse<ClientDto>(response, DefaultJsonSerializerOptions.Options);
    }

    public async Task<BaseResponse> DeleteClient(int id)
    {
        var response = await _httpClient.DeleteAsync($"clients/{id}");
        return await ResponseFactory.CreateBaseResponse(response);
    }

    public async Task<DataResponse<ClientDto>> GetClientById(int id)
    {
        var response = await _httpClient.GetAsync($"clients/{id}");
        return await ResponseFactory.CreateDataResponse<ClientDto>(response, DefaultJsonSerializerOptions.Options);
    }

    public async Task<DataResponse<GetClientsResultDto>> GetClients(GetClientsRequestDto data)
    {
        var response = await _httpClient.PostAsJsonAsync("clients/all", data);
        return await ResponseFactory.CreateDataResponse<GetClientsResultDto>(response, DefaultJsonSerializerOptions.Options);
    }
}

Dalej mamy implementację poszczególnych metod. Każda z nich jest oparta dokładnie na tej samej zasadzie:

  • wyślij żądanie na odpowiednią końcówkę
  • stwórz DataResponse/BaseResponse na podstawie otrzymanej odpowiedzi – HttpResponseMessage.

Zwróć uwagę tutaj na trzy rzeczy.

  1. Klasa DefaultJsonSerializerOptions – jest to klasa, która trzyma domyślne dla aplikacji ustawienia serializacji JSON. W naszej aplikacji nie chcemy, żeby serializacja brała pod uwagę wielkość znaków. Jeśliby brała wtedy taki obiekt:
public class MyClass
{
    public int Id { get; set; }
    public string Name { get; set; }
}

nie zostałby powiązany z takim jsonem:

{
  "id": 5,
  "name": "Adam"
}

Z tego powodu, że występuje różnica w wielkości znaków. Niestety domyślne ustwienia serializatora z Microsoft biorą pod uwagę wielkość znaków. My chcemy tego uniknąć, dlatego powstała klasa, która przechowuje odpowiednie opcje. Znajduje się w projekcie Common.

  1. ResponseFactory to pomocnicza klasa, która z odpowiedzi HttpRequestMessage tworzy interesujące nas obiekty DataResponse lub BaseResponse – omówimy ją za chwilę.
  2. Pobieranie danych za pomocą POST…

No właśnie, spójrz na metodę GetClients. Ona pobiera dane za pomocą POST, a nie GET. Dlaczego tak jest? Czyżby to jaka herezja?

Przyczyną jest obecność klasy GetClientsRequestDto:

public class GetClientsRequestDto
{
    public int Skip { get; set; }
    public int Take { get; set; }
}

Metoda GET nie może mieć żadnych danych w ciele żądania. Oczywiście w tym przypadku można by te dwie właściwości włączyć do query stringa, wywołując końcówkę np: api/clients/all?skip=0&take=10. Jeśli jednak masz sporo więcej do filtrowania, do tego jakieś sortowanie i inne rzeczy… lub z jakiegoś powodu takie dane nie powinny być w query stringu, to spokojnie możesz je wrzucić do POSTa. Nikt Cię za to nie wychłosta 😉 Co więcej – to jest normalną praktyką w niektórych WebAPI.

ResponseFactory

Jak już wspomniałem, klasa ResponseFactory jest odpowiedzialna za utworzenie BaseResponse/DataResponse na podstawie przekazanego HttpResponseMessage. Jej implementacja w naszym przykładzie wygląda tak:

internal static class ResponseFactory
{
    public static async Task<BaseResponse> CreateBaseResponse(HttpResponseMessage response)
    {
        if (response.IsSuccessStatusCode)
            return new BaseResponse((int)response.StatusCode);
        else
            return new BaseResponse((int)response.StatusCode, await GetErrorMsgFromResponse(response));
    }

    public static async Task<DataResponse<T>> CreateDataResponse<T>(HttpResponseMessage response, JsonSerializerOptions jsonOptions)
    {
        if (response.IsSuccessStatusCode)
        {
            T data = await GetDataFromResponse<T>(response, jsonOptions);
            return new DataResponse<T>(data, (int)response.StatusCode);
        }
        else
        {
            return new DataResponse<T>(default(T), (int)response.StatusCode, await GetErrorMsgFromResponse(response));
        }
    }

    private static async Task<T> GetDataFromResponse<T>(HttpResponseMessage response, JsonSerializerOptions jsonOptions)
    {
        string content = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<T>(content, jsonOptions);
    }

    private static async Task<string> GetErrorMsgFromResponse(HttpResponseMessage response)
    {
        string result = await response.Content.ReadAsStringAsync();
        if (string.IsNullOrEmpty(result))
            return response.ReasonPhrase;
        else
            return result;
    }
}

Nie ma tu niczego skomplikowanego. Wszystko sprowadza się do tego, że odczytuję dane z contentu odpowiedzi i deserializuję je do odpowiedniego obiektu. To wszystko. Jedyne, co może być ciekawe to metoda GetErrorMsgFromResponse, która ma zwrócić komunikat błędu. Zakładam, że jeśli błąd wystąpi, zostanie umieszczony po prostu jako content odpowiedzi – tak jest skonstruowane przykładowe WebAPI.

Implementacja IOrderOperations

Jest analogiczna jak IClientOperations, dlatego też nie będę jej omawiał. Kod wygląda tak:

internal class OrderOperations : IOrderOperations
{
    private readonly HttpClient _httpClient;

    public OrderOperations(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<DataResponse<OrderDto>> AddOrder(OrderDto order)
    {
        var response = await _httpClient.PostAsJsonAsync("orders", order);
        return await ResponseFactory.CreateDataResponse<OrderDto>(response, DefaultJsonSerializerOptions.Options);
    }

    public async Task<BaseResponse> DeleteOrder(int id)
    {
        var response = await _httpClient.DeleteAsync($"orders/{id}");
        return await ResponseFactory.CreateBaseResponse(response);
    }

    public async Task<DataResponse<GetOrdersResultDto>> GetOrders(GetOrdersResultDto data)
    {
        var response = await _httpClient.PostAsJsonAsync("orders/all", data);
        return await ResponseFactory.CreateDataResponse<GetOrdersResultDto>(response, DefaultJsonSerializerOptions.Options);
    }

    public async Task<DataResponse<GetOrdersResultDto>> GetOrdersForClient(int clientId, GetOrdersRequestDto data)
    {
        var response = await _httpClient.PostAsJsonAsync($"orders/client/{clientId}", data);
        return await ResponseFactory.CreateDataResponse<GetOrdersResultDto> (response, DefaultJsonSerializerOptions.Options);
    }
}

Implementacja ApiClient

OK, nadszedł wreszcie czas na napisanie implementacji głównego klienta API:

public class ApiClient : IApiClient
{
    public IClientOperations ClientOperations { get; private set; }
    public IOrderOperations OrderOperations { get; private set; }

    private readonly HttpClient _httpClient;

    public ApiClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
        ClientOperations = new ClientOperations(_httpClient);
        OrderOperations = new OrderOperations(_httpClient);
    }
}

Tutaj HttpClient przychodzi z dependency injection. Następnie są tworzone odpowiednie obiekty – ClientOperations i OrderOperations, do których przekazujemy tego HttpClienta. Prawda, że proste?

HttpPipeline, czyli zupełnie nowy świat

Żeby klient API był wymuskany, można do niego dodać HttpPipeline. Pisałem o tym w tym artykule, więc nie będę się powtarzał. Zostawię Ci tylko zajawkę, że dzięki Http Pipeline, możesz zrobić zupełnie wszystko z żądaniem (zanim dotrze do celu) i odpowiedzią (zanim wróci do HttpClient). To zupełnie nowy świat możliwości. Przede wszystkim możesz automatycznie ustawiać wersję API, możesz odświeżać bearer token, możesz logować całe żądanie. Nic Cię tu nie ogranicza. Dlatego koniecznie przeczytaj ten artykuł, żeby mieć pełen obraz.

Przykładowe użycie

W repozytorium do tego artykułu jest umieszczony projekt WebApp – jest to bardzo prosta aplikacja RazorPages, które po krótce pokazuje użycie klienta.

UWAGA! Kod w aplikacji przykładowej jak i w WebApi jest podatny na różne rodzaje ataków. Dlatego nie stosuj takich „uproszczeń” w prawdziwym życiu. Różne ataki i jak się przed nimi chronić zostały opisane w tej książce.

W ramach ćwiczeń możesz spróbować zaimplementować w tym rozwiązaniu paginację, a także resztę operacji związanych z zamówieniami.


Dzięki za przeczytanie artykułu. Mam nadzieję, że teraz będziesz przykładał większą wagę do klientów API, które tworzysz i artykuł podpowiedział Ci jak to zrobić dobrze. Jeśli czegoś nie zrozumiałeś lub znalazłeś jakiś błąd, koniecznie daj znać w komentarzu 🙂

Obrazek artykułu: Server icons created by Freepik – Flaticon

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.

Na szybko

Jak zarejestrować wersjonowanie

  1. Pobierz NuGet: Microsoft.AspNetCore.Mvc.Versioning
  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");
    }
}

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?

  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");
});

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: 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.


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:
Różne typy projektów w VisualStudio 2019

Różne typy projektów w VisualStudio 2019

Po pewnych zmianach w Visual Studio 2019, tworzenie projektów może być nieco niezrozumiałe zwłaszcza dla młodszych programistów. Ten artykuł wyjaśnia po kolei typy projektów w VisualStudio i jest kierowany głównie do początkujących.

Jaki utworzyć projekt

Stworzyłem specjalnie na potrzeby tego artykułu prosty i nieidealny algorytm do wyboru typu projektu. Możesz go pobrać stąd

Algorytm wyboru odpowiedniego typu projektu

Co możemy utworzyć

Visual Studio daje nam właściwie nieograniczone możliwości. Pomaga utworzyć projekt na dowolną platformę w konkretnym języku i w wybranej technologii (gdzie można). Tak wygląda okienko tworzenia nowego projektu:

Okienko tworzenia nowego projektu w Visual Studio
Tworzenie nowego projektu

Okienko jest podzielone na dwie główne grupy. Z lewej strony znajduje się lista ostatnich używanych typów projektów. Po prostu możesz kliknąć na to, co już ostatnio tworzyłeś i nie musisz szukać w liście po prawej stronie.

Z prawej strony masz z kolei grupę podzieloną na dwa elementy. Na górze filtr, na dole listę dostępnych typów projektów. One mogą być różne w zależności od tego, jak zainstalowałeś Visual Studio (typy też można dodawać i usuwać z poziomu instalatora: Visual Studio Installer, który znajduje się na Twoim komputerze)

Jeśli teraz zaczniesz wpisywać w filtrze na górze nazwę typu projektu, który chcesz utworzyć, lista automatycznie zacznie się zmieniać. Ja przykładowo wpisałem Console i zobacz, jak wygląda moja lista na obrazku wyżej. Tych projektów jest dużo więcej. Teraz postaram się część z nich omówić:

Add-In

To jest po prostu rozszerzenie (plugin) do Visual Studio. Tak, można pisać własne pluginy. Będę o tym pisał w niedalekiej przyszłości. Tak naprawdę całe Visual Studio składa się z pluginów. Jak widzisz są to projekty VSIX.

VSIX Project

To projekt rozszerzenia VisualStudio z dodanymi już pewnymi plikami i kodami. Takich projektów jest kilka w zależności od języka, w którym piszesz (C#, VisualBasic)

WIdok projektu Add-In

Empty VSIX Project

jest to zupełnie pusty projekt rozszerzenia VisualStudio. Od tego powyżej różni się brakiem kodów i domyślnych plików. Wszystko piszesz od zera. Ma to swoje plusy i minusy.

Widok pustego projektu Add-In

Empty VSIX Project (Community)

to samo, co wyżej z tą różnicą, że zawiera referencje do biblioteki Community.VisualStudio.Toolkit, co ułatwia pisanie rozszerzeń

VSIX Project w/Command (Community)

ten template zawiera to co VSIXProject, dodatkowo posiada referencje do Community.VisualStudio.Toolkit i dodatkowo posiada napisaną już komendę. Ten suffiks w/Command czytaj jako „with command”

Widok projektu Add-In Community

VSIX Project w/Tool Window (Community)

Uuuu, to jest dopiero mocarz. Zawiera wszystko to, co VSIX Project w/Command (Community), ale dodatkowo własne okno narzędziowe (Tool window)

Widok projektu Add-In z okienkiem

Który projekt wybrać do tworzenia rozszerzenia? To oczywiście zależy. Najbardziej obszernym zdecydowanie jest VSIX Project w/Tool Window (Community) i może się wydawać najlepszym. Tam, gdzie używasz okien i komend może to być szybki start. Oczywiście jeśli wcześniej nie tworzyłeś żadnych rozszerzeń, polecam zaczynać od najmniejszego projektu Empty VSIX Project. Nie przytłoczy Cię duża ilość plików na początek i będziesz mógł rozwijać rozszerzenie w swoim tempie, ucząc się powoli nowych rzeczy.

Console

Czym są aplikacje konsolowe?

Aplikacje konsolowe tworzone są na komputery. Często są to jakieś narzędzia dostępne z wiersza poleceń lub proste programy testowe, gdzie szybko chcemy sprawdzić jakieś funkcjonalności.

Konsola

Ale aplikacje konsolowe potrafią być często naprawdę dużymi, poważnymi narzędziami. Czasami posiadają nakładki graficzne, dzięki którym łatwiej je obsługiwać. Zdarzają się też proste gry tekstowe lub z grafiką w postaci ASCII Art.

Typy aplikacji konsolowych

  • Console Application (dostępna w różnych językach: C#, F#, VisualBasic…) – podstawowa aplikacja konsolowa na komputery. Podczas jej tworzenia zostaniesz zapytany o wersję .NetCore, której używać
Wybór framework

Domyślnie w tej wersji Visual Studio aplikacje konsolowe są tworzone w .NetCore. W tym miejscu możesz wybrać wersję, której chcesz używać.

Jest to właściwie najmniejszy template. Domyślnie posiada tylko jeden plik Program.cs z małym fragmentem kodu.

Chociaż długo sam nie potrafiłem tego pojąć, to jednak aplikacje konsolowe są tym typem, od którego powinno zaczynać się naukę programowania. Po prostu jest wtedy dużo prościej pojąć pewne rzeczy.

Blank Node.js console application

są dwie wersje zależne od języka. JavaScript lub TypeScript. Jeśli nie wiesz, czym jest TypeScript, to po prostu JavaScript z typami. Sam TypeScript jest kompilowany później do JavaScript. Generalnie to jest najmniejszy template do tworzenia aplikacji w Node.js

Console App (.Net Framework)

to jest aplikacja konsolowa, zupełnie taka jak domyślna Console Application. Też dostępna w kilku językach. Różnica jest taka, że używając tego templatu, będziesz miał do dyspozycji cały .Net Framework. Ale uwaga, ten typ projektu tworzy aplikacje jedynie dla Windowsa.

Widok projektu konsolowego

Standalone code analysis tool

to służy do tworzenia analizatorów kodu. Jakiś czas temu w VisualStudio pojawiły się takie analizatory kodu, które za pomocą Roslyn potrafią podpowiedzieć Ci kilka rzeczy. Np., że powinieneś dany fragment kodu wywołać w wątku głównym. Albo, że możesz jakiś kod napisać czyściej. Dzięki temu VisualStudio potrafi też automatycznie dodać odpowiednie namespacey do pliku cs. Oczywiście nic nie stoi na przeszkodzie, żeby napisać własne analizatory. Do tego służy ten template.

Jak widzisz mamy tutaj właściwie 4 różne typy aplikacji do wyboru – Kosnola dla Windows, konsola niezależna od systemu (.NetCore)*, konsola w Node.js no i analizator kodu.

Pisząc „konsola niezależna od systemu” mam na myśli, że kiedyś tak może być. Na dzień dzisiejszy aplikacje konsolowe i okienkowe są tworzone tylko pod Windows. Ale w przyszłości może się to zmienić. Wtedy dużo prościej będzie przejść z aplikacji pisanej w .NetCore (lub będzie to w standardzie)

Desktop

To jest to, co lubię najbardziej. Stare, dobre aplikacje na komputery :). Tutaj tworzymy głównie aplikacje okienkowe i wszystko co z nimi powiązane (biblioteki, kontrolki, serwisy), ale możemy też znaleźć templaty do testów jednostkowych.

Opiszę pokrótce niektóre typy:

Windows Forms

  • Windows Forms App – tworzymy aplikację okienkową w technologii WindowsForms. Jedynie dla Windows. Używamy wybranej wersji .NetCore. Dostępne dla C# i VisualBasic
  • Windows Forms Class Library – biblioteka dll, której możemy używać w aplikacjach Windows Forms App. Jedynie dla Windows.
  • Windows Forms Control Library – biblioteka, w której możemy tworzyć dodatkowe kontrolki i używać ich w Windows Forms App. Tutaj też piszemy tylko dla Windows i w .NetCore.
  • Windows Forms * (.NET Framework) – te kilka pozycji jest analogiczne do tych powyżej. Z tą jedynie różnicą, że mamy do dyspozycji pełny .NET Framework

UWAGA! Pamiętaj o tym, że nie możesz mieszać ze sobą technologii .NetCore i .NET Framerowk. To znaczy, że jeśli tworzysz aplikację w .NetCore, to nie będziesz mógł użyć w niej .NET Framework.

WPF (Windows Presentation Foundation)

  • WPF Application – nowsza technologia niż Windows Forms. Używamy kodu XAML do projektowania okienek. Używamy wybranej wersji .NetCore. Dostępne jedynie dla Windows.
  • WPF Class Library – analogicznie jak w Windows Forms. Po prostu dllka, której możemy używać w WPF Application. Używamy wybranej wersji .NetCore i piszemy tylko dla Windows
  • WPF Custom Control Library – analogicznie jak w Windows Forms Control Library. Tworzymy kontrolki dla aplikacji WPF. Używamy wybranej wersji .NetCore i piszemy tylko dla Windows
  • WPF UserControl Library – tutaj mały haczyk. Możemy tworzyć „kontrolki” dziedziczące po UserControl i używać ich w aplikacji WPF. Nie ma to odpowiednika w WindowsForms, ponieważ WPF wymaga nieco innego podejścia. Używamy wybranej wersji .NetCore i piszemy w Windows.
  • WPF Application (.NET Framework) – to samo, co wyżej z tą różnicą, że używasz pełnej wersji .NET Framerowk

UWAGA! Pamiętaj o tym, że nie możesz mieszać ze sobą technologii .NetCore i .NET Framerowk. To znaczy, że jeśli tworzysz aplikację w .NetCore, to nie będziesz mógł użyć w niej .NET Framework.

Universal Windows

To są aplikacje, które mogą działać na wszystkich platformach z Windowsem. Na komputerach, telefonach (z Windowsem), XBox… Generalnie wszędzie tam, gdzie jest zainstalowany Windows. Co ciekawe, sam możesz tworzyć swoje urządzenia z odpowiednim chipem i tworzyć aplikacje, które będą na nim pracowały.

Te aplikacje też wykorzystują XAML do tworzenia okienek, ale robi się je zupełnie inaczej niż aplikacje w WPF. Mają zupełnie inny cykl życia i inne kontrolki.

Windows Service

To są usługi, które pracują w systemie. Po prostu takie aplikacje bez okienek, które pracują cały czas w tle.

Mobile

W tej kategorii znajdziemy templaty do tworzenia aplikacji mobilnych. Głównie w technologii Xamarin. Nie będę się tu za bardzo wysilał, bo też nie ma nad czym. Opiszę tylko w kilku słowach, czym jest Xamarin.

Xamarin

Wszystkie aplikacje oznaczone jako Xamarin, są pisane w technologii Xamarin. No tak, a masło jest z masła. Generalnie Xamarin to taka technologia, która próbuje połączyć ze sobą różne platformy. Android, iOS, Universal Windows i inne. Celem, jaki temu przyświeca jest to, żeby jak największa część kodu była współdzielona pomiędzy te wszystkie aplikacje. Zarówno część wizualna (Xamarin.Forms) jak i cała logika.

Mimo, że technologia ma już kilka lat, to jednak cały czas (maj 2021) są z nią jakieś problemy i czasami pewne rzeczy trzeba napisać inaczej niż by się chciało. Ale cały czas jest rozwijana i ulepszana. I faktycznie z roku na rok wygląda to coraz lepiej.

Co do reszty templatów (np. Photo Editing Extension) nie będę się rozpisywał, bo nie miałem z nimi do czynienia. Można się domyślić, że jeśli chcesz napisać aplikację na telewizor, użyjesz odpowiedniego templatu z oznaczeniem tvOS (chyba, że piszesz na Androida). Jeśli interesuje Cię coś konkretnego z tej listy poza Xamarin, odsyłam do google 🙂

Other

U siebie mam tylko jeden typ – Blank Solution. I mogłoby paść pytanie – po co komu pusta solucja z samym tylko plikiem sln? Trochę jest to przeszłość, a czasami tak po prostu zaczyna się większe projekty. Pozwala Ci to od początku panować całkowicie nad strukturą katalogów i dodatkowych plików, których chcesz używać.

Web

No i doszliśmy do momentu, w którym mamy aplikacje typowo Webowe. Czyli to, co dzisiaj zdecydowanie jest na topie. Visual Studio umożliwia nam tu utworzenie kilku typów:

ASP.NET Core Empty

To jest pusta aplikacja webowa. Możesz z niej zrobić WebAPI, stronę internetową lub co tylko dusza zapragnie w webie. Podczas tworzenia zostaniesz zapytany o pewne ustawienia:

Wybór framework
  • TargetFramework – podajesz tutaj, jakiego frameworka chcesz używać. Zazwyczaj wybierzesz .NetCore 3.1 lub .NET 5
  • Configure for HTTPS – dzisiaj świat na wysokim miejscu stawia bezpieczeństwo. Również w sieci. Jeśli zaznaczysz tę opcję, projekt będzie skonfigurowany do pracy z protokołem HTTPS. Podczas pierwszego debugowania takiego projektu, Visual Studio utworzy i zainstaluje dla Ciebie specjalny certyfikat (Self Signed Certificate), a przeglądarki podczas pierwszego uruchomienia ostrzegą Cię, że ten certyfikat jest niebezpieczny i nie pochodzi z zaufanego źródła. Nie jest to w tym momencie istotne. Ważne, że gdy opublikujesz taką aplikację w sieci, musisz mieć na serwerze poprawny certyfikat. Dzisiaj można go otrzymać za darmo od Let’s Encrypt. Serwer, na którym mam swoje aplikacje – HostedWindows umożliwia utworzenie takiego certyfikatu i automatyczne przedłużenie go, za pomocą jednego kliknięcia.
  • Enable Docker – jeśli nie wiesz, czym jest docker, to moje wyjaśnienie nic Ci nie powie. Jeśli wiesz, to wiesz do czego służy ta opcja 🙂

A czysta aplikacja wygląda tak:

Widok projektu webowego

ASP.NET Core WebAPI

To jest template, który wykorzystasz konkretnie do utworzenia restowego WebAPI. Jeśli nie wiesz, czym jest restowe WebAPI, to bardzo krótko mówiąc…

Wyobraź sobie, że masz aplikację, w której chcesz wyświetlić wszystkich swoich znajomych z Facebooka. Facebook wystawia takie WebAPI (nie jest to restowe, ale dla przykładu załóżmy, że jest) i teraz możesz się z nim skomunikować i zapytać go o listę swoich znajomych, wywołując taki adres: https://api.facebook.com/friends

Ten adres jest tzw. „endpointem”. Podany przeze mnie endpoint nie istnieje w rzeczywistości, został wymyślony na potrzeby artykułu. Ale tak to mniej więcej działa – aplikacja wywołuje odpowiedni adres (endpoint), a serwer, jeśli Twoje uprawnienia na to pozwolą, odpowiada w jakiś ustalony sposób. Najczęściej odpowiedź jest w formie JSON, rzadziej XML.

Podczas tworzenia projektu tego typu, zostaniesz poproszony o dodatkowe informacje:

Wybór framework

Większość jest analogicznie jak wyżej z jedną małą różnicą.

  • Authentication Type – w projekcie WebAPI możesz i powinieneś w jakiś sposób autentykować i autoryzować swoich użytkowników. Ten artykuł zdecydowanie nie jest miejscem na wyjaśnienia co, jak i dlaczego, bo zagadnienie jest dość obszerne. Wkrótce o tym napiszę. Jeśli nie wiesz o co chodzi, to zostaw na None.

Projekt WebAPI nieco różni się od pustego:

Widok projektu WebAPI

Jak widzisz doszedł folder Controllers, zawierający kontrolery i kilka innych plików. Kod też jest inny. Zatem, jeśli chcesz stworzyć WebAPI, to zdecydowanie zacznij od tego projektu. Własne proste WebAPI dzięki temu można stworzyć w 10 minut (co dzisiaj udowodniłem komuś :)).

ASP.NET Core Web App

To jest typowa strona internetowa. Frontend tworzy się w RazorPages. A całość po utworzeniu wygląda tak:

Widok projektu RazorPages

ASP.NET Core App (Model-View-Controller)

To jest już aplikacja webowa, używająca wzorca MVC. Front tworzy się w RazorViews. Generalnie jest to bardzo podobne do RazorPages z pewnymi niuansami. Same dwa templaty (ten i poprzedni) są do siebie bardzo podobne i czasem jest ciężko stwierdzić, który będzie lepszy.

Generalnie zasada jest taka, że do prostych stron używamy raczej Razor Pages (ASP.NET Core App tego pierwszego), natomiast do bardziej skomplikowanych używamy MVC. Dodatkowo ten projekt może zawierać WebAPI. Ale osobiście radziłbym nie mieszać.

Utworzony pusty projekt wygląda tak:

Widok projektu Razor MVC

ASP.NET Web Application (.NET Framework)

To jest stary typ aplikacji webowych, który został zastąpiony .NET Core. Niemniej jednak mnóstwo aplikacji jest w tym utrzymywanych i rozwijanych. Odradzam używanie tego templatu do nowych programów.


No to tyle jeśli chodzi o podstawowe typy projektów. Jest ich jeszcze sporo, dodatkowe przychodzą z innymi pluginami, ale ich nazwy raczej same się opisują.

Jeśli znalazłeś w tekście błąd lub masz problem, podziel się w komentarzu

Podziel się artykułem na: