分类目录归档:C#

Pro C#10 CHAPTER 31 Diving Into ASP.NET Core

CHAPTER 31

Diving Into ASP.NET Core

This chapter takes a deep look into the new features in ASP.NET Core. As you learn about the features, you will add them to the projects created in the previous chapter, Introducing ASP.NET Core.

What’s New in ASP.NET Core
In addition to supporting the base functionality of ASP.NET MVC and ASP.NET Web API, Microsoft has added a host of new features and improvements over the previous frameworks. In addition to the unification of frameworks and controllers, a new style of web applications is now supported using Razor pages. Listed here are some additional improvements and innovations:
•Razor Page based web applications
•Environment awareness
•Minimal templates with top level statements
•Cloud-ready, environment-based configuration system
•Built-in dependency injection
•The Options pattern
•The HTTP Client Factory
•Flexible development and deployment models
•Lightweight, high-performance, and modular HTTP request pipeline
•Tag helpers (covered in a later chapter)
•View components (covered in a later chapter)
•Vast improvements in performance
•Integrated logging

Razor Pages
Another option for creating web applications with ASP.NET Core is using Razor pages. Instead of using the MVC pattern, Razor page applications are (as the name suggests) page-centric. Each Razor page consists of two files, the page file (e.g. Index.cshtml) is the view, and the PageModel C# class (e.g. Index.cshtml.cs), which serves as the code behind file for the page file.

© 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_31

1355

■Note Razor page based web applications also support partial and layout views, which will be covered in detail in later chapters.

The Razor Page File
Just like MVC based web applications, the Razor page file is responsible for rendering the content of the page, and receiving input from the user. It should be lightweight, and hand off work to the PageModel class. Razor page files will be explored in depth in Chapter 34.

The PageModel Class
Like the Controller class for MVC style applications, the PageModel class provides helper methods for Razor page based web applications. Razor pages derive from the PageModel class, and are typically named with the Model suffix, like CreateModel. The Model suffix is dropped when routing to a page. Table 31-1 lists the most commonly used methods and Table 31-2 lists the HTTP Status Code Helpers.

Table 31-1. Some of the Helper Methods Provided by the Controller 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.
ViewData TempData Provide data to the view through the ViewDataDictionary and
TempDataDictionary
Page Returns a PageResult (derived from ActionResult) as the HTTP response.
PartialView Returns a PartialViewResult to the response pipeline.
ViewComponent Returns a ViewComponentResult to the response pipeline.
OnPageHandlerSelected Executes when a page handler method is selected but before model binding.
OnPageHandlerExecuting Executes before a page handler method executes.
OnPageHandlerExecutionAsync Async version of OnPageHandlerExecuting.
OnPageHandlerExecuted Executes after a page handler method executes.
(continued)

Table 31-1. (continued)

Helper Method Meaning in Life
OnPageHandlerSelectionAsync Async version of OnPageHandlerSelected.
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.
RedirectToAction RedirectToPage RedirectToRoute 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.
TryValidateModel Used for explicit model validation.

Table 31-2. Some of the HTTP Status Code Helper Methods Provided by the PageModelClass

Helper Method HTTP Status Code Action Result Status Code
NotFound NotFoundResult 404
Forbid ForbidResult 403
BadRequest BadRequestResult 400
StatusCode(int)StatusCode(int, object) StatusCodeResultObjectResult Defined by the int parameter.

You might be surprised to see some familiar methods from the Controller class. Razor page based applications share many of the features of MVC style applications as you have seen and will continue to see throughout these chapters.

Page Handler Methods
As discussed in the routing section, Razor pages define handler methods to handle HTTP Get and Post requests. The PageModel class supports both synchronous and asynchronous handler methods. The verb handled is based on the name of the method, with OnPost()/OnPostAsync() handling HTTP post requests, and OnGet()/OnGetAsync() handling HTTP get requests. The async versions are listed here:

public class DeleteModel : PageModel
{
public async Task OnGetAsync(int? id)
{
//handle the get request here
}

public async Task OnPostAsync(int? id)
{
//handle the post request here
}
}

The names of the handler methods can be changed, and multiple handler methods for each HTTP verb can exist, however overloaded versions with the same name are not permitted. This will be covered in Chapter 34.

Environmental Awareness
ASP.NET Core applications’ awareness of their execution environment includes host environment variables and file locations through an instance of IWebHostEnvironment, which implement the IHostEnvironment interface. Table 31-3 shows the properties available through these interfaces.

Table 31-3. The IWebHostEnvironment Properties

Property Functionality Provided
ApplicationName Gets or sets the name of the application. Defaults to the name of the entry assembly.
ContentRootPath Gets or sets the absolute path to the directory that contains the application content files.
ContentRootFileProvider Gets or sets an IFileProvider pointing to the ContentRootPath.
EnvironmentName Gets or sets the name of the environment. Sets to the value of the ASPNETCORE_ ENVIRONMENT environment variable.
WebRootFileProvider Gets or sets an IFileProvider pointing at the WebRootPath.
WebRootPath Gets or sets the absolute path to the directory that contains the web-servable application content files.

In addition to accessing the relevant file paths, IWebHostEnvironment is used to determine the runtime environment.

Determining the Runtime Environment
ASP.NET Core automatically reads the value of the environment variable named ASPNETCORE_ENVIRONMENT
to set the runtime environment. If the ASPNETCORE_ENVIRONMENT variable is not set, ASP.NET Core sets the value to Production. The value set is accessible through the EnvironmentName property on the IWebHostEnvironment.
While developing ASP.NET Core applications, this variable is typically set using the launchSettings. json file or the command line. Downstream environments (staging, production, etc.) typically use standard operating system environment variables.
You are free to use any name for the environment or the three that are supplied by the Environments
static class.

public static class Environments
{
public static readonly string Development = "Development"; public static readonly string Staging = "Staging";
public static readonly string Production = "Production";
}

The HostEnvironmentEnvExtensions class provides extensions methods on the IHostEnvironment for working with the environment name property. Table 31-4 lists the convenience methods available.

Table 31-4. The HostEnvironmentEnvExtensions Methods

Method Functionality Provided
IsProduction Returns true if the environment variable is set to Production (case insensitive)
IsStaging Returns true if the environment variable is set to Staging (case insensitive)
IsDevelopment Returns true if the environment variable is set to Development (case insensitive)
IsEnvironment Returns true if the environment variable matches the string passed into the method (case insensitive)

These are some examples of using the environment setting:
•Determining which configuration files to load
•Setting debugging, error, and logging options
•Loading environment-specific JavaScript and CSS files
Examine the Program.cs file in the AutoLot.Mvc project. Near the end of the file, the environment is checked to determine if the standard exception handler and HSTS (HTTP Strict Transport Security Protocol) should be used:

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error"); app.UseHsts();
}

Update this block to flip it around:

if (app.Environment.IsDevelopment())
{
}
else
{
app.UseExceptionHandler("/Home/Error"); app.UseHsts();
}

Next, add the developer exception page into the pipeline when the app is running development. This provides debugging details like the stack trace, detailed exception information, etc. The standard exception handler renders a simple error page.

if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error"); app.UseHsts();
}

Update the AutoLot.Web project block to the following (notice the different route for the error handle in the Razor page based application):

if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error"); app.UseHsts();
}

The AutoLot.Api project is a little different. It is checking for the development environment, and if it is running in development, Swagger and SwaggerUI are added into the pipeline. For this app, we are going to move the Swagger code out of the if block so it will always be available (leave the if block there, as it will be used later in this chapter):

if (app.Environment.IsDevelopment())
{
//more code to be placed here later
}
app.UseSwagger(); app.UseSwaggerUI();

Since there isn’t a UI associated with RESTful services, there isn’t any need for the developer exception page. Swagger will be covered in depth in the next chapter.

■Note Whether the Swagger pages are available outside of the development environment is a business decision. We are moving the Swagger code to run in all environments so that the Swagger page is always available as you work through this book. Swagger will be covered in depth in the next chapter.

You will see many more uses for environment awareness as you build the ASP.NET Core applications in this book.

The WebAppBuilder and WebApp
Unlike classic ASP.NET MVC or ASP.NET Web API applications, ASP.NET Core applications are .NET console applications that create and configure a WebApplication, which is an instance of IHost. The creation of configuration of the IHost is what sets the application up to listen and respond to HTTP requests.
The default templates for ASP.NET Core 6 MVC, Razor, and Service application are minimal. These files will be added to as you progress through the ASP.NET Core chapters.

■Note prior to .net 6 and C# 10, a WebHost was created in the Main() method of the Program.cs file and configured for your application in the Startup.cs file. With the release of .net 6 and C# 10, the aSp.net Core template uses top level statements in the Program.cs file for creation and configuration and doesn’t have a Startup.cs file.

The Program.cs File with RESTful Services
Open the Program.cs class in the AutoLot.Api application, and examine the contents, shown here for reference:

var builder = WebApplication.CreateBuilder(args);
// Add services to the container. builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); var app = builder.Build();
// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment())
{
//more code to be placed here later
}
app.UseSwagger(); app.UseSwaggerUI();

app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers();

app.Run();

If you are coming from a previous version of ASP.NET Core, the preceding code was split between the Program.cs file and the Startup.cs file. With ASP.NET Core 6, those file are combined into top level statements in the Program.cs file. The code before the call to builder.Build() was contained in the
Program.cs file and the ConfigureServices() method in the Startup.cs file and is responsible for creating

the WebApplicationBuilder and registering services in the dependency injection container. The code including the builder.Build() call and the rest of the code in the file was contained in the Configure() method and is responsible for the configuration of the HTTP pipeline.
The CreateBuilder() method compacts the most typical application setup into one method call. It configures the app (using environment variables and appsettings JSON files), it configures the default logging provider, and it sets up the dependency injection container. The returned WebApplicationBuilder is used to register services, add additional configuration information, logging support, etc.
The next set of methods adds the necessary base services into the dependency injection container for building RESTful services. The AddControllers() method adds in support for using controllers and action methods, the AddEndpointsApiExplorer() method provides information about the API (and is used by Swagger), and AddSwaggerGen() creates the basic OpenAPI support.

■Note When adding services into the Dependency injection container, make sure to add them into the top level statements using the comment //Add services to the container.they must be added before the builder.Build() method is called.

The builder.Build() method generates the WebApplication and sets up the next group of method calls to configure the HTTP pipeline. The Environment section was discussed previously. The next set of calls ensures all requests use HTTPS, enables the authorization middleware, and maps the controllers to their endpoints. Finally, the Run() method starts the application and gets everything ready to receive web requests and respond to them.

The Program.cs File with MVC Style Applications
Open the Program.cs class in the AutoLot.Mvc application, and examine the contents, shown here for reference (with differences from the AutoLot.Api application’s file in bold):

var builder = WebApplication.CreateBuilder(args);

