14

Building Websites Using ASP.NET Core Razor Pages

This chapter is about building websites with a modern HTTP architecture on the server side using Microsoft ASP.NET Core. You will learn about building simple websites using the ASP.NET Core Razor Pages feature introduced with ASP.NET Core 2.0 and the Razor class library feature introduced with ASP.NET Core 2.1.

This chapter will cover the following topics:

Understanding web development

Developing for the web means developing with Hypertext Transfer Protocol (HTTP), so we will start by reviewing this important foundational technology.

Understanding HTTP

To communicate with a web server, the client, also known as the user agent, makes calls over the network using HTTP. As such, HTTP is the technical underpinning of the web. So, when we talk about websites and web services, we mean that they use HTTP to communicate between a client (often a web browser) and a server.

A client makes an HTTP request for a resource, such as a page, uniquely identified by a Uniform Resource Locator (URL), and the server sends back an HTTP response, as shown in Figure 14.1:

Graphical user interface, text

Description automatically generated

Figure 14.1: An HTTP request and response

You can use Google Chrome and other browsers to record requests and responses.

Good Practice: Google Chrome is available on more operating systems than any other browser, and it has powerful, built-in developer tools, so it is a good first choice of browser for testing your websites. Always test your web application with Chrome and at least two other browsers, for example, Firefox and Safari for macOS and iPhone. Microsoft Edge switched from using Microsoft's own rendering engine to using Chromium in 2019, so it is less important to test with it. If Microsoft's Internet Explorer is used at all, it tends to mostly be inside organizations for intranets.

Understanding the components of a URL

A URL is made up of several components:

Assigning port numbers for projects in this book

In this book, we will use the domain localhost for all websites and services, so we will use port numbers to differentiate projects when multiple need to execute at the same time, as shown in the following table:

Project

Description

Port numbers

Northwind.Web

ASP.NET Core Razor Pages website

5000 HTTP, 5001 HTTPS

Northwind.Mvc

ASP.NET Core MVC website

5000 HTTP, 5001 HTTPS

Northwind.WebApi

ASP.NET Core Web API service

5002 HTTPS, 5008 HTTP

Minimal.WebApi

ASP.NET Core Web API (minimal)

5003 HTTPS

Northwind.OData

ASP.NET Core OData service

5004 HTTPS

Northwind.GraphQL

ASP.NET Core GraphQL service

5005 HTTPS

Northwind.gRPC

ASP.NET Core gRPC service

5006 HTTPS

Northwind.AzureFuncs

Azure Functions nanoservice

7071 HTTP

Using Google Chrome to make HTTP requests

Let's explore how to use Google Chrome to make HTTP requests:

  1. Start Google Chrome.
  2. Navigate to More tools | Developer tools.
  3. Click the Network tab, and Chrome should immediately start recording the network traffic between your browser and any web servers (note the red circle), as shown in Figure 14.2:

    Figure 14.2: Chrome Developer Tools recording network traffic

  4. In Chrome's address box, enter the address of Microsoft's website for learning ASP.NET, as shown in the following URL:

    https://dotnet.microsoft.com/learn/aspnet

  5. In Developer Tools, in the list of recorded requests, scroll to the top and click on the first entry, the row where the Type is document, as shown in Figure 14.3:

    Figure 14.3: Recorded requests in Developer Tools

  6. On the right-hand side, click on the Headers tab, and you will see details about Request Headers and Response Headers, as shown in Figure 14.4:

    Figure 14.4: Request and response headers

    Note the following aspects:

  7. Close Chrome.

Understanding client-side web development technologies

When building websites, a developer needs to know more than just C# and .NET. On the client (that is, in the web browser), you will use a combination of the following technologies:

Although HTML5, CSS3, and JavaScript are the fundamental components of frontend web development, there are many additional technologies that can make frontend web development more productive, including Bootstrap, the world's most popular frontend open-source toolkit, and CSS preprocessors such as SASS and LESS for styling, Microsoft's TypeScript language for writing more robust code, and JavaScript libraries such as jQuery, Angular, React, and Vue. All these higher-level technologies ultimately translate or compile to the underlying three core technologies, so they work across all modern browsers.

As part of the build and deploy process, you will likely use technologies such as Node.js; Node Package Manager (npm) and Yarn, which are both client-side package managers; and webpack, which is a popular module bundler, a tool for compiling, transforming, and bundling website source files.

Understanding ASP.NET Core

Microsoft ASP.NET Core is part of a history of Microsoft technologies used to build websites and services that have evolved over the years:

Good Practice: Choose ASP.NET Core to develop websites and services because it includes web-related technologies that are modern and cross-platform.

ASP.NET Core 2.0 to 2.2 can run on .NET Framework 4.6.1 or later (Windows only) as well as .NET Core 2.0 or later (cross-platform). ASP.NET Core 3.0 only supports .NET Core 3.0. ASP.NET Core 6.0 only supports .NET 6.0.

Classic ASP.NET versus modern ASP.NET Core

Until now, ASP.NET has been built on top of a large assembly in the .NET Framework named System.Web.dll and it is tightly coupled to Microsoft's Windows-only web server named Internet Information Services (IIS). Over the years, this assembly has accumulated a lot of features, many of which are not suitable for modern cross-platform development.

ASP.NET Core is a major redesign of ASP.NET. It removes the dependency on the System.Web.dll assembly and IIS and is composed of modular lightweight packages, just like the rest of modern .NET. Using IIS as the web server is still supported by ASP.NET Core but there is a better option.

You can develop and run ASP.NET Core applications cross-platform on Windows, macOS, and Linux. Microsoft has even created a cross-platform, super-performant web server named Kestrel, and the entire stack is open source.

ASP.NET Core 2.2 or later projects default to the new in-process hosting model. This gives a 400% performance improvement when hosting in Microsoft IIS, but Microsoft still recommends using Kestrel for even better performance.

Creating an empty ASP.NET Core project

We will create an ASP.NET Core project that will show a list of suppliers from the Northwind database.

