Minimal API

Маршрутизація в ASP.NET Core: Основи

Маршрутизація (Routing) — це фундаментальний механізм будь-якого веб-фреймворку. Вона відповідає за аналіз вхідного HTTP-запиту (наприклад, GET /users/1) та виклик відповідної ділянки коду (обробника) для формування відповіді.

Маршрутизація в ASP.NET Core: Основи

Маршрутизація (Routing) — це фундаментальний механізм будь-якого веб-фреймворку. Вона відповідає за аналіз вхідного HTTP-запиту (наприклад, GET /users/1) та виклик відповідної ділянки коду (обробника) для формування відповіді.

У цьому розділі ми розберемо анатомію маршрутизації в ASP.NET Core Minimal APIs, навчимося створювати кінцеві точки (Endpoints) та працювати з параметрами маршрутів.

Що таке маршрутизація і навіщо вона потрібна?

Уявіть великий офісний центр. Коли ви заходите всередину, ви бачите турнікет і рецепцію зі списком компаній на різних поверхах. HTTP-запит — це ви. Маршрутизатор (Router) — це рецепція. Вона дивиться куди вам треба (/sales або /support) і направляє вас до потрібного кабінету (обробника).

Без маршрутизації нам довелося б вручну писати складний код з купою if/else, щоб аналізувати URL кожного запиту і вирішувати, що робити.

В ASP.NET Core маршрутизація складається з двох основних етапів (Middleware):

  1. EndpointRoutingMiddleware (UseRouting()): Знаходить кінцеву точку, яка відповідає запиту.
  2. EndpointMiddleware (UseEndpoints()): Виконує код (делегат) знайденої кінцевої точки.
У сучасних додатках Minimal APIs вам рідко доводиться викликати UseRouting та UseEndpoints вручну, оскільки клас WebApplication автоматично налаштовує їх для вас.

Аналізатор кінцевих точок (Endpoint)

Кінцева точка (Endpoint) складається з трьох основних речей:

  1. HTTP-метод (GET, POST, PUT, DELETE тощо).
  2. Шаблон маршруту (наприклад, / або /users/{id}).
  3. Обробник (функція, яка виконається).

Створення базових кінцевих точок

Для визначення маршрутів ми використовуємо методи об'єкта app (інстанс WebApplication), такі як MapGet, MapPost, MapPut, MapDelete.

Розглянемо найпростіший приклад:

Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Головна сторінка");
app.MapGet("/about", () => "Сторінка про компанію");

app.Run();

Анатомія коду:

  • Ми створили дві кінцеві точки:
    • Перша реагує на HTTP GET запит за кореневою адресою / і повертає рядок "Головна сторінка".
    • Друга реагує на HTTP GET запит за адресою /about і повертає "Сторінка про компанію".
  • Обробником тут виступає звичайна лямбда-функція () => ....

Повернення об'єктів (JSON Serializing)

Сучасні API рідко повертають просто текст. Зазвичай це структуровані дані у форматі JSON. ASP.NET Core Minimal APIs робить перетворення об'єкта в JSON автоматично!

Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/user", () => new Person("John Doe", 30));

app.Run();

record class Person(string Name, int Age);

Анатомія коду:

  • Ми створили record class Person.
  • У маршруті /user ми просто повертаємо new Person(...).
  • Магія під капотом: Фреймворк бачить, що ми повертаємо об'єкт (не рядок), автоматично серіалізує його в JSON та встановлює HTTP-заголовок Content-Type: application/json.

Результат запиту:

{
    "name": "John Doe",
    "age": 30
}

Базові CRUD операції (REST)

Використовуючи різні HTTP-методи (MapGet, MapPost, MapPut, MapDelete), ми можемо побудувати повноцінне RESTful API:

Program.cs
var app = WebApplication.Create();

app.MapGet("/items", () => "Отримання списку");
app.MapPost("/items", (Item item) => Results.Created($"/items/{item.Id}", item));
app.MapPut("/items/{id}", (int id, Item item) => Results.NoContent());
app.MapDelete("/items/{id}", (int id) => Results.Ok());

app.Run();

public record Item(int Id, string Name);

