Introduction to ASP.NET Core Razor Pages Architecture
Prerequisites and Setup
To utilize the features described, ensure .NET Core 2.0 SDK or later is installed. For development environments, Visual Studio 2017 version 15.3 or above is recommended, including the "ASP.NET and web development" workload.
Enabling Razor Pages in the Application Pipeline
In the Startup.cs configuration file, Razor Pages support is activated within the dependency injection container and the HTTP request pipeline.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Enables MVC services including Razor Pages and standard controllers
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc();
}
}
Page Structure and Syntax
A fundamental Razor page consists of a Razor view file marked with the @page directive. This directive transforms the file into a self-contained action handler, allowing it to process requests directly without routing through a separate controller class.
Below is a simple static page example:
@page
<h1>Welcome</h1>
<p>Current server time: @DateTime.Now</p>
Separating Logic with PageModel
For better maintainability and testability, the page logic can be separated from the presentation layer using the PageModel class. By convention, this file shares the same name as the view file but appends .cs (e.g., About.cshtml and About.cshtml.cs).
View File (Pages/About.cshtml):
@page
@using MyApp.Pages
@model AboutModel
<h2>System Information</h2>
<div>
Status: @Model.CurrentStatus
</div>
Code-Behind File (Pages/About.cshtml.cs):
using Microsoft.AspNetCore.Mvc.RazorPages;
using System;
namespace MyApp.Pages
{
public class AboutModel : PageModel
{
public string CurrentStatus { get; private set; } = "System Operational";
public void OnGet()
{
CurrentStatus += $" - Checked at {DateTime.Now.ToShortTimeString()}";
}
}
}
URL Routing Conventions
The routing of Razor Pages is determined by the physical file location within the "Pages" directory. The runtime matches URLs to these files automatically.
Implementing Form Handling and CRUD
Razor Pages provides built-in utilities for Model Binding, Tag Helpers, and validation. Let us examine a product management scenario involving an Entity Framework Core context.
Startup Configuration:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<InventoryDbContext>(options =>
options.UseInMemoryDatabase("InventoryDb"));
services.AddMvc();
}
Data Model:
using System.ComponentModel.DataAnnotations;
namespace MyApp.Models
{
public class Product
{
public int Id { get; set; }
[Required]
[MaxLength(150)]
public string Name { get; set; }
}
}
Database Context:
using Microsoft.EntityFrameworkCore;
using MyApp.Models;
namespace MyApp.Data
{
public class InventoryDbContext : DbContext
{
public InventoryDbContext(DbContextOptions<InventoryDbContext> options)
: base(options)
{
}
public DbSet<Product> Products { get; set; }
}
}
Creating a New Item
The "Create" page uses the [BindProperty] attribute to automatically map form data to properties on the POST request.
View (Pages/Products/Create.cshtml):
@page
@model MyApp.Pages.Products.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<h2>Add Product</h2>
<div asp-validation-summary="All" class="text-danger"></div>
<form method="POST">
<div>
<label asp-for="Product.Name"></label>
<input asp-for="Product.Name" class="form-control" />
<span asp-validation-for="Product.Name"></span>
</div>
<br />
<button type="submit" class="btn btn-primary">Save</button>
</form>
PageModel (Pages/Products/Create.cshtml.cs):
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MyApp.Data;
using MyApp.Models;
namespace MyApp.Pages.Products
{
public class CreateModel : PageModel
{
private readonly InventoryDbContext _context;
public CreateModel(InventoryDbContext context)
{
_context = context;
}
[BindProperty]
public Product Product { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Products.Add(Product);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
Listing and Deleting Items
The Index page lists items and handles deletion via a named handler method.
View (Pages/Products/Index.cshtml):
@page
@model MyApp.Pages.Products.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<h2>Product Inventory</h2>
<a asp-page="./Create" class="btn btn-success">Add New</a>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Product Name</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.InventoryList)
{
<tr>
<td>@item.Id</td>
<td>@item.Name</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.Id">Edit</a>
<form method="post" style="display:inline;">
<button type="submit" asp-page-handler="delete"
asp-route-id="@item.Id"
class="btn btn-danger">
Delete
</button>
</form>
</td>
</tr>
}
</tbody>
</table>
PageModel (Pages/Products/Index.cshtml.cs):
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MyApp.Data;
using MyApp.Models;
namespace MyApp.Pages.Products
{
public class IndexModel : PageModel
{
private readonly InventoryDbContext _context;
public IndexModel(InventoryDbContext context)
{
_context = context;
}
public IList<Product> InventoryList { get; set; }
public async Task OnGetAsync()
{
InventoryList = await _context.Products.ToListAsync();
}
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
var product = await _context.Products.FindAsync(id);
if (product != null)
{
_context.Products.Remove(product);
await _context.SaveChangesAsync();
}
return RedirectToPage();
}
}
}
Editing Data
The Edit page demonstrates route constraints and handling concurrency.
Route Directive:
@page "{id:int}"
This ansures the page only accepts integer values for the ID route parameter, returning a 404 otherwise.
PageModel Logic:
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid) return Page();
_context.Attach(Product).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
// Handle concurrency conflicts
if (!_context.Products.Any(e => e.Id == Product.Id))
{
return NotFound();
}
throw;
}
return RedirectToPage("./Index");
}
Multiple Handlers per Page
Complex forms often require multiple submit buttons targeting different logic. This is achieved using named handlers via the asp-page-handler attribute.
<input type="submit" asp-page-handler="Publish" value="Publish" />
<input type="submit" asp-page-handler="Archive" value="Archive" />
The corresponding backend methods follow the naming convention OnPost[HandlerName]Async (e.g., OnPostPublishAsync and OnPostArchiveAsync).
Custom Routing
To avoid query strings for handlers (e.g., ?handler=Publish), you can define a custom route template in the @page directive.
@page "{handler?}"
This places the handler name in the path, making it /Publish instead.
View Layouts and Global Imports
Layouts are defined in _Layout.cshtml and appplied to pages via _ViewStart.cshtml. The _ViewImports.cshtml file allows for global injection of Tag Helpers and namespaces.
@namespace MyApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers