Skip to main content
Razor Pages is a page-based programming model that makes building web UI easier and more productive. SupermarketWEB uses Razor Pages for all user-facing pages, providing a clean separation between UI and logic.

What are Razor Pages?

Razor Pages combine HTML markup with C# code using Razor syntax. Each page consists of two files:
  • .cshtml - The view file containing HTML and Razor markup
  • .cshtml.cs - The page model containing C# logic and data handling
Unlike MVC controllers, Razor Pages keep related functionality together in a single location, making the codebase easier to navigate and maintain.

Page Structure

The View File (.cshtml)

The view file contains HTML markup with embedded Razor syntax:
Pages/Categories/Index.cshtml
@page
@model SupermarketWEB.Pages.Categories.IndexModel
@{
    ViewData["Title"] = "Index Categories";
}
<h1>Index Categories</h1>
<p>
    <a asp-page="Create" class="btn btn-success">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Categories[0].Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Categories[0].Description)
            </th>
            <th>Actions</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Categories)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Description)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.Id" class="btn btn-warning">Edit</a>
                    <a asp-page="./Delete" asp-route-id="@item.Id" class="btn btn-danger">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>
Key elements:
The @page directive at the top of the file is required to make this a Razor Page. It enables routing and request handling.
@page
You can also specify route templates:
@page "{id:int}"
Specifies the page model type, enabling strongly-typed access to data and methods:
@model SupermarketWEB.Pages.Categories.IndexModel
Access page model properties using Model:
@foreach (var item in Model.Categories)
Mix C# code with HTML using @ symbol:
  • Expressions: @item.Name outputs the value
  • Code blocks: @{ var x = 5; } executes C# code
  • Control structures: @foreach, @if, @switch
HTML-friendly syntax for generating links and forms:
<a asp-page="Create" class="btn btn-success">Create New</a>
<a asp-page="./Edit" asp-route-id="@item.Id">Edit</a>
Benefits over regular HTML:
  • Automatic URL generation
  • Strongly-typed route parameters
  • IntelliSense support

The Page Model (.cshtml.cs)

The page model handles HTTP requests and contains business logic:
Pages/Categories/Index.cshtml.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using SupermarketWEB.Data;
using SupermarketWEB.Models;

namespace SupermarketWEB.Pages.Categories
{
    [Authorize]
    public class IndexModel : PageModel
    {
        private readonly SupermarketContext _context;

        public IndexModel(SupermarketContext context) 
        {
            _context = context;
        }

        public IList<Category> Categories { get; set; } = default!;

        public async Task OnGetAsync() 
        {
            if (_context.Categories != null) 
            {
                Categories = await _context.Categories.ToListAsync();
            }
        }
    }
}
Key components:
1

Dependency Injection

The constructor accepts dependencies registered in Program.cs:
private readonly SupermarketContext _context;

public IndexModel(SupermarketContext context) 
{
    _context = context;
}
2

Public Properties

Properties accessible from the view using Model:
public IList<Category> Categories { get; set; } = default!;
In the view: @Model.Categories
3

Handler Methods

Methods that respond to HTTP requests:
  • OnGet() / OnGetAsync() - Handle GET requests
  • OnPost() / OnPostAsync() - Handle POST requests
  • OnPut(), OnDelete() - Other HTTP methods

Handler Methods

Handler methods execute when a page is requested. The method name determines which HTTP verb it handles.

GET Request Handler

Loads data for display:
public async Task OnGetAsync() 
{
    if (_context.Categories != null) 
    {
        Categories = await _context.Categories.ToListAsync();
    }
}
Use async methods (OnGetAsync, OnPostAsync) when performing database operations or I/O to improve scalability.

POST Request Handler

Processes form submissions:
Pages/Products/Create.cshtml.cs
[Authorize]
public class CreateModel : PageModel
{
    private readonly SupermarketContext _context;

    public CreateModel(SupermarketContext context)
    {
        _context = context;
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    [BindProperty]
    public Product Products { get; set; } = default!;

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid || _context.Products == null || Products == null)
        {
            return Page();
        }

        _context.Products.Add(Products);
        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}
Key concepts: [BindProperty] - Automatically binds form data to the property:
[BindProperty]
public Product Products { get; set; } = default!;
ModelState Validation - Checks if submitted data is valid:
if (!ModelState.IsValid)
{
    return Page(); // Redisplay form with validation errors
}
Redirect After POST - Prevents duplicate submissions:
return RedirectToPage("./Index");
Always use RedirectToPage() after successful POST operations to prevent form resubmission when the user refreshes the page (Post-Redirect-Get pattern).

Routing in Razor Pages

Razor Pages uses convention-based routing where the file path determines the URL.

Default Routing

File PathURL
Pages/Index.cshtml/ or /Index
Pages/Privacy.cshtml/Privacy
Pages/Categories/Index.cshtml/Categories or /Categories/Index
Pages/Categories/Create.cshtml/Categories/Create
Pages/Products/Edit.cshtml/Products/Edit
The Pages directory is the root for routing. File paths relative to Pages/ become the URL path.

Route Parameters

