I was learning the Microsoft Learn Module - Create a web UI with ASP.NET Core.

Razor Pages is a server-side, page-centric programming model for building rich web UI with ASP.NET Core. Razor Pages makes it easy to get started building dynamic web applications when all you need is to define UI logic using a combination of HTML, CSS, and C#.

Razor pages have the extension .cshtml. Razor syntax is a combination of HTML and C# where the C# code defines the dynamic rendering logic for the page.

In a webpage that uses the Razor syntax, there can be two kinds of content: client content and server code:

  • Client content: Contains what you’re used to in webpages: HTML markup (elements), style information such as CSS, maybe some client script such as JavaScript, and plain text.
  • Server code: Razor syntax lets you add server code to your client content. If there is server code in the page, the server runs that code first, before it sends the page to the browser. From the browser’s perspective, client content that’s generated by your server code is no different than any other client content.

Razor Pages

Each Razor page is a pair of files:

  • A .cshtml file that contains markup with C# code using Razor syntax.
  • A .cshtml.cs PageModel class file that defines:
    • Page handlers for requests sent to the page.
    • Data used to render the page.

PageModel (.cshtml.cs)

A model object defines data properties and encapsulates logic or operations related to those data properties. A PageModel is essentially the same thing, but is a model that more specifically encapsulates the data properties and logic operations scoped just to its Razor page.

DataAnnotations

Models in ASP.NET Core often make use of data annotations to constrain or customize model properties.
Data annotations are attributes used to specify behavior that you want to enforce on the model properties to which they’re applied.
For example, a range of minimum and maximum acceptable values.

The Pages directory structure and routing requests

Razor Pages uses the directory structure within the Pages directory as the convention for routing requests by default. An index page located in the root of the Pages directory, for example, is the default page for the app’s site.

Razor Syntax

Razor syntax uses the @ symbol to transition from HTML to C#.

  • If the @ symbol is followed by a Razor reserved keyword, it transitions into Razor-specific markup;
  • otherwise, it transitions to C#. Razor evaluates the C# expressions and renders them in the HTML output.

Reserved Razor keywords:

@page

The @page directive is what makes this a Razor page. It indicates this page can handle HTTP requests. The @page directive must be the first directive on a Razor page.

@model

The @model directive is Razor syntax specifying the type of the model made available to the Razor page.

C# code

The @ character starts single-statement C# blocks. Multi-statement C# blocks can be created when using @{}. For example:

1
2
3
4
@{
var pageTitle = "Home page";
ViewData["Title"] = pageTitle;
}

Razor Tag Helpers

Here’s the example Pages/Products/Create.cshtml file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@page
@model ContosoPets.Ui.Pages.Products.CreateModel
@{
ViewData["Title"] = "Create";
}

<h1>Create Product</h1>

<form method="post">
<div class="form-group">
<label asp-for="Product.Name" class="control-label"></label>
<input asp-for="Product.Name" class="form-control" />
<span asp-validation-for="Product.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Product.Price" class="control-label"></label>
<input asp-for="Product.Price" class="form-control" />
<span asp-validation-for="Product.Price" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>

Tag Helpers are components for automating HTML generation in ASP.NET Core web applications. By design, they’re reusable, customizable building blocks that save you time. Most built-in Tag Helpers extend standard HTML elements you’re familiar with and provide additional server-side attributes for the element, making them more robust.

The Label Tag Helper

<label asp-for="Product.Name" class="control-label"></label>
The Label Tag Helper extends the standard HTML element. As is common for many Razor Tag Helpers, it uses an asp-for attribute which takes a specified PageModel property. The PageModel property is defined in C#. In this case, the value of the PageModel Name property will be rendered as the content for an HTML

The Input Tag Helper

In Create.cshtml:
<input asp-for="Product.Name" class="form-control" />
Generated output:
<input name="Product.Name" class="form-control" id="Product_Name" type="text" value="" data-val-required="The Name field is required." data-val="true">

  • It evaluates the Product.Name property defined in the PageModel in C#, adds an id and name based on that property and sets the input type appropriately.
  • It provides client-side validation using JQuery based on the model’s data annotation attributes provided through the PageModel.

The Validation Message Tag Helper

The following markup uses the Validation Message Tag Helper. It displays a validation message for a single property on your model.

1
<span asp-validation-for="Product.Price" class="text-danger"></span>

Generated output:

1
2
3
4
5
6
7
8
9
10
11
12
13
<input
name="Product.Price"
class="form-control"
id="Product_Price"
type="text"
value=""
data-val-required="The Price field is required."
data-val="true"
data-val-range-min="0.01"
data-val-range-max="9999.99"
data-val-range="The field Price must be between 0.01 and 9999.99."
data-val-number="The field Price must be a number."
/>

The type, data-val-range-min, data-val-range-max and error response are dynamically set by the model’s data annotations for the model’s Product.Price property.

PageModel

A Razor Page’s PageModel class file defines any page handlers for requests sent to the page, and data used to render the 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
using ContosoPets.Ui.Models;
using ContosoPets.Ui.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Threading.Tasks;

namespace ContosoPets.Ui.Pages.Products
{
public class CreateModel : PageModel
{
private readonly ProductService _productService;

[BindProperty]
public Product Product { get; set; }

public CreateModel(ProductService productService)
// Injecting the ContosoPets.UI ProductService service that handles HTTP requests
{
_productService = productService;
}

public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
} // Built-in server-side model validation using ASP.NET Core data annotations

await _productService.CreateProduct(Product);

return RedirectToPage("Index");
}
}
}

Typed HTTPClient service architecture

Typed HTTPClient service architecture is responsible for managing HTTP requests to the web API.
Provided as a typed service you have the advantage of injecting it as a constructor parameter directly into the PageModel classes in this project.
Using this architecture provides the advantage of letting the framework take on the responsibility of creating an instance of the HttpClient class and disposing of it when it’s no longer needed. This is a great feature for a project such as this one which will make use of HttpClient instances for each CRUD operation.