Appearance
Are you an LLM? You can read better optimized documentation at /architecture/razor_pages.md for this page in Markdown format
Razor Pages
This document provides a detailed technical overview of the Razor Pages implementation in TheExampleApp. It is intended for developers responsible for maintaining and extending the application's user interface and page logic.
What are Razor Pages
Razor Pages is a page-centric web development framework built on ASP.NET Core. It offers a streamlined alternative to the traditional Model-View-Controller (MVC) pattern by co-locating the view and its corresponding server-side logic. In this application, Razor Pages are the foundation for server-side rendering of all user-facing content.
Each page is self-contained, handling its own HTTP requests, data fetching, and rendering. This approach simplifies the project structure and makes it easier to manage individual pages. The application leverages this pattern to fetch content from the Contentful headless CMS and render it as HTML using the Razor view engine.
For more information on the high-level application design, see the Architecture Overview.
Page structure
The core of the Razor Pages framework is the conventional pairing of two files for each page:
.cshtmlfile: The view template, written using Razor syntax. This file contains the HTML structure of the page, mixed with C# code to render dynamic content, execute loops, and apply conditional logic..cshtml.csfile: The "code-behind" file, which contains a C# class known as thePageModel. This class inherits from the framework'sPageModeltype and is responsible for all server-side logic associated with the page, including handling HTTP requests (e.g., GET, POST) and preparing data for the view.
For example, the application's home page is defined by:
Pages/Index.cshtml(The view)Pages/Index.cshtml.cs(ThePageModellogic)
The framework automatically associates these two files based on their shared name and location within the Pages directory.
BasePageModel
To promote code reuse and enforce a consistent architecture, all PageModel classes in this application inherit from a custom BasePageModel class instead of directly from the standard Microsoft.AspNetCore.Mvc.RazorPages.PageModel.
This base class provides essential, shared functionality to all pages, primarily by ensuring that a configured IContentfulClient is always available.
csharp
// File: TheExampleApp/Models/BasePageModel.cs
using Contentful.Core;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TheExampleApp.Configuration;
namespace TheExampleApp.Models
{
/// <summary>
/// Base model for all other models containing some common configuration.
/// </summary>
public class BasePageModel : PageModel
{
/// <summary>
/// The client used to communicate with the Contentful API.
/// </summary>
protected readonly IContentfulClient _client;
/// <summary>
/// Initializes a new BasePageModel.
/// </summary>
/// <param name="client">The client used to communicate with the Contentful API.</param>
public BasePageModel(IContentfulClient client)
{
_client = client;
// This resolver is crucial for mapping Contentful content types
// to the application's C# model classes.
_client.ContentTypeResolver = new ModulesResolver();
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Key Responsibilities of BasePageModel:
- Dependency Injection: It uses constructor injection to receive an
IContentfulClientinstance from the ASP.NET Core dependency injection container. This makes the client readily available to all derived page models via theprotected _clientfield. - Contentful Configuration: It configures the
ContentTypeResolveron theIContentfulClient. This is a critical step that allows the Contentful SDK to correctly deserialize different content types (like 'Hero Image', 'Copy', etc.) into their corresponding C# classes.
For more details on how the application interacts with the CMS, refer to the Contentful Integration documentation.
Page models in the application
The application contains several key page models that handle the core functionality. Each model is responsible for a specific view, fetching data from Contentful, and handling user interactions.
| Page Model | File Location | Route | Primary Responsibility |
|---|---|---|---|
IndexModel | Pages/Index.cshtml.cs | / | Renders the home page layout by fetching the "home" layout entry from Contentful. |
CoursesModel | Pages/Courses.cshtml.cs | /courses or /courses?category={slug} | Displays a list of all available courses. Supports filtering by category. |
IndexModel | Pages/Courses/Index.cshtml.cs | /courses/{slug} | Displays the details of a single course, including its list of lessons. |
LessonsModel | Pages/Courses/Lessons.cshtml.cs | /courses/{course-slug}/lessons/{lesson-slug} | Displays the content of a specific lesson within a course. |
IndexModel (/)
This model is responsible for rendering the application's home page by fetching a Layout entry from Contentful.
- Dependencies:
IContentfulClient. - Logic: The
OnGethandler fetches the layout entry with a slug of "home" from Contentful. It uses an include depth of 4 to fetch nested content references and respects the locale stored in the session. The model also collects system properties from both the layout and its content modules for tracking purposes.
csharp
// File: TheExampleApp/Pages/Index.cshtml.cs
public async Task OnGet()
{
var queryBuilder = QueryBuilder<Layout>.New
.ContentTypeIs("layout")
.FieldEquals(f => f.Slug, "home")
.Include(4)
.LocaleIs(HttpContext?.Session?.GetString(Startup.LOCALE_KEY) ?? CultureInfo.CurrentCulture.ToString());
var indexPage = (await _client.GetEntries(queryBuilder)).FirstOrDefault();
IndexPage = indexPage;
var systemProperties = new List<SystemProperties> { indexPage.Sys };
if (indexPage.ContentModules != null && indexPage.ContentModules.Any())
{
systemProperties.AddRange(indexPage.ContentModules?.Select(c => c.Sys));
}
SystemProperties = systemProperties;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CoursesModel (/courses)
This model fetches and displays a list of courses. It also handles filtering by category via query string parameter.
- Dependencies:
IContentfulClient,IBreadcrumbsManager,IViewLocalizer. - Logic: In its
OnGethandler, it fetches allcategoryandcourseentries from Contentful. Courses are ordered by creation date (newest first). If acategoryquery parameter is present, it filters theCourseslist to only include courses belonging to that category and updates the breadcrumb navigation. It also handles cases where the requested category does not exist, returning a 404 Not Found result. The model exposes anEditorialFeaturesEnabledproperty that checks session state to determine if editorial features should be shown in the view.
csharp
// File: TheExampleApp/Pages/Courses.cshtml.cs
public async Task<IActionResult> OnGet(string category)
{
// Get all categories since they're always displayed in the left hand side of the view.
var categories = await _client.GetEntriesByType("category",
QueryBuilder<Category>.New.Include(5).LocaleIs(HttpContext?.Session?.GetString(Startup.LOCALE_KEY) ?? CultureInfo.CurrentCulture.ToString()));
var queryBuilder = QueryBuilder<Course>.New.ContentTypeIs("course").Include(5)
.LocaleIs(HttpContext?.Session?.GetString(Startup.LOCALE_KEY) ?? CultureInfo.CurrentCulture.ToString())
.OrderBy("-sys.createdAt");
var courses = await _client.GetEntries(queryBuilder);
Categories = categories.ToList();
SelectedCategory = categories.FirstOrDefault(c => c.Slug == category);
if (!string.IsNullOrEmpty(category))
{
// Filter the courses by the selected category.
Courses = courses.Where(c => c.Categories != null && c.Categories.Any(x => x.Slug == category.ToLower())).ToList();
var cat = Categories.FirstOrDefault(c => c.Slug == category.ToLower());
if(cat == null)
{
TempData["NotFound"] = _localizer["errorMessage404Category"].Value;
return NotFound();
}
// Replace the breadcrumb for the category to the title of the category, which is more readable.
_breadcrumbsManager.ReplaceCrumbForSlug(category.ToLower().Replace('-', ' '), cat.Title);
}
else
{
// If we don't have a category, just display all courses.
Courses = courses.ToList();
}
return Page();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
IndexModel (/courses/{slug})
This model is responsible for displaying a single course page.
- Dependencies:
IContentfulClient,IVisitedLessonsManager,IBreadcrumbsManager,IViewLocalizer. - Logic: The
OnGethandler takes aslugparameter from the route. It uses this slug to query Contentful for the specificcourseentry. If no course is found, it sets a localized error message inTempDataand returns a 404 Not Found result. When a course is found, it tracks the course as visited, updates the breadcrumb navigation with the course title, and adds the list of visited lessons toViewDatafor display in the view.
csharp
// File: TheExampleApp/Pages/Courses/Index.cshtml.cs
public async Task<IActionResult> OnGet(string slug)
{
var queryBuilder = QueryBuilder<Course>.New
.ContentTypeIs("course")
.FieldEquals(f => f.Slug, slug?.ToLower())
.Include(5)
.LocaleIs(HttpContext?.Session?.GetString(Startup.LOCALE_KEY) ?? CultureInfo.CurrentCulture.ToString());
Course = (await _client.GetEntries(queryBuilder)).FirstOrDefault();
if(Course == null)
{
TempData["NotFound"] = _localizer["errorMessage404Course"].Value;
// If the course is not found return a 404 result.
return NotFound();
}
// Add the current course as visited.
_visitedLessonsManager.AddVisitedLesson(Course.Sys.Id);
// Replace the label of the breadcrum with the title of the course.
_breadcrumbsManager.ReplaceCrumbForSlug(Course.Slug.ToLower().Replace('-', ' '), Course.Title);
// Add the visited lessons and courses to viewdata.
ViewData["VisitedLessons"] = _visitedLessonsManager.VisitedLessons;
return Page();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
LessonsModel (/courses/{course-slug}/lessons/{lesson-slug})
This model is responsible for displaying a specific lesson within a course.
- Dependencies:
IContentfulClient,IVisitedLessonsManager,IBreadcrumbsManager,IViewLocalizer. - Logic: The
OnGethandler accepts two route parameters:slug(the course slug) andlessonSlug(the lesson slug). It first fetches the course from Contentful using the course slug with an include depth of 5 to ensure all nested content is retrieved. If the course is not found, it returns a 404 Not Found result with a localized error message. Once the course is retrieved, it searches for the specific lesson within the course's lessons collection. If the lesson is not found, it also returns a 404 with an appropriate error message. When both the course and lesson are found, the model updates the breadcrumb navigation with both the course and lesson titles, tracks the lesson as visited, and adds the visited lessons toViewData. The model also collects system properties from both the lesson and its modules for tracking purposes. Additionally, it exposes aNextLessonSlugproperty that calculates the slug of the next lesson in the course sequence for navigation purposes.
csharp
// File: TheExampleApp/Pages/Courses/Lessons.cshtml.cs
public async Task<IActionResult> OnGet(string slug, string lessonSlug)
{
var queryBuilder = QueryBuilder<Course>.New
.ContentTypeIs("course")
.FieldEquals(f => f.Slug, slug?.ToLower())
.Include(5)
.LocaleIs(HttpContext?.Session?.GetString(Startup.LOCALE_KEY) ?? CultureInfo.CurrentCulture.ToString());
Course = (await _client.GetEntries(queryBuilder)).FirstOrDefault();
if (Course == null)
{
// If the course is not found return 404.
TempData["NotFound"] = _localizer["errorMessage404Course"].Value;
return NotFound();
}
SelectedLesson = Course.Lessons.FirstOrDefault(c => c.Slug == lessonSlug?.ToLower());
if (SelectedLesson == null)
{
// If the lesson is not found, also return a 404.
TempData["NotFound"] = _localizer["errorMessage404Lesson"].Value;
return NotFound();
}
// Replace the label of the breadcrum with the title of the course....
_breadcrumbsManager.ReplaceCrumbForSlug(Course.Slug.ToLower().Replace('-', ' '), Course.Title);
// ...and the lesson.
_breadcrumbsManager.ReplaceCrumbForSlug(SelectedLesson.Slug.ToLower().Replace('-', ' '), SelectedLesson.Title);
// Add the current lesson as visited.
_visitedLessonsManager.AddVisitedLesson(SelectedLesson.Sys.Id);
// Add the visited lessons and courses to viewdata.
ViewData["VisitedLessons"] = _visitedLessonsManager.VisitedLessons;
var systemProperties = new List<SystemProperties> { SelectedLesson.Sys };
if (SelectedLesson.Modules != null && SelectedLesson.Modules.Any())
{
systemProperties.AddRange(SelectedLesson.Modules?.Select(c => c.Sys));
}
SystemProperties = systemProperties;
return Page();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Routing
Routing in Razor Pages is primarily convention-based, but can be customized with route templates.
Convention-Based Routing: By default, the URL path to a page matches its file path within the
Pagesdirectory (relative to the project root).Pages/Index.cshtml->/Pages/Courses.cshtml->/courses
Route Templates: For more complex routes with parameters, the
@pagedirective is used in the.cshtmlfile to define a route template. The application uses this for dynamic course and lesson pages.Course Detail Page: The file
Pages/Courses/Index.cshtmlcontains the directive@page "/courses/{slug}". This maps URLs like/courses/hello-contentfulto this page. The{slug}segment is captured as a route parameter and passed to theOnGetmethod of itsPageModel.Lesson Detail Page: The file
Pages/Courses/Lessons.cshtmlcontains the directive@page "/courses/{slug}/lessons/{lessonSlug}". This maps URLs like/courses/hello-contentful/lessons/apisto the lesson page, capturing both the course slug and the lesson slug.
The PageModel handler methods are designed to accept these parameters:
csharp
// In Pages/Courses/Index.cshtml.cs - captures {slug}
public async Task<IActionResult> OnGet(string slug)
{
// ...
}
// In Pages/Courses/Lessons.cshtml.cs - captures {slug} and {lessonSlug}
public async Task<IActionResult> OnGet(string slug, string lessonSlug)
{
// ...
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Data binding
Data binding is the mechanism that connects the PageModel with its view. It works in two primary ways in this application.
Input Binding: From Request to PageModel
The ASP.NET Core framework automatically binds incoming request data (from route values, query strings, or form posts) to the parameters of the handler methods. As seen in the Routing section, route parameters like {slug} are automatically bound to the slug string parameter in the OnGet method.
Output Binding: From PageModel to View
Data fetched within the PageModel is exposed to the .cshtml view via public properties.
- A public property is declared in the
PageModelclass. - Inside an
OnGetor other handler method, this property is populated with data, typically from a Contentful query. - In the
.cshtmlview, the data is accessed using the@Modelkeyword, which refers to the instance of thePageModel.
Example: Binding a Course object
1. Define the property in the PageModel:
csharp
// File: TheExampleApp/Pages/Courses/Index.cshtml.cs
namespace TheExampleApp.Pages.Courses
{
public class IndexModel : BasePageModel
{
// ...
/// <summary>
/// The course being displayed.
/// </summary>
public Course Course { get; set; }
public async Task<IActionResult> OnGet(string slug)
{
// ... query logic ...
Course = (await _client.GetEntries(queryBuilder)).FirstOrDefault();
// ...
return Page();
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2. Access the property in the Razor View: A hypothetical Pages/Courses/Index.cshtml would use this property as follows:
html
@page "/courses/{slug}"
@model TheExampleApp.Pages.Courses.IndexModel
@{
ViewData["Title"] = Model.Course.Title;
}
<div class="container">
<h1>@Model.Course.Title</h1>
<p>@Model.Course.Description</p>
<h2>Lessons</h2>
<ul>
@foreach (var lesson in Model.Course.Lessons)
{
<li><a href="/courses/@Model.Course.Slug/lessons/@lesson.Slug">@lesson.Title</a></li>
}
</ul>
</div>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
This pattern of defining public properties on the PageModel and populating them in handler methods is the standard way to pass data to the view for rendering. The application also uses ViewData and TempData for auxiliary data, such as passing the list of visited lessons or displaying one-time error messages after a redirect.