Razor Pages

Від Minimal API до Razor Pages: концептуальний перехід

Плавний перехід від Minimal API до Razor Pages: навіщо потрібен server-side rendering, коли обирати API vs SSR, порівняльна таблиця концепцій, Convention-based routing через файлову структуру Pages/, перші кроки у Program.cs.

Від 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 може бути заблокований
Razor Pages — не застаріла технологія. Microsoft активно розвиває його. Bagel Shop, корпоративні портали, CMS, адмін-панелі — сотні тисяч продуктів побудовані на ньому. А для .NET Full Stack розробника знати Razor Pages — обов'язково.

Ментальна модель: той самий запит, різні підходи

Одне завдання — показати список товарів — на двох підходах:

Два файли: Program.cs + ProductService.cs

Program.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())


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 -->
Convention-based routing — це менше коду, але більше конвенцій. Назва файлу має точно відповідати бажаному URL (з урахуванням регістру на Linux-серверах). Виробіть звичку: перед створенням файлу — запитайте "який URL мені потрібен?"

Program.cs: мінімальні зміни

Для розробника з Minimal API Program.cs для Razor Pages виглядатиме знайомо — додається лише два рядки:

Program.cs
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();

Тільки два рядки відрізняються:

  • 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:

Pages/_ViewStart.cshtml
@{
    Layout = "_Layout";
    // Вказуємо який Layout файл використовувати для всіх сторінок
    // null = без Layout (має сенс для partial views або email templates)
}

_ViewImports.cshtml

Глобальний using для всіх сторінок. Щоб не писати @using MyApp.Models на кожній:

Pages/_ViewImports.cshtml
@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" для кожної сторінки.

Pages/Shared/_Layout.cshtml
<!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 APIRazor Pages
Маршрутapp.MapGet("/path", handler)Pages/Path/Index.cshtml + @page
GET-хендлерLambda / static methodPageModel.OnGet() / OnGetAsync()
POST-хендлерapp.MapPost("/path", handler)PageModel.OnPost() / OnPostAsync()
Відповідь (успіх)Results.Ok(data)return Page()
РедиректResults.Redirect("/path")return RedirectToPage("/Path")
404Results.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()
Middlewareapp.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:

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:

Pages/Index.cshtml
@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
</div>

І її PageModel Pages/Index.cshtml.cs:

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("/", () => { ... })
    }
}

Що далі

Ви зрозуміли загальну картину. Тепер — глибше у кожну частину:

PageModel у деталях

OnGet/Post/Async, [BindProperty], ModelState, повернення результатів, TempData — серце кожної Razor Page.

Razor синтаксис

@page, @model, @foreach, @if, layouts, sections, partial views — як C# і HTML живуть разом.

Tag Helpers

asp-for, asp-page, asp-validation-for — типізований HTML без магічних рядків.

Форми і валідація

Повний цикл: форма → POST → валідація → redirect або page з помилками.
Copyright © 2026