Pro C#10 CHAPTER 30 Introducing ASP.NET Core

PART IX

ASP.NET Core

CHAPTER 30

Introducing ASP.NET Core

The final section of this book covers ASP.NET Core, C# and the .NET web development framework. The chapter begins with an introduction of ASP.NET MVC and the basics of the MVC pattern as implemented in ASP.NET Core. Next, you will create the solution and the three ASP.NET Core projects that will be developed over the course of the rest of the book. The first application, AutoLot.Api is an ASP.NET Core RESTful service, the second is an ASP.NET Core web application using the Model-View-Controller pattern and the final application is an ASP.NET Core web application using Razor pages. The RESTFul services serves as
an optional back end to the MVC and Razor Page applications, and the AutoLot.Dal and AutoLot.Models projects that you built earlier in this book will serve as the data access layer for all of the applications.
After building the projects and solution, the next section demonstrates the many ways to run and debug ASP.NET Core projects using Visual Studio or Visual Studio Code. The rest of this chapter explores the many features from ASP.NET that were carried forward into ASP.NET Core. This includes controllers and actions, routing, model binding and validation, and finally filters.

A Quick Look Back at ASP.NET MVC
The ASP.NET MVC framework is based on the Model-View-Controller pattern and provided an answer to developers who were frustrated by WebForms, which was essentially a leaky abstraction over
HTTP. WebForms was created to help client-server developers move to the Web, and it was pretty successful in that respect. However, as developers became more accustomed to web development, many wanted more control over the rendered output, elimination of view state, and adherence to a proven web application design pattern. With those goals in mind, ASP.NET MVC was created.

Introducing the MVC Pattern
The Model-View-Controller (MVC) pattern has been around since the 1970s, originally created as a pattern for use in Smalltalk. The pattern has made a resurgence recently, with implementations in many different and varied languages, including Java (Spring Framework), Ruby (Ruby on Rails), and .NET(ASP.NET MVC).

The Model
The model is the data of your application. The data is typically represented by plain old CLR objects (POCOs). View models are composed of one or more models and shaped specifically for the consumer of the data. One way to think about models and view models is to relate them to database tables and database views.
Academically, models should be extremely clean and not contain validation or any other business rules. Pragmatically, whether or not models contain validation logic or other business rules depends

© Andrew Troelsen, Phil Japikse 2022
A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_30

1313

entirely on the language and frameworks used, as well as specific application needs. For example, EF Core contains many data annotations that double as a mechanism for shaping the database tables and a means for validation in ASP.NET Core web applications. In this book (and in my professional work), the examples focus on reducing duplication of code, which places data annotations and validations where they make the most sense.

The View
The view is the user interface of the application. Views accept commands and render the results of those commands to the user. The view should be as lightweight as possible and not actually process any of the work, but hand off all work to the controller. Views are typically strongly typed to a model, although that is not required.

The Controller
The controller is the brains of the application. Controllers take commands/requests from the user (via the view) or client (through API calls) through action methods, and handle them appropriately. The results of the operation are then returned to the user or client. Controllers should be lightweight and leverage other components or services to handle the details of the requests. This promotes separation of concerns and increases testability and maintainability.

ASP.NET Core and the MVC Pattern
ASP.NET Core is capable of creating many types of web applications and services. Two of the options are web applications using the MVC pattern and RESTful services. If you have worked with ASP.NET “classic,” these are analogous to ASP.NET MVC and ASP.NET Web API, respectively. The MVC web application and API application types share the “model” and the “controller” portion of the pattern, while MVC web applications also implement the “view” to complete the MVC pattern.

ASP.NET Core and .NET Core
Just as Entity Framework Core is a complete rewrite of Entity Framework 6, ASP.NET Core is a rewrite of the popular ASP.NET Framework. Rewriting ASP.NET was no small task, but it was necessary in order to remove the dependency on System.Web. Removing this dependency enabled ASP.NET applications to run on operating systems other than Windows and other web servers besides Internet Information Services (IIS), including self-hosted. This opened the door for ASP.NET Core applications to use a cross-platform, lightweight, fast, and open source web server called Kestrel. Kestrel presents a uniform development experience across all platforms.

■Note Kestrel was originally based on LibUV, but since ASP.NET Core 2.1, it is now based on managed sockets.

Like EF Core, ASP.NET Core is being developed on GitHub as a completely open source project (https://github.com/aspnet). It is also designed as a modular system of NuGet packages. Developers only install the features that are needed for a particular application, minimizing the application footprint,
reducing the overhead, and decreasing security risks. Additional improvements include a simplified startup, built-in dependency injection, a cleaner configuration system, and pluggable middleware.

One Framework, Many Uses
There are lots of changes and improvements in ASP.NET Core, as you will see throughout the rest of the chapters in this section. Besides the cross-platform capabilities, another significant change is the unification of the web application frameworks. ASP.NET Core encompasses ASP.NET MVC, ASP.NET Web API, and Razor Pages into a single development framework. Developing web applications and services with the ASP. NET (not ASP.NET Core) Framework presented several choices, including WebForms, MVC, Web API, Windows Communication Foundation (WCF), and WebMatrix. They all had their positives and negatives; some were closely related, and others were quite different. All of the choices available meant developers had to know each of them in order to select the proper one for the task at hand or just select one and hope for the best.
With ASP.NET Core, you can build applications that use Razor Pages, the Model-View-Controller pattern, RESTful services, and SPA applications using Blazor WebAssembly or JavaScript frameworks like Angular and React. While the UI rendering varies with choices between MVC, Razor Pages, and the
JavaScript frameworks, the underlying server side development framework is the same across all choices. Blazor WebAssembly is a client side development framework, and doesn’t have a server side component like the other ASP.NET Core application types. Two prior choices that have not been carried forward into ASP. NET Core are WebForms and WCF.

■Note With all of the separate frameworks brought under the same roof, the former names of ASP.NET MVC and ASP.NET Web API have been officially retired. In this book, I still refer to ASP.NET Core web applications using the Model-View-Controller pattern as MVC or MVC based applications and refer to ASP.NET rESTful services as API or rESTful services for simplicity.

Create and Configure the Solution and Projects
Before diving into the some of the major concepts in ASP.NET Core, let’s build the solution and projects that will be used through the rest of the chapters. The ASP.NET Core projects can be created using either Visual Studio or the command line. Both options will be covered in the next two sections.

Using Visual Studio 2022
Visual Studio has the advantage of a GUI to step you through the process of creating a solution and projects, adding NuGet packages, and creating references between projects.

Create the Solution and Projects
Start by creating a new project in Visual Studio. Select the C# template ASP.NET Core Web API from the “Create a new project” dialog. In the “Configure your new project” dialog, enter AutoLot.Api for the project name and AutoLot for the solution name, as shown in Figure 30-1.

Figure 30-1. Creating the AutoLot.Api project and AutoLot solution

On the Additional information screen, select .NET 6.0 (Long-term support) for the Framework and leave the “Configure for HTTPS” and “Enable OpenAPI” check boxes checked, as shown in Figure 30-2. Then click Create.

■Note Minimal APIs are a new feature in .NET 6 for creation without the traditional Controllers and action methods. These will not be covered in this text.

Figure 30-2. Selecting the ASP.NET Core Web API template

■Note hot reload is a new feature in .NET 6 that reloads your app while it is running when non-destructive changes are made, reducing the need to restart your application while developing to see changes. This has replaced razor runtime compilation, which was used in .NET 5 to accomplish the same goal.

Now add another ASP.NET Core web application to the solution. Select the “ASP.NET Core Web App (Model-View-Controller)” template. Name the project AutoLot.Mvc, then make sure that .NET Core 6.0 is selected, and the “Configure for HTTPS” option is checked as shown in Figure 30-3.

Figure 30-3. Configuring the ASP.NET Core MVC based Web Application template

Next, add the last ASP.NET Core web application to the solution. Select the “ASP.NET Core Web App” template. Name the project AutoLot.Web, then make sure that .NET Core 6.0 (Long-term support) is selected, and the “Configure for HTTPS” option is checked as shown in Figure 30-4.

Figure 30-4. Configuring the ASP.NET Core Razor page MVC Web Application template

Finally, add a C# Class Library to the project and name it AutoLot.Services.

Add in AutoLot.Models and AutoLot.Dal
The solution requires the completed data access layer which was completed in Chapter 23. You can also use the versions from Chapter 24, but that chapter was all about testing the finished data access layer and didn’t make any changes to the data access layer projects. You can either copy the files into the current solution directory or leave them in the place where you built them. Either way, you need to right-click your solution name in Solution Explorer, select Add ➤ Existing Project, and navigate to the AutoLot.Models.csproj file and select it. Repeat for the AutoLot.Dal project by selecting the AutoLot.Dal.csproj file.

Add the Project References
Add the following project references by right-clicking the project name in Solution Explorer and selecting Add ➤ Project Reference for each project.
AutoLot.Api, AutoLot.Web, and AutoLot.Mvc reference the following:
•AutoLot.Models
•AutoLot.Dal
•AutoLot.Services
AutoLot.Services references the following:
•AutoLot.Models
•AutoLot.Dal

Add the NuGet Packages
Additional NuGet packages are needed to complete the applications.
To the AutoLot.Api project, add the following packages:
•AutoMapper
•Microsoft.AspNetCore.Mvc.Versioning
•Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
•Microsoft.EntityFrameworkCore.Design
•Microsoft.EntityFrameworkCore.SqlServer
•Microsoft.VisualStudio.Web.CodeGeneration.Design
•Microsoft.VisualStudio.Threading.Analyzers
•System.Text.Json
•Swashbuckle.AspNetCore
•Swashbuckle.AspNetCore.Annotations
•Swashbuckle.AspNetCore.Swagger
•Swashbuckle.AspNetCore.SwaggerGen
•Swashbuckle.AspNetCore.SwaggerUI

■Note With the ASP.NET Core 6.0 API templates, Swashbuckle.AspNetCore is already referenced. The additional Swashbuckle packages add capabilities beyond the basic implementation.

To the AutoLot.Mvc project, add the following packages:
•AutoMapper
•System.Text.Json
•LigerShark.WebOptimizer.Core
•Microsoft.Web.LibraryManager.Build
•Microsoft.VisualStudio.Web.CodeGeneration.Design
•Microsoft.EntityFrameworkCore.Design
•Microsoft.EntityFrameworkCore.SqlServer
•Microsoft.VisualStudio.Threading.Analyzers
To the AutoLot.Web project, add the following packages:
•AutoMapper
•System.Text.Json
•LigerShark.WebOptimizer.Core
•Microsoft.Web.LibraryManager.Build

•Microsoft.VisualStudio.Web.CodeGeneration.Design
•Microsoft.EntityFrameworkCore.Design
•Microsoft.EntityFrameworkCore.SqlServer
•Microsoft.VisualStudio.Threading.Analyzers
To the AutoLot.Services project, add the following packages:
•Microsoft.Extensions.Hosting.Abstractions
•Microsoft.Extensions.Options
•Serilog.AspNetCore
•Serilog.Enrichers.Environment
•Serilog.Settings.Configuration
•Serlog.Sinks.Console
•Serilog.Sinks.File
•Serilog.Sinks.MSSqlServer
•System.Text.Json
•Microsoft.VisualStudio.Threading.Analyzers

Using the Command Line
As shown earlier in this book, .NET Core projects and solutions can be created using the command line. Open a prompt and navigate to the directory where you want the solution located.

■Note The commands listed use the Windows directory separator. If you are using a non-Windows operating system, adjust the separator characters as needed. They also use a specific directory path when adding the AutoLot. dal and AutoLot.Models projects to the solution, which will need to be updated based on the location of your projects.

The following commands create the AutoLot solution and add the existing AutoLot.Models and AutoLot.
Dal projects into the solution using the same options that were shown when using Visual Studio 2022:

rem create the solution dotnet new sln -n AutoLot
rem add autolot dal to solution update the path references as needed dotnet sln AutoLot.sln add ..\Chapter_23\AutoLot.Models
dotnet sln AutoLot.sln add ..\Chapter_23\AutoLot.Dal

Create the AutoLot.Service project, add it to the solution, add the NuGet packages and the project references.

rem create the class library for the application services and add it to the solution dotnet new classlib -lang c# -n AutoLot.Services -o .\AutoLot.Services -f net6.0 dotnet sln AutoLot.sln add AutoLot.Services

dotnet add AutoLot.Services package Microsoft.Extensions.Hosting.Abstractions dotnet add AutoLot.Services package Microsoft.Extensions.Options
dotnet add AutoLot.Services package Serilog.AspNetCore
dotnet add AutoLot.Services package Serilog.Enrichers.Environment dotnet add AutoLot.Services package Serilog.Settings.Configuration dotnet add AutoLot.Services package Serilog.Sinks.Console
dotnet add AutoLot.Services package Serilog.Sinks.File
dotnet add AutoLot.Services package Serilog.Sinks.MSSqlServer dotnet add AutoLot.Services package System.Text.Json
dotnet add AutoLot.Services package Microsoft.VisualStudio.Threading.Analyzers
rem update the path references as needed
dotnet add AutoLot.Services reference ..\Chapter_23\AutoLot.Models dotnet add AutoLot.Services reference ..\Chapter_23\AutoLot.Dal

Create the AutoLot.Api project, add it to the solution, add the NuGet packages and the project references.

dotnet new webapi -lang c# -n AutoLot.Api -au none -o .\AutoLot.Api -f net6.0 dotnet sln AutoLot.sln add AutoLot.Api
dotnet add AutoLot.Api package AutoMapper
dotnet add AutoLot.Api package Swashbuckle.AspNetCore
dotnet add AutoLot.Api package Swashbuckle.AspNetCore.Annotations dotnet add AutoLot.Api package Swashbuckle.AspNetCore.Swagger dotnet add AutoLot.Api package Swashbuckle.AspNetCore.SwaggerGen dotnet add AutoLot.Api package Swashbuckle.AspNetCore.SwaggerUI
dotnet add AutoLot.Api package Microsoft.VisualStudio.Web.CodeGeneration.Design dotnet add AutoLot.Api package Microsoft.EntityFrameworkCore.Design
dotnet add AutoLot.Api package Microsoft.EntityFrameworkCore.SqlServer dotnet add AutoLot.Api package Microsoft.VisualStudio.Threading.Analyzers dotnet add AutoLot.Api package System.Text.Json
dotnet add AutoLot.Api package Microsoft.AspNetCore.Mvc.Versioning
dotnet add AutoLot.Api package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
rem add project references
rem update the path references as needed
dotnet add AutoLot.Api reference ..\Chapter_23\AutoLot.Dal dotnet add AutoLot.Api reference ..\Chapter_23\AutoLot.Models dotnet add AutoLot.Api reference AutoLot.Services

Create the AutoLot.Mvc project, add it to the solution, add the NuGet packages and the project references.

dotnet new mvc -lang c# -n AutoLot.Mvc -au none -o .\AutoLot.Mvc -f net6.0 dotnet sln AutoLot.sln add AutoLot.Mvc
rem add packages
dotnet add AutoLot.Mvc package AutoMapper dotnet add AutoLot.Mvc package System.Text.Json
dotnet add AutoLot.Mvc package LigerShark.WebOptimizer.Core dotnet add AutoLot.Mvc package Microsoft.Web.LibraryManager.Build
dotnet add AutoLot.Mvc package Microsoft.EntityFrameworkCore.Design

dotnet add AutoLot.Mvc package Microsoft.EntityFrameworkCore.SqlServer dotnet add AutoLot.Mvc package Microsoft.VisualStudio.Threading.Analyzers
dotnet add AutoLot.Mvc package Microsoft.VisualStudio.Web.CodeGeneration.Design
rem add project references
rem update the path references as needed
dotnet add AutoLot.Mvc reference ..\Chapter_23\AutoLot.Models dotnet add AutoLot.Mvc reference ..\Chapter_23\AutoLot.Dal dotnet add AutoLot.Mvc reference AutoLot.Services

Finally, create the AutoLot.Web project, add it to the solution, add the NuGet packages, and add the project references.

dotnet new webapp -lang c# -n AutoLot.Web -au none -o .\AutoLot.Web -f net6.0 dotnet sln AutoLot.sln add AutoLot.Web
rem add packages
dotnet add AutoLot.Web package AutoMapper dotnet add AutoLot.Web package System.Text.Json
dotnet add AutoLot.Web package LigerShark.WebOptimizer.Core dotnet add AutoLot.Web package Microsoft.Web.LibraryManager.Build
dotnet add AutoLot.Web package Microsoft.EntityFrameworkCore.SqlServer
dotnet add AutoLot.Web package Microsoft.EntityFrameworkCore.SqlServer.Design dotnet add AutoLot.Web package Microsoft.VisualStudio.Web.CodeGeneration.Design dotnet add AutoLot.Web package Microsoft.VisualStudio.Threading.Analyzers
rem add project references
rem update the path references as needed
dotnet add AutoLot.Web reference ..\Chapter_23\AutoLot.Models dotnet add AutoLot.Web reference ..\Chapter_23\AutoLot.Dal dotnet add AutoLot.Web reference AutoLot.Services

That completes the setup using the command line. As you can probably see, it is much more efficient provided you don’t need the Visual Studio GUI to help you.

■Note At the time of this writing, Visual Studio created projects with files that use nested namespaces, while the CLI tooling creates files that use file scoped namespaces.

Update the Entity Framework Core Package Reference
Recall from the EF Core chapters that to clear out temporal tables, the Microsoft.EntityFrameworkCore. Design package reference must be modified. Update the reference in the project files for the AutoLot.Api, AutoLot.Mvc, and AutoLot.Web projects to the following:

<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0>

all

Disable Nullable Reference Types For All Projects
At this time, disable nullable reference types by updating each of the project files’ PropertyGroup to the following:


net6.0
disable
enable

Create a GlobalUsing.cs Class in Each Project
The final setup step is to add a file named GlobalUsings.cs to the root folder of each project and clear out any scaffolded code. These will be used to hold the using statements for each project.

Running ASP.NET Core Applications
Previous versions of ASP.NET web applications always ran using IIS (or IIS Express). With ASP.NET Core, applications typically run using the Kestrel web server with an option to use IIS, Apache, Nginx, etc., by way of a reverse proxy between Kestrel and the other web server. This shift from requiring IIS to allowing other web servers not only changes the deployment model, but also changes the development possibilities. During development, you can now run your applications in these ways:
•From Visual Studio, using Kestrel or IIS Express
•From a command prompt with the .NET CLI, using Kestrel
•From Visual Studio Code, using Kestrel, from the Run menu
•From Visual Studio Code’s Terminal window using the .NET CLI and Kestrel
When using any of these options, the application’s ports and environment are configured with a file named launchsettings.json located under the Properties folder. The launchsettings.json file for the AutoLot.Mvc project is listed here for reference (your ports for both the IIS Express and AutoLot.Mvc profiles will be different):

{
"iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": {
"applicationUrl": "http://localhost:42788", "sslPort": 44375
}
},
"profiles": {
"AutoLot.Mvc": { "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",
}
}
}
}

Using Visual Studio
The first profile defines the settings when using Kestrel as the web server. The Kestrel profile is always named after the project (e.g., AutoLot.Mvc) when it is created, but can be changed to any other name. The iisSettings section and the IIS Express profile together define the settings when running the application using IIS Express as the web server. The most important settings to note are the applicationUrl, which also defines the port, and the environmentVariables block, which defines the environment variables to
use when debugging. Any environment variables defined in this section supersede any user or machine environment settings with the same name.
The Run command in Visual Studio allows for choosing either the Kestrel or IIS Express profile, as shown in Figure 30-5. Once a profile is selected, you can run the project by pressing F5 (debug mode), pressing Ctrl+F5 (the same as “Start Without Debugging” in the Debug menu), or clicking the green run arrow (the same as “Start Debugging” in the Debug menu). New in .NET 6 and Visual Studio 2022, the Kestrel profile is set as the default.

Figure 30-5. The available Visual Studio debugging profiles

■Note There is also an option to create additional profiles, such as running using the WSL (Windows Subsystem for Linux). This feature isn’t covered in this book, but if you are curious to learn more, there is a book on the topic titled Pro Windows Subsystem for Linux (https://link.springer.com/ book/10.1007/978-1-4842-6873-5z)

Using Visual Studio Code
To run the projects from Visual Studio Code, open the folder where the solution is located. When you press F5 (or click Run), VS Code will prompt you to select the runtime to use (select .NET 6+ and .NET Core) and which project to run (AutoLot.Api, AutoLot.Web, or AutoLot.Mvc). It will then create a run configuration and place it in a file named launch.json. The launch settings only have to be configured the first time. Once the files exists, you can freely run/debug your application without having to make the selections again. Visual Studio Code uses the Kestrel profile when running your application.

Using the Command Line or Terminal Window
Running from the command line for ASP.NET Core apps is the same as any other .NET application that you have seen in this book. Simply navigate to the directory where the csproj file for your application is located and enter the following command:

dotnet run

This starts your application using the Kestrel profile. To end the process, press Ctrl+C.

Changing Code While Debugging
When running from the command line using dotnet run, the code in your application’s projects can be changed, but the changes won’t be reflected in the running app. To have the changes reflected in the running app, enter the following command:

dotnet watch

This command runs with Hot Reload enabled. Hot Reload is a new feature in .NET 6 that attempts to reload your app in real time when changes are made. This is a vast improvement over the .NET 5 dotnet watch run command, which restarted your entire app when a file watcher noticed changes.
Not all changes can be reloaded in real time, as some do require a restart of your app. If this is needed, you will be prompted to restart your application. If the prompt doesn’t appear, you can force a reload by using Ctrl+R in the terminal window. To cancel the application, hit Ctrl+C.
Hot Reload is also available when debugging with Visual Studio 2022 or Visual Studio Code.

Debugging ASP.NET Core Applications
When running your application from Visual Studio or Visual Studio Code, debugging works as expected. When running from the command line, you have to attach to the running process before you can debug your application.

Attaching with Visual Studio
After launching your app (with dotnet run or dotnet watch run), select Debug ➤ Attach to Process in Visual Studio. When the Attach to Process dialog appears, filter the process by your application name, as shown in Figure 30-6.

Figure 30-6. Attaching to the running applications for debugging in Visual Studio

Once attached to the running process, you can set breakpoints in Visual Studio, and debugging works as expected. Hot reload is enabled when you attach to the running process, so the edit and continue experience is getting better, although not yet to the level it was in prior versions of the .NET framework.

Attaching with Visual Studio Code
After launching your app (with dotnet run or dotnet watch run), select .NET Core Attach instead of .NET Core Launch (web) by clicking the green run arrow in VS Code, as shown in Figure 30-7.

Figure 30-7. Attaching to the running applications for debugging in Visual Studio Code

When you click the Run button, you will be prompted to select which process to attach. Select your application. You can now set breakpoints as expected.

Update the AutoLot.Api and AutoLot.Web Kestrel Ports
You might have noticed that AutoLot.Api, AutoLot.Web, and AutoLot.Mvc have different ports specified for the IIS Express and Kestrel profiles. Instead of remembering each of the randomly assigned ports, update all of the projects settings as follows:

//AutoLot.Mvc launchSettins.json "AutoLot.Mvc": {
"commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"dotnetRunMessages": true
},
//AutoLot.Api launchSettins.json "AutoLot.API": {
"commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger",
"applicationUrl": "https://localhost:5011;http://localhost:5010",
"environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development"
}
},
//AutoLot.Web launchSettins.json "AutoLot.Web": {
"commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true,
"applicationUrl": "https://localhost:5021;http://localhost:5020",
"environmentVariables": {

"ASPNETCORE_ENVIRONMENT": "Development"
}
},

ASP.NET Core Concepts from MVC/Web API
Many of the design goals and features that brought developers to use ASP.NET MVC and ASP.NET Web API are still supported (and improved) in ASP.NET Core. Some of these (but not all) are listed here:
•Convention over configuration
•Controllers and actions
•Cleaner directory structure
•Model binding
•Model validation
•Routing
•Filters
•Layouts and Razor Views
These items are all covered in the next sections, except for layouts and Razor views, which are covered in a later chapter.

■Note razor page based applications are new in ASP.NET Core, however many of the concepts covered in this section also apply to this new application type. razor page based applications will be formally introduced in the next chapter.

Convention over Configuration
ASP.NET MVC and ASP.NET Web API reduced the amount of configuration necessary by introducing certain conventions. When followed, these conventions reduce the amount of manual (or templated) configuration, but also require the developers to know the conventions in order to take advantage of them. Two of the main conventions include naming conventions and directory structure.

Naming Conventions
There are multiple naming conventions in ASP.NET Core, for MVC style and RESTful service applications as well as Razor page based applications. For example, controllers are typically named with the Controller suffix (e.g., HomeController) in addition to deriving from Controller (or ControllerBase). When accessed through routing, the Controller suffix is dropped. When routing to async action methods named with the suffix Async, the Async suffix is dropped. Razor pages’ code behind files are named with the Model suffix
(ErrorModel), which is dropped just like the Controller and Async suffixes. This convention of dropping the suffix is repeated throughout ASP.NET Core.
Another naming convention is used in locating the views for a controller’s action methods. When looking for a controller’s views, the controller name minus the suffix is the starting search location. By default, an action method will render the view of the same name as the method.
There will be many examples of ASP.NET Core conventions covered in the following chapters.

Controllers and Actions (MVC Based Web Apps and RESTful Services)
Just like ASP.NET MVC and ASP.NET Web API, controllers and action methods are the workhorses of an ASP. NET Core MVC style web application or RESTful service application.

■Note razor pages derive from the PageModel class, which will be covered in the next chapter.

The Controller Class
As mentioned already, ASP.NET Core unified ASP.NET MVC5 and ASP.NET Web API. This unification also combines the Controller, ApiController, and AsyncController base classes from MVC5 and Web API
2.1into one new class, Controller, which has a base class of its own named ControllerBase. ASP.NET Core web application controllers inherit from the Controller class, while ASP.NET Core service controllers inherit from the ControllerBase class (covered next).
The Controller class provides a host of helper methods for MVC style web applications. Table 30-1 lists the most commonly used methods.

Table 30-1. Some of the Helper Methods Provided by the Controller Class

Helper Method Meaning in Life
ViewDataTempDataViewBag Provide data to the view through the ViewDataDictionary, TempDataDictionary, and dynamic ViewBag transport.
View Returns a ViewResult (derived from ActionResult) as the HTTP response. Defaults to a view of the same name as the action method, with the option of specifying a specific view. All options allow specifying a view model that is strongly typed and sent to the View.
PartialView Returns a PartialViewResult to the response pipeline.
ViewComponent Returns a ViewComponentResult to the response pipeline.
Json Returns a JsonResult containing an object serialized as JSON as the response.
OnActionExecuting Executes before an action method executes.
OnActionExecutionAsync Async version of OnActionExecuting.
OnActionExecuted Executes after an action method executes.

The ControllerBase Class
The ControllerBase class provides the core functionality for both ASP.NET Core MVC style web applications and RESTful services, in addition to helper methods for returning HTTP status codes. Table 30-2 lists some of the core functionality in ControllerBase.

Table 30-2. Some of the Helper Methods Provided by the ControllerBase Class

Helper Method Meaning in Life
HttpContext Returns the HttpContext for the currently executing action.
Request Returns the HttpRequest for the currently executing action.
Response Returns the HttpResponse for the currently executing action.
RouteData Returns the RouteData for the currently executing action (routing is covered later in this chapter).
ModelState Returns the state of the model in regard to model binding and validation (both covered later in this chapter).
Url Returns an instance of the IUrlHelper, providing access to building URLs for ASP.NET Core MVC applications and services.
User Returns the ClaimsPrincipal user.
Content Returns a ContentResult to the response. Overloads allow for adding a content type and encoding definition.
File Returns a FileContentResult to the response.
Redirect A series of methods that redirect the user to another URL by returning a
RedirectResult.
LocalRedirect A series of methods that redirect the user to another URL only if the URL is local. More secure than the generic Redirect methods.
RedirectToActionRedirect ToPageRedirectToRoute A series of methods that redirect to another action method, Razor Page, or named route. Routing is covered later in this chapter.
TryUpdateModelAsync Used for explicit model binding (covered later in this chapter).
TryValidateModel Used for explicit model validation (covered later in this chapter).

And Table 30-3 covers some of the helper methods for returning HTTP status codes.

Table 30-3. Some of the HTTP Status Code Helper Methods Provided by the ControllerBase Class

Helper Method HTTP Status Code Action Result Status Code
NoContent NoContentResult 204
Ok OkResult 200
NotFound NotFoundResult 404
BadRequest BadRequestResult 400
CreatedCreatedAt ActionCreatedAtRoute CreatedResultCreatedAtAction ResultCreateAtRouteResult 201
AcceptedAcceptedAt ActionAcceptedAtRoute AcceptedResultAccepted AtActionResultAcceptedAtRouteResult 202

Actions
Actions are methods on a controller that return an IActionResult (or Task for async operations) or a class that implements IActionResult, such as ActionResult or ViewResult.
This example returns a ViewResult in an MVC based application:

public async Task Index()
=> View(await _serviceWrapper.GetCarsAsync());

This example returns an HTTP status code of 200 with a list of Car records as JSON:

[Produces("application/json")]
public ActionResult<IEnumerable> GetCarsByMake(int? id)
{
return Ok(MainRepo.GetAll());
}

Actions and their return values will be covered more in the following chapters.

Antiforgery Tokens
ASP.NET Core uses antiforgery middleware to help combat against cross-site request forgery attacks. ASP. NET Core uses the Synchronizer Token Pattern (STP) which creates a server side token that is unique and unpredictable. That token is sent to the browser with the response. Any request that comes back to the server must include the correct token, or the request is refused.

Opting In (MVC Style Web Apps)
Receiving the token and validating it is handled in the action methods for MVC style web applications(MVC). To validate the token, simply add the ValidateAntiForgeryToken attribute to every HTTP Post method in your application, like this:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task Create (Car entity)
{
//do important stuff
}

To opt out of the check, simply leave the ValidateAntiForgeryToken attribute off of the method.

■Note Adding the antiforgery token to the response will be covered along with tag helpers and views.

Opting Out (Razor Page Web Apps)
Razor page base applications automatically participate in the antiforgery pattern. To opt out of participating, add the IgnoreAntiforgeryToken to the PageModel class:

[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
//Omitted for brevity
}

Directory Structure Conventions
There are several folder conventions that you must understand to successfully build ASP.NET Core web applications and services. Many of the directories from Web API/MVC are the same (Controllers, Views, Areas) while there are some new ones (e.g., Pages, wwwroot). You will find the directory structure improved from MVC/WebAPI.

The Controllers Folder
By convention, the Controllers folder is where the ASP.NET Core MVC and API implementations (and the routing engine) expect that the controllers for your application are placed.

The Views Folder
The Views folder is where the views for an MVC style application are stored. Each controller gets its own folder under the main Views folder named after the controller name (minus the Controller suffix). The action methods will render views in their controller’s folder by default. For example, the Views/Home folder holds all the views for the HomeController controller class.

The Shared Folder
A special folder under Views is named Shared. This folder is accessible to all controllers and their action methods. After searching the folder named for the controller, if the view can’t be found, then the Shared folder is searched for the view.

The Pages Folder
The Pages folder is where the pages for the application are stored when building web applications using Razor pages. The directory structure under the Pages folder sets the base route for each page (more on routing later).

The Shared Folder
A special folder under Pages is named Shared. This folder is accessible to all pages in a Razor page based web application.

The Areas Folder
Areas are a feature that is used to organize related functionality into a group as a separate namespace for routing and folder structure for views and Razor pages. Each area gets its own set of controllers (API
applications), controllers and views (MVC style applications), and pages (Razor page based applications). An application can have zero to many areas, and each area goes under the parent Areas folder.

The wwwroot Folder
An improvement in ASP.NET Core web applications over the previous framework versions is the creation of a special folder named wwwroot. In ASP.NET MVC, the JavaScript files, images, CSS, and other client-side content were intermingled with all the other folders. In ASP.NET Core, the client side files are all contained under the wwwroot folder. This separation of compiled files from client-side files significantly cleans up the project structure when working with ASP.NET Core.
There is an exception to this conventions that relates to view and page specific CSS files. Those are stored alongside the views/pages they target. They will be covered in later chapters.

Routing
Routing is how ASP.NET Core matches HTTP requests to the proper code (the executable endpoints) to handle those requests as well as create URLs from the executable end points. This is accomplished with routing middleware registered in the Startup class (pre-C# 10) or the top level statements in the Program.cs file (C# 10 and later).
A route in an MVC based web application consists of an (optional) area, a controller, an action method, an HTTP verb (POST or GET), and (optional) additional values (called route values). Routes in ASP.NET Core RESTful services consists of an (optional) area, a controller, an (optional) action method, an HTTP Verb (POST, PUT, GET, DELETE, etc.), and (optional) additional route values. When defining routes for ASP.NET RESTful services, an action method is not usually specified. Instead, once the controller is located, the action method to execute is based on the HTTP verb of the request. In MVC style web applications and RESTful services, routes can be configured along in the middleware or through route attributes on controllers and action methods.
A route in a Razor page based application is the directory structure of the application, an HTTP Verb, the page itself, and (optional) additional route values. In addition to the default constructs, all ASP.NET style applications can create a route that ignores the standard templates. In Razor page based applications, routes are based on the directory structure of the application, with additional tokens available as part of the @page directive. All of this is covered in more detail shortly.

URL Patterns and Route Tokens
Route definitions are composed of URL patterns that contain variable placeholders (called tokens) and (optional) literals placed into an ordered collection known as the route table. Each entry in the route table must define a different URL pattern to match. Table 30-4 lists the reserved token names and their definitions.

Table 30-4. Reserved Route Tokens for MVC Styled and RESTful Service Applications

Token Meaning in Life
Area Defines the area for the route
Controller Defines the controller (minus the controller suffix)
Action Defines the action method name

  • or Catch-all parameter. When used as a prefix to a route parameter, it binds the rest of the URI. For example, car/{slug} matches any URI that starts with /car and has any value following it. The following value is assigned to the slug route value. Using a double * in the path preserves path separator characters while a single does not.

Handling Duplicate C# Method Signatures
The action token by default matches the C# method name on a controller. The ActionName attribute can be used to change in relation to routing. For example, the following code specifies the HTTP Get and HTTP Post action methods with the name and same signature. C# doesn’t allow two methods with the same signature with the same name, so a compiler error occurs:

[HttpGet]
public async Task Create()
{
//do something
}

[HttpPost] [ValidateAntiForgeryToken]
//Compiler error
public async Task Create()
{
//Do something else
}

The solution is to rename one of the methods and add the ActionName attribute to change the name used by the routing engine:

[HttpPost] [ActionName("Create")] [ValidateAntiForgeryToken]
public async Task CreateCar()
{
//Do something else
}

Custom Route Tokens
In addition to the reserved tokens, routes can contain custom tokens that are mapped (model bound) to a controller action method’s or Razor page handler method’s parameters. When defining routes using
tokens, there must be a literal value separating the tokens. While {controller}/{action}/{id?} is valid,
{controller}{action}{id?} is not.

Route Token Constraints
Route tokens can also be constrained to disambiguate similar routes. Table 30-5 shows the available route token constraints. Note that the constraints are not meant to be used for validation. If a route value is invalid based on a constraint (e.g., too big for the int constraint), the routing engine will not find a match and returns a 404 (Not Found). However, business logic would probably require that a 400 (Bad Request) be returned instead.

Table 30-5. Route Tokens Constraints for MVC Styled and RESTful Service Applications

Constraint Example Meaning in Life
Int {id:int} Matches any integer.
Bool {active:bool} Matches true or false. Case insensitive.
datetime {dob:datetime} Matches a valid DateTime value in the invariant culture.
decimal {price:decimal} Matches a valid decimal value in the invariant culture.
double {weight:double} Matches a valid double value in the invariant culture.
Float {weight:float} Matches a valid float value in the invariant culture.
Guid {id:guid} Matches a valid GUID value.
Long {ticks:long} Matches a valid long value in the invariant culture.
minlength(value) {name:minlength(4)} String must be at least value characters long.
maxlength(value) {name:maxlength(12)} String must be at most value characters long.
length(value) {name:length(12)} String must be exactly value characters long.
min(value) {age:min(18)} Integer must be at least value
max(value) {age:max(65)} Integer must be at most value
range(min,max) {age:range(12,65)} Integer must be between min and max
alpha {name:alpha} String must only contain letters a-z, case insensitive.
regex(expression) {ssn:regex(^\d{{3}}-
\d{{2}}-\d{{4}}$)} String must match regular expression.
required {name:required} Value is required.

Conventional Routing (MVC Style Web Apps)
Conventional routing builds the route table in the Startup class (pre-.NET6) or in the Program.cs file’s top level statements (.NET6+). The MapControllerRoute() method adds an endpoint into the route table. The method specifies a name, URL pattern, and any default values for the variables in the URL pattern. In the following code sample, the predefined {controller} and {action} placeholders refer to a controller (minus the Controller suffix) and an action method contained in that controller. The placeholder {id} is custom and is translated into a parameter (named id) for the action method. Adding a question mark to a
route token indicates that it is an optional route value and is represented in the action method as a nullable parameter.

app.UseRouting(); app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}"
);

When a URL is requested, it is checked against the route table. If there is a match, the HTTP Verb (Post, Get, etc.) of the request is also checked to make sure the code located at that application endpoint accepts the request’s verb. For example, an HTTP Post with the URL (minus the scheme and domain) Car/Delete/5 comes into the application. From the following two actions, the second delete action method would be targeted (since it is marked with the [HttpPost] attribute), passing in 5 as the value for the id parameter:

public class CarController : ControllerBase
{
[HttpGet]
public async Task Delete(int? id)
{
//return a view
}

[HttpPost] [ValidateAntiForgeryToken]
public async Task Delete(int id, Car car)
{
//delete the record
}
}

The defaults specify how to fill in the blanks for URLs that don’t contain all of the defined components. In the previous code, if no additional route values were specified in the URL (such as http:// localhost:5001), then the routing engine would call the Index() action method of the HomeController class, without an id parameter. The defaults are progressive, meaning that they can be excluded from right
to left. However, route parts can’t be skipped. Entering a URL like http://localhost:5001/Delete/5 will fail the {controller}/{action}/{id} pattern.
Conventional routing is order dependent. The routing engine starts at the top of the route table and will attempt to find the first matching route based on the (optional) area, controller, action, custom tokens, and HTTP verb. If the routing engine finds more than one end point that matches the route plus HTTP Verb it will throw an AmbiguousMatchException. If the routing engine can’t find a matching route, it will return a 404.
Notice that the route template doesn’t contain a protocol or hostname. The routing engine automatically prepends the correct information when creating routes and uses the HTTP verb, path, and parameters to determine the correct application endpoint. For example, if your site is running on https:// www.skimedic.com, the protocol (HTTPS) and hostname (www.skimedic.com) is automatically prepended
to the route when created (e.g., https://www.skimedic.com/Car/Delete/5). For an incoming request, the routing engine uses the Car/Delete/5 portion of the URL.

Area Routes
If your application contains an area, there is an additional route pattern that needs to be mapped. Instead of using MapControllerRoute(), call MapAreaControllerRoute(), like this:

app.UseRouting(); app.MapAreaControllerRoute(
name:"areaRoute", areaName:"Admin",
pattern:"Admin/{controller}/{action}/{id?}");

Since area routes are more specific than the non-area routes, they are typically added to the route table first, like this:

app.UseRouting(); app.MapAreaControllerRoute(
name:"areaRoute", areaName:"Admin",
pattern:"Admin/{controller}/{action}/{id?}"); app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

Named Routes
Route names can be used as a shorthand to generate URLs from within the application. In the preceding conventional route, the endpoint is assigned the name default.

Attribute Routing (MVC Style Web Apps and RESTful Services)
Attribute routing works the same way as conventional routing when matching routes with application endpoints and HTTP verbs. The difference is how the routes are configured. In attribute routing, routes are defined using C# attributes on controllers and their action methods. This can lead to more precise routing, but can also increase the amount of configuration, since every controller and action needs to have routing information specified. Before we look at examples of attribute routing, it must be enabled in the Startup class (pre .NET 6) or the Program.cs file’s top level statements (.NET6+) by calling MapControllers():

app.MapControllers();

For an example of attribute routing, take the following code snippet. The four Route attributes on the Index() action method equate to the same default route defined earlier. The Index() action method is the application endpoint for
•mysite.com ([Route("/")]),
•mysite.com/Home ([Route("/Home")]),
•mysite.com/Home/Index ([Route("/Home/Index")]), or
•mysite.com/Home/Index/5 ([Route("/Home/Index/{id?}")]).

public class HomeController : Controller
{
[Route("/")]
[Route("/Home")] [Route("/Home/Index")] [Route("/Home/Index/{id?}")]
public IActionResult Index(int? id)
{

}
}

In the previous example, the only route token used was for the optional id parameter. The same set of routes can be created using the controller and action route tokens, like this:

public class HomeController : Controller
{
[Route("/")]
[Route("/[controller]")]
[Route("/[controller]/[action]")]
[Route("/[controller]/[action]/{id?}")]
public IActionResult Index(int? id)
{

}
}

The major difference between conventional routing and attribute routing is that conventional routing covers the application, while attribute routing covers just the controller and its action methods with the Route attribute. If conventional routing is not used, every controller will need to have their route defined with attribute routing or they will not be able to be accessed. For example, if there wasn’t a default route defined in the route table (using conventional routing), the following code is not discoverable since the controller doesn’t have any routing configured:

public class CarController : Controller
{
public IActionResult Delete(int id)
{

}
}

■Note Conventional and attribute routing can be used together. If the default controller route was set as in the conventional routing example, the preceding controller would be located by the route table.

When routes are added at the controller level, the action methods derive from that base route. For example, the following controller route covers the Delete() (and any other) action method:

[Route("[controller]/[action]/{id?}")]
public class CarController : Controller
{
public IActionResult Delete(int id)
{

}
}

■Note The built-in tokens are distinguished with square brackets ([]) in attribute routing instead of the curly braces ({}) used in conventional routing. Custom tokens still use curly braces.

If an action method needs to restart the route pattern, prefix the route with a forward slash (/). For example, if the delete method should follow the URL pattern mysite.com/Delete/Car/5, configure the action as follows:

[Route("[controller]/[action]/{id?}")] public class CarController : Controller
{
[Route("/[action]/[controller]/{id}")]
public IActionResult Delete(int id)
{

}
}

As shown with the default attribute route example, route definitions can use literal route values instead of using token replacement. The following code will produce the same result for the Delete() action method as the previous code sample:

[Route("[controller]/[action]/{id?}")] public class CarController : Controller
{
[Route("/Delete/Car/{id}")]
public IActionResult Delete(int id)
{

}
}

Razor Page Routing
As mentioned already, Razor page routing is based on the folder structure of the application. To enable routing, call MapRazorPages() in the Startup class (pre .NET 6) or the Program.cs file’s top level statements (.NET6+):

app.UseRouting(); app.MapRazorPages();

For Razor page based web applications, the Index page is the default page for a directory. This means that if an Index.cshtml page is located at Pages/Cars/Index.cshtml, then both the routes that map to that page include /Cars and /Cars/Index.
Additional route tokens can be added after the @page directive to refine the route. Suppose you have a Car folder under the Pages folder, and in the Cars folder, there is a page named Delete.cshtml. The default route for this page is /Cars/Delete. To add an optional id token to accept a URI like /Cars/Delete/5, update the @page directive to the following:

@page "{id?}"

Just like routing for MVC style applications, the route can be reset using a forward slash (/). Once reset, you can add literals and tokens to completely change the route. For example, the route for the Delete. cshtml page can be updated to /Delete/Vehicle/5 by using the following:

@page "/Delete/Vehicle/{id?}

Routing and HTTP Verbs
As we have already discussed, none of the route templates define an HTTP verb. In MVC based applications and RESTful services, the action methods are decorated with attributes to indicate which HTTP verb it should handle. For Razor page based applications, the different HTTP verbs are handled with specific page handler methods.

HTTP Verbs in MVC Styled Web Application Attribute Routing
As discussed earlier, a common pattern in web applications using the MVC pattern, there will be two application endpoints that match a particular route template. The discriminator in these instances is the HTTP verb, as we saw with the CarController‘s two Delete() action methods.
Routes can also be modified using the HTTP verb attributes. For example, the following shows the optional id route token added to the route template for both Delete() methods:

[Route("[controller]/[action]")]
public class CarController : Controller
{
[HttpGet("{id?}")]
public IActionResult Delete(int? id)
{

}
[HttpPost("{id}")] [ValidateAntiForgeryToken]
public IActionResult Delete(int id, Car recordToDelete)
{

}
}

■Note Browsers only support get and post requests, so while you can decorate your MVC web application’s methods with additional hTTP Verb attributes (like HttpPut and HttpDelete), browsers will not be able to make the appropriate requests to leverage those endpoints.

Routes can also be restarted using the HTTP verbs; just preface the route templated with a forward slash (/), as the following example demonstrates:

[HttpGet("/[controller]/[action]/{makeId}/{makeName}")] public IActionResult ByMake(int makeId, string makeName)
{
ViewBag.MakeName = makeName;
return View(_repo.GetAllBy(makeId));
}

If an action method isn’t decorated with an HTTP verb attribute, it defaults to accepting HTTP get requests. However, in MVC styled web applications, unmarked action methods can also respond to HTTP post requests, which might cause unexpected results. For this reason, it’s considered a best practice to mark all action methods explicitly with the correct verb attribute.

HTTP Verbs in RESTful Service Routing
Route definitions used for RESTful services typically do not specify action methods. When action methods are not part of the route template, the action methods are selected based on the HTTP verb of the request (and optionally the content type). The following code shows an API controller with four methods that all match the same route template. Notice that the HTTP verb attributes are different for each of the action methods:

[Route("api/[controller]")] [ApiController]
public class CarController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetCarsById(int id)
{

}
[HttpPost("{id}")]
public IActionResult CreateANewCar(int id, Car entity)
{

}
[HttpPut("{id}")]
public IActionResult UpdateAnExistingCar(int id, Car entity)
{

}

[HttpDelete("{id}")]
public IActionResult DeleteACar(int id, Car entity)
{

}
}

If an action method doesn’t have an HTTP verb attribute, it is treated as the application endpoint for HTTP get requests. Just as with MVC style applications, if the route requested is matched but there isn’t an action method with the correct verb attribute, the server will return a 404 (not found).

■Note ASP.NET Web API allowed you to omit the hTTP verb for a method if the name started with Get, Put, Delete, or Post. This convention has been removed in ASP.NET Core. If an action method does not have an hTTP verb specified, it will be called using an hTTP get.

The final endpoint selector for API controllers is the optional Consumes attribute, which specifies the content type that is accepted by the endpoint. The request must use the matching content-type header, or a 415 Unsupported Media Type error will be returned. The following two example endpoints, both in the same controller, differentiate between JSON and XML:

[HttpPost] [Consumes("application/json")]
public IActionResult PostJson(IEnumerable values)
=> Ok(new { Consumes = "application/json", Values = values });
[HttpPost]
[Consumes("application/x-www-form-urlencoded")]
public IActionResult PostForm([FromForm] IEnumerable values)
=> Ok(new { Consumes = "application/x-www-form-urlencoded", Values = values });

■Note There is one additional (and optional) route selector for API controllers, and that involves versioning. Versioning API applications is covered in Chapter 32.

HTTP Verbs in Razor Page Routing
Once a Razor page is discovered as the endpoint for a route, the HTTP verb is used to determine the correct page handler method to execute. The following code sample for the Delete.cshtml page will execute the OnGet() method on get requests and the OnPost() method on post requests.

public class DeleteModel : PageModel
{
public IActionResult OnGet(int? id)
{
//handle the get request here
}

public IActionResult OnPost(int? id)
{
//handle the post request here
}
}

This will be covered in more depth in the next chapter.

Redirecting Using Routing
Another advantage of routing is that you no longer have to hard-code URLs for other pages in your site. The routing entries are used to match incoming requests as well as build URLs. When building URLs, the scheme, host, and port are added based on the values of the current request.
When redirecting in server side code (e.g., in a controller’s action method or in a Razor page), there are several redirect methods that can be used to redirect the execution path to another end point. Table 30-6 shows three of the redirect methods and their most commonly used overloads.

Table 30-6. Methods for Server-Side Redirecting of Requests

Method Meaning in Life
RedirectToAction() Redirects to an action. Overloaded parameter options include: actionName, controllerName, routeValues
If any of the parameters are not supplied, the values will be supplied by the current HTTP request.
RedirectToRoute() Redirects to a named route. Optional route values can be supplied.
RedirectToPage() Redirects to a Razor Page. Optional route values can be supplied.

For an example, the following code redirects the request from the Delete method to the Index method in the same controller (since a controller name wasn’t provided):

[HttpPost("{id}")]
public async Task Delete(int id, Car car)
{
//interesting code here
return RedirectToAction(nameof(Index));
}

Model Binding
Model binding is the process where ASP.NET Core uses the name-value pairs submitted in an HTTP Post call to assign values to models. The values are submitted using form fields, request body (for API style controllers), route data, query string parameters, or uploaded files. To bind to a reference type, the name- value pairs come from the form values or the request body, the reference types must have a public default constructor, and the properties to be bound must be public and writable.
When assigning values, implicit type conversions (such as setting a string property value using an int) are used where applicable. If type conversion doesn’t succeed, that property is flagged in error. Before discussing binding in greater detail, it’s important to understand the ModelState dictionary and its role in the binding (and validation) process.

The ModelState Dictionary
The ModelState dictionary contains an entry for every property being bound and an entry for the model itself. If an error occurs during model binding, the binding engine adds the errors to the dictionary entry for the property and sets ModelState.IsValid = false. If all matched properties are successfully assigned, the binding engine sets ModelState.IsValid = true.

■Note Model validation, which also sets the ModelState dictionary entries, happens after model binding. Both implicit and explicit model binding automatically call validation for the model. Validation is covered shortly.

How you handle the binding and/or validation errors varies based on the needs of your application. The following code sample from an API endpoint shows how to get all of the errors from the ModelState, create an anonymous object, and then return a BadRequestObject (HTTP status code 400) with the resulting object sent back in the body of the response as JSON:

[HttpPost] [ValidateAntiForgeryToken]
public async Task Update(Car entity)
{
if (!ModelState.IsValid)
{
IEnumerable errorList = ModelState.Values.SelectMany(v => v.Errors).Select(e=>e. ErrorMessage);
var responseContent =
new { Message = "One or more field validation errors occurred", Errors = errorList }; apiLogEntry.ResponseContentBody = JsonSerializer.Serialize(responseContent);
return new BadRequestObjectResult(responseContent);
}
//binding and validation was successful, execute the rest of the method
}

Adding Custom Errors to the ModelState Dictionary
In addition to the properties and errors added by the binding and validation engines, custom errors can be added to the ModelState dictionary. Errors can be added at the property level or for the entire model.
To add a specific error for a property (e.g., the PetName property of the Car entity), use the following:

ModelState.AddModelError("PetName","Name is required");

To add an error for the entire model, use string.Empty for the property name, like this:

ModelState.AddModelError(string.Empty, $"Unable to create record: {ex.Message}");

Clearing the ModelState Dictionary
There are times where you might need to clear the ModelState of all values and errors. To reset the ModelState, simply call the Clear() method, like this:

ModelState.Clear();

This is commonly used with explicit validation or when required properties are intentionally left out because of over posting concerns.

Implicit Model Binding
Implicit model binding occurs when the model to be bound is a parameter for an action method (MVC/API applications) or a handler method (Razor page applications). It uses reflection (and recursion for complex types) to match the model’s writable property names with the names contained in the name-value pairs posted to the action method. If there is a name match, the binder uses the value from the name-value pair to attempt to set the property value. If multiple names from the name-value pairs match, the first matching name’s value is used. If a property isn’t found in the name-value pairs, the property is set to its default value. The order the name-value pairs are searched is as follows:
•Form values from an HTTP Post method (including JavaScript AJAX posts)
•Request body (for API controllers)
•Route values provided through ASP.NET Core routing (for simple types)
•Query string values (for simple types)
•Uploaded files (for IFormFile types)
For example, the following method will attempt to set all of the properties on the Car type implicitly. If the binding process completes without error, the ModelState.IsValid property returns true.

[HttpPost] [ValidateAntiForgeryToken]
public ActionResult Create(Car entity)
{
if (ModelState.IsValid)
{
//Save the data;
}
}

Here an example OnPostAsync() method in a Razor page PageModel class that takes in an optional integer (from the route or query string) and an implicitly bound Car entity:

public async Task OnPostAsync(int? id, Car entity)
{
if (ModelState.IsValid)
{
//Save the data;
}
}

Explicit Model Binding
Explicit model binding is executed with a call to TryUpdateModelAsync(), passing in an instance of the type being bound and the list of properties to bind. The method then uses reflection to find matches between the property names and the names in the name-value pairs in the request. If the model binding fails, the method returns false and sets the ModelState errors in the same manner as implicit model binding.

When using explicit model binding, the type being bound isn’t a parameter of the action method (MVC/ API) or handler method (Razor pages). For example, you could write the previous Create() method this way and use explicit model binding:

[HttpPost] [ValidateAntiForgeryToken]
public async Task Create()
{
var newCar = new Car();
if (await TryUpdateModelAsync(newCar ,"", c=>c.Color,c=>c.PetName,c=>c.MakeId))
{
//do something important
}
}

Explicit model binding also works in Razor page handler methods:

public async Task OnPostAsync()
{
var newCar = new Car();
if (await TryUpdateModelAsync(newCar ,"", c=>c.Color,c=>c.PetName,c=>c.MakeId))
{
//do something important
}
}

■Note The second parameter (set to an empty string in these examples) will be covered shortly in the handling Property Name Prefixes section.

With implicit model binding, an instance is getting created for you. However, with explicit model binding, you must first create the instance then call TryUpdateModelAsync(), which attempts to update the values of that instance from the name value pairs sent in the request. Like explicit model binding, the binding engine ignores any properties without a matching name in the name-value pairs in the request.
Since you have to create the instance first with explicit model binding, you can set properties on your instance before calling TryUpdateModelAsync(), like this:

var newCar = new Car
{
Color = "Purple", MakeId = 1, PetName = "Prince"
};
if (await TryUpdateModelAsync(newCar,"", c=>c.Color,c=>c.PetName,c=>c.MakeId))
{
//do something important
}

In the previous example, any of the values set with the initial object initialization will retain their values if the property does not have a matching name in the name-value pairs in the request.

Property Model Binding
A property on a MVC style controller or a Razor page PageModel can be marked as the binding target for HTTP Post requests. This is accomplished by adding the public property to the class and marking it with the BindProperty attribute. When using property binding, the controller’s action methods or PageModel handler methods do not take the property as a parameter. Here are two examples that uses property binding on a Car property for an MVC style application and a Razor page PageModel:

//CarsController – MVC
public class CarsController : Controller
{
[BindProperty] public Car Entity { get; set; }

[HttpPost("{id}")] [ValidateAntiForgeryToken] [ActionName("Edit")]
public async Task Edit(int id)
{
//Handle the post request
} //omitted for brevity
}

//EditPage — Razor
public class EditModel : PageModel
{
[BindProperty] public Car Entity { get; set; }

public async Task OnGet(int? id)
{
//Handle the HTTP Get request
}
public async Task OnPost(int id)
{
//Handle the HTTP Get request
}
}

By default, property binding only works with HTTP Post requests. If you need HTTP Get requests to also bind to the property, update the BindProperty attribute as follows:

[BindProperty(Name="car",SupportsGet = true)] public Car Entity { get; set; }

Handling Property Name Prefixes
Sometimes the data will come into your action method from a table, a parent-child construct, or a complex object that adds a prefix to the names in the name-value pairs. Recall that both implicit and explicit model binding uses reflection to match property names with names in the name-value pairs of the request, and prefixes to the names will prevent the matches from being made.

With implicit model binding, the Bind attribute is used to specify a prefix for the property names. The following example sets a prefix for the names:

[HttpPost] [ValidateAntiForgeryToken]
public ActionResult Create([Bind(Prefix="CarList")]Car car)
{
if (ModelState.IsValid)
{
//Save the data;
}
//handle the binding errors
}

With explicit model binding, the prefix is set in the TryUpdateModelAsync() method using the second parameter (which was just an empty string in the previous examples):

[HttpPost] [ValidateAntiForgeryToken]
public async Task Create()
{
var newCar = new Car();
if (await TryUpdateModelAsync(newCar,"CarList", c=>c.Color,c=>c.PetName,c=>c.MakeId))
{
//Save the data
}
//handle the binding errors
}

Preventing Over Posting
Over posting is when the request submits more values than you are expecting (or wanting). This can be accidental (the developer left too many fields in the form), or malicious (someone used browser dev tools to modify the form before submitting it). For example, presume you want the application to allow changing colors, names, or makes, but not the prices on Car records.
The Bind attribute in HTTP Post methods allows you to limit the properties that participate in implicit model binding. If a Bind attribute is placed on a reference parameter, the fields listed in the Include list are the only fields that will be assigned through model binding. If the Bind attribute is not used, all fields are bindable.
The following example uses the Bind attribute to only allow updating the PetName and Color fields in the Update() method:

[HttpPost] [ValidateAntiForgeryToken]
public ActionResult Update([Bind(nameof(Car.PetName),nameof(Car.Color))]Car car)
{
//body omitted for brevity
}

Here is the same example in a Razor page:

public async Task OnPostAsync(int? id, [Bind(nameof(Car.PetName),nameof(Car. Color))]Car car)
{
//body omitted for brevity
}

To prevent over posting when using explicit model binding, you remove the properties in the body of the TryUpdateModel() method’s body that shouldn’t be accepted. The following example only allows updating the PetName and Color fields in the Update() method:

[HttpPost] [ValidateAntiForgeryToken]
public async Task Update()
{
var newCar = new Car();
if (await TryUpdateModelAsync(newCar,"", c=>c.Color,c=>c.PetName))
{
//save the data
}
//Handle the binding errors
}

When using this method of implicit method of model binding, the Bind attribute can be used to prevent over posting:

public async Task OnPostAsync(int? id, [Bind(nameof(Car.PetName),nameof(Car. Color))]Car car)
{
//body omitted for brevity
}

You can also specify the binding source in Razor pages. The following instructs the binding engine to get the data from the request’s Form data:

public async Task OnPostAsync(int? id, [FromForm]Car car)
{
//body omitted for brevity
}

Controlling Model Binding Sources in ASP.NET Core
Binding sources can be controlled through a set of attributes on the action parameters. Custom model binders can also be created; however, that is beyond the scope of this book. Table 30-7 lists the attributes that can be used to control model binding.

Table 30-7. Controlling Model Binding Sources

Attribute Meaning in Life
BindingRequired A model state error will be added if binding cannot occur instead of just setting the property to its default value.
BindNever Tells the model binder to never bind to this parameter.
FromHeaderFromQuery FromRouteFromForm Used to specify the exact binding source to apply (header, query string, route parameters, or form values).
FromServices Binds the type using dependency injection (covered later in this chapter).
FromBody Binds data from the request body. The formatter is selected based on the content of the request (e.g., JSON, XML, etc.). There can be at most one parameter decorated with the FromBody attribute.
ModelBinder Used to override the default model binder (for custom model binding).

Here are two examples that show using the FromForm attribute. The first is in a RESTful service controller, and the second is from a Razor page handler method:

//API ActionMethod [HttpPost]
public ActionResult Create([FromForm] Car entity)
{
//body omitted for brevity
}

//Razor page
public async Task OnPostAsync(int? id, [FromForm]Car car)
{
//body omitted for brevity
}

Model Validation
Model validation occurs immediately after model binding (both explicit and implicit). While model binding adds errors to the ModelState data dictionary due to conversion issues, validation failures add errors to the ModelState data dictionary due to broken validation rules. Examples of validation rules include required fields, strings that have a maximum allowed length, properly formatted phone numbers, or dates being within a certain allowed range.
Validation rules are set through validation attributes, either built-in or custom. Table 30-8 lists some of the built-in validation attributes. Note that several also double as data annotations for shaping the EF Core entities.

Table 30-8. Some of the Built-in Validation Attributes

Attribute Meaning in Life
CreditCard Performs a Luhn-10 check on the credit card number
Compare Validates the two properties in a model match
EmailAddress Validates the property has a valid email format
Phone Validates the property has a valid phone number format
Range Validates the property falls within a specified range
RegularExpression Validates the property matches a specified regular expression
Required Validates the property has a value
StringLength Validates the property doesn’t exceed a maximum length
Url Validates the property has a valid URL format
Remote Validates input on the client by calling an action method on the server

Custom validation attributes can also be developed and are covered later in this book.

Explicit Model Validation
Explicit model validation is executed with a call to TryValidateModel(), passing in an instance of the type being validated. The result of this call is a populated ModelState instance with any invalid properties. Valid properties do not get written to the ModelState object like they do with the binding/validation combination.
Take the following code as an example. This method uses explicit binding, leaving out the Color property. Since the color property is required, the ModelState reports as invalid. The Color is updated, explicit validation is called, and the ModelState is still invalid:

[HttpPost] [ValidateAntiForgeryToken]
public async Task CreateCar()
{
var newCar = new Car();
if (await TryUpdateModelAsync(newCar, "",c => c.PetName, c => c.MakeId))
{
//car is bound and valid – save it
}

var isValid = ModelState.IsValid; //false newCar.Color = "Purple"; TryValidateModel(newCar);
isValid = ModelState.IsValid; //still false
//rest of the method
}
}

In order for an object that has been through a validation pass to be revalidated, the ModelState must be cleared, as in the following update to the method:

[HttpPost] [ValidateAntiForgeryToken]
public async Task CreateCar()
{
var newCar = new Car();
if (await TryUpdateModelAsync(newCar, "",c => c.PetName, c => c.MakeId))
{
//car is bound and valid – save it
}

var isValid = ModelState.IsValid; //false newCar.Color = "Purple"; TryValidateModel(newCar);
isValid = ModelState.IsValid; //still false ModelState.Clear(); TryValidateModel(newCar);
isValid = ModelState.IsValid; //true
//rest of the method
}
}

Do know that calling Clear() clears out all ModelState data, including the binding information.

Filters
Filters in ASP.NET Core run code before or after specific stages of the request processing pipeline. There are built-in filters for authorization and caching, as well as options for assigning customer filters. Table 30-9 lists the types of filters that can be added into the pipeline, listed in their order of execution.

Table 30-9. Filters Available in ASP.NET Core

Filter Meaning in Life
Authorization filters Run first and determine if the user is authorized for the current request.
Resource filters Run immediately after the authorization filter and can run after the rest of the pipeline has completed. Run before model binding.
Action filters Run immediately before an action is executed and/or immediately after an action is executed. Can alter values passed into an action and the result returned from an action. Applies to MVC style web applications and RESTful services.
Page Filters Can be built to run code after a handler method has been selected but before model binding, after the handler method executes after model binding is complete or immediately after the handler executed. Similar to Action filters but apply to Razor Pages.
Exception filters Used to apply global policies to unhandled exceptions that occur before writing to the response body.
Result filters Run code immediately after the successful execution of action results. Useful for logic that surrounds view or formatter execution.

Authorization Filters
Authorization filters work with the ASP.NET Core Identity system to prevent access to controllers or actions that the user doesn’t have permission to use. It’s not recommended to build custom authorization filters since the built-in AuthorizeAttribute and AllowAnonymousAttribute usually provide enough coverage when using ASP.NET Core Identity.

Resource Filters
Resource filters have two methods. The OnResourceExecuting() method executes after authorization filters and prior to any other filters, and the OnResourceExecuted() method executes after all other filters. This enables resource filters to short-circuit the entire response pipeline. A common use for resource filters is for caching. If the response is in the cache, the filter can skip the rest of the pipeline.

Action Filters
The OnActionExecuting() method executes immediately before the execution of the action method, and the OnActionExecuted() method executes immediately after the execution of the action method. Action filters can short-circuit the action method and any filters that are wrapped by the action filter (order of execution and wrapping are covered shortly).

Page Filters
The OnHandlerSelected() method executes after a handler method has been selected but before model binding occurs. The OnPageHandlerExecuting() method executes after model binding is complete and the OnPageHandlerExecuted() method executes after the handler method executes.

Exception Filters
Exception filters enable implementation of consistent error handling in an application. The OnException() method executes when unhandled exceptions are thrown in controller creation, model binding, action filters, or action methods, page filters, or page handler methods.

Result Filters
Result filters wrap the execution of the IActionResult for an action method. A common scenario is to add header information into the HTTP response message using a result filter. The OnResultExecuting() method executes before the response and OnResultExecuted() executes after the response has started.

Summary
This chapter introduced ASP.NET Core and is the first of a set of chapters covering ASP.NET Core. This chapter began with a brief look back at the history of ASP.NET and then looked at the features from classic ASP.NET MVC and ASP.NET Web API that also exist in ASP.NET Core.
The next section created the solution and the projects, updated the ports, and examined running and debugging ASP.NET Core web applications and services.
In the next chapter, you will dive into many of the new features added in ASP.NET Core.

发表评论