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. You can also specify route templates:
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:
Dependency Injection
The constructor accepts dependencies registered in Program.cs: private readonly SupermarketContext _context ;
public IndexModel ( SupermarketContext context )
{
_context = context ;
}
Public Properties
Properties accessible from the view using Model: public IList < Category > Categories { get ; set ; } = default ! ;
In the view: @Model.Categories
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 Path URL Pages/Index.cshtml/ or /IndexPages/Privacy.cshtml/PrivacyPages/Categories/Index.cshtml/Categories or /Categories/IndexPages/Categories/Create.cshtml/Categories/CreatePages/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
Generating Links
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">
© 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:
_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:
Request is intercepted
User is redirected to /Account/Login
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:
Index.cshtml.cs
Index.cshtml
public async Task OnGetAsync ()
{
if ( _context . Categories != null )
{
Categories = await _context . Categories . ToListAsync ();
}
}
Create Page
Form for adding new entities:
Create.cshtml.cs
Create.cshtml
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:
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:
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
Use Async Methods
Always use async handlers for database operations: public async Task < IActionResult > OnPostAsync ()
{
await _context . SaveChangesAsync ();
}
Validate Input
Check ModelState before processing form data: if ( ! ModelState . IsValid )
{
return Page ();
}
Use PRG Pattern
Redirect after POST to prevent duplicate submissions: return RedirectToPage ( "./Index" );
Handle Null Cases
Always check for null when loading entities: if ( category == null )
{
return NotFound ();
}
Use Tag Helpers
Prefer Tag Helpers over hardcoded URLs: <a asp-page="Edit" asp-route-id="@item.Id">Edit</a>
Next Steps