ASP.NET Core 8 Website - Razor Page Models

PART 7: Introducing Razor Page Models

PART 6 created a shared layout, but the Home page still calculated the server time inside Index.cshtml. This part separates C# page logic from HTML markup by introducing Razor Page Model files.

1. Understanding the Razor Page pair

A typical Razor Page consists of two related files:

FileResponsibility
Index.cshtmlPage structure and displayed content
Index.cshtml.csC# properties, handlers and page logic

The .cshtml.cs file is commonly called the code-behind or Page Model.

2. Create Pages/Index.cshtml.cs

using Microsoft.AspNetCore.Mvc.RazorPages;

namespace basicdotnet.Pages;

public class IndexModel : PageModel
{
    public string ServerTime { get; private set; } = "";

    public void OnGet()
    {
        ServerTime = DateTime.Now.ToString("HH:mm:ss");
    }
}

3. Understand IndexModel

The class inherits from PageModel:

public class IndexModel : PageModel

PageModel provides the features needed to process a Razor Page request.

The public property

public string ServerTime { get; private set; } = "";

The Razor page can read this property. The private setter prevents unrelated code from changing it directly.

The GET handler

public void OnGet()
{
    ServerTime = DateTime.Now.ToString("HH:mm:ss");
}

OnGet() runs when the browser sends an HTTP GET request for the page. It calculates the server time and stores it in ServerTime.

4. Update Pages/Index.cshtml

Replace the file with:

@page
@model IndexModel

@{
    ViewData["Title"] = "Home";
}

<main class="page-content home-content">
    <h1>Welcome to basicdotnet</h1>

    <p>
        A simple ASP.NET Core web app built
        with Razor Pages.
    </p>

    <p>
        It's @Model.ServerTime on the server.
    </p>
</main>

5. Understand @model

@model IndexModel

This directive connects Index.cshtml to the IndexModel class. The page accesses its public members through Model:

@Model.ServerTime

The server produces output similar to:

It's 18:42:15 on the server.

6. Create Pages/Privacy.cshtml.cs

using Microsoft.AspNetCore.Mvc.RazorPages;

namespace basicdotnet.Pages;

public class PrivacyModel : PageModel
{
    public void OnGet()
    {
    }
}

The Privacy page has no special logic yet, but creating its Page Model gives it the standard Razor Pages structure and provides a place for future logic.

7. Update Pages/Privacy.cshtml

@page
@model PrivacyModel

@{
    ViewData["Title"] = "Privacy Policy";
}

<main class="page-content">
    <h1>@ViewData["Title"]</h1>

    <p>
        basicdotnet is a demonstration application.
    </p>

    <p>
        This website does not collect or store
        personal information.
    </p>
</main>

The addition is:

@model PrivacyModel

8. Create Pages/NotFound.cshtml.cs

using Microsoft.AspNetCore.Mvc.RazorPages;

namespace basicdotnet.Pages;

public class NotFoundModel : PageModel
{
    public void OnGet()
    {
        Response.StatusCode = StatusCodes.Status404NotFound;
    }
}

The 404 status-setting logic now belongs in the Page Model rather than in the markup.

9. Update Pages/NotFound.cshtml

@page
@model NotFoundModel

@{
    ViewData["Title"] = "Page Not Found";
}

<main class="page-content">
    <h1 class="error-code">
        404 - Page Not Found
    </h1>

    <p>
        The requested page could not be found.
    </p>

    <p>
        <a href="/">Return to the Home page</a>
    </p>
</main>

10. Add shared Razor imports

Create Pages/_ViewImports.cshtml:

@using basicdotnet
@namespace basicdotnet.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

This file makes the project namespace and ASP.NET Core Tag Helpers available to all Razor Pages. It also allows pages to refer to models such as IndexModel without writing the full namespace.

11. Keep Program.cs unchanged

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

app.UseStaticFiles();
app.UseRouting();

app.MapRazorPages();
app.MapFallbackToPage("/NotFound");

app.Run();

Page Models do not need to be registered individually. AddRazorPages() and MapRazorPages() already enable their discovery and use.

12. Check the project structure

basicdotnet/
├── Pages/
│   ├── Shared/
│   │   └── _Layout.cshtml
│   ├── _ViewImports.cshtml
│   ├── _ViewStart.cshtml
│   ├── Index.cshtml
│   ├── Index.cshtml.cs
│   ├── NotFound.cshtml
│   ├── NotFound.cshtml.cs
│   ├── Privacy.cshtml
│   └── Privacy.cshtml.cs
├── wwwroot/
│   └── css/
│       └── site.css
├── basicdotnet.csproj
└── Program.cs

13. Understand the request flow

Browser requests /
        ↓
Razor Pages selects Index.cshtml
        ↓
IndexModel.OnGet() runs
        ↓
ServerTime receives the current time
        ↓
Index.cshtml reads @Model.ServerTime
        ↓
_Layout.cshtml produces the complete document

14. Build and run

dotnet build
dotnet run

Open the address shown in the terminal and test:

URLExpected result
/Home page with server time
/privacyPrivacy Policy page
/contactShared-layout page with HTTP 404 status

Razor markup versus Page Model

Razor pagePage Model
HTML structureC# request processing
Displays propertiesPrepares property values
Uses @ModelInherits from PageModel
Focuses on presentationFocuses on page behaviour

What has improved?

  • Server-time logic has been removed from the HTML file.
  • Each page has a dedicated Page Model.
  • The 404 status is assigned in C# rather than Razor markup.
  • The project now follows the conventional Razor Pages file structure.
  • Future form handling, validation and dependency injection can be added cleanly.

The page is still refreshed completely to obtain a new time. The next part will introduce a named Razor Page handler and use JavaScript to request the server time independently.

Next: PART 8 adds OnGetTime() and uses fetch() to update the server time without reloading the entire Home page.