The dotnet tool has many project templates that do a lot of work for you, but it can be difficult to know which works best for a given situation, so we will start with the empty website project template and then add features step by step so that you can understand all the pieces:

  1. Use your preferred code editor to add a new project, as defined in the following list:
    1. Project template: ASP.NET Core Empty / web
    2. Language: C#
    3. Workspace/solution file and folder: PracticalApps
    4. Project file and folder: Northwind.Web
    5. For Visual Studio 2022, leave all other options as their defaults, for example, Configure for HTTPS selected, and Enable Docker cleared
  2. In Visual Studio Code, select Northwind.Web as the active OmniSharp project.
  3. Build the Northwind.Web project.
  4. Open the Northwind.Web.csproj file and note that the project is like a class library except that the SDK is Microsoft.NET.Sdk.Web, as shown highlighted in the following markup:
    <Project Sdk="Microsoft.NET.Sdk.Web">
      <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
      </PropertyGroup>
    </Project>
    
  5. If you are using Visual Studio 2022, in Solution Explorer, toggle Show All Files.
  6. Expand the obj folder, expand the Debug folder, expand the net6.0 folder, select the Northwind.Web.GlobalUsings.g.cs file, and note the implicitly imported namespaces include all the ones for a console app or class library, as well as some ASP.NET Core ones, such as Microsoft.AspNetCore.Builder, as shown in the following code:
    // <autogenerated />
    global using global::Microsoft.AspNetCore.Builder;
    global using global::Microsoft.AspNetCore.Hosting;
    global using global::Microsoft.AspNetCore.Http;
    global using global::Microsoft.AspNetCore.Routing;
    global using global::Microsoft.Extensions.Configuration;
    global using global::Microsoft.Extensions.DependencyInjection;
    global using global::Microsoft.Extensions.Hosting;
    global using global::Microsoft.Extensions.Logging;
    global using global::System;
    global using global::System.Collections.Generic;
    global using global::System.IO;
    global using global::System.Linq;
    global using global::System.Net.Http;
    global using global::System.Net.Http.Json;
    global using global::System.Threading;
    global using global::System.Threading.Tasks;
    
  7. Collapse the obj folder.
  8. Open Program.cs, and note the following:
    • An ASP.NET Core project is like a top-level console application, with a hidden Main method as its entry point that has an argument passed using the name args.
    • It calls WebApplication.CreateBuilder, which creates a host for the website using defaults for a web host that is then built.
    • The website will respond to all HTTP GET requests with plain text: Hello World!.
    • The call to the Run method is a blocking call, so the hidden Main method does not return until the web server stops running, as shown in the following code:
    var builder = WebApplication.CreateBuilder(args);
    var app = builder.Build();
    app.MapGet("/", () => "Hello World!");
    app.Run();
    
  9. At the bottom of the file, add a statement to write a message to the console after the call to the Run method and therefore after the web server has stopped, as shown highlighted in the following code:
    app.Run();
    Console.WriteLine("This executes after the web server has stopped!");
    

Testing and securing the website

We will now test the functionality of the ASP.NET Core Empty website project. We will also enable encryption of all traffic between the browser and web server for privacy by switching from HTTP to HTTPS. HTTPS is the secure encrypted version of HTTP.

  1. For Visual Studio:
    1. In the toolbar, make sure that Northwind.Web is selected rather than IIS Express or WSL, and switch the Web Browser (Microsoft Edge) to Google Chrome, as shown in Figure 14.5:

      Figure 14.5: Selecting the Northwind.Web profile with its Kestrel web server in Visual Studio

    2. Navigate to Debug | Start Without Debugging….
    3. The first time you start a secure website, you will be prompted that your project is configured to use SSL, and to avoid warnings in the browser you can choose to trust the self-signed certificate that ASP.NET Core has generated. Click Yes.
    4. When you see the Security Warning dialog box, click Yes again.
  2. For Visual Studio Code, in TERMINAL, enter the dotnet run command.
  3. In either Visual Studio's command prompt window or Visual Studio Code's terminal, note the Kestrel web server has started listening on random ports for HTTP and HTTPS, that you can press Ctrl + C to shut down the Kestrel web server, and the hosting environment is Development, as shown in the following output:
    info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:5001 
    info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5000 
    info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down. 
    info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development 
    info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\Code\PracticalApps\Northwind.Web
    

    Visual Studio will also start your chosen browser automatically. If you are using Visual Studio Code, you will have to start Chrome manually.

  4. Leave the web server running.
  5. In Chrome, show Developer Tools, and click the Network tab.
  6. Enter the address http://localhost:5000/, or whatever port number was assigned to HTTP, and note the response is Hello World! in plain text, from the cross-platform Kestrel web server, as shown in Figure 14.6:

    Figure 14.6: Plain text response from http://localhost:5000/

    Chrome also requests a favicon.ico file automatically to show in the browser tab but this is missing so it shows as a 404 Not Found error.

  7. Enter the address https://localhost:5001/, or whatever port number was assigned to HTTPS, and note if you are not using Visual Studio or if you clicked No when prompted to trust the SSL certificate, then the response is a privacy error, as shown in Figure 14.7:
    Graphical user interface, application

Description automatically generated

    Figure 14.7: Privacy error showing SSL encryption has not been enabled with a certificate

    You will see this error when you have not configured a certificate that the browser can trust to encrypt and decrypt HTTPS traffic (and so if you do not see this error, it is because you have already configured a certificate).

    In a production environment, you would want to pay a company such as Verisign for an SSL certificate because they provide liability protection and technical support.

    During development, you can tell your OS to trust a temporary development certificate provided by ASP.NET Core.

  8. At the command line or in TERMINAL, press Ctrl + C to shut down the web server, and note the message that is written, as shown highlighted in the following output:
    info: Microsoft.Hosting.Lifetime[0]
          Application is shutting down...
    This executes after the web server has stopped!
    C:\Code\PracticalApps\Northwind.Web\bin\Debug\net6.0\Northwind.Web.exe (process 19888) exited with code 0.
    
  9. If you need to trust a local self-signed SSL certificate, then at the command line or in TERMINAL, enter the dotnet dev-certs https --trust command, and note the message, Trusting the HTTPS development certificate was requested. You might be prompted to enter your password and a valid HTTPS certificate may already be present.

Enabling stronger security and redirect to a secure connection

It is good practice to enable stricter security and automatically redirect requests for HTTP to HTTPS.

Good Practice: HTTP Strict Transport Security (HSTS) is an opt-in security enhancement that you should always enable. If a website specifies it and a browser supports it, then it forces all communication over HTTPS and prevents the visitor from using untrusted or invalid certificates.

Let's do that now:

  1. In Program.cs, add an if statement to enable HSTS when not in development, as shown in the following code:
    if (!app.Environment.IsDevelopment())
    {
      app.UseHsts();
    }
    
  2. Add a statement before the call to app.MapGet to redirect HTTP requests to HTTPS, as shown in the following code:
    app.UseHttpsRedirection();
    
  3. Start the Northwind.Web website project.
  4. If Chrome is still running, close and restart it.
  5. In Chrome, show Developer Tools, and click the Network tab.
  6. Enter the address http://localhost:5000/, or whatever port number was assigned to HTTP, and note how the server responds with a 307 Temporary Redirect to port 5001 and that the certificate is now valid and trusted, as shown in Figure 14.8:

    Figure 14.8: The connection is now secured using a valid certificate and a 307 redirect

  7. Close Chrome.
  8. Shut down the web server.

Good Practice: Remember to shut down the Kestrel web server whenever you have finished testing a website.

Controlling the hosting environment