Add route parameters using the @page directive:
Pages/Categories/Edit.cshtml
@page "{id:int}"
@model SupermarketWEB.Pages.Categories.EditModel
Access in the page model:
public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var category = await _context.Categories.FindAsync(id);
    
    if (category == null)
    {
        return NotFound();
    }

    Category = category;
    return Page();
}
Route constraints ensure type safety:
  • {id:int} - Must be an integer
  • {slug:alpha} - Must be alphabetic
  • {id:int?} - Optional integer parameter
Use Tag Helpers to generate URLs:
<!-- Link to a page in the same folder -->
<a asp-page="Create">Create New</a>

<!-- Link with route parameter -->
<a asp-page="Edit" asp-route-id="@item.Id">Edit</a>

<!-- Link to page in different folder -->
<a asp-page="/Products/Index">Products</a>

<!-- Link with multiple parameters -->
<a asp-page="Search" asp-route-category="@catId" asp-route-page="2">Next</a>
Always use Tag Helpers instead of hardcoded URLs. They provide compile-time checking and automatically update if routing changes.

Shared Components

Layout Pages

The _Layout.cshtml file defines the common structure for all pages:
Pages/Shared/_Layout.cshtml (excerpt)
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - SupermarketWEB</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
</head>
<body>
    <nav class="navbar navbar-expand-lg bg-body-tertiary rounded">
        <!-- Navigation menu -->
    </nav>
    
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2024 - SupermarketWEB - <a asp-page="/Privacy">Privacy</a>
        </div>
    </footer>

    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
@RenderBody() - Inserts the content of the current page @RenderSectionAsync(“Scripts”) - Allows pages to inject custom scripts:
@section Scripts {
    <script src="~/js/custom-validation.js"></script>
}

Partial Views

Reusable UI components included in multiple pages:
Pages/Shared/_LoginStatusPartial.cshtml
@if(User.Identity.IsAuthenticated)
{
    <form method="post" class="form-inline" asp-page="/Account/Logout">
        Welcome @User.Identity.Name
        <button type="submit" class="m-lg-2 btn btn-link">Logout</button>
    </form>
}
else
{
    <a class="btn btn-l" asp-page="/Account/Login">Login</a>
}
Include partials using the <partial> tag helper:
<partial name="_LoginStatusPartial" />

View Configuration Files

_ViewStart.cshtml - Executes before each view, typically sets default layout:
Pages/_ViewStart.cshtml
@{
    Layout = "_Layout";
}
_ViewImports.cshtml - Imports namespaces and tag helpers for all views:
@using SupermarketWEB
@using SupermarketWEB.Models
@namespace SupermarketWEB.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Authorization

SupermarketWEB uses the [Authorize] attribute to protect pages:
[Authorize]
public class IndexModel : PageModel
{
    // Only authenticated users can access this page
}
Configured in Program.cs:
builder.Services.AddAuthentication().AddCookie("MyCookieAuth", option =>
{
    option.Cookie.Name = "MyCookieAuth";
    option.LoginPath = "/Account/Login";
});
When an unauthorized user tries to access a protected page:
  1. Request is intercepted
  2. User is redirected to /Account/Login
  3. After successful login, user is redirected back to the original page
Remove the [Authorize] attribute to make a page publicly accessible without authentication.

Common Patterns in SupermarketWEB

List Page (Index)

Displays all entities in a table with action buttons:
public async Task OnGetAsync() 
{
    if (_context.Categories != null) 
    {
        Categories = await _context.Categories.ToListAsync();
    }
}

Create Page

Form for adding new entities:
public IActionResult OnGet()
{
    return Page();
}

[BindProperty]
public Category Category { get; set; } = default!;

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Categories.Add(Category);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

Edit Page

Form for modifying existing entities:
Edit.cshtml.cs
public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Category = await _context.Categories.FindAsync(id);

    if (Category == null)
    {
        return NotFound();
    }
    return Page();
}

[BindProperty]
public Category Category { get; set; } = default!;

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Attach(Category).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!CategoryExists(Category.Id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return RedirectToPage("./Index");
}

Delete Page

Confirmation page before removing an entity:
Delete.cshtml.cs
public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Category = await _context.Categories.FindAsync(id);

    if (Category == null)
    {
        return NotFound();
    }
    return Page();
}

[BindProperty]
public Category Category { get; set; } = default!;

public async Task<IActionResult> OnPostAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var category = await _context.Categories.FindAsync(id);

    if (category != null)
    {
        _context.Categories.Remove(category);
        await _context.SaveChangesAsync();
    }

    return RedirectToPage("./Index");
}

Best Practices

1

Use Async Methods

Always use async handlers for database operations:
public async Task<IActionResult> OnPostAsync()
{
    await _context.SaveChangesAsync();
}
2

Validate Input

Check ModelState before processing form data:
if (!ModelState.IsValid)
{
    return Page();
}
3

Use PRG Pattern

Redirect after POST to prevent duplicate submissions:
return RedirectToPage("./Index");
4

Handle Null Cases

Always check for null when loading entities:
if (category == null)
{
    return NotFound();
}
5

Use Tag Helpers

Prefer Tag Helpers over hardcoded URLs:
<a asp-page="Edit" asp-route-id="@item.Id">Edit</a>

Next Steps

Build docs developers (and LLMs) love