У курсі Minimal API ви вже знаєте основи routing — як URL відповідає на запит. Тут ми не повторюємо цю базу, а зосереджуємося на тому, що унікально для MVC: два способи оголошення маршрутів (convention та attribute), спеціальні route tokens [controller] та [action], і — найпотужніше — Areas routing для великих застосунків.
{id:int}, query string). Тут ми вивчаємо лише MVC-специфіку поверх цих знань.Convention routing означає, що маршрути виводяться з шаблону заданого один раз — без атрибутів на кожному методі.
app.MapDefaultControllerRoute();
// Рівнозначно:
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}"
);
Цей один шаблон обслуговує всі Controllers і Actions у проєкті:
| URL | Controller | Action | id |
|---|---|---|---|
/ | Home | Index | null |
/home | Home | Index | null |
/product | Product | Index | null |
/product/details | Product | Details | null |
/product/details/42 | Product | Details | "42" |
/library/edit/7 | Library | Edit | "7" |
/blog/create | Blog | Create | null |
Можна оголосити кілька шаблонів для різних URL-структур:
// Спочатку — специфічніші маршрути
app.MapControllerRoute(
name: "blog",
pattern: "blog/{year:int}/{month:int}/{slug}",
defaults: new { controller = "Blog", action = "Post" }
);
app.MapControllerRoute(
name: "api-like",
pattern: "api/{controller}/{action}/{id?}"
);
// Потім — загальний default
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}"
);
Convention routing зручний, але має обмеження:
{controller}/{action}/blog/2024/03/my-post замість /blog/post?id=5MapControllerRouteТут на допомогу приходить attribute routing.
Attribute routing дозволяє оголосити маршрут безпосередньо на Controller або Action:
[Route("books")] // базовий префікс для всього Controller
public class BookController : Controller
{
[HttpGet("")] // GET /books
public IActionResult Index() => View();
[HttpGet("{id:int}")] // GET /books/42
public IActionResult Details(int id) => View();
[HttpGet("create")] // GET /books/create
public IActionResult Create() => View();
[HttpPost("create")] // POST /books/create
public IActionResult Create(CreateBookDto dto) { ... }
[HttpGet("{id:int}/edit")] // GET /books/42/edit
public IActionResult Edit(int id) => View();
[HttpPost("{id:int}/edit")] // POST /books/42/edit
public IActionResult Edit(int id, EditBookDto dto) { ... }
[HttpDelete("{id:int}")] // DELETE /books/42
public IActionResult Delete(int id) { ... }
}
// Варіант 1: [Route] тільки на Controller
[Route("catalog/products")]
public class ProductController : Controller
{
// GET /catalog/products (маршрут з Controller + "" від Action)
[HttpGet("")]
public IActionResult Index() => View();
// GET /catalog/products/5
[HttpGet("{id:int}")]
public IActionResult Details(int id) => View();
}
// Варіант 2: [Route] на кожному Action (без Route на Controller)
public class CategoryController : Controller
{
[Route("shop/categories")] // GET /shop/categories
public IActionResult Index() => View();
[Route("shop/categories/{slug}")] // GET /shop/categories/electronics
public IActionResult Details(string slug) => View();
}
Щоб не дублювати ім'я Controller у кожному атрибуті, використовуйте route tokens:
// [controller] замінюється іменем Controller (без суфікса)
// [action] замінюється іменем Action-методу
[Route("[controller]/[action]")]
public class ProductController : Controller
{
// GET /product/index (token [action] = "index")
public IActionResult Index() => View();
// GET /product/details (token [action] = "details")
public IActionResult Details(int id) => View();
// GET /product/create
[HttpGet]
public IActionResult Create() => View();
}
Це особливо корисно для базового шаблону всього Controller:
[Route("api/v1/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
// GET /api/v1/products
[HttpGet]
public IActionResult GetAll() => Ok();
// GET /api/v1/products/42
[HttpGet("{id:int}")]
public IActionResult GetById(int id) => Ok();
}
| Сценарій | Рекомендація |
|---|---|
| Простий CRUD, стандартні URL | Convention routing |
SEO-friendly URL (/blog/2024/post-slug) | Attribute routing |
REST API з версіонуванням (/api/v2/) | Attribute routing |
| Areas (великі застосунки) | Convention routing з MapAreaControllerRoute |
| Змішаний проєкт | Attribute routing на специфічних Controllers, convention для решти |
MapDefaultControllerRoute() для більшості і [Route] там де потрібні нестандартні URL.Areas — це спосіб розділити великий застосунок на незалежні частини (наприклад, Admin, Shop, Forum). Кожна Area має власні Controllers і Views.
Детальний розгляд Areas (файлова структура, asp-area Tag Helper, конфлікти імен) буде у спеціальній статті 08. Тут ми лише покажемо, як зареєструвати маршрут для Area, оскільки це частина загальної маршрутизації:
// Маршрути для Areas реєструються ПЕРЕД загальним маршрутом (вони специфічніші)
app.MapAreaControllerRoute(
name: "admin",
areaName: "Admin",
pattern: "admin/{controller=Dashboard}/{action=Index}/{id?}"
);
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}"
);
А на самому Controller достатньо повісити атрибут [Area]:
[Area("Admin")]
public class DashboardController : Controller { /* ... */ }
Будуємо покроково. Мета: публічний блог з URL формату /blog/2024/03/my-first-post.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
// Заглушка сервісу для демо
builder.Services.AddSingleton<IBlogService, InMemoryBlogService>();
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
// 1. Специфічний SEO-friendly маршрут для блогу — першим
app.MapControllerRoute(
name: "blog_post",
pattern: "blog/{year:int}/{month:int}/{slug}",
defaults: new { controller = "Blog", action = "Post" }
);
// 2. Default маршрут — останнім
app.MapDefaultControllerRoute();
app.Run();
namespace BlogApp.Models;
public record BlogPost(
int Id,
string Title,
string Slug,
string Content,
string Author,
DateOnly PublishedAt,
string Category
);
using Microsoft.AspNetCore.Mvc;
using BlogApp.Models;
using BlogApp.Services;
namespace BlogApp.Controllers;
public class BlogController : Controller
{
private readonly IBlogService _service;
public BlogController(IBlogService service) => _service = service;
// GET /blog — список всіх постів
// (default route: /blog = BlogController.Index())
public async Task<IActionResult> Index()
{
var posts = await _service.GetAllAsync();
return View(posts);
}
// GET /blog/2024/03/my-first-post
// (blog_post route: year=2024, month=3, slug="my-first-post")
public async Task<IActionResult> Post(int year, int month, string slug)
{
var post = await _service.GetBySlugAsync(year, month, slug);
if (post is null) return NotFound();
return View(post);
}
// GET /blog/category/tech
// Attribute routing для специфічних дій
[Route("blog/category/{categorySlug}")]
public async Task<IActionResult> Category(string categorySlug)
{
var posts = await _service.GetByCategoryAsync(categorySlug);
// Зберігаємо назву категорії у ViewBag для заголовка сторінки
// (Нагадуємо: ViewBag детально вивчається у статті 06)
ViewBag.Category = categorySlug;
return View(posts);
}
}
@model List<BlogApp.Models.BlogPost>
@{ ViewData["Title"] = "Блог"; }
<h1>Останні публікації</h1>
@foreach (var post in Model)
{
<article class="mb-4">
<h2>
@* Генеруємо SEO URL: /blog/2024/03/my-first-post *@
<a asp-action="Post"
asp-route-year="@post.PublishedAt.Year"
asp-route-month="@post.PublishedAt.Month"
asp-route-slug="@post.Slug">
@post.Title
</a>
</h2>
<p class="text-muted">
@post.Author · @post.PublishedAt.ToString("dd MMMM yyyy")
· <a asp-action="Category" asp-route-categorySlug="@post.Category">@post.Category</a>
</p>
<p>@post.Content[..Math.Min(200, post.Content.Length)]…</p>
</article>
}
Завдання 1.1. Додайте до BlogController Action Archive що повертає пости за конкретний рік та місяць: /blog/archive/2024/03. Використайте attribute routing. View повинен показати заголовок "Архів: Березень 2024" та список постів.
Завдання 1.2. Перевірте, що URL /blog/2024/03/my-first-post та /blog/2024/03/another-post коректно маршрутизуються до BlogController.Post() з різними slug. Додайте Console.WriteLine у метод і спостерігайте параметри.
Завдання 2.1. Створіть ProductController з attribute routing де:
GET /products → списокGET /products/{id:int} → деталі (404 якщо не знайдено)GET /products/by-category/{category} → список за категорієюGET /products/search?q=laptop&minPrice=1000&maxPrice=50000 → пошук з фільтрамиЗавдання 2.2. Виникла потреба: той самий ProductController має відповідати і на /products/ і на /shop/products/ (для SEO). Як реалізувати без дублювання коду? Підказка: Controller може мати кілька [Route] атрибутів.
Завдання 3.1. Спроєктуйте маршрутизацію для освітньої платформи:
/courses — каталог курсів/courses/{slug} — деталі курсу/courses/{courseSlug}/lessons/{lessonSlug} — урок у курсі/admin/courses — управління курсами (Area)/admin/courses/{id}/lessons — управління уроками (Area)/api/v1/courses — REST API для мобільного застосункуРеалізуйте Program.cs з усіма маршрутами та заглушки Controllers. Переконайтесь що URL не конфліктують.
{controller}/{action}/{id?}. Простий, але негнучкий для складних URL[Route], [HttpGet] безпосередньо на класі/методі. Гнучкий, SEO-friendly, але verbose[controller], [action] — динамічні частини маршруту що замінюються іменами[Area("Name")] + MapAreaControllerRouteMapDefaultControllerRoute — останнімУ наступній статті — Model Binding: як параметри HTTP-запиту (route, query, form, body) автоматично перетворюються на параметри ваших Action-методів.
Controllers та Actions: серце MVC
Глибокий розгляд Controller як класу в ASP.NET Core MVC: Controller vs ControllerBase,ієрархія IActionResult, всі типи результатів, HTTP-атрибути, контекст запиту. Демо-проєкт: LibraryController з повним CRUD покроково.
Model Binding: від HTTP до C#
Model Binding у ASP.NET Core MVC: прив'язка параметрів через методи (не [BindProperty]!), порядок пошуку Route→Query→Form→Body, атрибути [FromRoute]/[FromQuery]/[FromForm]/[FromBody]/[FromHeader]/[FromServices], Custom Model Binders через IModelBinder, TryUpdateModelAsync. Демо: ProductSearchController з Money custom binder.