In earlier versions of ASP.NET Core, the project template set a rule to say that while in development mode, any unhandled exceptions will be shown in the browser window for the developer to see the details of the exception, as shown in the following code:

if (app.Environment.IsDevelopment())
{
  app.UseDeveloperExceptionPage();
}

With ASP.NET Core 6 and later, this code is executed automatically by default so it is not included in the project template.

How does ASP.NET Core know when we are running in development mode so that the IsDevelopment method returns true? Let's find out.

ASP.NET Core can read from environment variables to determine what hosting environment to use, for example, DOTNET_ENVIRONMENT or ASPNETCORE_ENVIRONMENT.

You can override these settings during local development:

  1. In the Northwind.Web folder, expand the folder named Properties, open the file named launchSettings.json, and note the profile named Northwind.Web that sets the hosting environment to Development, as shown highlighted in the following configuration:
    {
      "iisSettings": {
        "windowsAuthentication": false,
        "anonymousAuthentication": true,
        "iisExpress": {
          "applicationUrl": "http://localhost:56111",
          "sslPort": 44329
        }
      },
      "profiles": {
        "Northwind.Web": {
          "commandName": "Project",
          "dotnetRunMessages": "true",
          "launchBrowser": true,
          "applicationUrl": "https://localhost:5001;http://localhost:5000",     
          "environmentVariables": {
            "ASPNETCORE_ENVIRONMENT": "Development"
          }
        },
        "IIS Express": {
          "commandName": "IISExpress",
          "launchBrowser": true, 
          "environmentVariables": {
            "ASPNETCORE_ENVIRONMENT": "Development"
          }
        }
      }
    }
    
  2. Change the randomly assigned port numbers for HTTP to 5000 and HTTPS to 5001.
  3. Change the environment to Production. Optionally, change launchBrowser to false to prevent Visual Studio from automatically launching a browser.
  4. Start the website and note the hosting environment is Production, as shown in the following output:
    info: Microsoft.Hosting.Lifetime[0] 
      Hosting environment: Production
    
  5. Shut down the web server.
  6. In launchSettings.json, change the environment back to Development.

The launchSettings.json file also has a configuration for using IIS as the web server using random port numbers. In this book, we will only be using Kestrel as the web server since it is cross-platform.

Separating configuration for services and pipeline

Putting all code to initialize a simple web project in Program.cs can be a good idea, especially for web services, so we will see this style again in Chapter 16, Building and Consuming Web Services.

However, for anything more than the most basic web project, you might prefer to separate configuration into a separate Startup class with two methods:

Figure 14.9: Startup class ConfigureServices and Configure methods diagram

Both methods will get called automatically by the runtime.

Let's create a Startup class now:

  1. Add a new class file to the Northwind.Web project named Startup.cs.
  2. Modify Startup.cs, as shown in the following code:
    namespace Northwind.Web;
    public class Startup
    {
      public void ConfigureServices(IServiceCollection services)
      {
      }
      public void Configure(
        IApplicationBuilder app, IWebHostEnvironment env)
      {
        if (!env.IsDevelopment())
        {
          app.UseHsts();
        }
        app.UseRouting(); // start endpoint routing
        app.UseHttpsRedirection();
        app.UseEndpoints(endpoints =>
        {
          endpoints.MapGet("/", () => "Hello World!");
        });
      }
    }
    

    Note the following about the code:

    • The ConfigureServices method is currently empty because we do not yet need any dependency services added.
    • The Configure method sets up the HTTP request pipeline and enables the use of endpoint routing. It configures a routed endpoint to wait for requests using the same map for each HTTP GET request for the root path / that responds to those requests by returning the plain text "Hello World!". We will learn about routed endpoints and their benefits at the end of this chapter.

    Now we must specify that we want to use the Startup class in the application entry point.

  3. Modify Program.cs, as shown in the following code:
    using Northwind.Web; // Startup
    Host.CreateDefaultBuilder(args)
      .ConfigureWebHostDefaults(webBuilder =>
      {
        webBuilder.UseStartup<Startup>();
      }).Build().Run();
    Console.WriteLine("This executes after the web server has stopped!");
    
  4. Start the website and note that it has the same behavior as before.
  5. Shut down the web server.

In all the other website and service projects that we create in this book, we will use the single Program.cs file created by .NET 6 project templates. If you like the Startup.cs way of doing things, then you will see in this chapter how to use it.

Enabling a website to serve static content

A website that only ever returns a single plain text message isn't very useful!

At a minimum, it ought to return static HTML pages, CSS that the web pages will use for styling, and any other static resources, such as images and videos.

By convention, these files should be stored in a directory named wwwroot to keep them separate from the dynamically executing parts of your website project.

Creating a folder for static files and a web page

You will now create a folder for your static website resources and a basic index page that uses Bootstrap for styling:

  1. In the Northwind.Web project/folder, create a folder named wwwroot.
  2. Add a new HTML page file to the wwwroot folder named index.html.
  3. Modify its content to link to CDN-hosted Bootstrap for styling, and use modern good practices such as setting the viewport, as shown in the following markup:
    <!doctype html>
    <html lang="en">
    <head>
      <!-- Required meta tags -->
      <meta charset="utf-8" />
      <meta name="viewport" content=
        "width=device-width, initial-scale=1 " />
      <!-- Bootstrap CSS -->
      <link href=
    "https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
      <title>Welcome ASP.NET Core!</title>
    </head>
    <body>
      <div class="container">
        <div class="jumbotron">
          <h1 class="display-3">Welcome to Northwind B2B</h1>
          <p class="lead">We supply products to our customers.</p>
          <hr />
          <h2>This is a static HTML page.</h2>
          <p>Our customers include restaurants, hotels, and cruise lines.</p>
          <p>
            <a class="btn btn-primary" 
              href="https://www.asp.net/">Learn more</a>
          </p>
        </div>
      </div>
    </body>
    </html>
    

Good Practice: To get the latest <link> element for Bootstrap, copy and paste it from the documentation at the following link: https://getbootstrap.com/docs/5.0/getting-started/introduction/#starter-template.

Enabling static and default files

If you were to start the website now and enter http://localhost:5000/index.html in the address box, the website would return a 404 Not Found error saying no web page was found. To enable the website to return static files such as index.html, we must explicitly configure that feature.

Even if we enable static files, if you were to start the website and enter http://localhost:5000/ in the address box, the website will return a 404 Not Found error because the web server does not know what to return by default if no named file is requested.