// Add services to the container. builder.Services.AddControllersWithViews(); var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error"); app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

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

app.Run();

The first difference is the call to AddControllersWithViews(). ASP.NET Core RESTful services use the same controller/action method pattern as MVC style applications, just without views. This adds support for views into the application. The calls for Swagger and the API Explorer are omitted, as they are used by API service.
The next difference is the call to UserExceptionHandler() when the application is not running in a development environment. This is a user friendly exception handler that displays simple information
(and no technical debugging data). That is followed by the UseHsts() method, which turns on HTTP Strict Transport Security, preventing users to switch back to HTTP once they have connected. It also prevents them from clicking through warnings regarding invalid certificates.
The call to UseStaticFiles() enables static content (images, JavaScript files, CSS files, etc.) to be rendered through the application. This call is not in the API style applications, as they don’t typically have a need to render static content. The final changes add end point routing support and the default route to the application.

The Program.cs File with Razor Page Based Applications
Open the Program.cs class in the AutoLot.Web application, and examine the contents, shown here for reference (with the differences from the AutoLot.Mvc application shown in bold):

var builder = WebApplication.CreateBuilder(args);
// Add services to the container.

builder.Services.AddRazorPages();
var app = builder.Build();

// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error"); app.UseHsts();
}
app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.Run();

There are only three differences between this file and the initial file for the AutoLot.Mvc application. Instead of adding support for controllers with views, there is a call to AddRazorPages() to add support for Razor pages, add routing for razor pages with the call to the MapRazorPages() method, and there isn’t a default route configured.