Анатомія коду:

  • MapGet("/items"): Обробляє отримання всіх елементів.
  • MapPost("/items"): Дані з тіла запиту (Request Body) автоматично десеріалізуються (JSON -> об'єкт) і передаються в параметр Item item.
  • MapPut та MapDelete: Містять у шляху параметр {id}, про який ми поговоримо детальніше нижче.
  • Results.Created, Results.Ok, Results.NoContent: Це вбудовані хелпери для повернення правильних HTTP статус-кодів (201 Created, 200 OK, 204 No Content).

Параметри маршрутів (Route Parameters)

Часто шлях містить динамічні значення. Наприклад, ми хочемо отримати користувача з ID 123 за адресою /users/123. Для цього ми використовуємо параметри маршрутів.

Параметри позначаються фігурними дужками {назва_параметра} всередині шаблону маршруту.

Просте зв'язування параметра

Program.cs
app.MapGet("/users/{id}", (int id) =>
{
    return $"ID користувача: {id}";
});

Анатомія коду:

  • Шаблон /users/{id} повідомляє роутеру, що після /users/ йде динамічна частина.
  • Значення цієї частини витягується зі шляху та автоматично конвертується в тип параметра нашої функції — int id.
  • Запит: GET /users/42 -> результат: "ID користувача: 42".
  • Що буде, якщо передати не число? Якщо ми зробимо запит GET /users/hello, фреймворк не зможе конвертувати "hello" в int і автоматично поверне помилку 400 Bad Request.

Декілька параметрів

Шлях може містити стільки параметрів, скільки потрібно, а як роздільник зазвичай використовують символ / (хоча можна й інші, наприклад -).

Program.cs
app.MapGet("/users/{userId}/books/{bookId}", (int userId, int bookId) =>
{
    return $"Користувач {userId} запитує книгу {bookId}";
});

Запит: GET /users/14/books/89 -> результат: "Користувач 14 запитує книгу 89".

Необов'язкові (Опціональні) параметри та дефолтні значення

Іноді параметр може бути відсутнім у URL. У такому випадку ми додаємо ? після імені параметра в шаблоні, а параметр у делегаті робимо nullable або задаємо дефолтне значення.

Program.cs
// Опціональний параметр
app.MapGet("/products/{category?}", (string? category) =>
{
    return $"Категорія: {category ?? "Усі товари"}";
});

// Параметр зі значенням за замовчуванням
app.MapGet("/articles/{page=1}", (int page) =>
{
    return $"Сторінка номер: {page}";
});

Анатомія коду:

  1. /products/{category?}: Запит /products/electronics поверне "Категорія: electronics". Запит /products — поверне "Категорія: Усі товари". Зверніть увагу на Nullable тип string?.
  2. /articles/{page=1}: Запит /articles встановить параметр page у значення 1.

Важливо: Опціональні параметри та параметри зі значеннями за замовчуванням мають бути останніми сегментами шаблону URL.

Валідація вхідних даних та стандарт ProblemDetails

Під час роботи з HTTP-запитами сервер постійно приймає дані ззовні (через тіло запиту, параметри маршруту або рядки запиту). Якщо клієнт надсилає некоректні дані — наприклад, порожній рядок замість імені або від'ємне число там, де очікується додатне, — сервер повинен зупинити виконання операції та повернути клієнту зрозуміле повідомлення про помилку зі статусом 400 Bad Request.

Стандартизація помилок через ProblemDetails

Раніше кожен розробник формував структуру JSON-відповідей для помилок на власний розсуд: хтось писав {"error": "bad request"}, хтось {"message": "error", "code": 123}. Це ускладнювало взаємодію між клієнтом і сервером, оскільки клієнтському коду доводилося щоразу адаптуватися під новий формат помилок.

Для уніфікації формату помилок у REST API організація IETF розробила специфікацію RFC 7807 (пізніше оновлену в RFC 9457), яка визначає стандарт Problem Details (application/problem+json).

В ASP.NET Core підтримка цього стандарту є вбудованою. Щоб увімкнути генерацію відповідей за стандартом ProblemDetails для стандартних помилок, викликається метод AddProblemDetails():

Program.cs
var builder = WebApplication.CreateBuilder(args);

// Реєструємо сервіси стандартизованих відповідей про помилки
builder.Services.AddProblemDetails();

var app = builder.Build();
Метод builder.Services.AddProblemDetails() реєструє сервіс, який автоматично конвертує необроблені HTTP-помилки або виклики результатів на кшталт Results.Problem() та Results.ValidationProblem() у стандартизований JSON-об'єкт із ключовими полями: type, title, status, detail, instance та словником помилок errors.

Примітивна валідація через MiniValidation та MinimalApis.Extensions

На відміну від класичних контролерів ASP.NET Core MVC (де атрибут [ApiController] автоматично запускає валідацію моделі за атрибутами), у Minimal APIs за замовчуванням автоматичної перевірки немає: якщо додати DataAnnotations-атрибути до DTO, фреймворк просто проігнорує їх без ручної перевірки.

Щоб швидко й зручно валідувати вхідні дані за допомогою стандартних атрибутів ([Required], [Range], [StringLength] тощо) без ручних перевірок if/else у кожному обробнику, застосовують зв'язку двох легких бібліотек: MiniValidation та MinimalApis.Extensions.

Встановлення пакетів

Встановіть пакети у ваш проєкт через .NET CLI:

dotnet add package MiniValidation
dotnet add package MinimalApis.Extensions

Налаштування та використання

Метод розширення .WithParameterValidation() перехоплює вхідні аргументи кінцевої точки перед запуском вашого коду, валідує їх за допомогою MiniValidation та у разі порушення правил автоматично формує відповідь 400 Bad Request у форматі ProblemDetails.

Program.cs
using System.ComponentModel.DataAnnotations;
using MinimalApis.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Додаємо підтримку ProblemDetails
builder.Services.AddProblemDetails();

var app = builder.Build();

// Додаємо .WithParameterValidation() до групи або окремого ендпоінта
app.MapPost("/items", (CreateItemDto dto) => TypedResults.Ok(dto))
   .WithParameterValidation();

app.Run();

// DTO з атрибутами
public record CreateItemDto(
    [Required] string Name,
    [Range(1, 100)] int Count
);

Анатомія коду:

  • public record CreateItemDto(...): Об'єкт передачі даних (DTO), параметри якого розмічені атрибутами з простору імен System.ComponentModel.DataAnnotations:
    • [Required]: значення обов'язкове (не може бути null або порожнім).
    • [Range(1, 100)]: числове значення повинно бути в діапазоні від 1 до 100.
  • .WithParameterValidation(): Метод розширення, який додає фільтр валідації. Якщо клієнт надішле невалідний об'єкт, обробник навіть не почне виконуватися — клієнт одразу отримає статус 400 Bad Request. Метод можна викликати як для окремого маршруту, так і для всієї групи (RouteGroupBuilder).
  • TypedResults.Ok(dto): Якщо всі правила виконані успішно, обробник повертає дані зі статусом 200 OK.

Якщо надіслати некоректний JSON-запит (наприклад, із відсутнім обов'язковим полем Name та Count поза допустимим діапазоном), клієнт отримає стандартизовану відповідь:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Name": [
      "The Name field is required."
    ],
    "Count": [
      "The field Count must be between 1 and 100."
    ]
  }
}
Завдяки комбінації AddProblemDetails() та .WithParameterValidation() помилки групуються у полі errors, де кожному полю моделі відповідає список конкретних причин помилки. Клієнтська частина застосунку може легко зв'язати ці помилки з відповідними полями форми введення.