You will now enable static files, explicitly configure default files, and change the URL path registered that returns the plain text Hello World! response:

  1. In Startup.cs, in the Configure method, add statements after enabling HTTPS redirection to enable static files and default files, and modify the statement that maps a GET request to return the Hello World! plain text response to only respond to the URL path /hello, as shown highlighted in the following code:
    app.UseHttpsRedirection();
    app.UseDefaultFiles(); // index.html, default.html, and so on
    app.UseStaticFiles();
    app.UseEndpoints(endpoints =>
    {
      endpoints.MapGet("/hello", () => "Hello World!");
    });
    

    The call to UseDefaultFiles must come before the call to UseStaticFiles, or it will not work! You will learn more about the ordering of middleware and endpoint routing at the end of this chapter.

  2. Start the website.
  3. Start Chrome and show Developer Tools.
  4. In Chrome, enter http://localhost:5000/ and note that you are redirected to the HTTPS address on port 5001, and the index.html file is now returned over that secure connection because it is one of the possible default files for this website.
  5. In Developer Tools, note the request for the Bootstrap stylesheet.
  6. In Chrome, enter http://localhost:5000/hello and note that it returns the plain text Hello World! as before.
  7. Close Chrome and shut down the web server.

If all web pages are static, that is, they only get changed manually by a web editor, then our website programming work is complete. But almost all websites need dynamic content, which means a web page that is generated at runtime by executing code.

The easiest way to do that is to use a feature of ASP.NET Core named Razor Pages.

Exploring ASP.NET Core Razor Pages

ASP.NET Core Razor Pages allow a developer to easily mix C# code statements with HTML markup to make the generated web page dynamic. That is why they use the .cshtml file extension.

By convention, ASP.NET Core looks for Razor Pages in a folder named Pages.

Enabling Razor Pages

You will now copy and change the static HTML page into a dynamic Razor Page, and then add and enable the Razor Pages service:

  1. In the Northwind.Web project folder, create a folder named Pages.
  2. Copy the index.html file into the Pages folder.
  3. For the file in the Pages folder, rename the file extension from .html to .cshtml.
  4. Remove the <h2> element that says that this is a static HTML page.
  5. In Startup.cs, in the ConfigureServices method, add a statement to add ASP.NET Core Razor Pages and its related services, such as model binding, authorization, anti-forgery, views, and tag helpers, to the builder, as shown in the following code:
    services.AddRazorPages();
    
  6. In Startup.cs, in the Configure method, in the configuration to use endpoints, add a statement to call the MapRazorPages method, as shown highlighted in the following code:
    app.UseEndpoints(endpoints =>
    {
      endpoints.MapRazorPages();
      endpoints.MapGet("/hello",  () => "Hello World!");
    });
    

Adding code to a Razor Page

In the HTML markup of a web page, Razor syntax is indicated by the @ symbol. Razor Pages can be described as follows:

Let's now convert the static HTML page into a Razor Page:

  1. In the Pages folder, open index.cshtml.
  2. Add the @page statement to the top of the file.
  3. After the @page statement, add an @functions statement block.
  4. Define a property to store the name of the current day as a string value.
  5. Define a method to set DayName that executes when an HTTP GET request is made for the page, as shown in the following code:
    @page
    @functions
    {
      public string? DayName { get; set; }
      public void OnGet()
      {
        Model.DayName = DateTime.Now.ToString("dddd");
      }
    }
    
  6. Output the day name inside the second HTML paragraph, as shown highlighted in the following markup:
    <p>It's @Model.DayName! Our customers include restaurants, hotels, and cruise lines.</p>
    
  7. Start the website.
  8. In Chrome, enter https://localhost:5001/ and note the current day name is output on the page, as shown in Figure 14.10:

    Figure 14.10: Welcome to Northwind page showing the current day

  9. In Chrome, enter https://localhost:5001/index.html, which exactly matches the static filename, and note that it returns the static HTML page as before.
  10. In Chrome, enter https://localhost:5001/hello, which exactly matches the endpoint route that returns plain text, and note that it returns Hello World! as before.
  11. Close Chrome and shut down the web server.

Using shared layouts with Razor Pages

Most websites have more than one page. If every page had to contain all of the boilerplate markup that is currently in index.cshtml, that would become a pain to manage. So, ASP.NET Core has a feature named layouts.

To use layouts, we must create a Razor file to define the default layout for all Razor Pages (and all MVC views) and store it in a Shared folder so that it can be easily found by convention. The name of this file can be anything, because we will specify it, but _Layout.cshtml is good practice.

We must also create a specially named file to set the default layout file for all Razor Pages (and all MVC views). This file must be named _ViewStart.cshtml.

Let's see layouts in action:

  1. In the Pages folder, add a file named _ViewStart.cshtml. (The Visual Studio item template is named Razor View Start.)
  2. Modify its content, as shown in the following markup:
    @{
      Layout = "_Layout";
    }
    
  3. In the Pages folder, create a folder named Shared.
  4. In the Shared folder, create a file named _Layout.cshtml. (The Visual Studio item template is named Razor Layout.)
  5. Modify the content of _Layout.cshtml (it is similar to index.cshtml so you can copy and paste the HTML markup from there), as shown in the following markup:
    <!doctype html>
    <html lang="en">
    <head>
      <!-- Required meta tags -->
      <meta charset="utf-8" />
      <meta name="viewport" content=
        "width=device-width, initial-scale=1, shrink-to-fit=no" />
      <!-- Bootstrap CSS -->
      <link href=
    "https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
      <title>@ViewData["Title"]</title>
    </head>
    <body>
      <div class="container">
        @RenderBody()
        <hr />
        <footer>
          <p>Copyright &copy; 2021 - @ViewData["Title"]</p>
        </footer>
      </div>
      <!-- JavaScript to enable features like carousel -->
      <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script>
      @RenderSection("Scripts", required: false)
    </body>
    </html>
    

    While reviewing the preceding markup, note the following:

    • <title> is set dynamically using server-side code from a dictionary named ViewData. This is a simple way to pass data between different parts of an ASP.NET Core website. In this case, the data will be set in a Razor Page class file and then output in the shared layout.
    • @RenderBody() marks the insertion point for the view being requested.
    • A horizontal rule and footer will appear at the bottom of each page.
    • At the bottom of the layout is a script to implement some cool features of Bootstrap that we can use later, such as a carousel of images.
    • After the <script> elements for Bootstrap, we have defined a section named Scripts so that a Razor Page can optionally inject additional scripts that it needs.
  6. Modify index.cshtml to remove all HTML markup except <div class="jumbotron"> and its contents, and leave the C# code in the @functions block that you added earlier.
  7. Add a statement to the OnGet method to store a page title in the ViewData dictionary, and modify the button to navigate to a suppliers page (which we will create in the next section), as shown highlighted in the following markup:
    @page 
    @functions
    {
      public string? DayName { get; set; }
      public void OnGet()
      {
        ViewData["Title"] = "Northwind B2B";
        Model.DayName = DateTime.Now.ToString("dddd");
      }
    }
    <div class="jumbotron">
      <h1 class="display-3">Welcome to Northwind B2B</h1>
      <p class="lead">We supply products to our customers.</p>
      <hr />
      <p>It's @Model.DayName! Our customers include restaurants, hotels, and cruise lines.</p>
      <p>
        <a class="btn btn-primary" href="suppliers">
          Learn more about our suppliers</a>
      </p>
    </div>
    
  8. Start the website, visit it with Chrome, and note that it has similar behavior as before, although clicking the button for suppliers will give a 404 Not Found error because we have not created that page yet.

