Przejdź do treści stopki
Iron Academy Logo
Naucz się C#
Naucz się C#

Inne Kategorie

Dodanie punktu końcowego Get By ID w .NET Aspire na Linuxie

[[academy-video-youtube({"vid": "hMhA5sLHUpY", "start_time": "0", "title": "Adding a Get By ID Endpoint in .NET Aspire on Linux", "creator": "Tim Corey", "length": "15m 20s"})]]

Zwrócenie wszystkich rekordów z tabeli bazy danych jest przydatne do stron listy, ale większość konsumentów API potrzebuje również pobierania pojedynczego rekordu poprzez jego identyfikator. Ten drugi punkt końcowy wprowadza decyzje, których trasa "GET all" nie wymagała: jaki typ zwrotu ma sens dla pojedynczego obiektu w porównaniu z kolekcją, co się dzieje, gdy ID nie pasuje do żadnego rekordu i jak przekazać to niepowodzenie osobie wywołującej z poprawnym kodem statusu HTTP.

W swoim filmie "Dodawanie punktu końcowego Get By ID w .NET Aspire na Linuxie", Tim Corey kontynuuje Tiny Ticket API, dodając końcowy punkt GET /api/tickets/{id}. Co zaczyna się jako kopiowanie-wklejanie istniejącej trasy, przekształca się w przegląd obsługi przypadków brzegowych na żywo: zwracanie pojedynczego obiektu zamiast tablicy, sprawdzanie wyników null i użycie TypedResults do zwrócenia albo 200 OK z biletem, albo 404 Not Found, gdy ID nie istnieje. Jeśli śledzisz serię C# na Linuxie lub tworzysz minimalne API, które wymagają odpowiednich odpowiedzi kodu statusu, ten odcinek obejmuje pełen proces myślowy.

Kopiowanie trasy Get All jako punktu wyjściowego

[0:35 - 1:45] Tim otwiera Program.cs usługi API i duplikuje istniejący punkt końcowy GET /api/tickets. Nowa trasa potrzebuje parametru ścieżki dla ID biletu, więc wzorzec URL zmienia się, aby uwzględniać {id:int}, a handler otrzymuje parametr int id. Odwołanie do procedury składowanej przełącza się z spTickets_GetAll do spTickets_Get, który oczekuje parametru ID.

app.MapGet("/api/tickets/{id:int}", async (int id, IDbConnection db) =>
{
    var tickets = await db.LoadSqlAsync<TicketModel>("spTickets_Get", new { id });
    // Initial version: returns a list, which we'll fix next
    return tickets;
});
app.MapGet("/api/tickets/{id:int}", async (int id, IDbConnection db) =>
{
    var tickets = await db.LoadSqlAsync<TicketModel>("spTickets_Get", new { id });
    // Initial version: returns a list, which we'll fix next
    return tickets;
});

Wygoda nazewnictwa warta zanotowania: parametr procedury składowanej jest małymi literami id, co dokładnie odpowiada nazwie parametru w C#. Oznacza to, że Dapper może mapować obiekt anonimowy new { id } bezpośrednio, bez podawania nazwy właściwości. Jeśli parametr SQL użyłby innego zapisu, obiekt anonimowy potrzebowałby jawnego przypisania właściwości jak new { Id = id }.

Zwracanie pojedynczego obiektu zamiast listy

[2:39 - 4:16] Pierwszy test przez Swagger ujawnia problem: przekazanie ID 2 zwraca kod 200 z prawidłowym biletem, ale korpus odpowiedzi jest zamknięty w tablicy JSON. Gdy osoba wywołująca żąda pojedynczego zasobu według ID, oczekują jednego obiektu, a nie kolekcji zawierającej jeden element.

Dołączenie .FirstOrDefault() do wyniku zapytania naprawia zawijanie. FirstOrDefault zwraca pierwszy element, jeśli lista ma elementy, lub null, jeśli lista jest pusta. To rozwiązuje problem z tablicą, ale wprowadza nowe pytanie: co powinno zwrócić API, gdy ID nie odpowiada żadnemu rekordowi?

var output = tickets.FirstOrDefault();
var output = tickets.FirstOrDefault();

Jednowierszowa zmiana daje poprawny kształt odpowiedzi dla istniejących rekordów. Testowanie z ID 4, który nie istnieje w bazie danych, ujawnia większą lukę. Odpowiedź zwraca się jako null z kodem statusu 200. To jest technicznie poprawne HTTP, ale wprowadza w błąd osobę wywołującą: kod 200 oznacza, że żądanie zakończyło się sukcesem, a zasób został znaleziony, podczas gdy w rzeczywistości nic nie pasowało.

Obsługa Not Found za pomocą TypedResults

[4:41 - 11:44] Ta sekcja pokazuje, jak Tim przepracowuje decyzje projektowe w czasie rzeczywistym, co czyni ją wartościową do oglądania, a nie tylko czytania końcowego kodu. Jego proces myślowy przechodzi przez kilka iteracji:

Najpierw rozważa użycie .First() zamiast .FirstOrDefault(), które rzuca wyjątek, gdy lista jest pusta. To generuje błąd 500, który jest gorszy niż null 200, ponieważ 500 oznacza błąd serwera, a nie brakujący zasób.

Następnie cofa się i buduje kontrolę null. Przechowuje wynik w zmiennej, sprawdza, czy jest NULL i zwraca różne odpowiedzi dla każdego przypadku. Wyzwanie polega na tym, że handler minimalnego API musi jawnie zadeklarować swój typ zwrotny, gdy może zwrócić więcej niż jeden kształt odpowiedzi.