Application Configuration
Previous versions of ASP.NET used the web.config file to configure services and applications, and developers accessed the configuration settings through the System.Configuration class. Of course, all configuration settings for the site, not just application-specific settings, were dumped into the web.config file making it a (potentially) complicated mess.
ASP.NET Core leverages the new .NET JSON-based configuration system first introduced in Chapter 16.
As a reminder, it’s based on simple JSON files that hold configuration settings. The default file for configuration is the appsettings.json file. The initial version of appsettings.json file (created by the ASP. NET Core web application and API service templates) simply contains configuration information for the logging, as well as allowing all hosts (e.g. https://localhost:xxxx) to bind to the app:

{
"Logging": {
"LogLevel": {
"Default": "Information", "Microsoft.AspNetCore": "Warning",
}
},
"AllowedHosts": "*"
}

The template also creates an appsettings.Development.json file. The configuration system works in conjunction with the runtime environment awareness to load additional configuration files based
on the runtime environment. This is accomplished by instructing the configuration system to load a file named appsettings.{environmentname}.json after the appSettings.json file. When running under Development, the appsettings.Development.json file is loaded after the initial settings file. If the environment is Staging, the appsettings.Staging.json file is loaded, etc. It is important to note that when more than one file is loaded, any settings that appear in both files are overwritten by the last file loaded; they are not additive.
For each of the web application projects, add the following connection string information (updating the actual connection string to match your environment from Chapter 23 into the appsettings.Development. json files:

"ConnectionStrings": {
"AutoLot": "Server=.,5433;Database=AutoLot;User ID=sa;Password=P@ssw0rd;"
}

■Note each item in the JSon must be comma delimited. When you add in the ConnectionStrings item, make sure to add a comma after the curly brace that proceeds the item you are adding.

Next, copy each of the appsettings.Development.json files to a new file named appsettings.
Production.json into each of the web application projects. Update the connection string entries to the following:

"ConnectionStrings": { "AutoLot": "It’s a secret"
},

This shows part of the power of the new configuration system. The developers don’t have access to the secret production information (like connection strings), just the non-secret information, yet everything can still be checked into source control.

■Note in production scenarios using this pattern, the secrets are typically tokenized. the build and release process replaces the tokens with the production information.

All configuration values are accessible through an instance of IConfiguration, which is available through the ASP.NET Core dependency injection system. In the top level statements prior to the web application being built, the configuration is available from the WebApplicationBuilder like this:

var config = builder.Configuration;

After the web application is built, the IConfiguration instance is available from the WebApplication
instance:

var config = app.Configuration;

Settings can be accessed using the traditional methods covered in Chapter 16. There is also a shortcut for getting application connection strings.

config.GetConnectionString("AutoLot")

Additional configuration features, including the Options pattern, will be discussed later in this chapter.

Built-in Dependency Injection
Dependency injection (DI) is a mechanism to support loose coupling between objects. Instead of directly creating dependent objects or passing specific implementations into classes and/or methods, parameters are defined as interfaces. That way, any implementation of the interface can be passed into the classes or methods and classes, dramatically increasing the flexibility of the application.
DI support is one of the main tenets in the rewrite ASP.NET Core. Not only do the top level statements in the Program.cs file (covered later in this chapter) accept all the configuration and middleware services through dependency injection, your custom classes can (and should) also be added to the service container to be injected into other parts of the application. When an item is configured into the ASP.NET Core DI container, there are three lifetime options, as shown in Table 31-5.

Table 31-5. Lifetime Options for Services

Lifetime Option Functionality Provided
Transient Created each time they are needed.
Scoped Created once for each request. Recommended for Entity Framework DbContext objects.
Singleton Created once on first request and then reused for the lifetime of the object. This is the recommended approach versus implementing your class as a Singleton.

■Note if you want to use a different dependency injection container, aSp.net Core was designed with that flexibility in mind. Consult the docs to learn how to plug in a different container: https://docs.microsoft. com/en-us/aspnet/core/fundamentals/dependency-injection.

Services are added into the DI container by adding them to the IServiceCollection for the application.
When using the top level statements template in .NET 6 web applications, the IServiceCollection is instantiated by the WebApplicationBuilder, and this instance is used to add services into the container.
When adding services to the DI container, make sure to add them before the line that builds the
WebApplication object:

var app = builder.Build();

■Note in pre-.net 6 web applications, the startup process involved both a Program.cs class and another class named Startup.cs (by convention). in .net 6, all of the configuration is done in top level statements in the Program.cs file. this will be covered in the section Configuring the Web application.

Adding Web App Support To The Dependency Injection Container
When creating MVC based web applications, the AddControllersWithView() method adds in the necessary services to support the MVC pattern in ASP.NET Core. The following code (in the Program.cs file of the AutoLot.Mvc project) accesses the IServiceCollection of the WebApplicationBuilder and adds the required DI support for controllers and views:

var builder = WebApplication.CreateBuilder(args);
// Add services to the container. builder.Services.AddControllersWithViews();

RESTful service web applications don’t use views but do need controller support, so they use the AddControllers() method. The API template for ASP.NET Core also adds in support for Swagger (a .NET implementation of OpenAPI) and the ASP.NET Core end point explorer:

var builder = WebApplication.CreateBuilder(args);
// Add services to the container. builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen();

■Note Swagger and openapi will be covered in the next chapter.

Finally, Razor page based applications must enable Razor page support with the
AddRazorPages() method:

var builder = WebApplication.CreateBuilder(args);
// Add services to the container. builder.Services.AddRazorPages();

Adding Derived DbContext Classes into the DI Container
Registering a derived DbContext class in an ASP.NET Core application allows the DI container to handle the initialization and lifetime of the context. The AddDbContext<>()/AddDbContextPool() methods add a properly configured context class into the DI container. The AddDbContextPool() version creates a pool of instances that are cleaned between requests, ensuring that there isn’t any data contamination between
requests. This can improve performance in your application, as it eliminates the startup cost for creating a context once it is added to the pool.
Start by adding the following global using statements into the GlobalUsings.cs for the AutoLot.Api, AutoLot.Mvc, AutoLot.Web projects:

global using AutoLot.Dal.EfStructures; global using AutoLot.Dal.Initialization; global using Microsoft.EntityFrameworkCore;

The following code (which must be added into each of the Program.cs files in the web projects) gets the connection string from the JSON configuration file and adds the ApplicationDbContext as a pooled resource into the services collection:

var connectionString = builder.Configuration.GetConnectionString("AutoLot"); builder.Services.AddDbContextPool(
options => options.UseSqlServer(connectionString,
sqlOptions => sqlOptions.EnableRetryOnFailure().CommandTimeout(60)));

Now, whenever an instance of the ApplicationDbContext is needed, the dependency injection (DI) system will take care of the creation (or getting it from the pool) and recycling of the instance (or returning it to the pool).
EF Core 6 introduced a set of minimal APIs for adding derived DbContext classes into the services collection. Instead of the previous code, you can use the following short cut:

builder.Services.AddSqlServer(connectionString, options =>
{
options.EnableRetryOnFailure().CommandTimeout(60);
});

Note that the minimal API doesn’t have the same level of capabilities. Several features, like DbContext pooling are not supported. For more information on the minimal APIs, refer to the documentation located here: https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-6.0/ whatsnew#miscellaneous-improvements.

Adding Custom Services To The Dependency Injection Container
Application services (like the repositories in the AutoLot.Dal project) can be added into the DI container using one of the lifetime options from Table 31-5. For example, to add the CarRepo as a service into the DI container, you would use the following

services.AddScoped<ICarRepo, CarRepo>();

The previous example added a scoped service (AddScoped<>()) into the DI container specifying the service type (ICarRepo) and the concrete implementation (CarRepo) to inject. You could add all of the repos into all of the web applications in the Program.cs file directly, or you can create an extension method to encapsulate the calls. This process keeps the Program.cs file cleaner.
Before creating the extension method, update the GlobalUsings.cs file in the AutoLot.Services project to the following:

global using AutoLot.Dal.Repos; global using AutoLot.Dal.Repos.Base;
global using AutoLot.Dal.Repos.Interfaces;

global using AutoLot.Models.Entities; global using AutoLot.Models.Entities.Base;

global using Microsoft.AspNetCore.Builder;

global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging;

global using Serilog;
global using Serilog.Context;
global using Serilog.Core.Enrichers; global using Serilog.Events;
global using Serilog.Sinks.MSSqlServer;

global using System.Data;
global using System.Runtime.CompilerServices;

Next, create a new folder named DataServices in the AutoLot.Services project. In this folder, create a new public static class named DataServiceConfiguration. Update this class to the following:
namespace AutoLot.Services.DataServices; public static class DataServiceConfiguration
{
public static IServiceCollection AddRepositories(this IServiceCollection services)
{
services.AddScoped<ICarDriverRepo, CarDriverRepo>(); services.AddScoped<ICarRepo, CarRepo>(); services.AddScoped<ICreditRiskRepo, CreditRiskRepo>();
services.AddScoped<ICustomerOrderViewModelRepo, CustomerOrderViewModelRepo>();

services.AddScoped<ICustomerRepo, CustomerRepo>(); services.AddScoped<IDriverRepo, DriverRepo>(); services.AddScoped<IMakeRepo, MakeRepo>(); services.AddScoped<IOrderRepo, OrderRepo>(); services.AddScoped<IRadioRepo, RadioRepo>(); return services;
}
}

Next, add the following to the GlobalUsings.cs file in each web application (AutoLot.Api, AutoLot.Mvc, AutoLot.Web):

global using AutoLot.Services.DataServices;

Finally, add the following code to the top level statements in each of the web applications, making sure to add them above the line that builds the WebApplication:

builder.Services.AddRepositories();

Dependency Hierarchies
When there is a chain of dependencies, all dependencies must be added into the DI container, or a run- time error will occur when the DI container tries to instantiate the concrete class. If you recall from our repositories, they each had a public constructor that took an instance of the ApplicationDbContext, which was added into the DI container before adding in the repositories. If ApplicationDbContext was not in the DI container, then the repositories that depend on it can’t be constructed.

Injecting Dependencies
Services in the DI container can be injected into class constructors and methods, into Razor views, and Razor pages and PageModel classes. When injecting into the constructor of a controller or PageModel class, add the type to be injected into the constructor, like this:

//Controller
public class CarsController : Controller
{
private readonly ICarRep _repo; public CarsController(ICarRepo repo)
{
_repo = repo;
}
//omitted for brevity
}

//PageModel
public class CreateModel : PageModel
{
private readonly ICarRepo _repo;

public CreateModel(ICarRepo repo)
{
_repo = repo;
}
//omitted for brevity
}

Method injection is supported for action methods and page handler methods. To distinguish between a binding target and a service from the DI container, the FromServices attribute must be used:

//Controller
public class CarsController : Controller
{
[HttpPost] [ValidateAntiForgeryToken]
public async Task CreateAsync([FromServices]ICarRepo repo)
{
//do something
}
//omitted for brevity
}

//PageModel
public class CreateModel : PageModel
{
public async Task OnPostAsync([FromServices]ICarRepo repo)
{
//do somethng
}
//omitted for brevity
}

■Note You might be wondering when you should use constructor injection vs method injection, and the answer, of course, is “it depends”. i prefer to use constructor injection for services used throughout the class and method injection for more focused scenarios.

To inject into an MVC view or a Razor page view, use the @inject command:

@inject ICarRepo Repo

Getting Dependencies in Program.cs
You might be wondering how to get dependencies out of the DI container when you are in the top level statements in the Program.cs file. For example, if you want to initialize the database, you need an instance of the ApplicationDbContext. There isn’t a constructor, action method, page handler method, or view to inject the instance into.

In addition to the traditional DI techniques, services can also be retrieved directly from the application’s ServiceProvider. The WebApplication exposes the configured ServiceProvider through the Services property. To get a service, first create an instance of the IServiceScope. This provides a lifetime container
to hold the service. Then get the ServiceProvider from the IServiceScope, which will then provide the services within the current scope.
Suppose you want to selectively clear and reseed the database when running in the development environment. To set this up, first add the following line to the appsettings.Development.json files in each of the web projects:

"RebuildDatabase": true,

Next, add the following line to each of the appsettings.Production.json files in each of the web projects:

"RebuildDatabase": false,

In the development block of the if statement in Program.cs, if the configured value for RebuildDatabase is true, then create a new IServiceScope to the an instance of the ApplicationDbContext and use that to call the ClearAndReseedDatabase() method (example shown from the AutoLot.Mvc project):

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
//Initialize the database
if (app.Configuration.GetValue("RebuildDatabase"))
{
using var scope = app.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService(); SampleDataInitializer.ClearAndReseedDatabase(dbContext);
}
}

Make the exact same changes to the Program.cs file in the AutoLot.Web project. The Program.cs file update in the AutoLot.Api project is shown here:

if (app.Environment.IsDevelopment())
{
//Initialize the database
if (app.Configuration.GetValue("RebuildDatabase"))
{
using var scope = app.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService(); SampleDataInitializer.ClearAndReseedDatabase(dbContext);
}
}

Build the Shared Data Services
To wrap up the discussion on dependency injection, we are going to build a set of data services that will be used by both the AutoLot.Mvc and AutoLot.Web projects. The goal of the services is to present a single set of interfaces for all data access. There will be two concrete implementations of the interfaces, one that will call into the AutoLot.Api project and another one that will call into the AutoLot.Dal code directly. The concrete implementations that are added into the DI container will be determined by a project configuration setting.

The Interfaces
Start by creating a new directory named Interfaces in the DataServices directory of the AutoLot.Services project. Next, add the following IDataServiceBase interface into the Interfaces directory:

namespace AutoLot.Services.DataServices.Interfaces;

public interface IDataServiceBase where TEntity : BaseEntity, new()
{
Task<IEnumerable> GetAllAsync(); Task FindAsync(int id);
Task UpdateAsync(TEntity entity, bool persist = true); Task DeleteAsync(TEntity entity, bool persist = true); Task AddAsync(TEntity entity, bool persist = true);
//implemented ghost method since it won’t be used by the API data service void ResetChangeTracker() {}
}

The IMakeDataService interface simply implements the IDataServiceBase interface:

namespace AutoLot.Services.DataServices.Interfaces;

public interface IMakeDataService : IDataServiceBase
{
}

The ICarDataService interface implements the IDataServiceBase interface and adds a method to get Car records by Make Id:
namespace AutoLot.Services.DataServices.Interfaces; public interface ICarDataService : IDataServiceBase
{
Task<IEnumerable> GetAllByMakeIdAsync(int? makeId);
}

Add the following global using statement into the GlobalUsing.cs file:

global using AutoLot.Services.DataServices.Interfaces;

The AutoLot.Dal Direct Implementation
Start by creating a new directory named Dal in the DataServices directory, and add a new directory named Base in the Dal directory. Add a new class named DalServiceBase, make it public abstract and implement the IDataServiceBase interface:

namespace AutoLot.Services.DataServices.Dal.Base;

public abstract class DalDataServiceBase : IDataServiceBase where TEntity : BaseEntity, new()
{
//implementation goes here
}

Add a constructor that takes in an instance of the IBaseRepo and assign it to a class level variable:

protected readonly IBaseRepo MainRepo;
protected DalDataServiceBase(IBaseRepo mainRepo)
{
MainRepo = mainRepo;
}

Recall that all of the create, read, update, and delete (CRUD) methods on the base interface were defined as Task or Task. They are defined this way because calls to a RESTful service are asynchronous calls. Also recall that the repo methods that were built with the AutoLot.Dal are not async. The reason for that was mostly for teaching purposes, and not to introduce additional friction into learning EF Core. As the rest of the data services implementation is complete, you can either leave the repo methods synchronous (as they will be shown here) or refactor the repos to add in async versions of the methods.
The base methods call the related methods in the MainRepo:

public async Task<IEnumerable> GetAllAsync()
=> MainRepo.GetAllIgnoreQueryFilters();
public async Task FindAsync(int id) => MainRepo.Find(id);
public async Task UpdateAsync(TEntity entity, bool persist = true)
{
MainRepo.Update(entity, persist); return entity;
}
public async Task DeleteAsync(TEntity entity, bool persist = true)
=> MainRepo.Delete(entity, persist);
public async Task AddAsync(TEntity entity, bool persist = true)
{
MainRepo.Add(entity, persist); return entity;
}

The final method resets the ChangeTracker on the context, clearing it out for reuse:

public void ResetChangeTracker()
{
MainRepo.Context.ChangeTracker.Clear();
}

Add the following global using statements into the GlobalUsings.cs file:

global using AutoLot.Services.DataServices.Dal; global using AutoLot.Services.DataServices.Dal.Base;

Add a new class named MakeDalDataService.cs in the Dal directory and update it to match the following:

namespace AutoLot.Services.DataServices.Dal;

public class MakeDalDataService : DalDataServiceBase,IMakeDataService
{
public MakeDalDataService(IMakeRepo repo):base(repo) { }
}

Finally, add a class named CarDalDataService.cs and update it to match the following, which implements the one extra method from the interface:

namespace AutoLot.Services.DataServices.Dal;

public class CarDalDataService : DalDataServiceBase,ICarDataService
{
private readonly ICarRepo _repo;
public CarDalDataService(ICarRepo repo) : base(repo)
{
_repo = repo;
}
public async Task<IEnumerable> GetAllByMakeIdAsync(int? makeId) => makeId.HasValue
? _repo.GetAllBy(makeId.Value)
: MainRepo.GetAllIgnoreQueryFilters();
}

The API Initial Implementation
Most of the implementation for the API version of the data services will be completed after you create the HTTP client factory in the next section. This section will just create the classes to illustrate toggling
implementations for interfaces. Start by creating a new directory named Api in the DataServices directory, and add a new directory named Base in the Api directory. Add a new class named ApiServiceBase, make it public abstract and implement the IDataServiceBase interface:

namespace AutoLot.Services.DataServices.Api.Base;

public abstract class ApiDataServiceBase : IDataServiceBase where TEntity : BaseEntity, new()
{
protected ApiDataServiceBase()
{
}

public async Task<IEnumerable> GetAllAsync()
=> throw new NotImplementedException();
public async Task FindAsync(int id) => throw new NotImplementedException(); public async Task UpdateAsync(TEntity entity, bool persist = true)
{
throw new NotImplementedException();
}

public async Task DeleteAsync(TEntity entity, bool persist = true)
=> throw new NotImplementedException();
public async Task AddAsync(TEntity entity, bool persist = true)
{
throw new NotImplementedException();
}
}

Add the following global using statement into the GlobalUsings.cs file:

global using AutoLot.Services.DataServices.Api; global using AutoLot.Services.DataServices.Api.Base;

Add a new class named MakeDalDataService.cs in the Dal directory and update it to match the following:

namespace AutoLot.Services.DataServices.Api;

public class MakeApiDataService : ApiDataServiceBase, IMakeDataService
{
public MakeApiDataService():base()
{
}
}

Finally, add a class named CarDalDataService.cs and update it to match the following, which implements the one extra method from the interface:

namespace AutoLot.Services.DataServices.Api;

public class CarApiDataService : ApiDataServiceBase, ICarDataService
{
public CarApiDataService() :base()
{
}

public async Task<IEnumerable> GetAllByMakeIdAsync(int? makeId) => throw new NotImplementedException();
}

Adding the Data Services to the DI Container
The final step is to add the ICarDataService and IMakeDataService into the services collection. Start by adding the following line into the appsettings.Development.json and appsettings.Production.json files in the AutoLot.Mvc and AutoLot.Web project:

"RebuildDatabase": false,
"UseApi": false,

Add the following global using statements to the GlobalUsings.cs file in both the AutoLot.Mvc and AutoLot.Web projects:

global using AutoLot.Services.DataServices.Api; global using AutoLot.Services.DataServices.Dal;

Finally, add a new public static method named AddDataServices() to the DataServiceConfiguration class. In this method, check the value of the UseApi configuration flag, and if it is set to true, add the API versions of the data services classes into the services collection. Otherwise use the data access layer versions:

public static IServiceCollection AddDataServices( this IServiceCollection services, ConfigurationManager config)
{
if (config.GetValue("UseApi"))
{
services.AddScoped<ICarDataService, CarApiDataService>(); services.AddScoped<IMakeDataService, MakeApiDataService>();
}
else
{
services.AddScoped<ICarDataService, CarDalDataService>(); services.AddScoped<IMakeDataService, MakeDalDataService>();
}
return services;
}

Call the new extension method in the top level statements in Program.cs in the AutoLot.Mvc and AutoLot.Web projects:

builder.Services.AddRepositories();
builder.Services.AddDataServices(builder.Configuration);

The Options Pattern in ASP.NET Core
The options pattern provides a mechanism to instantiate classes from configured settings and inject the configured classes into other classes through dependency injection. The classes are injected into another class using one of the versions of IOptions. There are several versions of this interface, as shown in Table 31-6.

Table 31-6. Some of the IOptions Interfaces

Interface Description
IOptionsMonitor Retrieves options and supports the following: notification of changes (with OnChange), configuration reloading, named options (with Get and CurrentValue), and selective options invalidation.
IOptionsMonitorCache Caches instances of T with support for full/partial invalidation/reload.
IOptionsSnaphot Recomputes options on every request.
IOptionsFactory Creates new instances of T.
IOptions Root interface. Doesn’t support IOptionsMonitor. Left in for backward compatibility.

Using the Options Pattern
A simple example is to retrieve car dealer information from the configuration, configure a class with that data, and inject it into a controller’s action method for display. By placing the information into the settings file, the data is customizable without having to redeploy the site.
Start by adding the dealer information into the appsettings.json files for the AutoLot.Mvc and AutoLot.Web projects:

{
"Logging": {
"LogLevel": {
"Default": "Information", "Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"DealerInfo": {
"DealerName": "Skimedic’s Used Cars", "City": "West Chester",
"State": "Ohio"
}
}

Next, we need to create a view model to hold dealer information. Create a new folder named ViewModels in the AutoLot.Services project. In that folder, add a new class named DealerInfo.cs. Update the class to the following:

namespace AutoLot.Services.ViewModels; public class DealerInfo
{
public string DealerName { get; set; } public string City { get; set; } public string State { get; set; }
}

■Note the class to be configured must have a public parameterless constructor and be non-abstract. properties are bound but fields are not. Default values can be set on the class properties.

Next, add the following global using statements to the AutoLot.Mvc and AutoLot.Web GlobalUsings. cs files:

global using AutoLot.Services.ViewModels; global using Microsoft.Extensions.Options;

The Configure<>() method of the IServiceCollection maps a section of the configuration files to a specific type. That type can then be injected into classes and views using the options pattern. In the
Program.cs files for the AutoLot.Mvc and AutoLot.Web Program.cs files, add the following after the line used to configure the repositories:

builder.Services.Configure(builder.Configuration.GetSection(nameof(Deal erInfo)));

Now that the DealerInfo is configured, instances are retrieved by injection one of the IOptions interfaces into the class constructor, controller action method, or Razor page handler method. Notice that what is injected in is not an instance of the DealerInfo class, but an instance of the IOptions interface. To get the configured instance, the CurrentValue (IOptionsMonitor) or Value
(IOptionsSnapshot) must be used. The following example uses method injection to pass an instance if IOptionsMonitor into the HomeController’s Index method in the AutoLot.Mvc project, then gets the CurrentValue and passes the configured instance of the DealerInfo class to the view (although the view doesn’t do anything with it yet).

public IActionResult Index([FromServices] IOptionsMonitor dealerMonitor)
{
var vm = dealerMonitor.CurrentValue;
return View(vm);
}

The following example replicates the process for the Index Razor page in the Pages folder in the AutoLot.Web project. Instead of passing the instance to the view, it is assigned to a property on the page. Update the Index.cshtml.cs by adding the same injection into the OnGet() method:

public DealerInfo DealerInfoInstance { get; set; }
public void OnGet([FromServices]IOptionsMonitor dealerOptions)
{
DealerInfoInstance = dealerOptions.CurrentValue;
}

■Note For more information on the options pattern in aSp.net Core, consult the documentation: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/ options?view=aspnetcore-6.0.

The HTTP Client Factory
ASP.NET Core 2.1 introduced the IHTTPClientFactory, which can be used to create and configure HttPClient instances. The factory manages the pooling and lifetime of the underlying HttpClientMessageHandler instance, abstracting that away from the developer. It provides four mechanisms of use:
•Basic usage
•Named clients,
•Typed clients, and
•Generated clients.
After exploring the basic, named client, and typed client usages, we will build the API service wrappers that will be utilized by the data services built earlier in this chapter.

■Note For information on generating clients, consult the documentation: https://docs.microsoft. com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-6.0.

Basic Usage
The basic usage registers the IHttpClientFactory with the services collection, and then uses the injected factory instance to create HttPClient instances. This method is a convenient way to refactor an existing application that is creating HttPClient instances. To implement this basic usage, add the following line into the top level statements in Program.cs (no need to actually do this, as the projects will use Typed clients):

builder.Services.AddHttpClient();

Then in the class that needs an HttpClient, inject the IHttpClientFactory into the constructor, and then call CreateClient():
namespace AutoLot.Services.DataServices.Api.Examples; public class BasicUsageWithIHttpClientFactory
{
private readonly IHttpClientFactory _clientFactory;
public BasicUsageWithIHttpClientFactory(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task DoSomethingAsync()
{
var client = _clientFactory.CreateClient();
//do something interesting with the client
}
}

Named Clients
Named clients are useful when your application uses distinct HttpClient instances, especially when they are configured differently. When registering the IHttpClientFactory, a name is provided along with any specific configuration for that HttpClient usage. To create a client named AutoLotApi, add the following into the top level statements in Program.cs (no need to actually do this, as the projects will use Typed clients):

using AutoLot.Services.DataServices.Api.Examples; builder.Services.AddHttpClient(NamedUsageWithIHttpClientFactory.API_NAME, client =>
{
//add any configuration here
});

Then in the class that needs an HttpClient, inject the IHttpClientFactory into the constructor, and then call CreateClient() passing in the name of the client to be created. The configuration from the AddHttpClient() call is used to create the new instance:
namespace AutoLot.Services.DataServices.Api.Examples; public class NamedUsageWithIHttpClientFactory
{
public const string API_NAME = "AutoLotApi"; private readonly IHttpClientFactory _clientFactory;
public NamedUsageWithIHttpClientFactory(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task DoSomethingAsync()
{
var client = _clientFactory.CreateClient(API_NAME);
//do something interesting with the client
}
}

Typed Clients
Typed clients are classes that accept an HttpClient instance through injection into its constructor. Since the typed client is a class, it can be added into the service collection and injected into other classes,
fully encapsulating the calls using an HttpClient. As an example, suppose you have a class named
ApiServiceWrapper that takes in an HttpClient and implements IApiServiceWrapper as follows:
namespace AutoLot.Services.DataServices.Api.Examples; public interface IApiServiceWrapperExample
{
//interesting methods places here
}

public class ApiServiceWrapperExample : IApiServiceWrapperExample
{
protected readonly HttpClient Client;
public ApiServiceWrapperExample(HttpClient client)
{
Client = client;
//common client configuration goes here
}
//interesting methods implemented here
}

With the interface and the class in place, they can be added to the service collection as follows:

builder.Services.AddHttpClient<IApiServiceWrapperExample,ApiServiceWrapperExample>();

Inject the IApiServiceWrapper interface into the class that needs to make calls to the API and use the methods on the class instance to call to the API. This pattern completely abstracts the HttpClient away from the calling code:
namespace AutoLot.Services.DataServices.Api.Examples; public class TypedUsageWithIHttpClientFactory
{
private readonly IApiServiceWrapperExample _serviceWrapper;

public TypedUsageWithIHttpClientFactory(IApiServiceWrapperExample serviceWrapper)
{
_serviceWrapper = serviceWrapper;
}
public async Task DoSomethingAsync()
{
//do something interesting with the service wrapper
}
}

In addition to the configuration options in the constructor of the class, the call to AddHttpClient() can also configure the client:

builder.Services.AddHttpClient<IApiServiceWrapperExample,ApiServiceWrapperExample>(client=>
{
//configuration goes here
});

The AutoLot API Service Wrapper
The AutoLot API service wrapper uses a typed base client and entity specific typed clients to encapsulate all of the calls to the AutoLot.Api project. Both the AutoLot.Mvc and AutoLot.Web projects will use the service wrapper through the data services classes stubbed out earlier in this chapter and completed at the end of this section. The AutoLot.Api project will be finished in the next chapter.

Update the Application Configuration
The AutoLot.Api application endpoints will vary based on the environment. For example, when developing on your workstation, the base URI is https://localhost:5011. In your integration environment, the URI might be https://mytestserver.com. The environmental awareness, in conjunction with the updated configuration system, will be used to add these different values.
The appsettings.Development.json file will add the service information for the local machine. As code moves through different environments, the settings would be updated in each environment’s specific file to match the base URI and endpoints for that environment. In this example, you only update the settings for the development environment. In the appsettings.Development.json files in the AutoLot.Mvc and AutoLot.Web projects, add the following after the ConnectionStrings entry (changes in bold):

"ConnectionStrings": {
"AutoLot": "Server=.,5433;Database=AutoLot;User ID=sa;Password=P@ssw0rd;"
},
"ApiServiceSettings": {
"Uri": "https://localhost:5011/", "UserName": "AutoLotUser", "Password": "SecretPassword", "CarBaseUri": "api/v1/Cars", "MakeBaseUri": "api/v1/Makes", "MajorVersion": 1,
"MinorVersion": 0, "Status": ""
}

■Note Make sure the port number matches your configuration for autoLot.api.

Create the ApiServiceSettings Class
The service settings will be populated from the settings the same way the dealer information was populated. In the AutoLot.Service project, create a new folder named ApiWrapper and in that folder, create a new folder named Models. In this folder, add a class named ApiServiceSettings.cs. The property names of the class need to match the property names in the JSON ApiServiceSettings section. The class is listed here:
namespace AutoLot.Services.ApiWrapper.Models; public class ApiServiceSettings
{
public ApiServiceSettings() { } public string UserName { get; set; } public string Password { get; set; } public string Uri { get; set; }
public string CarBaseUri { get; set; } public string MakeBaseUri { get; set; } public int MajorVersion { get; set; } public int MinorVersion { get; set; } public string Status { get; set; }

public string ApiVersion
=> $"{MajorVersion}.{MinorVersion}" + (!string.IsNullOrEmpty(Status)?$"-
{Status}":string.Empty);
}

■Note api versioning will be covered in depth in Chapter 32.

Add the following global using statement to the GlobalUsings.cs file in the AutoLot.Service project:

global using AutoLot.Services.ApiWrapper.Models;

Register the ApiServiceSettings Class
We are once again going to use an extension method to register everything needed for the API service wrapper, including configuring the ApiServiceSettings.cs using the Options pattern. Create a new folder named Configuration under the ApiWrapper folder, and in that folder create a new public static class named ServiceConfiguration, as shown here:
namespace AutoLot.Services.ApiWrapper.Configuration; public static class ServiceConfiguration
{
public static IServiceCollection ConfigureApiServiceWrapper(this IServiceCollection services, IConfiguration config)
{
services.Configure(config.GetSection(nameof(ApiServiceSettings))); return services;
}
}

Add the following global using statement to the GlobalUsings.cs file for both the AutoLot.Mvc and AutoLot.Web projects:

global using AutoLot.Services.ApiWrapper.Configuration;

Add the following to the top level statements in Program.cs (in both AutoLot.Mvc and AutoLot.Web) before the call to builder.Build():

builder.Services.ConfigureApiServiceWrapper(builder.Configuration);

The API Service Wrapper Base Class and Interface
The ApiServiceWrapperBase class is a generic base class that performs create, read, update, and delete (CRUD) operations against the AutoLot.Api RESTful service. This centralizes communication with the service, configuration of the HTTP client, error handling, and so on. Entity specific classes will inherit from this base class, and those will be added into the services collection.

The IApiServiceWrapperBase Interface
The AutoLot service wrapper interface contains the common methods to call into the AutoLot.Api service. Create a directory named Interfaces in the ApiWrapper directory. Add a new interface named IApiServiceWrapper.cs and update the interface to the code shown here:

namespace AutoLot.Services.ApiWrapper.Interfaces;

public interface IApiServiceWrapperBase where TEntity : BaseEntity, new()
{
Task<IList> GetAllEntitiesAsync(); Task GetEntityAsync(int id); Task AddEntityAsync(TEntity entity); Task UpdateEntityAsync(TEntity entity); Task DeleteEntityAsync(TEntity entity);
}

Add the following global using statement to the GlobalUsings.cs file for the AutoLot.Services project:

global using AutoLot.Services.ApiWrapper.Interfaces;

The ApiServiceWrapperBase Class
Before building the base class, add the following global using statements to the GlobalUsings.cs file:

global using AutoLot.Services.ApiWrapper.Base; global using Microsoft.Extensions.Options; global using System.Net.Http.Headers;
global using System.Net.Http.Json; global using System.Text;
global using System.Text.Json;

Create a new folder named Base in the ApiWrapper directory of the AutoLot.Services project and add a class named ApiServiceWrapperBase. Make the class public and abstract and implement the
IApiServiceWrapperBase interface. Add a protected constructor that takes an instance of HttpClient and IOptionsMonitor and a string for the entity specific endpoint. Add a protected readonly string to hold the ApiVersion from the settings and initialize them all from the constructor
like this:

namespace AutoLot.Services.ApiWrapper.Base;

public abstract class ApiServiceWrapperBase
: IApiServiceWrapperBase where TEntity : BaseEntity, new()
{
protected readonly HttpClient Client; private readonly string _endPoint;
protected readonly ApiServiceSettings ApiSettings; protected readonly string ApiVersion;

protected ApiServiceWrapperBase(
HttpClient client, IOptionsMonitor apiSettingsMonitor, string endPoint)
{
Client = client;
_endPoint = endPoint;
ApiSettings = apiSettingsMonitor.CurrentValue; ApiVersion = ApiSettings.ApiVersion;
}
}

Configuring the HttpClient in the Constructor
The constructor adds the standard configuration to the HttpClient that will be used by all methods, including an AuthorizationHeader that uses basic authentication. Basic authentication will be covered in depth in the next chapter, Restful Services with ASP.NET Core, so for now just understand that basic authentication takes a username and password, concatenates them together (separated by a colon) and Base64 encodes them. The rest of the code sets the BaseAddress for the HttpClient and specifies that the client is expecting JSON.

public ApiServiceWrapperBase(
HttpClient client, IOptionsMonitor apiSettingsMonitor, string endPoint)
{
Client = client;
_endPoint = endPoint;
ApiSettings = apiSettingsMonitor.CurrentValue; ApiVersion = ApiSettings.ApiVersion; Client.BaseAddress = new Uri(ApiSettings.Uri);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/ json"));
var authToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{apiSettingsMonitor. CurrentValue.UserName}:{apiSettingsMonitor.CurrentValue.Password}")); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("basic", authToken);
}

The Internal Support Methods
The class contains four support methods that are used by the public methods.

The Post and Put Helper Methods
These methods wrap the related HttpClient methods.

internal async Task PostAsJsonAsync(string uri, string json)
{
return await Client.PostAsync(uri, new StringContent(json, Encoding.UTF8, "application/ json"));
}

internal async Task PutAsJsonAsync(string uri, string json)
{
return await Client.PutAsync(uri, new StringContent(json, Encoding.UTF8, "application/ json"));
}

The HTTP Delete Helper Method Call
The final helper method is used for executing an HTTP delete. The HTTP 1.1 specification (and later) allows for passing a body in a delete statement, but there isn’t yet an extension method of the HttpClient for doing this. The HttpRequestMessage must be built up from scratch.
The first step is to then create a request message using object initialization to set Content, Method, and RequestUri. Once this is complete, the message is sent, and the response is returned to the calling code. The method is shown here:

internal async Task DeleteAsJsonAsync(string uri, string json)
{
HttpRequestMessage request = new HttpRequestMessage
{
Content = new StringContent(json, Encoding.UTF8, "application/json"), Method = HttpMethod.Delete,
RequestUri = new Uri(uri)
};
return await Client.SendAsync(request);
}

The HTTP Get Calls
There are two Get calls: one to get all records and one to get a single record. They both follow the same pattern. The GetAsync() method is called to return an HttpResponseMessage. The success or failure of the call is checked with the EnsureSuccessStatusCode() method, which throws an exception if the call did not return a successful status code. Then the body of the response is serialized back into the property type
(either as an entity or a list of entities) and returned to the calling code. The endpoint is constructed from the settings, and the ApiVersion is appended as a query string value. Each of these methods is shown here:

public async Task<IList> GetAllEntitiesAsync()
{
var response = await Client.GetAsync($"{ApiSettings.Uri}{_endPoint}?v={ApiVersion}"); response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<IList>(); return result;
}

public async Task GetEntityAsync(int id)
{
var response = await Client.GetAsync($"{ApiSettings.Uri}{_endPoint}/{id}?v={ApiVersion}"); response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync(); return result;
}

Notice the improvement in serializing the response body into an item (or list of items). Prior versions of ASP.NET Core would have to use the following code instead of the shorter ReadFromJsonAsync() method:

var result = await JsonSerializer.DeserializeAsync<IList>(await response.Content. ReadAsStreamAsync());

The HTTP Post Call
The method to add a record uses an HTTP Post request. It uses the helper method to post the entity as JSON and then returns the record from the response body. The method is listed here:

public async Task AddEntityAsync(TEntity entity)
{
var response = await PostAsJsonAsync($"{ApiSettings.Uri}{_endPoint}?v={ApiVersion}", JsonSerializer.Serialize(entity));
if (response == null)
{
throw new Exception("Unable to communicate with the service");
}

var location = response.Headers?.Location?.OriginalString;
return await response.Content.ReadFromJsonAsync() ?? await GetEntityAsync(entity.Id);
}

There are two lines of note. The first is the line getting the location. Typically, when an HTTP Post call is successful, the service returns an HTTP 201 (Created at) status code. The service will also add the URI of the newly created resource in the Location header. The preceding code demonstrates getting the Location header but isn’t using the location in the code process.
The second line of note is reading the response content and creating an instance of the updated record. The service wrapper method needs to get the updated instance from the service to guarantee server generated values (like Id and TimeStamp) and are updated in the client app. Returning the updated entity in the response from HTTP post calls is not a guaranteed function of a service. If the service doesn’t return the entity, then the method uses the GetEntityAsync() method. It could also use the URI from the location if
necessary, but since the GetEntityAsync() supplies everything needed, getting the location value is merely
for demo purposes.

The HTTP Put Call
The method to update a record uses an HTTP Put request by way of the PutAsJsonAsync() helper method. This method also assumes that the updated entity is in the body of the response, and if it isn’t, calls the GetEntityAsync() to refresh the server generated values.

public async Task UpdateEntityAsync(TEntity entity)
{
var response = await PutAsJsonAsync($"{ApiSettings.Uri}{_endPoint}/{entity. Id}?v={ApiVersion}",
JsonSerializer.Serialize(entity)); response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync() ?? await GetEntityAsync(entity.Id);
}

The HTTP Delete Call
The final method to add, is for executing an HTTP Delete. The pattern follows the rest of the methods: use the helper method and check the response for success. There isn’t anything to return to the calling code since the entity was deleted. The method is shown here:

public async Task DeleteEntityAsync(TEntity entity)
{
var response = await DeleteAsJsonAsync($"{ApiSettings.Uri}{_endPoint}/{entity. Id}?v={ApiVersion}",
JsonSerializer.Serialize(entity)); response.EnsureSuccessStatusCode();
}

The Entity Specific Interfaces
In the Interfaces directory, create two new interfaces named ICarApiServiceWrapper.cs and
IMakeApiServiceWrapper.cs. Update the interfaces to the following:

//ICarApiServiceWrapper.cs
namespace AutoLot.Services.ApiWrapper.Interfaces;

public interface ICarApiServiceWrapper : IApiServiceWrapperBase
{
Task<IList> GetCarsByMakeAsync(int id);
}

//IMakeApiServiceWrapper.cs
namespace AutoLot.Services.ApiWrapper.Interfaces;

public interface IMakeApiServiceWrapper : IApiServiceWrapperBase
{
}

The Entity Specific Classes
In the ApiWrapper directory, create two new classes named CarApiServiceWrapper.cs and MakeApiServiceWrapper.cs. The constructor for each class takes an instance of HttpClient and IOptions and passes them into the base class along with the entity specific end point:

//CarApiServiceWrapper.cs
namespace AutoLot.Services.ApiWrapper;

public class CarApiServiceWrapper : ApiServiceWrapperBase, ICarApiServiceWrapper
{
public CarApiServiceWrapper(HttpClient client, IOptionsMonitor apiSettingsMonitor)
: base(client, apiSettingsMonitor, apiSettingsMonitor.CurrentValue.CarBaseUri) { }
}

//MakeApiServiceWrapper.cs
namespace AutoLot.Services.ApiWrapper;

public class MakeApiServiceWrapper : ApiServiceWrapperBase, IMakeApiServiceWrapper
{
public MakeApiServiceWrapper(HttpClient client, IOptionsMonitor apiSettingsMonitor)
: base(client, apiSettingsMonitor, apiSettingsMonitor.CurrentValue.MakeBaseUri) { }
}

The MakeApiServiceWrapper only needs the methods exposed in the ApiServiceWrapperBase to do its job. The CarApiServiceWrapper class has one additional method to get the list of Car records by Make. The method follows the same pattern as the base class’s HTTP Get methods but uses a unique end point:

public async Task<IList> GetCarsByMakeAsync(int id)
{
var response = await Client.GetAsync($"{ApiSettings.Uri}{ApiSettings.CarBaseUri}/bymake/
{id}?v={ApiVersion}"); response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<IList>(); return result;
}

As a last step, register the two typed clients by updating the ConfigureApiServiceWrapper method in the ServiceConfiguration class to the following:

public static IServiceCollection ConfigureApiServiceWrapper(this IServiceCollection services, IConfiguration config)
{
services.Configure(config.GetSection(nameof(ApiServiceSettings))); services.AddHttpClient<ICarApiServiceWrapper, CarApiServiceWrapper>(); services.AddHttpClient<IMakeApiServiceWrapper, MakeApiServiceWrapper>();
return services;
}

Complete the API Data Services
Now that the API service wrappers are complete, it’s time to revisit the API data services and complete the class implementations.

Complete the ApiDataServiceBase Class
The first step to complete the base class is to update the constructor to receive an instance of the IApiServic eWrapperBase interface and assign it to a protected field:

protected readonly IApiServiceWrapperBase ServiceWrapper;
protected ApiDataServiceBase(IApiServiceWrapperBase serviceWrapperBase)
{
ServiceWrapper = serviceWrapperBase;
}

The implementation for each of the CRUD methods calls the related method on the ServiceWrapper:

public async Task<IEnumerable> GetAllAsync()
=> await ServiceWrapper.GetAllEntitiesAsync();
public async Task FindAsync(int id)
=> await ServiceWrapper.GetEntityAsync(id);
public async Task UpdateAsync(TEntity entity, bool persist = true)
{
await ServiceWrapper.UpdateEntityAsync(entity); return entity;
}

public async Task DeleteAsync(TEntity entity, bool persist = true)
=> await ServiceWrapper.DeleteEntityAsync(entity);

public async Task AddAsync(TEntity entity, bool persist = true)
{
await ServiceWrapper.AddEntityAsync(entity); return entity;
}

Complete the Entity Specific Classes
The CarApiDataService and MakeApiDataService classes need their constructors updated to receive their entity specific derived instance of the IApiServiceWrapperBase interface and pass it to the
base class:

public class CarApiDataService : ApiDataServiceBase, ICarDataService
{
public CarApiDataService(ICarApiServiceWrapper serviceWrapper) : base(serviceWrapper) { }
}

public class MakeApiDataService : ApiDataServiceBase, IMakeDataService
{
public MakeApiDataService(IMakeApiServiceWrapper serviceWrapper):base(serviceWrapper) { }
}

The GetAllByMakeIdIdAsync() method determines if a value was passed in for the makeId parameter.
If there is a value, the relevant method on the ICarApiServiceWrapper is called. Otherwise, the base
GetAllAsync() is called:

public async Task<IEnumerable> GetAllByMakeIdAsync(int? makeId)
=> makeId.HasValue
? await ((ICarApiServiceWrapper)ServiceWrapper). GetCarsByMakeAsync(makeId.Value)
: await GetAllAsync();

Deploying ASP.NET Core Applications
Prior versions of ASP.NET applications could only be deployed to Windows servers using IIS as the web server. ASP.NET Core can be deployed to multiple operating systems in multiple ways, using a variety of web servers. ASP.NET Core applications can also be deployed outside of a web server. The high-level options are as follows:
•On a Windows server (including Azure) using IIS
•On a Windows server (including Azure app services) outside of IIS
•On a Linux server using Apache or NGINX
•On Windows or Linux in a container
This flexibility allows organizations to decide the deployment platform that makes the most sense for them, including popular container-based deployment models (such as using Docker), as opposed to being locked into Windows servers.

Lightweight and Modular HTTP Request Pipeline
Following along with the principles of .NET, you must opt in for everything in ASP.NET Core. By default, nothing is loaded into an application. This enables applications to be as lightweight as possible, improving performance, and minimizing the surface area and potential risk.

Logging
Logging in ASP.NET Core is based on an ILoggerFactory. This enables different logging providers to hook into the logging system to send log messages to different locations, such as the Console. The ILoggerFactory is used to create an instance of ILogger, which provides the following methods for logging by way of the LoggerExtensions class:

public static class LoggerExtensions
{
public static void LogDebug(this ILogger logger, EventId eventId, Exception exception, string message, params object[] args)
public static void LogDebug(this ILogger logger, EventId eventId, string message, params object[] args)
public static void LogDebug(this ILogger logger, Exception exception, string message, params object[] args)
public static void LogDebug(this ILogger logger, string message, params object[] args)

public static void LogTrace(this ILogger logger, EventId eventId, Exception exception, string message, params object[] args)
public static void LogTrace(this ILogger logger, EventId eventId, string message, params object[] args)
public static void LogTrace(this ILogger logger, Exception exception, string message, params object[] args)
public static void LogTrace(this ILogger logger, string message, params object[] args)

public static void LogInformation(this ILogger logger, EventId eventId, Exception exception, string message, params object[] args)
public static void LogInformation(this ILogger logger, EventId eventId, string message, params object[] args)
public static void LogInformation(this ILogger logger, Exception exception, string message, params object[] args)
public static void LogInformation(this ILogger logger, string message, params object[] args)

public static void LogWarning(this ILogger logger, EventId eventId, Exception exception, string message, params object[] args)
public static void LogWarning(this ILogger logger, EventId eventId, string message, params object[] args)
public static void LogWarning(this ILogger logger, Exception exception, string message, params object[] args)
public static void LogWarning(this ILogger logger, string message, params object[] args)

public static void LogError(this ILogger logger, EventId eventId, Exception exception, string message, params object[] args)
public static void LogError(this ILogger logger, EventId eventId, string message, params object[] args)
public static void LogError(this ILogger logger, Exception exception, string message, params object[] args)
public static void LogError(this ILogger logger, string message, params object[] args)

public static void LogCritical(this ILogger logger, EventId eventId, Exception exception, string message, params object[] args)
public static void LogCritical(this ILogger logger, EventId eventId, string message, params object[] args)
public static void LogCritical(this ILogger logger, Exception exception, string message, params object[] args)
public static void LogCritical(this ILogger logger, string message, params object[] args)

public static void Log(this ILogger logger, LogLevel logLevel, string message, params object[] args)
public static void Log(this ILogger logger, LogLevel logLevel, EventId eventId, string message, params object[] args)
public static void Log(this ILogger logger, LogLevel logLevel, Exception exception, string message, params object[] args)
public static void Log(this ILogger logger, LogLevel logLevel, EventId eventId, Exception exception, string message, params object[] args)
}

Add Logging with Serilog
Any provider that supplies an ILoggerFactory extension can be used for logging in ASP.NET Core, and Serilog is one such logging framework. The next sections cover creating a logging infrastructure based on Serilog and configuring the ASP.NET Core applications to use the new logging code.

The Logging Settings
To configure Serilog, we are going to use the application configuration files in conjunction with a C# class. Start by adding a new folder named Logging to the AutoLot.Service project. Create a new folder named Settings in the Logging folder, and in that new folder, add a class named AppLoggingSettings. Update the code to the following:
namespace AutoLot.Services.Logging.Settings; public class AppLoggingSettings
{
public GeneralSettings General { get; set; } public FileSettings File { get; set; }
public SqlServerSettings MSSqlServer { get; set; }

public class GeneralSettings
{
public string RestrictedToMinimumLevel { get; set; }
}
public class SqlServerSettings
{
public string TableName { get; set; } public string Schema { get; set; }
public string ConnectionStringName { get; set; }
}

public class FileSettings
{
public string Drive { get; set; } public string FilePath { get; set; } public string FileName { get; set; } public string FullLogPathAndFileName =>

$"{Drive}{Path.VolumeSeparatorChar}{Path.DirectorySeparatorChar}{FilePath}{Path. DirectorySeparatorChar}{FileName}";
}
}

Add the following global using statement to the GlobalUsings.cs file in the AutoLot.Service project.

global using AutoLot.Services.Logging.Settings;

Next, use the following JSON to replace the scaffolded Logging details in the appsettings.
Development.json files for the AutoLot.Api, AutoLot.Mvc, and AutoLot.Web projects:

"AppLoggingSettings": { "MSSqlServer": {
"TableName": "SeriLogs", "Schema": "Logging", "ConnectionStringName": "AutoLot"
},

"File": { "Drive": "c",
"FilePath": "temp", "FileName": "log_AutoLot.txt"
},
"General": {
"RestrictedToMinimumLevel": "Information"
}
},

Next, add the following AppName node to each of the files, customized for each app:

//AutoLot.Api "AppName":"AutoLot.Api – Dev"

//AutoLot.Mvc "AppName":"AutoLot.Mvc – Dev"

//AutoLot.Web "AppName":"AutoLot.Web – Dev"

For reference, here is the complete listing for each project (notice the extra line at the beginning of the AutoLot.Web file – that will be covered in chapter 34):

//AutoLot.Api
{
"AppLoggingSettings": { "MSSqlServer": {
"TableName": "SeriLogs", "Schema": "Logging", "ConnectionStringName": "AutoLot"
},
"File": { "Drive": "c",
"FilePath": "temp", "FileName": "log_AutoLot.txt"
},
"General": {
"RestrictedToMinimumLevel": "Information"
}
},
"ConnectionStrings": {
"AutoLot": "Server=.,5433;Database=AutoLot;User ID=sa;Password=P@ssw0rd;"
},
"RebuildDatabase": true, "AppName": "AutoLot.Api – Dev"
}

//AutoLot.Mvc
{
"AppLoggingSettings": { "MSSqlServer": {

"TableName": "SeriLogs", "Schema": "Logging", "ConnectionStringName": "AutoLot"
},
"File": { "Drive": "c",
"FilePath": "temp", "FileName": "log_AutoLot.txt"
},
"General": {
"RestrictedToMinimumLevel": "Information"
}
},
"ConnectionStrings": {
"AutoLot": "Server=.,5433;Database=AutoLot;User ID=sa;Password=P@ssw0rd;"
},
"RebuildDatabase": true, "UseApi": false, "ApiServiceSettings": {
"Uri": "https://localhost:5011/", "UserName": "AutoLotUser", "Password": "SecretPassword", "CarBaseUri": "api/v1/Cars", "MakeBaseUri": "api/v1/Makes"
},
"AppName": "AutoLot.Mvc – Dev"
}

//AutoLot.Web
{
"DetailedErrors": true, "AppLoggingSettings": {
"MSSqlServer": { "TableName": "SeriLogs", "Schema": "Logging",
"ConnectionStringName": "AutoLot"
},
"File": { "Drive": "c",
"FilePath": "temp", "FileName": "log_AutoLot.txt"
},
"General": {
"RestrictedToMinimumLevel": "Information"
}
},
"ConnectionStrings": {
"AutoLot": "Server=.,5433;Database=AutoLot;User ID=sa;Password=P@ssw0rd;"
},
"RebuildDatabase": true, "UseApi": false,

"ApiServiceSettings": {
"Uri": "https://localhost:5011/", "UserName": "AutoLotUser", "Password": "SecretPassword", "CarBaseUri": "api/v1/Cars", "MakeBaseUri": "api/v1/Makes"
},
"AppName": "AutoLot.Web – Dev"
}

The final step is to clear out the Logging section of each of the appsettings.json files, leaving just the AllowedHosts entry in the AutoLot.Api project, and the AllowedHosts and the DealerInfo in the AutoLot. Mvc and AutoLot.Web projects:

//AutoLot.Api
{
"AllowedHosts": "*"
}

//AutoLot.Mvc
{
"AllowedHosts": "*", "DealerInfo": {
"DealerName": "Skimedic’s Used Cars", "City": "West Chester",
"State": "Ohio"
}
}

//AutoLot.Web
{
"AllowedHosts": "*", "DealerInfo": {
"DealerName": "Skimedic’s Used Cars", "City": "West Chester",
"State": "Ohio"
}
}

The Logging Configuration
The next step is to configure Serilog. Get started by adding a new folder named Configuration in the Logging folder of the AutoLot.Service project. In this folder add a new class named LoggingConfiguration. Make the class public and static, as shown here:
namespace AutoLot.Services.Logging.Configuration; public static class LoggingConfiguration
{
}

Serilog uses sinks to write to different logging targets. With this mechanism, a single call to SeriLog will write to many places. The targets we will use for logging in the ASP.NET Core apps are a text file, the
database, and the console. The text file and database sinks require configuration, an output template for the text file sink, and a list of fields for the database sink.
To set up the file template, create the following static readonly string:

internal static readonly string OutputTemplate =
@"[{Timestamp:yy-MM-dd HH:mm:ss} {Level}]{ApplicationName}:{SourceContext}{NewLine} Message:{Message}{NewLine}in method {MemberName} at {FilePath}:{LineNumber}{NewLine}
{Exception}{NewLine}";

The SQL Server sink needs a list of columns identified using the SqlColumn type. Add the following code to configure the database columns:

internal static readonly ColumnOptions ColumnOptions = new ColumnOptions
{
AdditionalColumns = new List
{
new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "ApplicationName"}, new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "MachineName"},
new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "MemberName"}, new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "FilePath"}, new SqlColumn {DataType = SqlDbType.Int, ColumnName = "LineNumber"},
new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "SourceContext"}, new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "RequestPath"}, new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "ActionName"},
}
};