Using code-behind files with Razor Pages

Sometimes, it is better to separate the HTML markup from the data and executable code, so Razor Pages allows you to do this by putting the C# code in code-behind class files. They have the same name as the .cshtml file but end with .cshtml.cs.

You will now create a page that shows a list of suppliers. In this example, we are focusing on learning about code-behind files. In the next topic, we will load the list of suppliers from a database, but for now, we will simulate that with a hardcoded array of string values:

  1. In the Pages folder, add two new files named Suppliers.cshtml and Suppliers.cshtml.cs. (The Visual Studio item template is named Razor Page - Empty and it creates both files.)
  2. Add statements to the code-behind file named Suppliers.cshtml.cs, as shown in the following code:
    using Microsoft.AspNetCore.Mvc.RazorPages; // PageModel
    namespace Northwind.Web.Pages;
    public class SuppliersModel : PageModel
    {
      public IEnumerable<string>? Suppliers { get; set; }
      public void OnGet()
      {
        ViewData["Title"] = "Northwind B2B - Suppliers";
        Suppliers = new[]
        {
          "Alpha Co", "Beta Limited", "Gamma Corp"
        };
      }
    }
    

    While reviewing the preceding markup, note the following:

    • SuppliersModel inherits from PageModel, so it has members such as the ViewData dictionary for sharing data. You can right-click on PageModel and select Go To Definition to see that it has lots more useful features, such as the entire HttpContext of the current request.
    • SuppliersModel defines a property for storing a collection of string values named Suppliers.
    • When an HTTP GET request is made for this Razor Page, the Suppliers property is populated with some example supplier names from an array of string values. Later, we will populate this from the Northwind database.
  3. Modify the contents of Suppliers.cshtml, as shown in the following markup:
    @page
    @model Northwind.Web.Pages.SuppliersModel
    <div class="row">
      <h1 class="display-2">Suppliers</h1>
      <table class="table">
        <thead class="thead-inverse">
          <tr><th>Company Name</th></tr>
        </thead>
        <tbody>
        @if (Model.Suppliers is not null)
        {
          @foreach(string name in Model.Suppliers)
          {
            <tr><td>@name</td></tr>
          }
        }
        </tbody>
      </table>
    </div>
    

    While reviewing the preceding markup, note the following:

    • The model type for this Razor Page is set to SuppliersModel.
    • The page outputs an HTML table with Bootstrap styles.
    • The data rows in the table are generated by looping through the Suppliers property of Model if it is not null.
  4. Start the website and visit it using Chrome.
  5. Click on the button to learn more about suppliers, and note the table of suppliers, as shown in Figure 14.11:

    Figure 14.11: The table of suppliers loaded from an array of strings

Using Entity Framework Core with ASP.NET Core

Entity Framework Core is a natural way to get real data into a website. In Chapter 13, Introducing Practical Applications of C# and .NET, you created two pairs of class libraries: one for the entity models and one for the Northwind database context, for either SQL Server or SQLite or both. You will now use them in your website project.

Configure Entity Framework Core as a service

Functionality such as Entity Framework Core database contexts that are needed by ASP.NET Core must be registered as a service during website startup. The code in the GitHub repository solution and below uses SQLite, but you can easily use SQL Server if you prefer.

Let's see how:

  1. In the Northwind.Web project, add a project reference to the Northwind.Common.DataContext project for either SQLite or SQL Server, as shown in the following markup:
    <!-- change Sqlite to SqlServer if you prefer -->
    <ItemGroup>
      <ProjectReference Include="..\Northwind.Common.DataContext.Sqlite\
    Northwind.Common.DataContext.Sqlite.csproj" />
    </ItemGroup>
    

    The project reference must go all on one line with no line break.

  2. Build the Northwind.Web project.
  3. In Startup.cs, import namespaces to work with your entity model types, as shown in the following code:
    using Packt.Shared; // AddNorthwindContext extension method
    
  4. Add a statement to the ConfigureServices method to register the Northwind database context class, as shown in the following code:
    services.AddNorthwindContext();
    
  5. In the Northwind.Web project, in the Pages folder, open Suppliers.cshtml.cs, and import the namespace for our database context, as shown in the following code:
    using Packt.Shared; // NorthwindContext
    
  6. In the SuppliersModel class, add a private field to store the Northwind database context and a constructor to set it, as shown in the following code:
    private NorthwindContext db;
    public SuppliersModel(NorthwindContext injectedContext)
    {
      db = injectedContext;
    }
    
  7. Change the Suppliers property to contain Supplier objects instead of string values.
  8. In the OnGet method, modify the statements to set the Suppliers property from the Suppliers property of the database context, sorted by country and then company name, as shown highlighted in the following code:
    public void OnGet()
    {
      ViewData["Title"] = "Northwind B2B - Suppliers";
      Suppliers = db.Suppliers
        .OrderBy(c => c.Country).ThenBy(c => c.CompanyName);
    }
    
  9. Modify the contents of Suppliers.cshtml to import the Packt.Shared namespace and render multiple columns for each supplier, as shown highlighted in the following markup:
    @page
    @using Packt.Shared
    @model Northwind.Web.Pages.SuppliersModel
    <div class="row">
      <h1 class="display-2">Suppliers</h1>
      <table class="table">
        <thead class="thead-inverse">
          <tr>
            <th>Company Name</th>
            <th>Country</th>
            <th>Phone</th>
          </tr>
        </thead>
        <tbody>
        @if (Model.Suppliers is not null)
        {
          @foreach(Supplier s in Model.Suppliers)
          {
            <tr>
              <td>@s.CompanyName</td>
              <td>@s.Country</td>
              <td>@s.Phone</td>
            </tr>
          }
        }
        </tbody>
      </table>
    </div>
    
  10. Start the website.
  11. In Chrome, enter https://localhost:5001/.
  12. Click Learn more about our suppliers and note that the supplier table now loads from the database, as shown in Figure 14.12:

Figure 14.12: The suppliers table loaded from the Northwind database

Manipulating data using Razor Pages

You will now add functionality to insert a new supplier.

Enabling a model to insert entities

First, you will modify the supplier model so that it responds to HTTP POST requests when a visitor submits a form to insert a new supplier:

  1. In the Northwind.Web project, in the Pages folder, open Suppliers.cshtml.cs and import the following namespace:
    using Microsoft.AspNetCore.Mvc; // [BindProperty], IActionResult
    
  2. In the SuppliersModel class, add a property to store a single supplier and a method named OnPost that adds the supplier to the Suppliers table in the Northwind database if its model is valid, as shown in the following code:
    [BindProperty]
    public Supplier? Supplier { get; set; }
    public IActionResult OnPost()
    {
      if ((Supplier is not null) && ModelState.IsValid)
      {
        db.Suppliers.Add(Supplier);
        db.SaveChanges();
        return RedirectToPage("/suppliers");
      }
      else
      {
        return Page(); // return to original page
      }
    }
    

