Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Introduction to ASP.NET Core Razor Pages Architecture

Tech May 12 2

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

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.