🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

Minimal API i routing w ASP.NET Core — dopasowanie ścieżek

Minimal API i routing w ASP.NET Core — szablon trasy {id:int} i handler MapGet

Twoje pierwsze API działa, ale pod spodem odpowiada na pytanie, którego jeszcze nie zadałeś: skąd ASP.NET Core wie, że żądanie GET /api/produkty/5 ma trafić akurat do tego endpointu i że 5 to id? To jest routing — warstwa dopasowująca ścieżkę i metodę żądania do konkretnego fragmentu Twojego kodu. Gdy dostajesz 404 na endpoint, który „przecież istnieje”, prawie zawsze zawinił routing, nie logika.

Szablon trasy

Każdy Map* przyjmuje szablon ścieżki. Segmenty w klamrach to parametry trasy:

app.MapGet("/api/produkty", ...);            // stała ścieżka
app.MapGet("/api/produkty/{id}", ...);       // {id} — parametr
app.MapGet("/api/kategorie/{kat}/produkty", ...); // parametr w środku

Wartość parametru trafia do argumentu metody o tej samej nazwie:

app.MapGet("/api/produkty/{id}", (string id) =>
    $"Pytasz o produkt {id}");

Domyślnie parametr trasy jest tekstem. /api/produkty/5 da id = "5" (string), a /api/produkty/abc też się dopasuje — co zwykle nie jest tym, czego chcesz.

Ograniczenia tras — filtr już na poziomie dopasowania

Do parametru dopisujesz ograniczenie (route constraint), które zawęża, co się dopasuje:

app.MapGet("/api/produkty/{id:int}", (int id) => ...);        // tylko liczby całkowite
app.MapGet("/api/faktury/{numer:guid}", (Guid numer) => ...); // tylko GUID
app.MapGet("/api/strony/{n:int:min(1)}", (int n) => ...);     // int >= 1
OgraniczenieDopasujeOdrzuci
{id:int}5, -3abc, 5.0
{cena:decimal}9.99dużo
{aktywny:bool}true1
{numer:guid}a1b2...123
{nazwa:alpha}laptoplaptop2
{n:int:min(1)}1, 500, -5

Z {id:int} żądanie /api/produkty/abc nie pasuje do trasy i dostaje 404 — a parametr metody może być od razu typu int, bez ręcznego parsowania. To dwie korzyści naraz: silne typowanie i odrzucenie śmieci, zanim dotknie Twojego kodu.

Ograniczenie trasy to nie walidacja biznesowa — to filtr dopasowania. {id:int} decyduje, *czy* żądanie trafi do endpointu, nie *czy* dane mają sens. Regułę „cena > 0″ nadal sprawdzasz w kodzie.

Skąd binding bierze każdy parametr

ASP.NET Core wypełnia parametry endpointu z różnych źródeł według jasnych reguł:

ŹródłoKiedy używaneWymuszenie
Trasanazwa parametru = nazwa w {...}[FromRoute]
Query stringtyp prosty spoza trasy[FromQuery]
Body (JSON)typ złożony[FromBody]
Nagłówekjawnie[FromHeader]
DItyp zarejestrowany w kontenerze[FromServices]

Zwykle nie musisz nic wymuszać — konwencja wystarcza:

app.MapGet("/api/produkty/{id:int}", (
    int id,                 // z trasy (nazwa pasuje)
    bool szczegoly,         // z query: ?szczegoly=true
    MagazynProduktow magazyn) => // z DI
    ...);

Atrybutów [FromQuery] itp. używasz, gdy nazwa albo źródło jest niejednoznaczne — np. gdy chcesz parametr id wziąć z query, mimo że w trasie też jest {id}.

Grupowanie tras — MapGroup

Powtarzanie prefiksu /api/produkty przy każdym endpoincie się rozłazi. MapGroup wyciąga wspólny prefiks i pozwala dołożyć wspólną konfigurację (autoryzację, filtry) w jednym miejscu:

var produkty = app.MapGroup("/api/produkty");

produkty.MapGet("/", (MagazynProduktow m) => Results.Ok(m.Wszystkie()));
produkty.MapGet("/{id:int}", (int id, MagazynProduktow m) =>
    m.Znajdz(id) is { } p ? Results.Ok(p) : Results.NotFound());
produkty.MapPost("/", (NowyProdukt d, MagazynProduktow m) => ...);

Cała grupa dziedziczy prefiks, a produkty.RequireAuthorization() zabezpiecza wszystkie endpointy grupy naraz.

TypedResults — wynik, który da się testować

Results.Ok(...) zwraca IResult — nieprzezroczysty typ, po którym test nie pozna, jaki to był status. TypedResults zwraca konkretny typ, który możesz asertować w teście jednostkowym bez stawiania serwera:

app.MapGet("/api/produkty/{id:int}",
    (int id, MagazynProduktow m) =>
        m.Znajdz(id) is { } p
            ? TypedResults.Ok(p)          // Ok<Produkt>
            : TypedResults.NotFound());   // NotFound

Sygnatura endpointu staje się Results<Ok<Produkt>, NotFound> — samodokumentująca i testowalna. To ten sam wzorzec, który testy integracyjne ASP.NET Core wykorzystują do sprawdzania odpowiedzi.

Pułapki routingu

Pułapka #1 — brak ograniczenia daje zły status. Bez {id:int} żądanie /api/produkty/abc dopasuje się do endpointu, a próba użycia id jako liczby wyrzuci wyjątek → 500. Z {id:int} dostajesz czyste 404 — bo trasa się nie dopasowała. Zawsze ograniczaj parametry, które mają być liczbami.

Pułapka #2 — niejednoznaczne trasy. Dwa endpointy pasujące do tej samej ścieżki (/api/produkty/{id} i /api/produkty/{nazwa}) to AmbiguousMatchException w czasie żądania — nie przy starcie. Różnicuj ograniczeniami ({id:int} vs {nazwa:alpha}), żeby dopasowanie było jednoznaczne.

Pułapka #3 — kolejność vs specyficzność. Routing w ASP.NET Core dopasowuje po specyficzności, nie po kolejności rejestracji. Trasa /api/produkty/nowe (literał) wygra z /api/produkty/{id} (parametr), niezależnie od tego, którą zarejestrowałeś pierwszą — literał jest bardziej specyficzny. Nie polegaj na kolejności Map*.

Pułapka #4 — złożony typ w GET. Domyślnie typ złożony bindowany jest z body. GET z body to antywzorzec (i wiele klientów go nie wyśle) — filtry przekazuj jako parametry query, nie jako obiekt w body.

Podsumowanie

Routing to warstwa, która z metody i ścieżki żądania wybiera Twój endpoint i wypełnia jego parametry. Kluczowe narzędzia: szablony tras z parametrami {...}, ograniczenia ({id:int}, {numer:guid}) odrzucające śmieci już na etapie dopasowania i dające silne typy, MapGroup eliminujący powtórzony prefiks i wspólną konfigurację, oraz TypedResults czyniący odpowiedzi testowalnymi. Najczęstsze 404 i 500 w młodym API to nie błędy logiki, lecz brakujące ograniczenie trasy albo niejednoznaczne dopasowanie — routing rozwiązany porządnie usuwa całą tę klasę problemów.

Co dalej

Wiesz już, jak żądanie trafia do endpointu. Ale zanim tam dotrze, przechodzi przez łańcuch warstw — uwierzytelnianie, logowanie, obsługę błędów. Pokazuje to Middleware w ASP.NET Core. Warto też wrócić do protokołu HTTP, jeśli semantyka metod i statusów jeszcze nie leży, oraz zajrzeć do wstrzykiwania zależności, na którym stoi bindowanie serwisów.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 Co dalej?

Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.

Dodaj komentarz

czytanie to początek

Zamień wiedzę w umiejętności

Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.

Pobieram roadmapę →