Swapping the default logger with Serilog is a three-step process. The first is to clear the existing provider, the second is to add Serilog into the WebApplicationBuilder, and the third is to finish configuring Serilog. Add a new method named ConfigureSerilog(), which is an extension method for the WebApplicationBuilder. The first line clears out the default loggers, and the last line adds the fully configured Serilog framework into the WebApplicationBuilder‘s logging framework.

public static void ConfigureSerilog(this WebApplicationBuilder builder)
{
builder.Logging.ClearProviders(); var config = builder.Configuration;
var settings = config.GetSection(nameof(AppLoggingSettings)).Get(); var connectionStringName = settings.MSSqlServer.ConnectionStringName;
var connectionString = config.GetConnectionString(connectionStringName); var tableName = settings.MSSqlServer.TableName;
var schema = settings.MSSqlServer.Schema;
string restrictedToMinimumLevel = settings.General.RestrictedToMinimumLevel; if (!Enum.TryParse(restrictedToMinimumLevel, out var logLevel))
{
logLevel = LogEventLevel.Debug;
}
var sqlOptions = new MSSqlServerSinkOptions
{
AutoCreateSqlTable = false,

SchemaName = schema, TableName = tableName,
};
if (builder.Environment.IsDevelopment())
{
sqlOptions.BatchPeriod = new TimeSpan(0, 0, 0, 1);
sqlOptions.BatchPostingLimit = 1;
}

var log = new LoggerConfiguration()
.MinimumLevel.Is(logLevel)
.Enrich.FromLogContext()
.Enrich.With(new PropertyEnricher("ApplicationName", config.GetValue("Applicati onName")))
.Enrich.WithMachineName()
.WriteTo.File(
path: builder.Environment.IsDevelopment()? settings.File.FileName : settings.File. FullLogPathAndFileName,
rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: logLevel, outputTemplate: OutputTemplate)
.WriteTo.Console(restrictedToMinimumLevel: logLevel)
.WriteTo.MSSqlServer( connectionString: connectionString, sqlOptions, restrictedToMinimumLevel: logLevel, columnOptions: ColumnOptions);
builder.Logging.AddSerilog(log.CreateLogger(),false);
}

With everything in place, it’s time to create the logging framework that will use Serilog.

The AutoLot Logging Framework
The AutoLot logging framework leverages the built-in logging capabilities of ASP.NET Core to simplify using Serilog. It starts with the IAppLogging interface.

The IAppLogging Interface
The IApplogging interface holds the logging methods for the custom logging system. Add new directory named Interfaces in the Logging directory in the AutoLot.Service project. In this directory, add a new interface named IAppLogging. Update the code in this interface to match the following:
namespace AutoLot.Services.Logging.Interfaces; public interface IAppLogging
{
void LogAppError(Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0);

void LogAppError(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0);

void LogAppCritical(Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0);

void LogAppCritical(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0);

void LogAppDebug(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0);

void LogAppTrace(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0);

void LogAppInformation(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0);

void LogAppWarning(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0);
}

The attributes CallerMemberName, CallerFilePath, and CallerLineNumber inspect the call stack to get the values they are named for from the calling code. For example, if the line that calls LogAppWarning() is in the DoWork() function, in a file named MyClassFile.cs, and resides on line number 36, then the call:

_appLogger.LogAppError(ex, "ERROR!");

is converted into the equivalent of this:

_appLogger.LogAppError(ex,"ERROR","DoWork ","c:/myfilepath/MyClassFile.cs",36);

