Від Minimal API до Razor Pages: концептуальний перехід
Від Minimal API до Razor Pages: концептуальний перехід
Ви вже знаєте Minimal API: MapGet, MapPost, DI, middleware, конфігурацію. Ви будували API, до якого звертаються JavaScript-клієнти. Тепер зробіть крок убік — і подивіться на інший підхід до побудови веб-застосунків: Razor Pages.
Не кращий, не гірший — інший. Розуміння обох — ознака зрілого ASP.NET Core розробника.
Навіщо серверний рендеринг у 2024?
Коли є React, Vue, Angular — навіщо рендерити HTML на сервері? Давайте чесно.
Minimal API + SPA — правильний вибір коли:
- Є окремий фронтенд (мобільний застосунок, React SPA, інший клієнт)
- Дані споживаються кількома клієнтами одночасно
- Потрібна максимальна гнучкість на фронтенді
- Команда має окремих фронтенд і бекенд розробників
Razor Pages — правильний вибір коли:
- Ви будуєте адмін-панель для внутрішнього використання
- Проєкт нескладний і JavaScript-фреймворк — overhead
- SEO критично важливий (HTML готовий для краулера одразу)
- Команда невелика і хоче весь стек на C#
- Потрібна висока швидкість першого завантаження (TTFB)
- Корпоративний інтранет, де JavaScript може бути заблокований
Ментальна модель: той самий запит, різні підходи
Одне завдання — показати список товарів — на двох підходах:
Два файли: Program.cs + ProductService.cs
// Явне оголошення маршруту
app.MapGet("/products", async (ProductService svc) =>
{
var products = await svc.GetAllAsync();
return Results.Ok(products); // JSON-відповідь
});
Клієнт отримує JSON:
[{ "id": 1, "name": "Coffee", "price": 120 }]
Клієнт сам будує HTML (React, Vue, або fetch())
Два файли: Pages/Products/Index.cshtml + Pages/Products/Index.cshtml.cs
public class IndexModel : PageModel
{
private readonly ProductService _svc;
public IndexModel(ProductService svc) => _svc = svc;
public List<Product> Products { get; set; } = [];
// Маршрут /products → OnGet автоматично
public async Task OnGetAsync()
{
Products = await _svc.GetAllAsync();
}
}
@page
@model IndexModel
<h1>Товари</h1>
@foreach (var product in Model.Products)
{
<div>@product.Name — @product.Price ₴</div>
}
Клієнт отримує готовий HTML:
<h1>Товари</h1>
<div>Coffee — 120 ₴</div>
<div>Tea — 80 ₴</div>
Браузер просто відображає — не потрібен JS-фреймворк
Convention-based routing: файлова система як маршрутизатор
Це найважливіша концептуальна зміна для розробника, що прийшов з Minimal API.
У Minimal API маршрут — явний: app.MapGet("/products/featured", ...). Ви бачите його у коді.
У Razor Pages маршрут виводиться з файлової системи. Назва файлу = маршрут:
| Файл | Маршрут |
|---|---|
Pages/Index.cshtml | / |
Pages/Products/Index.cshtml | /products |
Pages/Products/Details.cshtml | /products/details |
Pages/Products/Create.cshtml | /products/create |
Pages/Admin/Users/Index.cshtml | /admin/users |
Pages/Blog/Post.cshtml | /blog/post |
Параметри маршруту — у директиві @page:
@page "{id:int}"
<!-- Pages/Products/Details.cshtml {id:int} → /products/details/42 -->
@page "{id:int?}"
<!-- Необов'язковий параметр: /products/details або /products/details/42 -->
@page "{category}/{id:int}"
<!-- /products/electronics/42 -->
Program.cs: мінімальні зміни
Для розробника з Minimal API Program.cs для Razor Pages виглядатиме знайомо — додається лише два рядки:
var builder = WebApplication.CreateBuilder(args);
// Ваші сервіси — без змін
builder.Services.AddScoped<ProductService>();
builder.Services.AddMemoryCache();
var app = builder.Build();
// Ваш middleware — без змін
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
// Маршрути — явні lambdas
app.MapGet("/products", async (ProductService svc) => { /* ... */ });
app.MapPost("/products", async (CreateProductRequest req, ProductService svc) => { /* ... */ });
app.Run();
var builder = WebApplication.CreateBuilder(args);
// Ваші сервіси — ТОЧНО ТІ САМІ, без жодних змін
builder.Services.AddScoped<ProductService>();
builder.Services.AddMemoryCache();
// ← НОВЕ: реєструємо Razor Pages замість Endpoints API Explorer
builder.Services.AddRazorPages();
var app = builder.Build();
// Ваш middleware — ТОЧНО ТОЙ САМИЙ, без змін
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
// ← НОВЕ: маршрути для Razor Pages замість явних MapGet
app.MapRazorPages();
app.Run();
Тільки два рядки відрізняються:
AddRazorPages()замістьAddEndpointsApiExplorer()+AddSwaggerGen()MapRazorPages()замість всіхMapGet/Post/Put/Delete
DI, конфігурація, middleware, логування, кешування, автентифікація — все те саме, без змін.
Структура проєкту Razor Pages
MyApp/
├── Pages/
│ ├── _Layout.cshtml ← Спільний шаблон (header, footer, nav)
│ ├── _ViewStart.cshtml ← Вказує який Layout використовувати
│ ├── _ViewImports.cshtml ← using, @addTagHelper (глобально)
│ ├── Index.cshtml → /
│ ├── Index.cshtml.cs ← PageModel для /
│ ├── Error.cshtml → /error
│ └── Products/
│ ├── Index.cshtml → /products
│ ├── Index.cshtml.cs
│ ├── Create.cshtml → /products/create
│ ├── Create.cshtml.cs
│ ├── Edit.cshtml → /products/edit/{id}
│ ├── Edit.cshtml.cs
│ └── Details.cshtml → /products/details/{id}
│ Details.cshtml.cs
├── wwwroot/ ← Статичні файли (css, js, images)
│ ├── css/
│ ├── js/
│ └── lib/ ← Bootstrap, jQuery (автоматично)
├── Program.cs
└── appsettings.json
Порівняйте зі структурою Minimal API проектів які ви вже знаєте — wwwroot, Program.cs, appsettings.json — це те саме. Нове: папка Pages/ зі спареними .cshtml + .cshtml.cs файлами.
Підтримуючі файли: _Layout, _ViewStart, _ViewImports
_ViewStart.cshtml
Виконується перед кожною сторінкою у папці (і підпапках). Аналог middleware але для view. Один рядок для вказання Layout:
@{
Layout = "_Layout";
// Вказуємо який Layout файл використовувати для всіх сторінок
// null = без Layout (має сенс для partial views або email templates)
}
_ViewImports.cshtml
Глобальний using для всіх сторінок. Щоб не писати @using MyApp.Models на кожній:
@using MyApp.Models
@using MyApp.Services
@namespace MyApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers — цей рядок активує вбудовані Tag Helpers. Без нього asp-for, asp-page та інші — не працюватимуть.
_Layout.cshtml
Спільний HTML-каркас для всіх сторінок. Аналогія: якщо у Minimal API ви додаєте middleware до pipeline — він виконується для кожного запиту. _Layout.cshtml — це HTML-"middleware" для кожної сторінки.
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@ViewData["Title"] — MyApp</title>
<link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="~/css/site.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" asp-page="/Index">MyApp</a>
<div class="navbar-nav">
<a class="nav-link" asp-page="/Products/Index">Товари</a>
</div>
</div>
</nav>
<div class="container mt-4">
@* Тут вставляється вміст кожної сторінки *@
@RenderBody()
</div>
<script src="~/lib/jquery/jquery.min.js"></script>
<script src="~/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
@* Секція для додаткових скриптів на конкретних сторінках *@
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
Відображення понять: шпаргалка
Тримайте цю таблицю перед очима поки звикаєте до Razor Pages:
| Концепція | Minimal API | Razor Pages |
|---|---|---|
| Маршрут | app.MapGet("/path", handler) | Pages/Path/Index.cshtml + @page |
| GET-хендлер | Lambda / static method | PageModel.OnGet() / OnGetAsync() |
| POST-хендлер | app.MapPost("/path", handler) | PageModel.OnPost() / OnPostAsync() |
| Відповідь (успіх) | Results.Ok(data) | return Page() |
| Редирект | Results.Redirect("/path") | return RedirectToPage("/Path") |
| 404 | Results.NotFound() | return NotFound() |
| З тіла запиту | [FromBody] | [BindProperty] |
| З query string | [FromQuery] | [BindProperty(SupportsGet = true)] |
| З маршруту | {id:int} + параметр | @page "{id:int}" + [BindProperty] або параметр OnGet |
| DI у хендлері | Параметр методу або [FromServices] | Конструктор PageModel або [FromServices] |
| Передати дані у view | Нема (JSON відповідь) | Властивість PageModel |
| Помилки валідації | Results.ValidationProblem() | ModelState.AddModelError() + return Page() |
| Middleware | app.Use(...) | Той самий app.Use(...) |
| DI реєстрація | builder.Services.Add...() | Точно той самий код |
Перший проєкт: створюємо за 5 хвилин
# Новий Razor Pages проєкт
dotnet new webapp -n MyFirstRazorApp
cd MyFirstRazorApp
# Запускаємо
dotnet run
# → http://localhost:5000 — відкривається стандартна сторінка
dotnet new webapp — шаблон для Razor Pages. Не плутайте з dotnet new web (порожній Minimal API) або dotnet new mvc (MVC з Controllers).Подивіться на згенерований Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages(); // Один рядок замість всього API Explorer
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles(); // wwwroot — знайоме!
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages(); // Один рядок замість всіх MapGet/Post
app.Run();
Відкрийте Pages/Index.cshtml — перша Razor Page:
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
</div>
І її PageModel Pages/Index.cshtml.cs:
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MyFirstRazorApp.Pages;
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
public void OnGet()
{
// Виконується при GET /
// Аналог: app.MapGet("/", () => { ... })
}
}
Що далі
Ви зрозуміли загальну картину. Тепер — глибше у кожну частину:
Output Cache: серверний кеш HTTP-відповідей (.NET 7+)
Детальний розгляд Output Caching у ASP.NET Core Minimal API (.NET 7+): реєстрація, іменовані та inline-політики, SetVaryByQuery/RouteValue/Header, теги та EvictByTagAsync, кешування авторизованих запитів, власні IOutputCachePolicy, Redis store для multi-instance, блокування (locking), моніторинг.
PageModel: логіка сторінки Razor Pages
Детальний розгляд PageModel в Razor Pages: OnGet/OnPost/OnPutAsync handler-методи, [BindProperty] і [BindProperty(SupportsGet)], ModelState і повідомлення валідації, return Page/RedirectToPage/NotFound/Content, TempData vs ViewData vs властивості PageModel, Page Handlers для кількох форм.