Rozwiązaniem jest TypedResults, które pozwala określić możliwe typy odpowiedzi w sygnaturze metody:

app.MapGet("/api/tickets/{id:int}", async Task<Results<Ok<TicketModel>, NotFound>> (int id, IDbConnection db) =>
{
    var tickets = await db.LoadSqlAsync<TicketModel>("spTickets_Get", new { id });
    var output = tickets?.FirstOrDefault();

    if (output is null)
    {
        return TypedResults.NotFound();
    }

    return TypedResults.Ok(output);
});
app.MapGet("/api/tickets/{id:int}", async Task<Results<Ok<TicketModel>, NotFound>> (int id, IDbConnection db) =>
{
    var tickets = await db.LoadSqlAsync<TicketModel>("spTickets_Get", new { id });
    var output = tickets?.FirstOrDefault();

    if (output is null)
    {
        return TypedResults.NotFound();
    }

    return TypedResults.Ok(output);
});

Ten typ zwracany, Task<Results<Ok<TicketModel>, NotFound>>, informuje framework (i Swagger), że ten punkt końcowy generuje albo status 200 z ciałem TicketModel, albo 404 bez ciała. Kolorowe dopasowanie nawiasów w VS Code pomaga nawigować po zagnieżdżonych nawiasach trójkątnych, które szybko rosną z generycznymi typami wyników.

Podczas testowania wychodzi na jaw subtelny błąd: pierwsza wersja wywołuje TypedResults.NotFound(), ale nie return. Punkt końcowy kompiluje się, ponieważ NotFound() jest prawidłowym wyrażeniem, ale bez słowa kluczowego return, wykonanie przechodzi do ścieżki Ok. Tim wychwytuje to, gdy Swagger nadal pokazuje 200 dla brakującego ID, dodaje return i 404 pojawia się poprawnie w następnym uruchomieniu.

Testowanie obu ścieżek w Swagger

[11:44 - 14:09] Z końcowym kodem na miejscu, Tim przeprowadza oba scenariusze w Swagger UI. Przekazanie ID 3 zwraca kod 200 z obiektem biletu. Przekazanie ID 4 zwraca kod 404 z pustym korpusem odpowiedzi.

Wskazuje również na szczegół w interfejsie Swagger, który może dezorientować nowych użytkowników: sekcja "Responses" poniżej przycisku execute pokazuje możliwe kody odpowiedzi (200 i 404), a nie rzeczywisty wynik. Rzeczywista odpowiedź serwera pojawia się w oddzielnym panelu powyżej tej sekcji. Mylenie tych dwóch paneli jest częstym źródłem "dlaczego dostaję kod 200?" zamieszania.

Podejście TypedResults również automatycznie poprawia dokumentację Swaggera. Ponieważ typ zwracany deklaruje zarówno Ok<TicketModel>, jak i NotFound, Swagger pokazuje oba jako potencjalne wyniki z ich odpowiednimi schematami. Osoby czytające dokumentację API wiedzą, że muszą obsłużyć przypadek 404 bez potrzeby pisania osobnych adnotacji OpenAPI.

Podsumowanie: przypadki brzegowe przed funkcjami

[14:09 - 15:09] Co zaczęło się jako prosta kserokopia punktu końcowego get-all, zamieniło się w głębsze ćwiczenie w projektowaniu API. Ostateczna wersja obsługuje szczęśliwą ścieżkę (znaleziony rekord), przewidywalną porażkę (rekord nie znaleziony) oraz defensywną kontrolę null dla nieprzewidzianych scenariuszy (zapytanie zwracające NULL). Podejście Tima do pracy nad tymi przypadkami na żywo, zamiast prezentowania wypolerowanego kodu, pokazuje rodzaj iteracyjnego myślenia, które wymaga się od produkcyjnych punktów końcowych.

Wnioski

[15:09 - 15:20] Dodanie punktu końcowego get-by-ID do minimalnego API wymaga trzech decyzji wykraczających poza definicję trasy: użycie FirstOrDefault do rozpakowania kolekcji, sprawdzenie null do rozróżniania "nie znaleziono" od "znaleziono" i zadeklarowanie TypedResults w typie zwracanym, aby framework zwrócił poprawny kod statusu HTTP. Wzorzec Results<Ok<t>, NotFound> jest wielokrotnego użytku w każdym punkcie końcowym, który musi komunikować sukces lub brak.

Nawigacja po serii: Ten artykuł jest częścią serii C# na Linux budującej aplikację Tiny Ticket. Poprzednio: Dodawanie Swagger UI. Następnie: Dodawanie punktu końcowego POST Insert.

Przykład porady: Gdy handler Twojego minimalnego API zwraca wiele możliwych kodów statusu, zawsze deklaruj je w Results<> generycznym. To generuje dokładną dokumentację Swagger automatycznie i zmusza kompilator do weryfikacji, że każda ścieżka kodu zwraca ważny typ wyniku.

Obejrzyj pełne wideo na jego kanale YouTube i zdobywaj więcej informacji na temat budowania solidnych punktów końcowych API w serii C# on Linux.

Hero Worlddot related to Dodanie punktu końcowego Get By ID w .NET Aspire na Linuxie
Hero Affiliate related to Dodanie punktu końcowego Get By ID w .NET Aspire na Linuxie

Zarabiaj więcej, dzieląc się tym, co kochasz

Tworzysz treści dla deweloperów pracujących z .NET, C#, Java, Python, czy Node.js? Zamień swoją wiedzę specjalistyczną na dodatkowy dochód!

Zespół wsparcia Iron

Jesteśmy online 24 godziny, 5 dni w tygodniu.
Czat
E-mail
Zadzwoń do mnie