Overview
ASP.NET Core Razor Pages uses the PageModel pattern to separate presentation logic from the view. Each .cshtml Razor Page has a corresponding .cshtml.cs code-behind file containing a PageModel class that handles HTTP requests and prepares data for the view.
PageModel Architecture
PageModel classes inherit from PageModel base class and serve as the controller for a Razor Page. They encapsulate:
Request handling (OnGet, OnPost, etc.)
Model binding
Data access through injected services
Validation logic
Navigation and redirects
Class Structure
A typical PageModel in SupermarketWEB follows this structure:
[ Authorize ] // Optional: Require authentication
public class IndexModel : PageModel
{
private readonly SupermarketContext _context ;
// Constructor with dependency injection
public IndexModel ( SupermarketContext context )
{
_context = context ;
}
// Properties for data binding
public IList < Product > Products { get ; set ; } = default ! ;
// Handler methods
public async Task OnGetAsync ()
{
// GET request logic
}
}
Handler Methods
Handler methods are called automatically based on HTTP verb and naming convention.
OnGet / OnGetAsync
Handles GET requests, typically used to retrieve and display data:
List Page (Categories/Index.cshtml.cs)
Edit Page (Products/Edit.cshtml.cs)
Create Page (Categories/Create.cshtml.cs)
public async Task OnGetAsync ()
{
if ( _context . Categories != null )
{
Categories = await _context . Categories . ToListAsync ();
}
}
OnPost / OnPostAsync
Handles POST requests, typically for form submissions:
Create Operation (Categories/Create.cshtml.cs)
Update Operation (Products/Edit.cshtml.cs)
Login (Account/Login.cshtml.cs)
[ BindProperty ]
public Category Category { get ; set ; } = default ! ;
public async Task < IActionResult > OnPostAsync ()
{
if ( ! ModelState . IsValid || _context . Categories == null || Category == null )
{
return Page ();
}
_context . Categories . Add ( Category );
await _context . SaveChangesAsync ();
return RedirectToPage ( "./Index" );
}
Model Binding
The [BindProperty] attribute enables automatic binding of form data to properties:
[ BindProperty ]
public Category Category { get ; set ; } = default ! ;
By default, [BindProperty] only binds on POST requests. To bind on GET requests, use [BindProperty(SupportsGet = true)].
When a form is submitted:
ASP.NET Core automatically maps form fields to the bound property
Model validation is performed based on data annotations
The bound data is available in OnPost handler methods
Common Patterns
CRUD Operations Pattern
SupermarketWEB follows a consistent pattern across all entities:
Show Index Page - List all records
public IList < Category > Categories { get ; set ; } = default ! ;
public async Task OnGetAsync ()
{
if ( _context . Categories != null )
{
Categories = await _context . Categories . ToListAsync ();
}
}
Show Create Page - Add new record
[ BindProperty ]
public Category Category { get ; set ; } = default ! ;
public IActionResult OnGet ()
{
return Page ();
}
public async Task < IActionResult > OnPostAsync ()
{
if ( ! ModelState . IsValid || _context . Categories == null || Category == null )
{
return Page ();
}
_context . Categories . Add ( Category );
await _context . SaveChangesAsync ();
return RedirectToPage ( "./Index" );
}
Show Edit Page - Update existing record
[ BindProperty ]
public Product Products { get ; set ; } = default ! ;
public async Task < IActionResult > OnGetAsync ( int ? id )
{
if ( id == null || _context . Products == null )
{
return NotFound ();
}
var product = await _context . Products . FirstOrDefaultAsync ( m => m . Id == id );
if ( product == null )
{
return NotFound ();
}
Products = product ;
return Page ();
}
public async Task < IActionResult > OnPostAsync ()
{
if ( ! ModelState . IsValid )
{
return Page ();
}
_context . Attach ( Products ). State = EntityState . Modified ;
try
{
await _context . SaveChangesAsync ();
}
catch ( DbUpdateConcurrencyException )
{
if ( ! ProductExists ( Products . Id ))
{
return NotFound ();
}
else
{
throw ;
}
}
return RedirectToPage ( "./Index" );
}
private bool ProductExists ( int id )
{
return ( _context . Products ? . Any ( e => e . Id == id )). GetValueOrDefault ();
}
Dependency Injection Pattern
All PageModels receive services through constructor injection:
private readonly SupermarketContext _context ;
public IndexModel ( SupermarketContext context )
{
_context = context ;
}
Location Examples:
Pages/Categories/Index.cshtml.cs:15
Pages/Products/Index.cshtml.cs:15
Pages/Account/Login.cshtml.cs:15
Authorization Pattern
Protected pages use the [Authorize] attribute:
[ Authorize ]
public class IndexModel : PageModel
{
// Only authenticated users can access this page
}
Most CRUD pages (Categories, Products, Customers, PayModes) require authentication, while the Login page does not.
Return Types
PageModel handler methods commonly return:
Used when the page should be rendered automatically
Provides flexibility to return different results (Page, Redirect, NotFound, etc.)
Renders the associated Razor Page with current model state
Redirects to another Razor Page, typically after successful POST operations
Returns HTTP 404 when requested resource doesn’t exist
ModelState Validation
PageModels automatically validate bound properties based on data annotations:
public async Task < IActionResult > OnPostAsync ()
{
if ( ! ModelState . IsValid )
{
return Page (); // Re-render page with validation errors
}
// Proceed with data operations
}
Page Organization
SupermarketWEB organizes pages by feature:
Pages/
├── Categories/
│ ├── Index.cshtml.cs - List categories
│ ├── Create.cshtml.cs - Add new category
│ ├── Edit.cshtml.cs - Update category
│ └── Delete.cshtml.cs - Remove category
├── Products/
│ └── [Same CRUD structure]
├── Customers/
│ └── [Same CRUD structure]
├── PayModes/
│ └── [Same CRUD structure]
└── Account/
├── Login.cshtml.cs - User authentication
├── Register.cshtml.cs - User registration
└── Logout.cshtml.cs - User logout
Best Practices
Use async/await: All database operations should be asynchronous
Validate input: Always check ModelState before processing POST data
Handle null cases: Check for null DbSets and parameters
Use PRG pattern: Redirect after POST to prevent duplicate submissions
Return appropriate results: Use NotFound() for missing resources
Follow naming conventions: OnGetAsync, OnPostAsync, etc.
Keep logic thin: Complex business logic should be in services, not PageModels