While reviewing the preceding code, note the following:

Defining a form to insert a new supplier

Next, you will modify the Razor Page to define a form that a visitor can fill in and submit to insert a new supplier:

  1. In Suppliers.cshtml, add tag helpers after the @model declaration so that we can use tag helpers such as asp-for on this Razor Page, as shown in the following markup:
    @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
    
  2. At the bottom of the file, add a form to insert a new supplier, and use the asp-for tag helper to bind the CompanyName, Country, and Phone properties of the Supplier class to the input box, as shown in the following markup:
    <div class="row">
      <p>Enter details for a new supplier:</p>
      <form method="POST">
        <div><input asp-for="Supplier.CompanyName" 
                    placeholder="Company Name" /></div>
        <div><input asp-for="Supplier.Country" 
                    placeholder="Country" /></div>
        <div><input asp-for="Supplier.Phone" 
                    placeholder="Phone" /></div>
        <input type="submit" />
      </form>
    </div>
    

    While reviewing the preceding markup, note the following:

    • The <form> element with a POST method is normal HTML, so an <input type="submit" /> element inside it will make an HTTP POST request back to the current page with values of any other elements inside that form.
    • An <input> element with a tag helper named asp-for enables data binding to the model behind the Razor Page.
  3. Start the website.
  4. Click Learn more about our suppliers, scroll down to the bottom of the page, enter Bob's Burgers, USA, and (603) 555-4567, and click Submit.
  5. Note that you see a refreshed suppliers table with the new supplier added.
  6. Close Chrome and shut down the web server.

Injecting a dependency service into a Razor Page

If you have a .cshtml Razor Page that does not have a code-behind file, then you can inject a dependency service using the @inject directive instead of constructor parameter injection, and then directly reference the injected database context using Razor syntax in the middle of the markup.

Let's create a simple example:

  1. In the Pages folder, add a new file named Orders.cshtml. (The Visual Studio item template is named Razor Page - Empty and it creates two files. Delete the .cs file.)
  2. In Orders.cshtml, write code to output the number of orders in the Northwind database, as shown in the following markup:
    @page
    @using Packt.Shared
    @inject NorthwindContext db
    @{
      string title = "Orders";
      ViewData["Title"] = $"Northwind B2B - {title}";
    }
    <div class="row">
      <h1 class="display-2">@title</h1>
      <p>
        There are @db.Orders.Count() orders in the Northwind database.
      </p>
    </div>
    
  3. Start the website.
  4. Navigate to /orders and note that you see that there are 830 orders in the Northwind database.
  5. Close Chrome and shut down the web server.

Using Razor class libraries

Everything related to a Razor Page can be compiled into a class library for easier reuse in multiple projects. With ASP.NET Core 3.0 and later, this can include static files such as HTML, CSS, JavaScript libraries, and media assets such as image files. A website can either use the Razor Page's view as defined in the class library or override it.

Creating a Razor class library

Let's create a new Razor class library:

Use your preferred code editor to add a new project, as defined in the following list:

  1. Project template: Razor Class Library / razorclasslib
  2. Checkbox/switch: Support pages and views / -s
  3. Workspace/solution file and folder: PracticalApps
  4. Project file and folder: Northwind.Razor.Employees

-s is short for the --support-pages-and-views switch that enables the class library to use Razor Pages and .cshtml file views.

Disabling compact folders for Visual Studio Code

Before we implement our Razor class library, I want to explain a Visual Studio Code feature that confused some readers of a previous edition because the feature was added after publishing.

The compact folders feature means that nested folders such as /Areas/MyFeature/Pages/ are shown in a compact form if the intermediate folders in the hierarchy do not contain files, as shown in Figure 14.13:

Figure 14.13: Compact folders enabled or disabled

If you would like to disable the Visual Studio Code compact folders feature, complete the following steps:

  1. On Windows, navigate to File | Preferences | Settings. On macOS, navigate to Code | Preferences | Settings.
  2. In the Search settings box, enter compact.
  3. Clear the Explorer: Compact Folders checkbox, as shown in Figure 14.14:
    Graphical user interface, text, application, email

Description automatically generated

    Figure 14.14: Disabling compact folders for Visual Studio Code

  4. Close the Settings tab.

Implementing the employees feature using EF Core

Now we can add a reference to our entity models to get the employees to show in the Razor class library:

  1. In the Northwind.Razor.Employees project, add a project reference to the Northwind.Common.DataContext project for either SQLite or SQL Server and note the SDK is Microsoft.NET.Sdk.Razor, as shown highlighted in the following markup:
    <Project Sdk="Microsoft.NET.Sdk.Razor">
      <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <AddRazorSupportForMvc>true</AddRazorSupportForMvc>
      </PropertyGroup>
      <ItemGroup>
        <FrameworkReference Include="Microsoft.AspNetCore.App" />
      </ItemGroup>
      <!-- change Sqlite to SqlServer if you prefer -->
      <ItemGroup>
        <ProjectReference Include="..\Northwind.Common.DataContext.Sqlite
    \Northwind.Common.DataContext.Sqlite.csproj" />
      </ItemGroup>
    </Project>
    

    The project reference must go all on one line with no line break. Also, do not mix our SQLite and SQL Server projects or you will see compiler errors. If you used SQL Server in the Northwind.Web project, then you must use SQL Server in the Northwind.Razor.Employees project as well.

  2. Build the Northwind.Razor.Employees project.
  3. In the Areas folder, right-click the MyFeature folder, select Rename, enter the new name PacktFeatures, and press Enter.
  4. In the PacktFeatures folder, in the Pages subfolder, add a new file named _ViewStart.cshtml. (The Visual Studio item template is named Razor View Start. Or just copy it from the Northwind.Web project.)
  5. Modify its content to inform this class library that any Razor Pages should look for a layout with the same name as used in the Northwind.Web project, as shown in the following markup:
    @{
      Layout = "_Layout";
    }
    

    We do not need to create the _Layout.cshtml file in this project. It will use the one in its host project, for example, the one in the Northwind.Web project.

  6. In the Pages subfolder, rename Page1.cshtml to Employees.cshtml, and rename Page1.cshtml.cs to Employees.cshtml.cs.
  7. Modify Employees.cshtml.cs to define a page model with an array of Employee entity instances loaded from the Northwind database, as shown in the following code:
    using Microsoft.AspNetCore.Mvc.RazorPages; // PageModel
    using Packt.Shared; // Employee, NorthwindContext
    namespace PacktFeatures.Pages;
    public class EmployeesPageModel : PageModel
    {
      private NorthwindContext db;
      public EmployeesPageModel(NorthwindContext injectedContext)
      {
        db = injectedContext;
      }
      public Employee[] Employees { get; set; } = null!;
      public void OnGet()
      {
        ViewData["Title"] = "Northwind B2B - Employees";
        Employees = db.Employees.OrderBy(e => e.LastName)
          .ThenBy(e => e.FirstName).ToArray();
      }
    }
    
  8. Modify Employees.cshtml, as shown in the following markup:
    @page
    @using Packt.Shared
    @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 
    @model PacktFeatures.Pages.EmployeesPageModel
    <div class="row">
      <h1 class="display-2">Employees</h1>
    </div>
    <div class="row">
    @foreach(Employee employee in Model.Employees)
    {
      <div class="col-sm-3">
        <partial name="_Employee" model="employee" />
      </div>
    }
    </div>
    