Як подивитися всі доступні маршрути?

Іноді під час розробки корисно побачити повний список зареєстрованих маршрутів. ASP.NET Core зберігає цю інформацію і дозволяє отримати до неї доступ через залежність IEnumerable<EndpointDataSource>.

Program.cs
app.MapGet("/debug/routes", (IEnumerable<EndpointDataSource> endpointSources) =>
{
    var endpoints = endpointSources.SelectMany(source => source.Endpoints);
    var routes = new List<string>();

    foreach (var endpoint in endpoints)
    {
        if (endpoint is RouteEndpoint routeEndpoint)
        {
            routes.Add(routeEndpoint.RoutePattern.RawText);
        }
    }

    return routes;
});

Анатомія коду:

  • Ми інжектуємо (додаємо як параметр) IEnumerable<EndpointDataSource>. Це внутрішня колекція фреймворку, яка тримає інформацію про всі роути.
  • Проходимось по всіх endpoint-ах і, якщо це RouteEndpoint, дістаємо його "сирий текст" (RoutePattern.RawText).
  • Повертаємо список як JSON масив string[].

Практичні завдання

Створення калькулятора (Додавання) Створіть Minimal API додаток з однією кінцевою точкою GET /add/{a}/{b}. Вона повинна приймати два цілих числа як параметри маршруту і повертати їх суму у вигляді рядка "Сума: X". Перевірте, як відреагує API, якщо замість числа передати текст.
Copyright © 2026