Маршрутизація в ASP.NET Core: Основи
Маршрутизація в 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):
- EndpointRoutingMiddleware (
UseRouting()): Знаходить кінцеву точку, яка відповідає запиту. - EndpointMiddleware (
UseEndpoints()): Виконує код (делегат) знайденої кінцевої точки.
UseRouting та UseEndpoints вручну, оскільки клас WebApplication автоматично налаштовує їх для вас.Аналізатор кінцевих точок (Endpoint)
Кінцева точка (Endpoint) складається з трьох основних речей:
- HTTP-метод (GET, POST, PUT, DELETE тощо).
- Шаблон маршруту (наприклад,
/або/users/{id}). - Обробник (функція, яка виконається).
Створення базових кінцевих точок
Для визначення маршрутів ми використовуємо методи об'єкта app (інстанс WebApplication), такі як MapGet, MapPost, MapPut, MapDelete.
Розглянемо найпростіший приклад:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Головна сторінка");
app.MapGet("/about", () => "Сторінка про компанію");
app.Run();
Анатомія коду:
- Ми створили дві кінцеві точки:
- Перша реагує на HTTP GET запит за кореневою адресою
/і повертає рядок"Головна сторінка". - Друга реагує на HTTP GET запит за адресою
/aboutі повертає"Сторінка про компанію".
- Перша реагує на HTTP GET запит за кореневою адресою
- Обробником тут виступає звичайна лямбда-функція
() => ....
Повернення об'єктів (JSON Serializing)
Сучасні API рідко повертають просто текст. Зазвичай це структуровані дані у форматі JSON. ASP.NET Core Minimal APIs робить перетворення об'єкта в JSON автоматично!
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:
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. Для цього ми використовуємо параметри маршрутів.
Параметри позначаються фігурними дужками {назва_параметра} всередині шаблону маршруту.
Просте зв'язування параметра
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.
Декілька параметрів
Шлях може містити стільки параметрів, скільки потрібно, а як роздільник зазвичай використовують символ / (хоча можна й інші, наприклад -).
app.MapGet("/users/{userId}/books/{bookId}", (int userId, int bookId) =>
{
return $"Користувач {userId} запитує книгу {bookId}";
});
Запит: GET /users/14/books/89 -> результат: "Користувач 14 запитує книгу 89".
Необов'язкові (Опціональні) параметри та дефолтні значення
Іноді параметр може бути відсутнім у URL. У такому випадку ми додаємо ? після імені параметра в шаблоні, а параметр у делегаті робимо nullable або задаємо дефолтне значення.
// Опціональний параметр
app.MapGet("/products/{category?}", (string? category) =>
{
return $"Категорія: {category ?? "Усі товари"}";
});
// Параметр зі значенням за замовчуванням
app.MapGet("/articles/{page=1}", (int page) =>
{
return $"Сторінка номер: {page}";
});
Анатомія коду:
/products/{category?}: Запит/products/electronicsповерне"Категорія: electronics". Запит/products— поверне"Категорія: Усі товари". Зверніть увагу на Nullable типstring?./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():
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.
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>.
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[].
Практичні завдання
GET /add/{a}/{b}.
Вона повинна приймати два цілих числа як параметри маршруту і повертати їх суму у вигляді рядка "Сума: X".
Перевірте, як відреагує API, якщо замість числа передати текст.POST /pets, яка приймає JSON-об'єкт Pet (Name, Species, Age).
Збережіть його у static List<Pet> в пам'яті. Поверніть створеного улюбленця разом зі статусом 201 Created та URL /pets/ім'я-тварини.
Створіть GET /pets, щоб повертати весь поточний список.Міні-сервіс управління товарами Реалізуйте:
- Отримання всього списку:
GET /products - Пошук за категорією з дефолтним значенням (в посилання):
GET /products/category/{category=all} - Інформація про один товар:
GET /products/{id} - Якщо товару за ID не існує — поверніть статус 404 (використовуючи
Results.NotFound()). - Зробіть спеціальний маршрут
GET /routes, який виведе всю зареєстровану "мапу гілок", як було показано вище.
Конвеєр запитів та Middleware
Вичерпний посібник з архітектури Pipeline в ASP.NET Core. Розбираємо Middleware (Use, Run, Map), HttpContext, Request та Response об'єкти.
Маршрутизація в ASP.NET Core: Розширені можливості
У попередньому матеріалі ми дізналися, як створювати базові маршрути та приймати параметри. Однак реальні проєкти вимагають значно більшого контролю над тим, які запити має обробляти конкретна кінцева точка.