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.

发表评论