While reviewing the preceding markup, note the following:

Implementing a partial view to show a single employee

The <partial> tag helper was introduced in ASP.NET Core 2.1. A partial view is like a piece of a Razor Page. You will create one in the next few steps to render a single employee:

  1. In the Northwind.Razor.Employees project, in the Pages folder, create a Shared folder.
  2. In the Shared folder, create a file named _Employee.cshtml. (The Visual Studio item template is named Razor View - Empty.)
  3. Modify _Employee.cshtml, as shown in the following markup:
    @model Packt.Shared.Employee
    <div class="card border-dark mb-3" style="max-width: 18rem;">
      <div class="card-header">@Model?.LastName, @Model?.FirstName</div>
      <div class="card-body text-dark">
        <h5 class="card-title">@Model?.Country</h5>
        <p class="card-text">@Model?.Notes</p>
      </div>
    </div>
    

While reviewing the preceding markup, note the following:

Using and testing a Razor class library

You will now reference and use the Razor class library in the website project:

  1. In the Northwind.Web project, add a project reference to the Northwind.Razor.Employees project, as shown in the following markup:
    <ProjectReference Include=
      "..\Northwind.Razor.Employees\Northwind.Razor.Employees.csproj" />
    
  2. Modify Pages\index.cshtml to add a paragraph with a link to the Packt feature employees page after the link to the suppliers page, as shown in the following markup:
    <p>
      <a class="btn btn-primary" href="packtfeatures/employees">
        Contact our employees
      </a>
    </p>
    
  3. Start the website, visit the website using Chrome, and click the Contact our employees button to see the cards of employees, as shown in Figure 14.15:

    Figure 14.15: A list of employees from a Razor class library feature

Configuring services and the HTTP request pipeline

Now that we have built a website, we can return to the Startup configuration and review how services and the HTTP request pipeline work in more detail.

Understanding endpoint routing

In earlier versions of ASP.NET Core, the routing system and the extendable middleware system did not always work easily together; for example, if you wanted to implement a policy such as CORS in both middleware and MVC. Microsoft has invested in improving routing with a system named endpoint routing introduced with ASP.NET Core 2.2.

Good Practice: Endpoint routing replaces the IRouter-based routing used in ASP.NET Core 2.1 and earlier. Microsoft recommends every older ASP.NET Core project migrates to endpoint routing if possible.

Endpoint routing is designed to enable better interoperability between frameworks that need routing, such as Razor Pages, MVC, or Web APIs, and middleware that needs to understand how routing affects them, such as localization, authorization, CORS, and so on.

Endpoint routing gets its name because it represents the route table as a compiled tree of endpoints that can be walked efficiently by the routing system. One of the biggest improvements is the performance of routing and action method selection.

It is on by default with ASP.NET Core 2.2 or later if compatibility is set to 2.2 or later. Traditional routes registered using the MapRoute method or with attributes are mapped to the new system.

The new routing system includes a link generation service registered as a dependency service that does not need an HttpContext.

Configuring endpoint routing

Endpoint routing requires a pair of calls to the UseRouting and UseEndpoints methods:

Middleware such as localization that runs in between these methods can see the selected endpoint and can switch to a different endpoint if necessary.

Endpoint routing uses the same route template syntax that has been used in ASP.NET MVC since 2010 and the [Route] attribute introduced with ASP.NET MVC 5 in 2013. Migration often only requires changes to the Startup configuration.

MVC controllers, Razor Pages, and frameworks such as SignalR used to be enabled by a call to UseMvc or similar methods, but they are now added inside the UseEndpoints method call because they are all integrated into the same routing system along with middleware.

Reviewing the endpoint routing configuration in our project

Review the Startup.cs class file, as shown in the following code:

using Packt.Shared; // AddNorthwindContext extension method
namespace Northwind.Web;
public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  {
    services.AddRazorPages();
    services.AddNorthwindContext();
  }
  public void Configure(
    IApplicationBuilder app, IWebHostEnvironment env)
  {
    if (!env.IsDevelopment())
    {
      app.UseHsts();
    }
    app.UseRouting();
    app.UseHttpsRedirection();
    app.UseDefaultFiles(); // index.html, default.html, and so on
    app.UseStaticFiles();
    app.UseEndpoints(endpoints =>
    {
      endpoints.MapRazorPages();
      endpoints.MapGet("/hello", () => "Hello World!");
    });
  }
}

The Startup class has two methods that are called automatically by the host to configure the website.

The ConfigureServices method registers services that can then be retrieved when the functionality they provide is needed using dependency injection. Our code registers two services: Razor Pages and an EF Core database context.

Registering services in the ConfigureServices method

Common methods that register dependency services, including services that combine other method calls that register services, are shown in the following table:

Method

Services that it registers

AddMvcCore

Minimum set of services necessary to route requests and invoke controllers. Most websites will need more configuration than this.

AddAuthorization

Authentication and authorization services.

AddDataAnnotations

MVC data annotations service.

AddCacheTagHelper

MVC cache tag helper service.

AddRazorPages

Razor Pages service including the Razor view engine. Commonly used in simple website projects. It calls the following additional methods:

AddMvcCore

AddAuthorization

AddDataAnnotations

AddCacheTagHelper

AddApiExplorer

Web API explorer service.

AddCors

CORS support for enhanced security.

AddFormatterMappings

Mappings between a URL format and its corresponding media type.

AddControllers

Controller services but not services for views or pages. Commonly used in ASP.NET Core Web API projects. It calls the following additional methods:

AddMvcCore

AddAuthorization

AddDataAnnotations

AddCacheTagHelper

AddApiExplorer

AddCors

AddFormatterMappings

AddViews

Support for .cshtml views including default conventions.

AddRazorViewEngine

Support for Razor view engine including processing the @ symbol.

AddControllersWithViews

Controller, views, and pages services. Commonly used in ASP.NET Core MVC website projects. It calls the following additional methods:

AddMvcCore

AddAuthorization

AddDataAnnotations

AddCacheTagHelper

AddApiExplorer

AddCors

AddFormatterMappings

AddViews

AddRazorViewEngine

AddMvc

Similar to AddControllersWithViews, but you should only use it for backward compatibility.

AddDbContext<T>

Your DbContext type and its optional DbContextOptions<TContext>.

AddNorthwindContext

A custom extension method we created to make it easier to register the NorthwindContext class for either SQLite or SQL Server based on the project referenced.

You will see more examples of using these extension methods for registering services in the next few chapters when working with MVC and Web API services.

Setting up the HTTP request pipeline in the Configure method

The Configure method configures the HTTP request pipeline, which is made up of a connected sequence of delegates that can perform processing and then decide to either return a response themselves or pass processing on to the next delegate in the pipeline. Responses that come back can also be manipulated.

Remember that delegates define a method signature that a delegate implementation can plug into. The delegate for the HTTP request pipeline is simple, as shown in the following code:

public delegate Task RequestDelegate(HttpContext context);

You can see that the input parameter is an HttpContext. This provides access to everything you might need to process the incoming HTTP request, including the URL path, query string parameters, cookies, and user agent.

These delegates are often called middleware because they sit in between the browser client and the website or service.

Middleware delegates are configured using one of the following methods or a custom method that calls them itself:

For convenience, there are many extension methods that make it easier to build the pipeline, for example, UseMiddleware<T>, where T is a class that has:

  1. A constructor with a RequestDelegate parameter that will be passed the next pipeline component
  2. An Invoke method with a HttpContext parameter and returns a Task

Summarizing key middleware extension methods

Key middleware extension methods used in our code include the following:

Visualizing the HTTP pipeline

The HTTP request and response pipeline can be visualized as a sequence of request delegates, called one after the other, as shown in the following simplified diagram, which excludes some middleware delegates, such as UseHsts:

Diagram

Description automatically generated

Figure 14.16: The HTTP request and response pipeline

As mentioned before, the UseRouting and UseEndpoints methods must be used together. Although the code to define the mapped routes such as /hello are written in UseEndpoints, the decision about whether an incoming HTTP request URL path matches and therefore which endpoint to execute is made at the UseRouting point in the pipeline.

Implementing an anonymous inline delegate as middleware

A delegate can be specified as an inline anonymous method. We will register one that plugs into the pipeline after routing decisions for endpoints have been made.

It will output which endpoint was chosen, as well as handling one specific route: /bonjour. If that route is matched, it will respond with plain text, without calling any further into the pipeline:

  1. In Startup.cs, statically import Console, as shown in the following code:
    using static System.Console;
    
  2. Add statements after the call to UseRouting and before the call to UseHttpsRedirection to use an anonymous method as a middleware delegate, as shown in the following code:
    app.Use(async (HttpContext context, Func<Task> next) =>
    {
      RouteEndpoint? rep = context.GetEndpoint() as RouteEndpoint;
      if (rep is not null)
      {
        WriteLine($"Endpoint name: {rep.DisplayName}");
        WriteLine($"Endpoint route pattern: {rep.RoutePattern.RawText}");
      }
      if (context.Request.Path == "/bonjour")
      {
        // in the case of a match on URL path, this becomes a terminating
        // delegate that returns so does not call the next delegate
        await context.Response.WriteAsync("Bonjour Monde!");
        return;
      }
      // we could modify the request before calling the next delegate
      await next();
      // we could modify the response after calling the next delegate
    });
    
  3. Start the website.
  4. In Chrome, navigate to https://localhost:5001/, look at the console output and note that there was a match on an endpoint route /, it was processed as /index, and the Index.cshtml Razor Page was executed to return the response, as shown in the following output:
    Endpoint name: /index 
    Endpoint route pattern:
    
  5. Navigate to https://localhost:5001/suppliers and note that you can see that there was a match on an endpoint route /Suppliers and the Suppliers.cshtml Razor Page was executed to return the response, as shown in the following output:
    Endpoint name: /Suppliers 
    Endpoint route pattern: Suppliers
    
  6. Navigate to https://localhost:5001/index and note that there was a match on an endpoint route /index and the Index.cshtml Razor Page was executed to return the response, as shown in the following output:
    Endpoint name: /index 
    Endpoint route pattern: index
    
  7. Navigate to https://localhost:5001/index.html and note that there is no output written to the console because there was no match on an endpoint route but there was a match for a static file, so it was returned as the response.
  8. Navigate to https://localhost:5001/bonjour and note that there is no output written to the console because there was no match on an endpoint route. Instead, our delegate matched on /bonjour, wrote directly to the response stream, and returned with no further processing.
  9. Close Chrome and shut down the web server.

Practicing and exploring

Test your knowledge and understanding by answering some questions, get some hands-on practice, and explore this chapter's topics with deeper research.

Exercise 14.1 – Test your knowledge

Answer the following questions:

  1. List six method names that can be specific in an HTTP request.
  2. List six status codes and their descriptions that can be returned in an HTTP response.
  3. In ASP.NET Core, what is the Startup class used for?
  4. What does the acronym HSTS stand for and what does it do?
  5. How do you enable static HTML pages for a website?
  6. How do you mix C# code into the middle of HTML to create a dynamic page?
  7. How can you define shared layouts for Razor Pages?
  8. How can you separate the markup from the code-behind in a Razor Page?
  9. How do you configure an Entity Framework Core data context for use with an ASP.NET Core website?
  10. How can you reuse Razor Pages with ASP.NET Core 2.2 or later?

Exercise 14.2 – Practice building a data-driven web page

Add a Razor Page to the Northwind.Web website that enables the user to see a list of customers grouped by country. When the user clicks on a customer record, they then see a page showing the full contact details of that customer, and a list of their orders.

Exercise 14.3 – Practice building web pages for console apps

Reimplement some of the console apps from earlier chapters as Razor Pages, for example, from Chapter 4, Writing, Debugging, and Testing Functions, provide a web user interface to output times tables, calculate tax, and generate factorials and the Fibonacci sequence.

Exercise 14.4 – Explore topics

Use the links on the following page to learn more about the topics covered in this chapter:

https://github.com/markjprice/cs10dotnet6/blob/main/book-links.md#chapter-14---building-websites-using-aspnet-core-razor-pages

Summary

In this chapter, you learned about the foundations of web development using HTTP, how to build a simple website that returns static files, and you used ASP.NET Core Razor Pages with Entity Framework Core to create web pages that were dynamically generated from information in a database.

We reviewed the HTTP request and response pipeline, what the helper extension methods do, and how you can add your own middleware that affects processing.

In the next chapter, you will learn how to build more complex websites using ASP.NET Core MVC, which separates the technical concerns of building a website into models, views, and controllers to make them easier to manage.