If values are passed into the method call for any of the attributed parameters, the values passed in are used instead of the values from the attributes.
Add the following global using statement to the GlobalUsings.cs file in the AutoLot.Services project:

global using AutoLot.Services.Logging.Interfaces;

The AppLogging Class
The AppLogging class implements the IAppLogging interface. Add a new class named AppLogging to the Logging directory. Make the class public and implement IAppLogging and add a constructor that takes an instance of ILogger and stores it in a class level variable.

namespace AutoLot.Services.Logging;

public class AppLogging : IAppLogging
{
private readonly ILogger _logger;

public AppLogging(ILogger logger)
{
_logger = logger;
}
}

Serilog enables adding additional properties into the standard logging process by pushing them onto the LogContext. Add an internal method to log an event with an exception and push the MemberName, FilePath, and LineNumber properties. The PushProperty() method returns an IDisposable, so the method disposes everything before leaving the method.

internal static void LogWithException(string memberName,
string sourceFilePath, int sourceLineNumber, Exception ex, string message, Action<Exception, string, object[]> logAction)
{
var list = new List
{
LogContext.PushProperty("MemberName", memberName), LogContext.PushProperty("FilePath", sourceFilePath), LogContext.PushProperty("LineNumber", sourceLineNumber),
};
logAction(ex,message,null); foreach (var item in list)
{
item.Dispose();
}
}

Repeat the process for log events that don’t include an exception:

internal static void LogWithoutException(string memberName, string sourceFilePath, int sourceLineNumber, string message, Action<string, object[]> logAction)
{
var list = new List
{
LogContext.PushProperty("MemberName", memberName), LogContext.PushProperty("FilePath", sourceFilePath), LogContext.PushProperty("LineNumber", sourceLineNumber),
};

logAction(message, null); foreach (var item in list)
{
item.Dispose();
}
}

For each type of logging event, call the appropriate help method to write to the logs:

public void LogAppError(Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
LogWithException(memberName, sourceFilePath, sourceLineNumber, exception, message, _ logger.LogError);
}

public void LogAppError(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
LogWithoutException(memberName, sourceFilePath, sourceLineNumber, message, _logger. LogError);
}

public void LogAppCritical(Exception exception, string message, [CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
LogWithException(memberName, sourceFilePath, sourceLineNumber, exception, message, _ logger.LogCritical);
}

public void LogAppCritical(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
LogWithoutException(memberName, sourceFilePath, sourceLineNumber, message, _logger. LogCritical);
}

public void LogAppDebug(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
LogWithoutException(memberName, sourceFilePath, sourceLineNumber, message, _logger.LogDebug);
}

public void LogAppTrace(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
LogWithoutException(memberName, sourceFilePath, sourceLineNumber, message, _logger. LogTrace);
}

public void LogAppInformation(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
LogWithoutException(memberName, sourceFilePath, sourceLineNumber, message, _logger. LogInformation);
}

public void LogAppWarning(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
LogWithoutException(memberName, sourceFilePath, sourceLineNumber, message, _logger. LogWarning);
}

Final Configuration
The final configuration is to add the IAppLogging<> interface into the DI container and call the extension method to add SeriLog into the WebApplicationBuilder. Start by creating a new extension method in the LoggingConfiguration class:

public static IServiceCollection RegisterLoggingInterfaces(this IServiceCollection services)
{
services.AddScoped(typeof(IAppLogging<>), typeof(AppLogging<>)); return services;
}

Next, add the following global using statement to the GlobalUsings.cs file in each web project:

global using AutoLot.Services.Logging.Configuration; global using AutoLot.Services.Logging.Interfaces;

Next, add both of the extension methods to the top level statements in the Program.cs file. Note that the ConfigureSerilog() method extends off of the WebAppBuilder (the builder variable) and the RegisterLoggingInterfaces() method extends off of the IServiceCollection:

//Configure logging builder.ConfigureSerilog(); builder.Services.RegisterLoggingInterfaces();

Add Logging to the Data Services
With Serilog and the AutoLot logging system in place, it’s time to update the data services to add logging capabilities.

Update the Base Classes
Starting with the ApiDataServiceBase and DalDataServiceBase classes, update the generic definition to also accept a class that implements IDataServiceBase. This is to allow for strongly typing the IAppLogging interface to each of the derived class. Here are the updated classes definitions:

//ApiDataServiceBase.cs
public abstract class ApiDataServiceBase<TEntity, TDataService> : IDataServiceBase where TEntity : BaseEntity, new()
where TDataService : IDataServiceBase
{
//omitted for brevity
}

//DalDataServiceBase.cs
public abstract class DalDataServiceBase<TEntity, TDataService> : IDataServiceBase where TEntity : BaseEntity, new()
where TDataService : IDataServiceBase
{
//omitted for brevity
}

Next, update each of the constructors to take an instance of IAppLogging and assign it to a protected class field:

//CarApiDataService.cs
protected readonly IApiServiceWrapperBase ServiceWrapper;
protected readonly IAppLogging AppLoggingInstance;

protected ApiDataServiceBase(IAppLogging appLogging,
IApiServiceWrapperBase serviceWrapperBase)
{
ServiceWrapper = serviceWrapperBase;
AppLoggingInstance = appLogging;
}

//MakeApiDataService.cs
protected readonly IBaseRepo MainRepo;
protected readonly IAppLogging AppLoggingInstance;

protected DalDataServiceBase(IAppLogging appLogging, IBaseRepo mainRepo)
{
MainRepo = mainRepo;
AppLoggingInstance = appLogging;
}

Update the Entity Specific Data Services Classes
Each of the entity specific classes need to change their inheritance signature to use the new generic parameter and also take an instance of IAppLogging in the constructor and pass it to the base class. Here is the code for the updated classes:

//CarApiDataService.cs
public class CarApiDataService : ApiDataServiceBase<Car,CarApiDataService>, ICarDataService
{
public CarApiDataService( IAppLogging appLogging, ICarApiServiceWrapper serviceWrapper)
: base(appLogging, serviceWrapper) { }
//omitted for brevity
}

//MakeApiDataService.cs
public class MakeApiDataService
: ApiDataServiceBase<Make,MakeApiDataService>, IMakeDataService
{
public MakeApiDataService( IAppLogging appLogging, IMakeApiServiceWrapper serviceWrapper)
: base(appLogging,serviceWrapper) { }
}

//CarDalDataService.cs
public class CarDalDataService : DalDataServiceBase<Car,CarDalDataService>,ICarDataService
{
private readonly ICarRepo _repo;
public CarDalDataService(IAppLogging appLogging, ICarRepo repo) : base(appLogging, repo)
{
_repo = repo;
}
//omitted for brevity
}

//MakeApiDataService.cs
public class MakeDalDataService : DalDataServiceBase<Make,MakeDalDataService>,IMakeD ataService
{
public MakeDalDataService(IAppLogging appLogging,IMakeRepo repo)
: base(appLogging, repo) { }
}

Next, update the constructors to take the instance of IAppLogging and assign it to a protected class field:

Test-Drive the Logging Framework
To close out the chapter, let’s test the logging framework. The first step is to update the HomeController in the AutoLot.Mvc project to use the new logging framework. Replace the ILogger parameter with IAppLogging and update the type of the field, like this:

public class HomeController : Controller
{
private readonly IAppLogging _logger; public HomeController(IAppLogging logger)
{
_logger = logger;
}
//omitted for brevity
}

With this in place, log a test error in the Index() method:

public IActionResult Index([FromServices]IOptionsMonitor dealerMoitor)
{
_logger.LogAppError("Test error"); var vm = dealerMonitor.CurrentValue; return View(vm);
}

Run the AutoLot.Mvc project. When the app starts, a record will be logged in the SeriLogs table as well as written to a file named log_AutoLotYYYYMMDD.txt.
When you open the log file, you might be surprised to see that there are a lot of additional entries that didn’t come from the one call to the logger. That is because EF Core and ASP.NET Core emit very verbose logging when the log level is set to Information. To eliminate the noise, update the appsettings. Development.json files in the AutoLot.Api, AutoLot,Mvc, and AutoLot.Web projects so that the log level is Warning, like this:

"RestrictedToMinimumLevel": "Warning"

String Utilities
Recall that one of the conventions in ASP.NET Core removes the Controller suffix from controllers and the Async suffix from action methods when routing controllers and actions. When manually building up routes, it’s common to have to remove the suffix through code. While the code is simple to write, it can be repetitive. To cut down on repeating the string manipulation code, the next step will create two string extension methods.
Add a new directory named Utilities in the AutoLot.Services project, and in that directory, create a new public static class named StringExtensions. In that class, add the following two extension methods:
namespace AutoLot.Services.Utilities public static class StringExtensions
{
public static string RemoveControllerSuffix(this string original)
=> original.Replace("Controller", "", StringComparison.OrdinalIgnoreCase);

public static string RemoveAsyncSuffix(this string original)
=> original.Replace("Async", "", StringComparison.OrdinalIgnoreCase); public static string RemovePageModelSuffix(this string original)
=> original.Replace("PageModel", "", StringComparison.OrdinalIgnoreCase);
}

Next, add the following global using statement to the GlobalUsings.cs file in the AutoLot.Services project and all three web applications:

global using AutoLot.Services.Utilities;

Summary
This chapter dove into the new features introduced in ASP.NET Core and began the process of updating the three ASP.NET Core applications. In the next chapter, you will finish the AutoLot.Api application.

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.

Pro C#10 CHAPTER 29 WPF Notifications, Validations, Commands, and MVVM

CHAPTER 29

WPF Notifications, Validations, Commands, and MVVM

This chapter will conclude your investigation of the WPF programming model by covering the capabilities that support the Model-View-ViewModel (MVVM) pattern. The first section covers the Model-View- ViewModel pattern. Next, you learn about the WPF notification system and its implementation of the Observable pattern through observable models and observable collections. Having the data in the UI accurately portray the current state of the data automatically improves the user experience significantly and reduces the manual coding required in older technologies (such as WinForms) to achieve the same result.
Building on the Observable pattern, you will examine the mechanisms to add validation into your application. Validation is a vital part of any application—not only letting the user know that something is wrong but also letting them know what is wrong. To inform the user what the error is, you will also learn how to incorporate validation into the view markup.
Next, you will take a deeper dive into the WPF command system and create custom commands to encapsulate program logic, much as you did in Chapter 25 with the built-in commands. There are several advantages to creating custom commands, including (but not limited to) enabling code reuse, logic encapsulation, and separation of concerns.
Finally, you will bring all of this together in a sample MVVM application.

Introducing Model-View-ViewModel
Before you dive into notifications, validations, and commands in WPF, it would be good to understand the end goal of this chapter, which is the Model-View-ViewModel pattern (MVVM). Derived from Martin
Fowler’s Presentation Model pattern, MVVM leverages XAML-specific capabilities, discussed in this chapter, to make your WPF development faster and cleaner. The name itself describes the main components of the pattern: model, view, view model.

The Model
The model is the object representation of your data. In MVVM, models are conceptually the same as the models from your data access layer (DAL). Sometimes they are the same physical class, but there is no requirement for this. As you read this chapter, you will learn how to decide whether you can use your DAL models or whether you need to create new ones.
Models typically take advantage of the built-in (or custom) validations through data annotations and the INotifyDataErrorInfo interface and are configured as observable to tie into the WPF notification system. You will see all of this later in this chapter.

© 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_29

1273

The View
The view is the UI of the application, and it is designed to be very lightweight. Think of the menu board at a drive-thru restaurant. The board displays menu items and prices, and it has a mechanism so the user can communicate with the back-end systems. However, there isn’t any intelligence built into the board, unless it is specifically user interface logic, such as turning on the lights if it gets dark.
MVVM views should be developed with the same goals in mind. Any intelligence should be built into the application elsewhere. The only code in the code-behind file (e.g., MainWindow.xaml.cs) should be directly related to manipulating the UI. It should not be based on business rules or anything that needs to be persisted for future use. While not a main goal of MVVM, well-developed MVVM applications typically have very little code in the code-behind.

The View Model
In WPF and other XAML technologies, the view model serves two purposes.
•The view model provides a single stop for all the data needed by the view. This doesn’t mean the view model is responsible for getting the actual data; instead, it is merely a transport mechanism to move the data from the data store to the view. Usually, there is a one-to-one correlation between views and view models, but architectural differences exist, and your mileage may vary.
•The second job is to act as the controller for the view. Just like the menu board, the view model takes direction from the user and relays that call to the relevant code to make sure the proper actions are taken. Quite often this code is in the form of custom commands.

Anemic Models or Anemic View Models
In the early days of WPF, when developers were still working out how best to implement the MVVM pattern, there were significant (and sometimes heated) discussions about where to implement items like validation and the Observable pattern. One camp (the anemic model camp) argued that it all should be in the view model since adding those capabilities to the model broke separation of concerns. The other camp (the anemic view model camp) argued it should all be in the models since that reduced duplication of code.
The real answer is, of course, it depends. When INotifyPropertyChanged, IDataErrorInfo, and INotifyDataErrorInfo are implemented on the model classes, this ensures that the relevant code is close to the target of the code (as you will see in this chapter) and is implemented only once for each model.
That being said, there are times when your view model classes will need to be developed as observables themselves. At the end of the day, you need to determine what makes the most sense for your application, without over-complicating your code or sacrificing the benefits of MVVM.

■Note There are multiple MVVM frameworks available for WPF, such as MVVMLite, Caliburn.Micro, and Prism (although Prism is much more than just an MVVM framework). This chapter discusses the MVVM pattern and the features in WPF that support implementing the pattern. I leave it to you, the reader, to examine the different frameworks and select the one that best matches your app’s needs.

The WPF Binding Notification System
A significant shortcoming in the binding system for WinForms is a lack of notifications. If the data represented in the view is updated programmatically, the UI must also be refreshed programmatically to keep them in sync. This leads to a lot of calls to Refresh() on controls, typically more than are absolutely necessary in order to be safe. While usually not a significant performance issue to include too many calls to Refresh(), if you don’t include enough, the experience for the user could be affected negatively.
The binding system built into XAML-based applications corrects this problem by enabling you to hook your data objects and collections into a notification system by developing them as observables. Whenever a property’s value changes on an observable model or the collection changes (e.g., items are added, removed, or reordered) on an observable collection, an event is raised (either NotifyPropertyChanged
or NotifyCollectionChanged). The binding framework automatically listens for those events to occur
and updates the bound controls when they fire. Even better, as a developer, you have control over which properties raise the notifications. Sounds perfect, right? Well, it’s not quite perfect. There can be a fair amount of code involved in setting this up for observable models if you are doing it all by hand. Fortunately, there is an open source framework that makes it much simpler, as you shall soon see.

Observable Models and Collections
In this section, you will create an application that uses observable models and collections. To get started, create a new WPF application named WpfNotifications. The application will be a master-detail form, allowing the user to select a specific car using a ComboBox, and then the details for that car will be displayed in the following TextBox controls. Update MainWindow.xaml by replacing the default Grid with the following markup: