标签归档:Pro-csharp10-with-net6

Pro C#10 CHAPTER 34 Web Applications using Razor Pages

CHAPTER 34 Web Applications using Razor Pages

This chapter builds on what you learned in the previous chapter and completes the AutoLot.Web Razor Page based application. The underlying architecture for Razor Page based applications is very similar to MVC style applications, with the main difference being that they are page based instead of controller based. This chapter will highlight the differences as the AutoLot.Web application is built, and assumes that you have read the previous chapters on ASP.NET Core.

■Note The sample code for this chapter is in the Chapter 34 directory of this book’s repo. Feel free to continue working with the solution you started in the previous ASP.NET Core chapters.

Anatomy of a Razor Page
Unlike MVC style applications, views in Razor Page based applications are part of the page. To demonstrate, add a new empty Razor Page named RazorSyntax by right clicking the Pages directory in the AutoLot.Web project in Visual Studio, select Add ➤ Razor Page, and chose the Razor Page – Empty template. You will see two files created, RazorSyntax.cshtml and RazorSyntax.cshtml.cs. The RazorSyntax.cshtml file is the view for the page and the RazorSyntax.cshtml.cs file is the code behind file for the view.
Before proceeding, add the following global using statements to the GlobalUsings.cs file in the AutoLot.Web project:

global using AutoLot.Models.Entities; global using Microsoft.AspNetCore.Mvc;
global using Microsoft.AspNetCore.Mvc.RazorPages; global using AutoLot.Services.DataServices.Interfaces; global using Microsoft.Build.Framework;

Razor Page PageModel Classes and Page Handler Methods
The code behind the file for a Razor Page derives from the PageModel base class and is named with the Model suffix, like RazorSyntaxModel. The PageModel base class, like the Controller base class in MVC style applications, provides many helper methods useful for building web applications. Unlike

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

1559

the Controller class, Razor Pages are tied to a single view, have directory structure based routes, and have a single set of page handler methods to service the HTTP get (OnGet()/OnGetAsync()) and post (OnPost()/OnPostAsync()) requests.
Change the scaffolded RazorSyntaxModel class so the OnGet() page handler method is asynchronous and update the name to OnGetAsync(). Next, add an async OnPostAsync() page handler method for HTTP Post requests:

namespace AutoLot.Web.Pages;

public class RazorSyntaxModel : PageModel
{
public async Task OnGetAsync()
{
}
public async Task OnPostAsync()
{
}
}

■Note The default names can be changed. This will be covered later in this chapter.

Notice how the page handler methods don’t return a value like their action method counterparts. When the page handler method does return a value, the page implicitly returns the view that the page is associated with. Razor Page handler methods also support returning an IActionResult, which then requires explicitly returning an IActionResult. If the method is to return the class’s view, the Page() method is returned. The method could also redirect to another page. Both scenarios are shown in this code sample:

public async Task OnGetAsync()
{
return Page();
}
public async Task OnPostAsync()
{
return RedirectToPage("Index")
}

Derived PageModel classes support both method and constructor injection. When using method injection, the parameter must be marked with the [FromService] attribute, like this:

public async Task OnGetAsync([FromServices] ICarDataService carDataService)
{
//Get a car instance
}

Since PageModel classes are focused on a single view, it is more common to use constructor injection instead of method injection. Update the RazorSyntaxModel class by adding a constructor that takes an instance of the ICarDataService and assigns it to a class level field:

private readonly ICarDataService _carDataService;

public RazorSyntaxModel(ICarDataService carDataService)
{
_carDataService = carDataService;
}

If you inspect the Page() method, you will see that there isn’t an overload that takes an object. While the related View() method in MVC is used to pass the model to the view, Razor Pages use properties on the PageModel class to send data to the view. Add a new public property named Entity of type Car to the RazorSyntaxModel class:

public Car Entity { get; set; }

Now, use the data service to get a Car record and assign it to the public property (if the UseApi flag in
appsettings.Development.json is set to true, make sure AutoLot.Api is running):

public async Task OnGetAsync()
{
Entity = await _carDataService.FindAsync(6); return Page();
}

Razor Pages can use implicit binding to get data from a view, just like MVC action methods:

public async Task OnPostAsync(Car entity)
{
//do something interesting return RedirectToPage("Index");
}

Razor Pages also support explicit binding:

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

However, the common practice is to declare the property used by the HTTP get method as a
BindProperty:

[BindProperty]
public Car Entity { get; set; }

This property will then be implicitly bound during HTTP post requests, and the
OnPost()/OnPostAsync() methods use the bound property:

public async Task OnPostAsync()
{
await _carDataService.UpdateAsync(Entity); return RedirectToPage("Index");
}

Razor Page Views
Razor Pages views are specific for a Razor PageModel, begin with the @page directive and are typed to the code behind file, like this for the scaffolded RazorSyntax page:

@page
@model AutoLot.Web.Pages.RazorSyntaxModel @{
}

Note that the view is not bound to the BindProperty (if one exists), but rather the PageModel derived class. The properties on the PageModel derived class (like the Entity property on the RazorSyntax page) are an extension of the @Model. To create the form necessary to test the different binding scenarios, add the following to the RazorSyntax.cshtml view, run the app, and navigate to https://localhost:5021/ RazorSyntax:

Razor Syntax










Note that the property doesn’t need to be a BindProperty to access the values in the view. It only needs to be a BindProperty for the HTTP post method to implicitly bind the values.
Just like with MVC based applications, HTML, CSS, JavaScript, and Razor all work together in Razor Page views. All of the basic Razor syntax explored in the previous chapter is supported in Razor Page views, including tag helpers and HTML helpers. The only difference is in referring to the properties on the model, as previously demonstrated. To confirm this, update the view by adding the following from the Chapter 33 example, with the changes in bold (for the full discussion on the syntax, please refer to the previous chapter):

Razor Syntax

@for (int i = 0; i < 15; i++)
{
//do something
}
@{
//Code Block
var foo = "Foo"; var bar = "Bar";
var htmlString = "

  • one
  • two

";
}
@foo
@htmlString
@foo.@bar
@foo.ToUpper()
@Html.Raw(htmlString)


@{
@:Straight Text

Value:@Model.Entity.Id


Lines without HTML tag

}


Email Address Handling:
[email protected]
@@foo
test@foo
test@(foo)
@
Multiline Comments Hi.
@
@functions {
public static IList SortList(IList strings) { var list = from s in strings orderby s select s;
return list.ToList();
}
}
@{
var myList = new List {"C", "A", "Z", "F"}; var sortedList = SortList(myList);
}
@foreach (string s in sortedList)
{
@s@: 
}


This will be bold: @b("Foo")


The Car named @Model.Entity.PetName is a @Model. Entity.Color@Model.Entity.MakeNavigation.Name


Display For examples Make:
@Html.DisplayFor(x=>x.Entity.MakeNavigation) Car:
@Html.DisplayFor(c=>c.Entity) @Html.EditorFor(c=>c.Entity)
Note the change in the last two lines. The _DisplayForModel()/EditorForModel() methods behave differently in Razor Pages since the view is bound to the PageModel, and not the entity/viewmodel like in MVC applications.

Razor Views
MVC style razor views (without a derived PageModel class as the code behind) and partial views are also supported in Razor Page applications. This includes the _ViewStart.cshtml, _ViewImports.cshtml (both in the \Pages directory) and the _Layout.cshtml files, located in the Pages\Shared directory. All three provide the same functionality as in MVC based applications. Layouts with Razor Pages will be covered shortly.

The _ViewStart and _ViewImports Views
The _ViewStart.cshtml executes its code before any other Razor Page view is rendered and is used to set the default layout. The _ViewStart.cshtml file is shown here:

@{
Layout = "_Layout";
}

The _ViewImports.cshtml file is used for importing shared directives, like @using statements. The contents apply to all views in the same directory or subdirectory of the _ViewImports file. This file is the view equivalent of a GlobalUsings.cs file for C# code.

@using AutoLot.Web @namespace AutoLot.Web.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

The @namespace declaration defines the default namespace where the application’s pages are located.

The Shared Directory
The Shared directory under Pages holds partial views, display and editor templates, and layouts that are available to all Razor Pages.

The DisplayTemplates Folder
Display templates work the same in MVC and Razor Pages. They are placed in a directory named DisplayTemplates and control how types are rendered when the DisplayFor() method is called. The search path starts in the Pages{CurrentPageRoute}\DisplayTemplates directory and, if it’s not found, it then looks in the Pages\Shared\DisplayTemplates folder. Just like with MVC, the engine looks for a template with the same name as the type being rendered or looks to a template that matches the name passed into the method.

The DateTime Display Template
Create a new folder named DisplayTemplates under the Pages\Shared folder. Add a new view named DateTime.cshtml into that folder. Clear out all of the generated code and comments and replace them with the following:

@model DateTime? @if (Model == null)
{
@:Unknown
}
else
{
if (ViewData.ModelMetadata.IsNullableValueType)
{
@:@(Model.Value.ToString("d"))
}
else
{
@:@(((DateTime)Model).ToString("d"))
}
}

Note that the @model directive that strongly types the view uses a lowercase m. When referring to the assigned value of the model in Razor, an uppercase M is used. In this example, the model definition is nullable. If the value for the model passed into the view is null, the template displays the word Unknown. Otherwise, it displays the date in Short Date format, using the Value property of a nullable type or the actual model itself.
With this template in place, if you run the application and navigate to the RazorSyntax page, you can see that the BuiltDate value is formatted as a Short Date.

The Car Display Template
Create a new directory named Cars under the Pages directory, and add a directory named DisplayTemplates under the Cars directory. Add a new view named Car.cshtml into that folder. Clear out all of the generated code and comments and replace them with the following code, which displays a Car entity:

@model AutoLot.Models.Entities.Car

@Html.DisplayNameFor(model => model.MakeId)
@Html.DisplayFor(model => model.MakeNavigation.Name)
@Html.DisplayNameFor(model => model.Color)
@Html.DisplayFor(model => model.Color)
@Html.DisplayNameFor(model => model.PetName)
@Html.DisplayFor(model => model.PetName)
@Html.DisplayNameFor(model => model.Price)
@Html.DisplayFor(model => model.Price)
@Html.DisplayNameFor(model => model.DateBuilt)
@Html.DisplayFor(model => model.DateBuilt)
@Html.DisplayNameFor(model => model.IsDrivable)
@Html.DisplayFor(model => model.IsDrivable)

The DisplayNameFor() HTML helper displays the name of the property unless the property is decorated with either the Display(Name="") or DisplayName("") attribute, in which case the display value is used. The DisplayFor() method displays the value for the model’s property specified in the expression. Notice that the navigation property for MakeNavigation is being used to get the make name.
To use a template from another directory structure, you have to specify the name of the view as well as the full path and file extension. To use this template on the RazorSyntax view, update the DisplayFor() method to the following:

@Html.DisplayFor(c=>c.Entity,"Cars/DisplayTemplates/Car.cshtml")

Another option is to move the display templates to the Pages\Shared\DisplayTemplates directory

The Car with Color Display Template
Copy the Car.cshtml view to another view named CarWithColors.cshtml in the Cars\DisplayTemplates directory. The difference is that this template changes the color of the Color text based on the model’s Color property value. Update the new template’s

tag for Color to the following:

@Html.DisplayFor(model => model.Color)

The EditorTemplates Folder
The EditorTemplates folder works the same as the DisplayTemplates folder, except the templates are used for editing.

The Car Edit Template
Create a new directory named EditorTemplates under the Pages\Cars directory. Add a new view named Car.cshtml into that folder. Clear out all of the generated code and comments and replace them with the following code, which represents the markup to edit a Car entity:

@model Car













Editor templates are invoked with the EditorFor() HTML helper. To use this with the RazorSyntax
page, update the call to EditorFor() to the following:

@Html.EditorFor(c=>c.Entity,"Cars/EditorTemplates/Car.cshtml")

View CSS Isolation
Razor Pages also support CSS isolation. Right click on the \Pages directory, and select Add ➤ New Item, navigate to ASP.NET Core/Web/Content in the left rail, and select Style Sheet and name it Index.cshtml. css. Update the content to the following:

h1 {
background-color: blue;
}

This change makes the tag on the Index Razor Page blue but doesn’t affect any other pages.
The same rules apply in Razor Pages as MVC with view CSS isolation: the CSS file is only generated when running in Development or when the site is published. To see the CSS in other environments, you have to opt-in:

//Enable CSS isolation in a non-deployed session if (!builder.Environment.IsDevelopment())
{
builder.WebHost.UseWebRoot("wwwroot"); builder.WebHost.UseStaticWebAssets();
}

Layouts
Layouts in Razor Pages function the same as they do in MVC applications, except they are located in Pages\Shared and not Views\Shared. _ViewStart.cshtml is used to specify the default layout for a directory structure, and Razor Page views can explicitly define their layout using a Razor block:

@{
Layout = "_MyCustomLayout";
}

Injecting Data
Add the following to the top of the _Layout.cshtml file, which injects the IWebHostEnvironment: @inject IWebHostEnvironment _env

Next, update the footer to show the environment that the application is currently running in:

© 2021 – AutoLot.Web – @_env.EnvironmentName – Privacy

Partial Views
The main difference with partial views in Razor Pages is that a Razor Page view can’t be rendered as a partial. In Razor Pages, they are used to encapsulate UI elements and are loaded from another view or a view component. Next, we are going to split the layout into partials to make the markup easier to maintain.

Create the Partials
Create a new directory named Partials under the Shared directory. Right click on the new directory and select Add ➤ New Item. Enter Razor View in the search box and select Razor View -Empty. Create three empty views named _Head.cshtml, _JavaScriptFiles.cshtml, and _Menu.cshtml.
Cut the content in the layout that is between the tags and paste it into the _Head.cshtml
file. In _Layout.cshtml, replace the deleted markup with the call to render the new partial:

For the menu partial, cut all the markup between the

tags (not the
tags) and paste it into the _Menu.cshtml file. Update the _Layout to render the Menu partial.

The final step at this time is to cut out the






The final change is to update the location of jquery.validation and add the and






Add and Configure WebOptimizer
Open the Program.cs file in the AutoLot.Web project and add the following line (just before the app. UseStaticFiles() call):

app.UseWebOptimizer();

The next step is to configure what to minimize and bundle. The open source libraries already have the minified versions downloaded through Library Manager, so the only files that need to be minified are the project specific files, including the generated CSS file if you are using CSS isolation. In the Program.cs file, add the following code block before var app = builder.Build():

if (builder.Environment.IsDevelopment() || builder.Environment.IsEnvironment("Local"))
{
builder.Services.AddWebOptimizer(false,false);
/*
builder.Services.AddWebOptimizer(options =>
{

});
*/
}

options.MinifyCssFiles("AutoLot.Web.styles.css"); options.MinifyCssFiles("css/site.css"); options.MinifyJsFiles("js/site.js");

else
{
builder.Services.AddWebOptimizer(options =>
{
options.MinifyCssFiles("AutoLot.Web.styles.css"); options.MinifyCssFiles("css/site.css"); options.MinifyJsFiles("js/site.js");
});
}

In the development scope, the code is setup for you to comment/uncomment the different options so you can replicate the production environment without switching to production.
The final step is to add the WebOptimizer tag helpers into the system. Add the following line to the end of the _ViewImports.cshtml file:

@addTagHelper *, WebOptimizer.Core

Tag Helpers
Razor Pages views (and layout and partial views) also support Tag helpers. They function the same as in MVC applications, with only a few differences. Any tag helper that is involved in routing uses page-
centric attributes instead of MVC centric attributes. Table 34-1 lists the tag helpers that use routing, their corresponding HTML helper, and the available Razor Page attributes. The differences will be covered in detail after the table.

Table 34-1. Commonly Used Built-in Tag Helpers

Tag Helper HTML Helper Available Attributes
Form Html.BeginForm Html.BeginRouteForm Html.AntiForgeryToken asp-route—for named routes (can’t be used with controller, page, or action attributes).asp-antiforgery—if the antiforgery should be added (true by default).asp-area—the name of the area.asp- route-—adds the parameter to the route, e.g., asp-route-id="1".asp-page—the name of the Razor Page.asp- page-handler—the name of the Razor Page handler.asp-all- route-data—dictionary for additional route values.
Form Action (button
or input type=image) N/A Asp-route—for named routes (can’t be used with controller, page, or action attributes).asp-antiforgery—if the antiforgery should be added (true by default).asp-area—the name of the area. asp- route-—adds the parameter to the route, e.g., asp-route-id="1".asp-page—the name of the Razor Page.asp- page-handler—the name of the Razor Page handler.asp-all- route-data—dictionary for additional route values.
Anchor Html.ActionLink asp-route—for named routes (can’t be used with controller, page, or action attributes). asp-area—the name of the area. asp- protocol—HTTP or HTTPS.asp-fragment—URL fragment.asp- host—the host name.asp-route-—adds the parameter to the route, e.g., asp-route-id="1".
asp-page—the name of the Razor Page.
asp-page-handler—the name of the Razor Page handler.asp- all-route-data—dictionary for additional route values.

Enabling Tag Helpers
Tag helpers must be enabled in your project in the _ViewImports.html file by adding the following line (which was added by the default template):

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

The Form Tag Helper
With Razor Pages, the

tag helper uses asp-page instead of asp-controller and asp-action:


Another option available is to specify the name of the page handler method. When modifying the name, the format On() (OnPostCreateNewCar()) or OnAsync() (OnPostCreateNewCarAsync()) must be followed. With the HTTP post method renamed, the handler method is specified like this:


All Razor Page HTTP post handler methods automatically check for the antiforgery token, which is added whenever a tag helper is used.

The Form Action Button/Image Tag Helper
The form action tag helper is used on buttons and images to change the action for the form that contains them and supports the asp-page and asp-page-handler attributes in the same manner as the

tag helper.

The Anchor Tag Helper
The tag helper replaces the Html.ActionLink HTML helper and uses many of the same routing tags as the

tag helper. For example, to create a link for the RazorSyntax view, use the following code:

Razor Syntax

To add the navigation menu item for the RazorSyntax page, update the _Menu.cshtml to the following, adding the new menu item between the Home and Privacy menu items:

...

The anchor tag helper can be combined with the views model. For example, using the Car instance in the RazorSyntax page, the following anchor tag routes to the Details page passing in the Id as the route parameter:

@Model.Entity.PetName

Custom Tag Helpers
Building custom tag helpers for Razor Pages is very similar to building them for MVC apps. They both inherit from TagHelper and must implement the Process() method. For AutoLot.Web, the difference from the MVC version is how the links are created since routing is different. Before starting, add the following global using statement to the GlobalUsings.cs file:

global using Microsoft.AspNetCore.Mvc.Infrastructure; global using Microsoft.AspNetCore.Mvc.Routing;
global using Microsoft.AspNetCore.Razor.TagHelpers;
global using Microsoft.Extensions.DependencyInjection.Extensions;

Update Program.cs
Once again we need to use an UrlHelperFactory and IActionContextAccessor to create the links based on routing. To create an instance of the UrlFactory from a non-PageModel-derived class, the
IActionContextAccessor must be added to the services collection. Call the following line in Program.cs to add the IActionContextAccessor into the services collection:

builder.Services.TryAddSingleton();

Create the Base Class
Create a new folder named TagHelpers in the root of the AutoLot.Web project. In this folder, create a new folder named Base, and in that folder, create a class named ItemLinkTagHelperBase.cs, make the class public and abstract, and inherit from TagHelper:

namespace AutoLot.Web.TagHelpers.Base;

public abstract class ItemLinkTagHelperBase : TagHelper
{
}

Add a constructor that takes instances of IActionContextAccessor and IUrlHelperFactory. Use the UrlHelperFactory with the ActionContextAccessor to create an instance of IUrlHelper, and store that in a class-level variable. The code is shown here:

protected readonly IUrlHelper UrlHelper;
protected ItemLinkTagHelperBase(IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)
{
UrlHelper = urlHelperFactory.GetUrlHelper(contextAccessor.ActionContext);
}

In the constructor, use the contextAccessor instance to get the current Page and assign it to a class level field. The page route value is in the form of /, like Cars/Index: The string is split to get only the directory name:

protected readonly IUrlHelper UrlHelper;
private readonly string _pageName;
protected ItemLinkTagHelperBase(IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)
{

UrlHelper = urlHelperFactory.GetUrlHelper(contextAccessor.ActionContext);
_pageName = contextAccessor.ActionContext.ActionDescriptor.
RouteValues["page"]?.Split("/",StringSplitOptions.RemoveEmptyEntries)[0];
}

Add a protected property so the derived classes can indicate the action name for the route:

protected string ActionName { get; set; }

Add a single public property to hold the Id of the item, as follows:

public int? ItemId { get; set; }

As a reminder, public properties on custom tag helpers are exposed as HTML attributes on the tag. The naming convention is that the property name is converted to lower-kabob-casing. This means every capital letter is lower cased and dashes (-) are inserted before each letter that is changed to lower case (except for the first one). This converts ItemId to item-id (like words on a shish-kabob).
The BuildContent() method is called by the derived classes to build the HTML that gets rendered instead of the tag helper:

protected void BuildContent(
TagHelperOutput output, string cssClassName, string displayText, string fontAwesomeName)
{
output.TagName = "a";
var target = (ItemId.HasValue)
? UrlHelper.Page($"/{_pageName}/{ActionName}", new { id = ItemId })
: UrlHelper.Page($"/{_pageName}/{ActionName}"); output.Attributes.SetAttribute("href", target); output.Attributes.Add("class",cssClassName);
output.Content.AppendHtml($@"{displayText} ");
}

The first line changes the tag to the anchor tag. The next uses the UrlHelper.Page() static method to generate the route, including the route parameter if one exists. The next two set the HREF of the anchor tag to the generated route and add the CSS class name. The final line adds the display text and a Font Awesome font as the text that is displayed to the user.
As the final step, add the following global using statement to the GlobalUsings.cs file:

global using AutoLot.Web.TagHelpers.Base;

The Item Details Tag Helper
Create a new class named ItemDetailsTagHelper.cs in the TagHelpers folder. Make the class public and inherit from ItemLinkTagHelperBase.

namespace AutoLot.Web.TagHelpers;

public class ItemDetailsTagHelper : ItemLinkTagHelperBase
{
}

Add a constructor to take in the required object instances and pass them to the base class. The constructor also needs to assign the ActionName:

public ItemDetailsTagHelper( IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)
: base(contextAccessor, urlHelperFactory)
{
ActionName = "Details";
}

Override the Process() method, calling the BuildContent() method in the base class.

public override void Process(TagHelperContext context, TagHelperOutput output)
{
BuildContent(output, "text-info", "Details", "info-circle");
}

This creates a Details link using the CSS class text-info, the text of Details with the Font Awesome info image:

Details

When invoking tag helpers, the TagHelper suffix is dropped, and the remaining name of class is lower- kebob-cased. In this case, the HTML tag is . The asp-route-id value comes from the item- id attribute on the tag helper:

The Item Delete Tag Helper
Create a new class named ItemDeleteTagHelper.cs in the TagHelpers folder. Make the class public and inherit from ItemLinkTagHelperBase. Add the constructor to take in the required object instances and set the ActionName using the DeleteAsync() method name:

public ItemDeleteTagHelper( IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)
: base(contextAccessor, urlHelperFactory)
{
ActionName = "Delete";
}

Override the Process() method, calling the BuildContent() method in the base class.

public override void Process(TagHelperContext context, TagHelperOutput output)
{
BuildContent(output,"text-danger","Delete","trash");
}

This creates the Delete link with the Font Awesome garbage can image.

Delete

The asp-route-id value comes from the item-id attribute on the tag helper:

The Item Edit Tag Helper
Create a new class named ItemEditTagHelper.cs in the TagHelpers folder. Make the class public, inherit from ItemLinkTagHelperBase and add the constructor that assigns Edit as the ActionName:

namespace AutoLot.Web.TagHelpers;

public class ItemEditTagHelper : ItemLinkTagHelperBase
{
public ItemEditTagHelper( IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)
: base(contextAccessor, urlHelperFactory)
{
ActionName = "Edit";
}
}

Override the Process() method, calling the BuildContent() method in the base class.

public override void Process(TagHelperContext context, TagHelperOutput output)
{
BuildContent(output,"text-warning","Edit","edit");
}

This creates the Edit link with the Font Awesome pencil image:

Edit

The asp-route-id value comes from the item-id attribute on the tag helper:

The Item Create Tag Helper
Create a new class named ItemCreateTagHelper.cs in the TagHelpers folder. Make the class public, inherit from ItemLinkTagHelperBase and add the constructor that assigns Create as the ActionName:

namespace AutoLot.Web.TagHelpers;

public class ItemCreateTagHelper : ItemLinkTagHelperBase
{
public ItemCreateTagHelper( IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)
: base(contextAccessor, urlHelperFactory)
{
ActionName = "Create";
}

}

Override the Process() method, calling the BuildContent() method in the base class.

public override void Process(TagHelperContext context, TagHelperOutput output)
{
BuildContent(output,"text-success","Create New","plus");
}

This creates the Create link with the Font Awesome plus image:

Create New

There isn’t a route parameter with the Create action:

The Item List Tag Helper
Create a new class named ItemListTagHelper.cs in the TagHelpers folder. Make the class public, inherit from ItemLinkTagHelperBase and add the constructor that assigns List as the ActionName:

namespace AutoLot.Web.TagHelpers;

public class ItemListTagHelper : ItemLinkTagHelperBase
{
public ItemListTagHelper( IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)
: base(contextAccessor, urlHelperFactory)
{
ActionName = "IndexAsync";
}
}

Override the Process() method, calling the BuildContent() method in the base class.

public override void Process(TagHelperContext context, TagHelperOutput output)
{
BuildContent(output,"text-default","Back to List","list");
}

This creates the Index link with the Font Awesome plus image:

There isn’t a route parameter with the Create action:

Making Custom Tag Helpers Visible
To make custom tag helpers visible, the @addTagHelper command must be executed for any views that use the tag helpers or are added to the _ViewImports.cshtml file. Open the _ViewImports.cshtml file in the root of the Views folder and add the following line:

@addTagHelper *, AutoLot.Web

The Cars Razor Pages
Next, we are going to create a base class that handles the common code across all pages. Before beginning, add the following global using statement to the GlobalUsings.cs file:

global using AutoLot.Models.Entities.Base;

The BasePageModel Class
Add a new directory named Base in the Pages directory. In that new directory, add a new class named BasePageModel. Make it abstract and generic (taking an entity type for data access and a class type for logging) and inherit from PageModel:

namespace AutoLot.Web.Pages.Base;

public abstract class BasePageModel : PageModel where TEntity : BaseEntity, new()
{
}

Next, add a protected constructor that takes an instance of IAppLogging, an instance of the IDataServiceBase, and a string for the page’s title. The interface instances get assigned to protected class fields, and the string gets assign to the Title ViewData property.

protected readonly IAppLogging AppLoggingInstance; protected readonly IDataServiceBase DataService;

[ViewData]
public string Title { get; init; }

protected BasePageModel( IAppLogging appLogging,

IDataServiceBase dataService, string pageTitle)
{
AppLoggingInstance = appLogging; DataService = dataService;
Title = pageTitle;
}

The base class has three public properties. An TEntity instance that is the BindProperty, a SelectList
for lookup values, and an Error property to display a message in an error banner in the view:

[BindProperty]
public TEntity Entity { get; set; }
public SelectList LookupValues { get; set; } public string Error { get; set; }

Next, add a method that takes in an instance of the IDataServiceBase, the dataValue and dataText
property names, and builds the SelectList:

protected async Task GetLookupValuesAsync( IDataServiceBase lookupService, string lookupKey, string lookupDisplay) where TLookupEntity : BaseEntity, new()
{
LookupValues = new(await lookupService.GetAllAsync(), lookupKey, lookupDisplay);
}

The GetOneAsync() method attempts to get a TEntity record by Id. If the id parameter is null or the record can’t be located, the Error property is set. Otherwise, it assigns the record to the Entity BindProperty:

protected async Task GetOneAsync(int? id)
{
if (!id.HasValue)
{
Error = "Invalid request"; Entity = null;
return;
}
Entity = await DataService.FindAsync(id.Value); if (Entity == null)
{
Error = "Not found"; return;
}
Error = string.Empty;
}

The SaveOneAsync() method checks for ModelState validity, then attempts to save or update a record. If ModelState is invalid, the data is displayed in the view for the user to correct. If an error happens during the save/update call, the exception message is added to the Error property and ModelState, and then the view is returned to the user. The method takes in a Func> so it can be called for both AddAsync() and UpdateAsync():

protected virtual async Task SaveOneAsync(Func> persistenceTask)
{
if (!ModelState.IsValid)
{
return Page();
}
try
{
await persistenceTask(Entity, true);
}
catch (Exception ex)
{
Error = ex.Message; ModelState.AddModelError(string.Empty, ex.Message); AppLoggingInstance.LogAppError(ex, "An error occurred"); return Page();
}
return RedirectToPage("./Details", new { id = Entity.Id });
}

The SaveWithLookupAsync() method does the same process as the SaveOneAsync(), but it also repopulates the SelectList when necessary. It takes in the data service to get the data for the lookup values, and the dataValue and dataText property names to build the SelectList:

protected virtual async Task SaveWithLookupAsync( Func> persistenceTask,
IDataServiceBase lookupService, string lookupKey, string lookupDisplay) where TLookupEntity : BaseEntity, new()
{
if (!ModelState.IsValid)
{
await GetLookupValuesAsync(lookupService, lookupKey, lookupDisplay); return Page();
}
try
{
await persistenceTask(Entity, true);
}
catch (Exception ex)
{
Error = ex.Message; ModelState.AddModelError(string.Empty, ex.Message);
await GetLookupValuesAsync(lookupService, lookupKey, lookupDisplay); AppLoggingInstance.LogAppError(ex, "An error occurred");
return Page();
}
return RedirectToPage("./Details", new { id = Entity.Id });
}

The DeleteOneAsync() method functions the same way as the Delete() HTTP Post method in the MVC version of AutoLot. The view is streamlined to only send the values needed by EF Core to delete a record, which is the Id and TimeStamp. If the deletion fails for some reason, ModelState is cleared, the ChangeTracker is reset, the entity is retrieved, and the Error property is set to the exception message:

public async Task DeleteOneAsync(int? id)
{
if (!id.HasValue || id.Value != Entity.Id)
{
Error = "Bad Request"; return Page();
}
try
{
await DataService.DeleteAsync(Entity); return RedirectToPage("./Index");
}
catch (Exception ex)
{
ModelState.Clear(); DataService.ResetChangeTracker();
Entity = await DataService.FindAsync(id.Value); Error = ex.Message;
AppLoggingInstance.LogAppError(ex, "An error occurred"); return Page();
}
}

Finally, add the following global using statement to the GlobalUsings.cs file:

global using AutoLot.Web.Pages.Base;

The Index Razor Page
The Index page will show a list of Car records and provide links to the other CRUD pages. The list will either be all of Car records in inventory, or just those with a certain Make value. Recall that in Razor Page routing, the Index Razor page is the default for a directory, reachable from both the /Cars and /Cars/Index URLs, so no additional routing is needed (unlike in the MVC version).
Start by adding an empty Razor Page named Index.cshtml to the Pages\Cars directory. The Index page doesn’t need any of the functionality of the BasePageModel class, so leave it as inheriting from PageModel.
Add a constructor that receives instances of IAppLogging and the ICarDataService and assigns them to class level fields:
namespace AutoLot.Web.Pages.Cars; public class IndexModel : PageModel
{
private readonly IAppLogging _appLogging; private readonly ICarDataService _carService;
public IndexModel(IAppLogging appLogging, ICarDataService carService)
{

_appLogging = appLogging;
_carService = carService;
}
}

Add three public properties on the class. Two hold the MakeName and MakeId properties used by the list of cars by Make, and the third holds the actual list of Car records. Note that it isn’t a BindProperty since there won’t be any HTTP post requests for the Index page.

public string MakeName { get; set; } public int? MakeId { get; set; }
public IEnumerable CarRecords { get; set; }

The HTTP get method takes in optional parameters for makeId and makeName, then sets the public properties to those parameter values (even if they are null). The parameters are part of the route, which will be updated with the view. It then calls into the GetAllByMakeIdAsync() method of the data service, which will return all records if the makeId is null, otherwise it will return just the Car records to that Make:

public async Task OnGetAsync(int? makeId, string makeName)
{
MakeId = makeId;
MakeName = makeName;
CarRecords = await _carService.GetAllByMakeIdAsync(makeId);
}

The Car List Partial View
There are two views available for the Index page. One shows the entire inventory of cars and one shows the list of cars by make. Since the UI is the same, the lists will be rendered using a partial view. This partial view is the same as for the MVC application, demonstrating the cross framework support for partial views.
Create a new directory named Partials under the Pages\Cars directory. In this directory, add a new view named _CarListPartial.cshtml, and clear out the existing code. Set IEnumerable as the type and add a Razor block to determine if the Makes should be displayed. When this partial is used by the entire inventory list, the Makes should be displayed. When it is showing only a single Make, the Make field should be hidden as it will be in the header of the page.

@model IEnumerable< Car>

@{
var showMake = true;
if (bool.TryParse(ViewBag.ByMake?.ToString(), out bool byMake))
{
showMake = !byMake;
}
}

The next markup uses the ItemCreateTagHelper to create a link to the Create HTTP Get method (recall that tag helpers are lower-kebab-cased when used in Razor views). In the table headers, a Razor HTML helper is used to get the DisplayName for each of the properties. This section uses a Razor block to show the Make information based on the view-level variable set earlier.

@if (showMake)
{

}

The final section loops through the records and displays the table records using the DisplayFor Razor HTML helper. This block also uses the item-edit, item-details, and item-delete custom tag helpers.

@foreach (var item in Model)
{

@if (showMake)
{

}

}

@Html.DisplayNameFor(model => model.MakeId) @Html.DisplayNameFor(model => model.Color) @Html.DisplayNameFor(model => model.PetName) @Html.DisplayNameFor(model => model.Price) @Html.DisplayNameFor(model => model.DateBuilt) @Html.DisplayNameFor(model => model.IsDrivable)
@Html.DisplayFor(modelItem => item.MakeNavigation.Name) @Html.DisplayFor(modelItem => item.Color) @Html.DisplayFor(modelItem => item.PetName) @Html.DisplayFor(modelItem => item.Price) @Html.DisplayFor(modelItem => item.DateBuilt) @Html.DisplayFor(modelItem => item.IsDrivable) |
|

The Index Razor Page View
With the _CarListPartial partial in place, the Index Razor Page view is quite small, demonstrating the benefit of using partial views to cut down on repetitive markup. The first step is to update the route information by adding two optional route tokens to the @page directive:

@page "{makeId?}/{makeName?}"
@model AutoLot.Web.Pages.Cars.IndexModel

The next step is to determine if the PageModel’s MakeId nullable in has a value. Recall that the MakeId and MakeName are update in the OnGetAsync() method based on the route parameters. If there is a value, display the MakeName in the header, and create a new ViewDataDictionary containing the ByMake property. This is then passed into the partial, along with the CarRecords model property, both of which are used by the _CarListPartial partial view. If MakeId doesn’t have a value, invoke the _CarListPartial partial view with the CarRecords property but without the ViewDataDictionary:

@{
if (Model.MakeId.HasValue)
{

Vehicle Inventory for @Model.MakeName

var mode = new ViewDataDictionary(ViewData) { { "ByMake", true } }; }
else
{

Vehicle Inventory

}
}

To see this view in action, run the application and navigate to https://localhost:5001/Cars/Index (or https://localhost:5001/Cars) to see the full list of vehicles. To see the list of BMW’s, navigate to https://localhost:5001/Cars/Index/5/BMW (or https://localhost:5001/Cars/5/BMW).

The Details Razor Page
The Details page is used to display a single record when called with an HTTP get request. The route is extended with an optional id value. Update the code to the following, which takes advantage of the BasePageModel class:

namespace AutoLot.Web.Pages.Cars;

public class DetailsModel : BasePageModel
{

public DetailsModel( IAppLogging appLogging,
ICarDataService carService) : base(appLogging, carService, "Details") { } public async Task OnGetAsync(int? id)

{
await GetOneAsync(id);
}
}

The constructor takes instances of IAppLogging and ICarDataService and passes them to the base class along with the page title. The OnGetAsync() page handler method takes in the optional route parameter then calls the base GetOneAsync() method. Since the method doesn’t return an IActionResult, the view gets rendered when the method completes.

The Details Razor Page View
The first step is to update the route to include the optional id route token and add a header:

@page "{id?}"
@model AutoLot.Web.Pages.Cars.DetailsModel

Details for @Model.Entity.PetName

If there is a value in the Errors property, then the message needs to be displayed in a banner. If there isn’t an error value, then use the Car display template to display the records information. Close out the view with the custom navigation tag helpers:

@if (!string.IsNullOrEmpty(Model.Error))
{

}
else
{
@Html.DisplayFor(m => m.Entity)

|
|

}

The @Html.DisplayFor() line can be replaced with @Html.DisplayFor(m=>m.Entity,"CarWithColors")
to display the template that uses color in the display.

The Create Razor Page
The Create Razor Page inherits from BasePageModel. Clear out the scaffolded code and replace it with the following:

namespace AutoLot.Web.Pages.Cars;

public class CreateModel : BasePageModel
{
//implementation goes here
}

In addition to the IAppLogging and ICarDataService for the base class, the constructor takes an instance of the IMakeDataService and assigns it to a class level field:

private readonly IMakeDataService _makeService;
public CreateModel( IAppLogging appLogging, ICarDataService carService,
IMakeDataService makeService) : base(appLogging, carService, "Create")
{
_makeService = makeService;
}

The HTTP get handler method populates the LookupValues property. Since there isn’t a return value, the view is rendered when the method ends:

public async Task OnGetAsync()
{
await GetLookupValuesAsync(_makeService, nameof(Make.Id), nameof(Make.Name));
}

The HTTP post handler method uses the base SaveWithLookupAsync() method and then returns the IActionResult from the base method. Note the non-standard name of the method. This will be addressed in the view form with the

tag helper:

public async Task OnPostCreateNewCarAsync()
{
return await SaveWithLookupAsync( DataService.AddAsync,
_makeService, nameof(Make.Id), nameof(Make.Name));
}

The Create Razor Page View
The view uses the base route, so no changes are needed on the @page directive. Add the head and the error block:

@page
@model AutoLot.Web.Pages.Cars.CreateModel

Create a New Car


@if (!string.IsNullOrEmpty(Model.Error))
{

}
else
{
}

The

tag uses two tag helpers. The asp-page helper set the form’s action to post back to the Create route (in the current directory, which is Cars). The asp-page-handler tag helper specifies the method name, less the OnPost prefix and Async suffix. Remember that if

tag helpers are used, the anti-forgery token is automatically added to the form data:

else
{

}

The contents of the form is mostly layout. The two lines of note are the asp-validation-summary tag helper and the EditorFor() HTML helper. The EditorFor() method invokes the editor template for the Car class. The second parameter adds the SelectList into the ViewBag. The validation summary shows only model level errors since the editor template shows field level errors:

@Html.EditorFor(x => x.Entity, new { LookupValues = Model.LookupValues })

  |  

The final update is to add the _ValidationScriptsPartial partial in the Scripts section. Recall that in the layout this section occurs after loading jQuery. The sections pattern helps ensure that the proper dependencies are loaded before the contents of the section:

@section Scripts { }

The create form can be viewed at /Cars/Create.

The Edit Razor Page
The Edit Razor Page follows the same pattern as the Create Razor Page. It inherits from BasePageModel. Clear out the scaffolded code and replace it with the following:

namespace AutoLot.Web.Pages.Cars;

public class EditModel : BasePageModel
{
//implementation goes here
}

In addition to the IAppLogging and ICarDataService for the base class, the constructor takes an instance of the IMakeDataService and assigns it to a class level field:

private readonly IMakeDataService _makeService;
public EditModel( IAppLogging appLogging, ICarDataService carService,
IMakeDataService makeService) : base(appLogging, carService, "Edit")
{
_makeService = makeService;
}

The HTTP get handler method populates the LookupValues property and attempts to get the entity.
Since there isn’t a return value, the view is rendered when the method ends:

public async Task OnGetAsync(int? id)
{
await GetLookupValuesAsync(_makeService, nameof(Make.Id), nameof(Make.Name)); GetOneAsync(id);
}

The HTTP post handler method uses the base SaveWithLookupAsync() method and then returns the
IActionResult from the base method:

public async Task OnPostAsync()
{
return await SaveWithLookupAsync( DataService.UpdateAsync,
_makeService, nameof(Make.Id), nameof(Make.Name));
}

The Edit Razor Page View
The view takes in an optional id as a route token, which gets added to the @page directive. Update the directive and add the head and the error block:

@page "{id?}"
@model AutoLot.Web.Pages.Cars.EditModel

Edit @Model.Entity.PetName


@if (!string.IsNullOrEmpty(Model.Error))
{

}
else
{
}

The

tag uses two tag helpers. The asp-page helper set the form’s action to post back to the Edit route (in the current directory, which is Cars). The asp-route-id tag helper specifies the value for the id route parameter:

else
{

}

The contents of the form is mostly layout. The four lines of note are the asp-validation-summary tag helper, the EditorFor() HTML helper, and the two hidden input tags. The EditorFor() method invokes the editor template for the Car class. The second parameter adds the SelectList into the ViewBag. The validation summary show only model level errors since the editor template shows field level errors. The two hidden input tags hold the values for the Id and TimeStamp properties, which are required for the update process, but have no meaning to the user:

@Html.EditorFor(x => x.Entity, new { LookupValues = Model.LookupValues })

& nbsp; |  

The final update is to add the _ValidationScriptsPartial partial in the Scripts section. Recall that in the layout this section occurs after loading jQuery. The sections pattern helps ensure that the proper dependencies are loaded before the contents of the section:

@section Scripts { }

The edit form can be viewed at /Cars/Edit/1.

The Delete Razor Page
The Delete razor page inherits from BasePageModel and has a constructor that takes the required two parameters:

namespace AutoLot.Web.Pages.Cars;

public class DeleteModel : BasePageModel
{
public DeleteModel( IAppLogging appLogging,
ICarDataService carService) : base(appLogging, carService, "Delete")
{
}

public async Task OnPostAsync(int? id)
{
return await DeleteOneAsync(id);
}
}

This view doesn’t use the SelectList values, so the HTTP get handler method simply gets the entity.
Since there isn’t a return value for the method, the view is rendered when the method ends:

public async Task OnGetAsync(int? id)
{
await GetOneAsync(id);
}

The HTTP post handler method uses the base DeleteOneAsync() method and then returns the
IActionResult from the base method:

public async Task OnPostAsync(int? id)
{
return await DeleteOneAsync(id)
}

The Delete Razor Page View
The view takes in an optional id as a route token, which gets added to the @page directive. Update the directive and add the title, head, and the error block:

@page "{id?}"
@model AutoLot.Web.Pages.Cars.DeleteModel

Delete @Model.Entity.PetName

@if (!string.IsNullOrEmpty(Model.Error))
{

}
else
{
}

The

tag uses two tag helpers. The asp-page helper set the form’s action to post back to the Delete route (in the current directory, which is Cars). The asp-route-id tag helper specifies the value for the id route parameter:

else
{

}

The view uses the Car display template outside of the form and hidden fields for the Id and TimeStamp properties inside the form. There isn’t a validation summary since any errors in the delete process will show in the error banner:

Are you sure you want to delete this car?

@Html.DisplayFor(c=>c.Entity)



  |  

The _ValidationScriptsPartial partial isn’t needed, so that completed the Delete page view. The delete form can be viewed at /Cars/Delete/5.

View Components
View components in Razor Page based applications are built and function the same as in MVC styled applications. The main difference is where the partial views must be located. To get started in the AutoLot. Web project, add the following global using statement to the GlobalUsings.cs file:

global using Microsoft.AspNetCore.Mvc.ViewComponents;

Create a new folder named ViewComponents in the root directory. Add a new class file named MenuViewComponent.cs into this folder and update the code to the following (the same as was built in the previous chapter).

public class MenuViewComponent : ViewComponent
{
private readonly IMakeDataService _dataService;

public MenuViewComponent(IMakeDataService dataService)
{
_dataService = dataService;
}

public async Task InvokeAsync()
{
var makes = (await _dataService.GetAllAsync()).ToList(); if (!makes.Any())
{
return new ContentViewComponentResult("Unable to get the makes");
}
return View("MenuView", makes);
}
}

Build the Partial View
In Razor Pages, the menu items must use the asp-page anchor tag helper instead of the asp-controller and asp-action tag helpers. Create a new folder named Components under the Pages\Shared folder. In this new folder, create another new folder named Menu. In this folder, create a partial view named MenuView.cshtml. Clear out the existing code and add the following markup:

@model IEnumerable

To invoke the view component with the tag helper syntax, the following line must be added to the
_ViewImports.cshtml file, which was already added for the custom tag helpers:

@addTagHelper *, AutoLot.Web

Finally, open the _Menu.cshtml partial and navigate to just after the

  • block that maps to the
    /Index page. Copy the following markup to the partial:

    Now when you run the application, you will see the Inventory menu with the Makes listed as submenu items.

    Areas
    Areas in Razor Pages are slightly different than in MVC based applications. Since Razor Pages are routed based on directory structure, there isn’t any additional routing configuration to be handled. The only rule is that the pages must go in the Areas\[AreaName]\Pages directory. To add an area in AutoLot.Web, first add a directory named Areas in the root of the project. Next, add a directory named Admin, then add a new directory name Pages. Finally, add a new directory named Makes.

    Area Routing with Razor Pages
    When navigating to Razor Pages in an area, the Areas directory name is omitted. For example, the Index page in the Areas\Admin\Makes\Pages directory can be found at the /Admin/Makes (or Admin/Makes/Index) route.

    _ViewImports and _ViewStart
    In Razor Pages, the _ViewImports.cshtml and _ViewStart.cshtml files apply to all views at the same directory level and below. Move the _ViewImports.cshtml and _ViewStart.cshtml files to the root on the project so they are applied to the entire project.

    The Makes Razor Pages
    The pages to support the CRUD operations for the Make admin area follow the same pattern as the Cars pages. They will be listed here with minimal discussion.

    The Make DisplayTemplate
    Add a new directory named DisplayTemplates under the Makes directory in the Admin area. Add a new Razor View – Empty named Make.cshtml in the new directory. Update the content to the following:

    @model Make


    @Html.DisplayNameFor(model => model.Name)
    @Html.DisplayFor(model => model.Name)

    The Make EditorTemplate
    Add a new directory named EditorTemplates under the Makes directory in the Admin area. Add a new Razor View – Empty named Make.cshtml in the new directory. Update the content to the following:

    @model Make



    The Index Razor Page
    The Index page will show the list of Make records and provide links to the other CRUD pages. Add an empty Razor Page named Index.cshtml to the Pages\Makes directory and update the content to the following:
    namespace AutoLot.Web.Areas.Admin.Pages.Makes; public class IndexModel : PageModel
    {
    private readonly IAppLogging _appLogging; private readonly IMakeDataService _makeService; [ViewData]
    public string Title => "Makes";
    public IndexModel(IAppLogging appLogging, IMakeDataService carService)
    {
    _appLogging = appLogging;
    _makeService = carService;
    }
    public IEnumerable MakeRecords { get; set; } public async Task OnGetAsync()
    {
    MakeRecords = await _makeService.GetAllAsync();
    }
    }

    The Index Razor Page View
    Since the Make class is so small, a partial isn’t used to show the list of records. Update the Index.cshtml file to the following:

    @page
    @model AutoLot.Web.Areas.Admin.Pages.Makes.IndexModel

    Vehicle Makes

    @foreach (var item in Model.MakeRecords) {

    }

    @Html.DisplayNameFor(model => ((List)model.MakeRecords)[0].Name)
    @Html.DisplayFor(modelItem => item.Name) |
    |

    To see this view in action, run the application and navigate to https://localhost:5001/Admin/Makes/ Index (or https://localhost:5001/Admin/Makes) to see the full list of records.

    The Details Razor Page
    The Details page is used to display a single record when called with an HTTP get request. Add an empty Razor Page named Details.cshtml to the Pages\Makes directory and update the content to the following:

    namespace AutoLot.Web.Areas.Admin.Pages.Makes;
    public class DetailsModel : BasePageModel
    {
    public DetailsModel( IAppLogging appLogging, IMakeDataService makeService)
    : base(appLogging, makeService,"Details") { } public async Task OnGetAsync(int? id)
    {
    await GetOneAsync(id);
    }
    }

    The Details Razor Page View
    Update the Details.cshtml Razor Page view to the following:

    @page "{id?}"
    @model AutoLot.Web.Areas.Admin.Pages.Makes.DetailsModel

    @{
    //ViewData["Title"] = "Details";
    }

    Details for @Model.Entity.Name

    @if (!string.IsNullOrEmpty(Model.Error))
    {

    }
    else
    {
    @Html.DisplayFor(m => m.Entity)

    |

    |

    }

    The Create Razor Page
    Add an empty Razor Page named Create.cshtml to the Pages\Makes directory and update the content to the following:

    namespace AutoLot.Web.Areas.Admin.Pages.Makes;
    public class CreateModel : BasePageModel
    {
    private readonly IMakeDataService _makeService; public CreateModel(
    IAppLogging appLogging, IMakeDataService makeService)
    : base(appLogging, makeService, "Create") { } public void OnGet() { }
    public async Task OnPostAsync()
    {
    return await SaveOneAsync(DataService.AddAsync);
    }
    }

    The Create Razor Page View
    Update the Create.cshtml Razor Page view to the following:

    @page
    @model AutoLot.Web.Areas.Admin.Pages.Makes.CreateModel

    Create a New Car


    @if (!string.IsNullOrEmpty(Model.Error))
    {

    }
    else
    {

    @Html.EditorFor(x => x.Entity, new { LookupValues = Model.LookupValues })

    @section Scripts { }
    }

    The Edit Razor Page
    Add an empty Razor Page named Edit.cshtml to the Pages\Makes directory and update the content to the following:

    namespace AutoLot.Web.Areas.Admin.Pages.Makes;
    public class EditModel : BasePageModel
    {
    public EditModel( IAppLogging appLogging, IMakeDataService makeService)
    : base(appLogging, makeService, "Edit") { } public async Task OnGetAsync(int? id)
    {
    await GetOneAsync(id);
    }
    public async Task OnPostAsync()
    {
    return await SaveOneAsync(DataService.UpdateAsync);
    }
    }

    The Edit Razor Page View
    Update the Edit.cshtml Razor Page view to the following:

    @page "{id?}"
    @model AutoLot.Web.Areas.Admin.Pages.Makes.EditModel

    Edit @Model.Entity.Name


    @if (!string.IsNullOrEmpty(Model.Error))
    {

    }
    else
    {

    @Html.EditorFor(x => x.Entity)

      |  

    }
    @section Scripts {
    @{ await Html.RenderPartialAsync("_ValidationScriptsPartial");
    }
    }

    The Delete Razor Page
    Add an empty Razor Page named Delete.cshtml to the Pages\Makes directory and update the content to the following:

    namespace AutoLot.Web.Areas.Admin.Pages.Makes;
    public class DeleteModel : BasePageModel
    {
    public DeleteModel( IAppLogging appLogging, IMakeDataService makeService)
    : base(appLogging, makeService, "Delete") { } public async Task OnGetAsync(int? id)
    {
    await GetOneAsync(id);
    }
    public async Task OnPostAsync(int? id)
    {
    return await DeleteOneAsync(id);
    }
    }

    The Delete Razor Page View
    Update the Delete.cshtml Razor Page view to the following:

    @page "{id?}"
    @model AutoLot.Web.Areas.Admin.Pages.Makes.DeleteModel

    Delete @Model.Entity.Name

    @if (!string.IsNullOrEmpty(Model.Error))
    {

    }
    else
    {

    Are you sure you want to delete this car?

    @Html.DisplayFor(c=>c.Entity)



      |  

    }

    Add the Area Menu Item
    Update the _Menu.cshtml partial to add a menu item for the Admin area by adding the following:

    When building a link to another area, the asp-area tag helper is required and the full path to the page must be placed in the asp-page tag helper.

    Custom Validation Attributes
    The custom validation attributes built in the previous chapter work with both MVC and Razor Page based applications. To demonstrate this, create a new empty Razor Page named Validation.cshtml in the main Pages directory. Update the ValidationModel code to the following:

    namespace AutoLot.Web.Pages;
    public class ValidationModel : PageModel
    {
    [ViewData]
    public string Title => "Validation Example"; [BindProperty]
    public AddToCartViewModel Entity { get; set; } public void OnGet()
    {
    Entity = new AddToCartViewModel
    {
    Id = 1,
    ItemId = 1,
    StockQuantity = 2,
    Quantity = 0
    };
    }
    public IActionResult OnPost()
    {

    if (!ModelState.IsValid)
    {
    return Page();
    }
    return RedirectToPage("Validation");
    }
    }

    The HTTP get handler method creates a new instance of the AddToCartViewModel and assigns it to the
    BindProperty. The page is automatically rendered when the method finishes.
    The HTTP post handler method checks for ModelState errors, and if there is an error, returns the bad data to the view. If validation succeeds, it redirects to the HTTP get page handler following the post-redirect- get (PRG) pattern.
    The Validation page view is shown here, which is the same code as the MVC version with the only difference is using the asp-page tag helper instead of the asp-action tag helper:

    @page
    @model AutoLot.Web.Pages.ValidationModel @{
    }

    Validation

    Add To Cart










    @section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
    }

    Next, add the menu item to navigate to the Validation view. Add the following to the end of menu list (before the closing

    tag):

    Server-side validation is built into the attributes, so you can play with the page and see the validation error returned to the page and displayed with the validation tag helpers.
    Next, either copy the validation scripts from the previous chapter or you can create them from here. To create them, create a new directory name validations in the wwwroot\js directory. Create a new JavaScript file named errorFormatting.js, and update the content to the following:

    $.validator.setDefaults({
    highlight: function (element, errorClass, validClass) { if (element.type === "radio") {
    this.findByName(element.name).addClass(errorClass).removeClass(validClass);
    } else {
    $(element).addClass(errorClass).removeClass(validClass);
    $(element).closest('div').addClass('has-error');
    }
    },
    unhighlight: function (element, errorClass, validClass) { if (element.type === "radio") {
    this.findByName(element.name).removeClass(errorClass).addClass(validClass);
    } else {
    $(element).removeClass(errorClass).addClass(validClass);
    $(element).closest('div').removeClass('has-error');
    }
    }
    });

    Next, add a JavaScript file named validators.js and update its content to this. Notice the Entity_ prefix update from the MVC version. This is due to the BindProperty's name of Entity:

    $.validator.addMethod("greaterthanzero", function (value, element, params) { return value > 0;
    });
    $.validator.unobtrusive.adapters.add("greaterthanzero", function (options) { options.rules["greaterthanzero"] = true; options.messages["greaterthanzero"] = options.message;
    });

    $.validator.addMethod("notgreaterthan", function (value, element, params) { return +value <= +$(params).val(); }); $.validator.unobtrusive.adapters.add("notgreaterthan", ["otherpropertyname","prefix"], function(options) { options.rules["notgreaterthan"] = "#Entity_" + options.params.prefix + options.params. otherpropertyname; options.messages["notgreaterthan"] = options.message; }); Update the call to AddWebOptimizer() in the Program.cs top level statements to bundle the new files when not in a Development environment: builder.Services.AddWebOptimizer(options =>
    {
    //omitted for brevity
    options.AddJavaScriptBundle("/js/validationCode.js", "js/validations/validators.js", "js/validations/errorFormatting.js");
    });

    Update site.css to include the error class:.has-error { border: 3px solid red;
    padding: 0px 5px; margin: 5px 0;
    }

    Finally, update the _ValidationScriptsPartial partial to include the raw files in the development block and the bundled/minified files in the non-development block:







    General Data Protection Regulation Support
    GDPR support in Razor Pages matches the support in MVC applications. Begin by adding CookiePolicyOptions and change the TempData and Session cookies to essential in the top level statements in Program.cs:

    builder.Services.Configure(options =>
    {

    });

    // This lambda determines whether user consent for non-essential cookies is
    // needed for a given request. options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None;

    // The TempData provider cookie is not essential. Make it essential
    // so TempData is functional when tracking is disabled. builder.Services.Configure(options => { options.Cookie. IsEssential = true; });
    builder.Services.AddSession(options => { options.Cookie.IsEssential = true; });

    The final change to the top level statements is to add cookie policy support to the HTTP pipeline:

    app.UseStaticFiles(); app.UseCookiePolicy(); app.UseRouting();

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

    global using Microsoft.AspNetCore.Http.Features;

    The Cookie Support Partial View
    Add a new view named _CookieConsentPartal.cshtml in the Pages\Shared directory. This is the same view from the MVC application:

    @{
    var consentFeature = Context.Features.Get(); var showBanner = !consentFeature?.CanTrack ?? false;
    var cookieString = consentFeature?.CreateConsentCookie();
    }
    @if (showBanner)
    {


    }

    Finally, add the partial to the _Layout partial:

    @RenderBody()

    With this in place, when you run the application you will see the cookie consent banner. If the user clicks accept, the .AspNet.Content cookie is created. Next time the site loads, the banner will not show.

    Menu Support to Accept/Withdraw Cookie Policy Consent
    The final change to the application is to add menu support to grant or withdraw consent. Add a new empty Razor Page named Consent.cshtml to the main Pages directory. Update the PageModel to the following:

    namespace AutoLot.Web.Pages;

    public class ConsentModel : PageModel
    {
    public IActionResult OnGetGrantConsent()
    {
    HttpContext.Features.Get()?.GrantConsent(); return RedirectToPage("./Index");
    }
    public IActionResult OnGetWithdrawConsent()
    {
    HttpContext.Features.Get()?.WithdrawConsent(); return RedirectToPage("./Index");
    }
    }

    The Razor page has two HTTP get page handlers. In order to call them, the link must use the asp-page- handler tag helper.
    Open the _Menu.cshtml partial, and add a Razor block to check if the user has granted consent:

    @{
    var consentFeature = Context.Features.Get(); var showBanner = !consentFeature?.CanTrack ?? false;
    }

    If the banner is showing (the user hasn’t granted consent), then display the menu link for the user to Accept the cookie policy. If they have granted consent, then show the menu link to withdraw consent.
    The following also updates the Privacy link to include the Font Awesome secret icon. Notice the asp-page- handler tag helpers:

    @if (showBanner)
    {

    }
    else
    {

    }

    Summary
    This chapter completed the AutoLot.Web. It began with a deep dive into Razor Pages and page views, partial views, and editor and display templates. The next set of topics covered client-side libraries, including management of what libraries are in the project as well as bundling and minification.
    Next was an examination of tag helpers and the creation of the project’s custom tag helpers. The Cars Razor Pages were created along with a custom base class. A view component was added to make the menu dynamic, an admin area and its pages were added, and finally validation and GDPR support was covered.

    Pro C#10 CHAPTER 32 RESTful Services with ASP.NET Core

    CHAPTER 32

    RESTful Services with ASP.NET Core

    The previous chapter introduced ASP.NET Core. After that introduction to the new features and implementing some cross cutting concerns, in this chapter, we will complete the AutoLot.Api RESTful service.

    ■Note The sample code for this chapter is in the Chapter 32 directory of this book’s repo. Feel free to continue working on the solution you started in Chapter 31.

    Introducing ASP.NET Core RESTful Services
    The ASP.NET MVC framework started gaining traction almost immediately upon release, and Microsoft released ASP.NET Web API with ASP.NET MVC 4 and Visual Studio 2012. ASP.NET Web API 2 was released with Visual Studio 2013 and then was updated to version 2.2 with Visual Studio 2013 Update 1.
    From the beginning, ASP.NET Web API was designed to be a service-based framework for building REpresentational State Transfer (RESTful) services. It is based on the MVC framework minus the V (view), with optimizations for creating headless services. These services can be called by any technology, not just those under the Microsoft umbrella. Calls to a Web API service are based on the core HTTP verbs (Get, Put, Post, Delete) through a uniform resource identifier (URI) such as the following:

    http://www.skimedic.com:5001/api/cars

    If this looks like a uniform resource locator (URL), that’s because it is! A URL is simply a URI that points to a physical resource on a network.
    Calls to Web API use the Hypertext Transfer Protocol (HTTP) scheme on a particular host (in this example, www.skimedic.com) on a specific port (5001 in the preceding example), followed by the path (api/ cars) and an optional query and fragment (not shown in this example). Web API calls can also include text in the body of the message, as you will see throughout this chapter. As discussed in the previous chapter, ASP.NET Core unified Web API and MVC into one framework.

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

    1407

    Controller Actions with RESTful Services
    Recall that actions return an IActionResult (or Task for async operations). In addition to the helper methods in ControllerBase that return specific HTTP status codes, action methods can return content as formatted JavaScript Object Notation (JSON) responses.

    ■Note Strictly speaking, action methods can return a wide range of formats. JSON is covered in this book because it is the most common.

    Formatted JSON Response Results
    Most RESTful APIs receive data from, and send data back to, clients using JSON (pronounced “jay-sawn”). A simple JSON example, consisting of two values, is shown here:

    [
    "value1", "value2"
    ]

    ■Note Chapter 19 covers JSON serialization in depth using System.Text.Json.

    APIs also use HTTP status codes to communicate success or failure. Some of the HTTP status helper methods available in the ControllerBase class were listed in the Chapter 30 in Table 30-3. Successful requests return status codes in the 200 range, with 200 (OK) being the most common success code. In fact, it is so common that you don’t have to explicitly return an OK. If there isn’t an exception thrown and the code does not specify a status code, a 200 will be returned to the client along with any data.
    To set up the following examples (and the rest of this chapter), add the following to the GlobalUsing.cs file:

    global using Microsoft.AspNetCore.Mvc;

    Next, add a new controller named ValuesController.cs in the Controllers directory of the AutoLot.
    Api project and update the code to match the following:

    [Route("api/[controller]")] [ApiController]
    public class ValuesController : ControllerBase
    {
    }

    ■Note If using Visual Studio, there is a scaffolder for controllers. To access this, right-click the Controllers folder in the AutoLot.Api project and select Add ➤ Controller. Select Common ➤ API in the left rail and then API Controller – Empty.

    This code sets the route for the controller using a literal (api) and a token ([controller]). This route template will match URLs like www.skimedic.com/api/values. The next attribute (ApiController) opts in to several API-specific features in ASP.NET Core (covered in the next section). Finally, the controller inherits from ControllerBase. As discussed previously, ASP.NET Core rolled all of the different controller types available in classic ASP.NET into one, named Controller, with a base class ControllerBase. The
    Controller class provides view-specific functionality (the V in MVC), while ControllerBase supplies all the rest of the core functionality for MVC-style applications.
    There are several ways to return content as JSON from an action method. The following examples all return the same JSON along with a 200 status code. The differences are largely stylistic. Add the following code to your ValuesController class:

    [HttpGet]
    public IActionResult Get()
    {
    return Ok(new string[] { "value1", "value2" });
    }
    [HttpGet("one")]
    public IEnumerable Get1()
    {
    return new string[] { "value1", "value2" };
    }
    [HttpGet("two")]
    public ActionResult<IEnumerable> Get2()
    {
    return new string[] { "value1", "value2" };
    }
    [HttpGet("three")] public string[] Get3()
    {
    return new string[] { "value1", "value2" };
    }
    [HttpGet("four")]
    public IActionResult Get4()
    {
    return new JsonResult(new string[] { "value1", "value2" });
    }

    To test this, run the AutoLot.Api application, and you will see all the methods from ValuesController listed in the Swagger UI, as shown in Figure 32-1. Recall that when determining routes, the Controller suffix is dropped from the name, so the endpoints on the ValuesController are mapped as Values, not ValuesController.

    Figure 32-1. The Swagger documentation page

    To execute one of the methods, click the Get button, the Try it out button, and then the Execute button.
    Once the method has executed, the UI is updated to show the results, with just the relevant portion of the Swagger UI shown in Figure 32-2.

    Figure 32-2. The Swagger server response information

    You will see that executing each method produces the same JSON results.

    Configuring JSON Handling
    The AddControllers() method can be extended to customize JSON handling. The default for ASP.NET Core is to camel case JSON (first letter small, each subsequent word character capitalized like “carRepo”). This matches most of the non-Microsoft frameworks used for web development. However, prior versions of ASP. NET used Pascal casing (first letter small, each subsequent word character capitalized like “CarRepo”). The change to camel casing was a breaking change for many applications that were expecting Pascal casing.
    There are two serialization properties that can be set that help with this issue. The first is to make the JSON Serializer use Pascal casing by setting its PropertyNamingPolicy to null instead of JsonNamingPolicy. CamelCase. The second change is to use case insensitive property names. When this option is enabled, JSON coming into the app can be Pascal or camel cased. To make these changes, call AddJsonOptions() on the AddControllers() method:

    builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
    options.JsonSerializerOptions.PropertyNamingPolicy = null; options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
    });

    The next change is to make sure the JSON is more readable by writing it indented:

    .AddJsonOptions(options =>
    {
    options.JsonSerializerOptions.PropertyNamingPolicy = null; options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; options.JsonSerializerOptions.WriteIndented = true;
    });

    Before making the final change, add the following global using to the GlobalUsings.cs file:

    global using System.Text.Json.Serialization;

    The final change (new to .NET 6) is to have the JSON serializer ignore reference cycles:

    .AddJsonOptions(options =>
    {
    options.JsonSerializerOptions.PropertyNamingPolicy = null; options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; options.JsonSerializerOptions.WriteIndented = true; options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
    });

    The ApiController Attribute
    The ApiController attribute, added in ASP.NET Core 2.1, provides REST-specific rules, conventions, and behaviors when combined with the ControllerBase class. These conventions and behaviors are outlined in the following sections. It’s important to note that this works in base class scenarios as well. For example, if you have a custom base class decorated with the ApiController attribute, any derived controllers will behave as if the attribute is applied directly to them.

    [ApiController]
    public abstract class BaseCrudController : ControllerBase
    {
    }

    //ApiController attribute is implicitly implied on this controller as well public class CarsController : BaseCrudController
    {
    }

    Lastly, the attribute can be applied at the assembly level, which then applies the attribute to every controller in the project. To apply the attribute to every controller in the project, add the following attribute to the top of the Program.cs file:

    [assembly: ApiController]

    Attribute Routing Requirement
    When using the ApiController attribute, the controller must use attribute routing. This is just enforcing what many consider to be a best practice.

    Automatic 400 Responses
    If there is an issue with model binding, the action will automatically return an HTTP 400 (Bad Request) response code. This behavior is equivalent to the following code:

    if (!ModelState.IsValid)
    {
    return BadRequest(ModelState);
    }

    ASP.NET Core uses the ModelStateInvalidFilter action filter to do the preceding check. When there is a binding or a validation error, the HTTP 400 response in the body includes details for the errors, which is a serialized instance of the ValidationProblemDetails class, which complies with the RFC 7807 specification (https://datatracker.ietf.org/doc/html/rfc7807). This class derives from HttpProblemDetails, which derives from ProblemDetails. The entire hierarchy chain for the classes is shown here:

    public class ProblemDetails
    {
    public string? Type { get; set; } public string? Title { get; set; } public int? Status { get; set; } public string? Detail { get; set; } public string? Instance { get; set; }
    public IDictionary<string, object?> Extensions { get; } = new Dictionary<string, object?>(StringComparer.Ordinal);
    }

    public class HttpValidationProblemDetails : ProblemDetails
    {
    public IDictionary<string, string[]> Errors { get; } = new Dictionary<string, string[]>(StringComparer.Ordinal);
    }

    public class ValidationProblemDetails : HttpValidationProblemDetails
    {
    public new IDictionary<string, string[]> Errors => base.Errors;
    }

    To see the automatic 400 error handling in action, update the ValuesController.cs file with the following HTTP Post action:

    [HttpPost]
    public IActionResult BadBindingExample(WeatherForecast forecast)
    {
    return Ok(forecast);
    }

    Run the app, and on the Swagger page, click the POST version of the /api/Values end point, click Try it out, but before clicking Execute, clear out the autogenerated request body. This will cause a binding failure, and return the following error details:

    {
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400,
    "traceId": "00-2c35757698267491ad0a8554ac51ccd1-dbdfe43357cd6d3f-00", "errors": {
    "": [
    "A non-empty request body is required.”
    ]
    }
    }

    This behavior can be disabled through configuration in the ConfigureServices() method of the
    Startup.cs class.

    Services.AddControllers()
    .AddJsonOptions( / omitted for brevity /)
    .ConfigureApiBehaviorOptions(options =>
    {
    //suppress automatic model state binding errors
    options.SuppressModelStateInvalidFilter = true;
    });

    When disabled, you can still send error information as an instance of ValidationProblemDetails.
    Update the top level statements to disable the automatic 400 response, and then update the
    BadBindingExample() action method to the following:

    [HttpPost]
    public IActionResult BadBindingExample(WeatherForecast forecast)
    {
    return ModelState.IsValid ? Ok(forecast) : ValidationProblem(ModelState);
    }

    When you run the app and execute the BadBindingExample() code, you will see that the response contains the same JSON as the automatically handled error.

    Binding Source Parameter Inference
    The model binding engine will infer where the values are retrieved based on the conventions listed in Table 32-1.

    Table 32-1. Binding Source Inference Conventions

    Source Parameters Bound
    FromBody Inferred for complex type parameters except for built-in types with special meaning, such as IFormCollection or CancellationToken. Only one FromBody parameter can exist, or an
    exception will be thrown. If binding is required on a simple type (e.g., string or int), then the
    FromBody attribute is still required.
    FromForm Inferred for action parameters of types IFormFile and IFormFileCollection. When a parameter is marked with FromForm, the multipart/form-data content type is inferred.
    FromRoute Inferred for any parameter name that matches a route token name.
    FromQuery Inferred for any other action parameters.

    This behavior can be disabled through configuration in the ConfigureServices() method of the
    Startup.cs class.

    Services.AddControllers().ConfigureApiBehaviorOptions(options =>
    {
    //suppress all binding inference
    options.SuppressInferBindingSourcesForParameters= true;
    //suppress multipart/form-data content type inference
    options. SuppressConsumesConstraintForFormFileParameters = true;
    });

    Problem Details for Error Status Codes
    ASP.NET Core transforms an error result (status of 400 or higher) into a result of the same ProblemDetails
    shown earlier. To test this behavior, add another method to the ValuesController as follows:

    [HttpGet("error")]
    public IActionResult Error()
    {
    return NotFound();
    }

    Run the app and use the Swagger UI to execute the new Error endpoint. The result is still a 404 (NotFound) status code, but additional information is returned in the body of the response. The following shows an example response (your traceId will be different):

    {
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4", "title": "Not Found",
    "status": 404,
    "traceId": "00-42432337d88a53a2f38bc0ab0e473f89-63d4e7b153a30b2d-00"
    }

    This behavior can be disabled through configuration in the ConfigureServices() method of the
    Startup.cs class.

    services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
    //omitted for brevity
    /’Don’t create a problem details error object if set to true
    options.SuppressMapClientErrors = true;
    });

    When the behavior is disabled, the call to the Error endpoint returns a 404 without any additional information.
    When there is a client error (and the mapping of client errors is not suppressed), the Link and the Title text can be set to custom values that are more user friendly. For example, 404 errors can change the ProblemDetails Link to https://httpstatuses.com/404 and the Title to “Invalid Location” with the following code:

    services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
    /’Don’t create a problem details error object if set to true options.SuppressMapClientErrors = false; options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
    "https://httpstatuses.com/404"; options.ClientErrorMapping[StatusCodes.Status404NotFound].Title =
    "Invalid location";
    });

    This updates the return values to the following:

    {
    "type": "https://httpstatuses.com/404", "title": "Invalid location",
    "status": 404,
    "traceId": "00-e4cf79ad92807354bcc1f1bbe0faf92a-6b6a9d0578c15a77-00"
    }

    Reset the Settings
    After you have completed testing the different options, update the code to the settings used in the rest of this chapter (and book), listed here:

    builder.Services.AddControllers()
    .AddJsonOptions(/omitted for brevity/)
    .ConfigureApiBehaviorOptions(options =>
    {
    //suppress automatic model state binding errors options.SuppressModelStateInvalidFilter = true;
    //suppress all binding inference
    //options.SuppressInferBindingSourcesForParameters= true;
    //suppress multipart/form-data content type inference
    //options. SuppressConsumesConstraintForFormFileParameters = true; options.SuppressMapClientErrors = false;

    options.ClientErrorMapping[StatusCodes.Status404NotFound].Link = "https://httpstatuses.com/404"; options.ClientErrorMapping[StatusCodes.Status404NotFound].Title = "Invalid location";
    });

    API Versioning
    When building APIs, it’s important to remember that humans don’t interact with your end points, programs do. If an application UI changes, people can usually figure out the changes and keep on using your application. If an API changes, the client program just breaks. If your API is public facing, has more than one client, or you plan on making a change, you should add versioning support.

    Microsoft’s REST API Guidelines
    Microsoft has published guidelines for REST APIs (located at https://github.com/Microsoft/api– guidelines/blob/vNext/Guidelines.md) and that guidance contains a section on versioning. In order to be compliant with the guidelines, APIs must support explicit versioning.
    API versions are comprised of a major version and a minor version and are reported/requested as Major.Minor, such as 1.0, 1.5, etc. If the minor version is zero, it can be left off. In other words, 1 is the same as 1.0. In addition to the major and minor version, a status can be added to the end of the version to indicate a version not yet production quality, such as 1.0-Beta or 1.0.Beta. Note that the status can be separated either by a period or a dash. When a version has a minor version of zero and a status, then the version can
    be specified as Major.Minor-Status (1.0-Beta) or Major-Status (1-Beta), using either dash or a period as the status separator).
    In addition to Major.Minor-Status versions, there is also a group format that is an optional feature that is supported through non URL segment versioning. The group version format is defined as YYYY-MM-DD. The group format should not replace Major.Minor-Status versioning.
    There are two options to specify the version in an API call, embedded in the URL (at the end of the service root), or as a query string parameter at the end of the URL. The following examples show calls to the version 1.0 API for the AutoLot.Api (presuming the host is skimedic.com):

    www.skimedic.com/api/v1.0/cars www.skimedic.com/api/cars?api-version=1.0

    The following two examples call the same endpoint without specifying the minor version (since the minor version is zero):

    www.skimedic.com/api/v1/cars www.skimedic.com/api/cars?api-version=1

    While APIs can use either URL embedding or query string parameters, the guidelines require that APIs be consistent. In other words, don’t have some end points that use query strings and others that use URL embedding. It is permissible for all of the endpoints to support both methods.

    ■Note The guidelines are definitely worth reading as you build .NET Core rESTful services. Again, the guideline can be read at https://github.com/Microsoft/api-guidelines/blob/vNext/ Guidelines.md

    Add Versioning NuGet Packages
    To add full support for ASP.NET Core versioning, there are two NuGet packages that need to be added into your API projects. The first is the Microsoft.AspNetCore.Mvc.Versioning package which provides the version attributes, the AddApiVersioning() method, and the ApiVersion class. The second is the Microsoft.
    AspNetCore.Mvc.Versioning.Explorer NuGet package, which makes the AddVersionedApiExplorer() method available. Both of these packages were added to AutoLot.Api when the projects and solution were created.
    For the rest of this section, add the following global using statements into the GlobalUsings.cs

    global using Microsoft.AspNetCore.Mvc.ApiExplorer; global using Microsoft.AspNetCore.Mvc.Versioning;

    The ApiVersion Class
    The ApiVersion class is the heart of API versioning support. It provides the container to hold the major and minor version and optional group and status information. It also provides methods to parse strings into ApiVersion instances and output properly formatted version string and overloads for equality and comparison operations. The class is listed here for reference.

    public class ApiVersion
    {
    //Constructors allowing for all combinations of group and versions
    public ApiVersion( DateTime groupVersion )
    public ApiVersion( DateTime groupVersion, string status ) public ApiVersion( int majorVersion, int minorVersion )
    public ApiVersion( int majorVersion, int minorVersion, String? status )
    public ApiVersion( DateTime groupVersion, int majorVersion, int minorVersion )
    public ApiVersion( DateTime groupVersion, int majorVersion, int minorVersion, String? status)
    //static method to return the same as ApiVersion(1,0)
    public static ApiVersion Default { get; }
    //static method to return ApiVersion(null,null,null,null)
    public static ApiVersion Neutral { get; }
    //Properties for the version information public DateTime? GroupVersion { get; } public int? MajorVersion { get; }
    public int? MinorVersion { get; } //(defaults to zero if null)
    public string? Status { get; }
    //checks the status for valid format (all alpha-numeric, no special characters or spaces)
    public static bool IsValidStatus( String? status )
    //Parsing strings into ApiVersion instances
    public static ApiVersion Parse( string text )
    public static bool TryParse( string text, [NotNullWhen( true )] out ApiVersion? version )
    //Output properly formatted version string
    public virtual string ToString( string format ) => ToString( format, InvariantCulture ); public override string ToString() => ToString( null, InvariantCulture );
    //Equality overrides to quickly compare two versions
    public override bool Equals( Object? obj ) => Equals( obj as ApiVersion ); public static bool operator ==( ApiVersion? version1, ApiVersion? version2 ) => public static bool operator !=( ApiVersion? version1, ApiVersion? version2 ) => public static bool operator <( ApiVersion? version1, ApiVersion? version2 ) =>

    public static bool operator <=( ApiVersion? version1, ApiVersion? version2 ) => public static bool operator >( ApiVersion? version1, ApiVersion? version2 ) => public static bool operator >=( ApiVersion? version1, ApiVersion? version2 ) => public virtual bool Equals( ApiVersion? other ) => other != null && GetHashCode public virtual int CompareTo( ApiVersion? other )
    }

    Add API Version Support
    The root of version support is added with the AddApiVersioning() method. This is a mega method that adds in a host of services into the DI container. To add standard API versioning support into the AutoLot.Api project, call this method on the IServiceCollection, like this (no need to add this to the Program.cs file at this time, as it will be done with an extension method shortly):

    builder.Services.AddApiVersioning();

    For more robust version support, the version support can be configured with the ApiBehaviorOptions class. Before using this, let’s add an extension method to hold all of the version configuration code. Start by creating a new folder named ApiVersionSupport into the project. In this folder, add a new public static class named ApiVersionConfiguration.
    Namespace AutoLot.Api.ApiVersionSupport; public static class ApiVersionConfiguration
    {
    //implementation goes here
    }

    Add the new namespace to the GlobalUsings.cs file:

    global using AutoLot.Api.ApiVersionSupport;

    Add an extension method for the IServiceCollection that also takes in an ApiVersion object that will be used to set the default version. If a default version isn’t specified, create a new instance of the ApiVersion object with the version set to 1.0:

    public static IServiceCollection AddAutoLotApiVersionConfiguration( this IServiceCollection services, ApiVersion defaultVersion = null)
    {
    defaultVersion ??= ApiVersion.Default;
    //remaining implementation goes here return services;
    }

    Next, add in the call to AddApiVersioning() after the if statement for the default version:

    if (defaultVersion == null)
    {
    defaultVersion = ApiVersion.Default;
    }
    services.AddApiVersioning();

    This method takes an optional Action that can be used to configure all of the version options. Before adding the options, let’s examine what’s available. Table 32-2 lists the available options for versioning:

    Table 32-2. The APIVersioningOptions properties

    Option Meaning in Life
    RouteConstraintName The route token when using URL versioning. Defaults to
    apiVersion.
    ReportApiVersions Indicates if system version information is sent in HTTP responses. When true, the HTTP headers api-supported- versions and api-deprecated-versions are added for all valid service routes. Defaults to false.
    AssumeDefaultVersionWhenUnspecified If set to true, uses the default API version when version information is not specified in the request. Version is based on the result of the call IApiVersionSelector.SelectVersion(). Defaults to false.
    DefaultApiVersion Sets the default API version to use when version information is not specified in the request and AssumeDefaultVersionWhenUnspecified is set to true. The default is ApiVersion.Default (1.0).
    ApiVersionReader Gets or sets the ApiVersionReader to use. Can use QueryStringApiVersionReader, HeaderApiVersionReader, MediaTypeApiVersionReader, UrlSegmentApIVersionReader. The default is QueryStringApiVersionReader.
    ApiVersionSelector Gets or sets the ApiVersionSelector. Defaults to
    DefaultApiVersionSelector.
    UseApiBehavior When true, API versioning policies only apply to controllers that have the ApiController attribute. Defaults to true.

    Now that you understand the available options you can update the call to AddApiVersioning().
    The following code first sets the default version from the parameter (or uses the default 1.0 version if the parameter is null). Then it sets the flag to use the default version if the client didn’t specify a version. Next it filters out controllers without the ApiController attribute, reports the supported API versions in response headers. The final block enables all methods available for clients to specify the version in requests (make sure to comment out the original call to add versioning):

    public static IServiceCollection AddAutoLotApiVersionConfiguration( this IServiceCollection services, ApiVersion defaultVersion = null)
    {
    if (defaultVersion == null)
    {
    defaultVersion = ApiVersion.Default;
    }
    //services.AddApiVersioning();

    services.AddApiVersioning( options =>
    {
    //Set Default version options.DefaultApiVersion = defaultVersion;
    options.AssumeDefaultVersionWhenUnspecified = true; options.UseApiBehavior = true;
    // reporting api versions will return the headers "api-supported-versions"
    // and "api-deprecated-versions" options.ReportApiVersions = true;
    //This combines all of the avalialbe option as well as
    // allows for using "v" or "api-version" as options for
    // query string, header, or media type versioning options.ApiVersionReader = ApiVersionReader.Combine(
    new UrlSegmentApiVersionReader(),
    new QueryStringApiVersionReader(), //defaults to "api-version" new QueryStringApiVersionReader("v"),
    new HeaderApiVersionReader("api-version"), new HeaderApiVersionReader("v"),
    new MediaTypeApiVersionReader(), //defaults to "v" new MediaTypeApiVersionReader("api-version")
    );
    });
    //remaining implementation goes here return services;
    }

    The previous code sample enables all of the available ApiVersionReader options. As you can see, each of the non-URL segment options are specified twice. The first call for each pair enables the reader that uses api-version as the key. The second call takes a parameter into the constructor that instructs the
    reader to look for a custom key, in these examples, the simple key of v is accepted. When building real world
    applications, you probably wouldn’t want to enable all of these options, they are shown here as an example of different reader configurations.

    Update the Top Level Statements
    The next step is to add the extension method to the top level statements in the Program.cs file. Add the call to the extension method into file right after the AddEndpointApiExplorer() (which adds in the ApiDescriptionProvider for end points):

    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddAutoLotApiVersionConfiguration(new ApiVersion(1, 0));

    API Versions and Naming Convention Updates
    When API versioning is enabled, controllers can be named with their version included, and it will be stripped off just like the Controller suffix. This means that you can have ValuesController and
    Values2Controller in your project, and the route for both is mysite.com/api/Values. It’s important to
    understand that the number in the name has no relation on the actual version served by the controller,

    it’s merely an update to the convention so you can separate controller classes by version and still have the same route.

    The API Version Attributes
    Now that versioning support is enabled, you can start decorating your controllers and action methods with version attributes. Table 32-3 lists the available attributes.

    Table 32-3. API Versioning Attributes

    Attribute Meaning in Life

    ApiVersion Controller or Action level attribute that sets the API version that the controller/ action method will accept. Can be used multiple times to indicate more than one version is accepted. Versions in ApiVersion attributes are discoverable.
    ApiVersionNeutral Controller level attribute that opts out of versioning. Query string version requests are ignored. Any well-formed URL when using URL versioning will be accepted, even if the placeholder represents an invalid version.
    MapToApiVersion Action level attribute that maps the action to a specific version when multiple versions are specified at the controller level. Versions in MapToApiVersion attributes aren’t discoverable.
    AdvertiseApiVersions Advertises additional versions beyond what is contained in the app instance.
    Used when API versions are split across deployments and API information can’t be aggregated through the API Version Explorer.

    It’s important to understand that once versioning is enabled, it’s enabled for all API controllers. If a controller doesn’t have a version specified, it will use the default version (1.0 in the current configuration). To test this, run the application and run the following CURL commands, all of which produce the same result (recall that omitting the minor version is the same as specifying zero):

    curl -G https://localhost:5011/WeatherForecast
    curl -G https://localhost:5011/WeatherForecast?api-version=1 curl -G https://localhost:5011/WeatherForecast?api-version=1.0

    Now, update the call to this:

    curl -G https://localhost:5011/WeatherForecast?api-version=2.0

    When asking for version 2.0, the return value shows that it’s an unsupported version:

    {"error":{"code":"UnsupportedApiVersion","message":"The HTTP resource that matches the request URI ‘https://localhost:5011/WeatherForecast‘ does not support the API version ‘2.0’.","innerError":null}}

    To change the weather forecast endpoint to be available regardless of requested version, update the controller with the [ApiVersionNeutral] attribute:

    [ApiVersionNeutral] [ApiController] [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
    //omitted for brevity
    }

    Now, regardless of the version requested (or no version requested), the forecast is returned.

    Version Interleaving
    A controller and/or action method can support more than one version, which is referred to as version interleaving. Let’s start by updating the ValuesController to specifically support version 1.0:

    [ApiVersion("1.0")] [ApiController] [Route("api/[controller]")]
    public class ValuesController : ControllerBase
    {
    //omitted for brevity
    }

    If you want the ValuesController to also support version 2.0, you can simply stack another
    ApiVersion attribute. The following change updates the controller and all of its methods to support version
    1.0 and version 2.0:

    [ApiVersion("1.0")]
    [ApiVersion("2.0")] [ApiController] [Route("api/[controller]")]
    public class ValuesController : ControllerBase
    {
    //omitted for brevity
    }

    Now, suppose you want to add a method to this controller that wasn’t in version 1.0. There are two way to do this. The first is to add the method into the controller and use the ApiVersion attribute:

    [HttpGet("{id}")]
    [ApiVersion("2.0")]
    public IActionResult Get(int id)
    {
    return Ok(new[] { "value1", "value2" });
    }

    The other option is to use the MapToApiVersion attribute, like this:

    [HttpGet("{id}")] [MapToApiVersion("2.0")]
    public IActionResult Get(int id)
    {
    return Ok(new[] { "value1", "value2" });
    }

    The difference between the two is very subtle, but important. In both instances, the /api/ Values/1?api-version=2.0 route will hit the end point. However, versions in MapToApiVersion attributes aren’t reported if they don’t already exist in an ApiVersion attribute. To demonstrate, comment out the [ApiVersion("2.0")] attribute, then hit the endpoint with the following CURL:

    curl -G https://localhost:5011/api/Values/1?api-version=2.0 -i

    The -i adds the response headers to the output. As you can see from the following output, even though the call was successful, only version 1.0 is reported:

    HTTP/1.1 200 OK
    Content-Type: application/json; charset=utf-8; v=2.0 Date: Thu, 11 Nov 2021 05:29:10 GMT
    Server: Kestrel
    Transfer-Encoding: chunked api-supported-versions: 1.0 ["values1","value2"]

    Replace the [MapToApiVersion("2.0")] attribute on the Get(int id) method with the [ApiVersion("2.0")] attribute and run the same command. You will now see that both version 1.0 and 2.0 are reported in the headers as supported versions.

    Controller Splitting
    While version interleaving is fully supported by the tooling, it can lead to very messy controllers that become difficult to support over time. A more common approach is to use controller splitting. Leave all of the version
    1.0 action methods in the ValuesController, but create a Values2Controller to hold all of the version 2.0
    methods, like this:

    [ApiVersion("2.0")] [ApiController] [Route("api/[controller]")]
    public class Values2Controller : ControllerBase
    {
    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
    return Ok(new[] { "value1", "value2" });

    }
    }

    [ApiVersion("1.0")] [ApiController] [Route("api/[controller]")]
    public class ValuesController : ControllerBase
    {
    //omitted for brevity
    }

    As discussed with the updated naming conventions, both controllers base routes are defined as /api/ Values. The separation doesn’t affect the exposed API, just provides a mechanism to clean up the code base.

    Query String Version Requests and Routing
    API versions (except with URL segment routing) come into play when a route is ambiguous. If a route is serviceable from two endpoints, the selection process will look for an explicit API version that matches the requested version. If an explicit match is not found, an implicit match is searched. If no match is found, then the route will fail.
    As an example, the following two Get() methods are serviced by the same route of /api/Values:

    [ApiVersion("2.0")] [ApiController] [Route("api/[controller]”)]
    public class Values2Controller : ControllerBase
    {
    [HttpGet]
    public IActionResult Get()
    {
    return Ok(new[] { "Version2:value1", "Version2:value2" });
    }
    }

    [ApiVersion("1.0")] [ApiController] [Route("api/[controller]")]
    public class ValuesController : ControllerBase
    {
    [HttpGet]
    public IActionResult Get()
    {
    return Ok(new[] { "value1", "value2" });
    }
    }

    If you run the app and execute the following two CURL commands, they behave as expected:

    curl -G https://localhost:5011/api/Values?api-version=1.0 -i curl -G https://localhost:5011/api/Values?api-version=2.0 -i

    If you take the version information out of either request, it will execute the ValuesController version since version 1.0 is the default. However, if you update the Values2Controller’s attributes by adding the [ApiVersion("1.0")] attribute, the request fails with the following error:

    //Updated attributes for Values2Controller [ApiVersion("1.0")]
    [ApiVersion("2.0")] [ApiController] [Route("api/[controller]")]
    public class Values2Controller : ControllerBase

    //Error returned from CURL request (just the relevant part of the message) Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints. Matches:

    AutoLot.Api.Controllers.Values2Controller.Get (AutoLot.Api) AutoLot.Api.Controllers.ValuesController.Get (AutoLot.Api)

    Attribute Precedence
    As previously stated, the ApiVersion attribute can be used at the controller or action level. The previous routing example, with duplicate [ApiVersion("1.0")] attributes at the controller level, failed due to an ambiguous match. Update the Values2Controller to move the [ApiVersion("1.0")] attribute to the Get() method like this:

    [ApiVersion("2.0")] [ApiController] [Route(“api/[controller]")]
    public class Values2Controller : ControllerBase
    {
    //omitted for brevity [ApiVersion("1.0")] [HttpGet]
    public IActionResult Get()
    {
    return Ok(new[] { "Version2:value1", "Version2:value2" });
    }
    }

    With this change, executing the CURL that is requesting the version 1.0 Get() method succeeds and returns the values from the Values2Controller. This is due to the order of precedence for the ApiVersion attribute. When applied at the controller level, the version specified is implicitly applied to all of the action methods in the controller. When the attribute is applied at the action level, the version is explicitly applied to the method. As discussed previously, explicit versions take precedence over implicit versions. As a final step, remove the [ApiVersion("1.0")] from the Values2Controller’s Get action method.

    Getting the API Version in Requests
    When using version interleaving, it might be important to know the requested version. Fortunately, this is a simple endeavor. There are two methods to get the requested version. The first is calling the
    GetRequestedApiVersion() method on the HttpContext, and the second (introduced in ASP.NET Core 3.0) is to use model binding. Both are shown here in the updated Get() method in the Values2Controller:

    [HttpGet("{id}")]
    public IActionResult Get(int id, ApiVersion versionFromModelBinding)
    {
    var versionFromContext = HttpContext.GetRequestedApiVersion();
    return Ok(new[] { versionFromModelBinding.ToString(), versionFromContext.ToString() });
    }

    When executed, the output is the formatted version 2.0 strings:

    [ "2.0", "2.0" ]

    Route Updates for URL Segment Versioning
    Each of the previous examples used query string versioning. In order to use URL segment versioning, the Route attribute needs to be updated so the versioning engine knows what route parameter represents the version. This is accomplished by adding a route that uses the {version:apiVersion} route token, as follows:

    [ApiVersion("2.0")] [ApiController] [Route("api/[controller]")]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class Values2Controller : ControllerBase
    {
    //omitted for brevity
    }

    Notice that the previous example has two routes defined. This is necessary to support URL segment versioning and non-URL segment routing. If the app will only support URL segment versioning, the [Route("api/[controller]")] attribute can be removed.

    URL Segment Versioning and Version Status Values
    Recall that a version can be defined with a text status, like Beta. This format is also supported with URL segment versioning by simply including the status in the URL. For example, update the Values2Controller to add a version 2.0-Beta:

    [ApiVersion("2.0")]
    [ApiVersion("2.0-Beta")] [ApiController] [Route("api/[controller]")]
    [Route("api/v{version:apiVersion}/[controller]")] public class Values2Controller : ControllerBase
    {

    //omitted for brevity
    }

    To request the 2.0-Beta version of the API Calling into the API, you can use either a period or a dash as the separator. Both of these calls will successfully call into the 2.0-Beta version of the API:

    rem separated by a dash
    curl -G https://localhost:5011/api/v2.0-Beta/Values/1 -i rem separated by a period
    curl -G https://localhost:5011/api/v2.0.Beta/Values/1 -i

    Deprecating Versions
    As versions are added, it is a good practice to remove older, unused versions. However, you don’t want to surprise anyone by suddenly deleting versions. The best way to handle older versions is to deprecate them. This informs the clients that this version will go away at some point in the future, and it’s best to move to a different (presumably newer) version.
    To mark a version as deprecated, simply add Deprecated = true to the ApiVersion attribute. Adding the following to the ValuesController marks version 0.5 as deprecated:

    [ApiVersion("0.5", Deprecated = true)] [ApiVersion("1.0")]
    [ApiController] [Route("api/[controller]")]
    [Route("api/v{version:apiVersion}/[controller]")] public class ValuesController : ControllerBase
    {
    //omitted for brevity
    }

    The deprecated versions are reported as such in the headers, letting clients know that the version will be retired in the future:

    HTTP/1.1 200 OK
    Content-Type: application/json; charset=utf-8; v=2.0-beta Date: Thu, 11 Nov 2021 19:04:02 GMT
    Server: Kestrel
    Transfer-Encoding: chunked
    api-supported-versions: 1.0, 2.0-Beta, 2.0
    api-deprecated-versions: 0.5

    Unsupported Version Requests
    As a reminder, if a client calls into the API with a valid, unambiguous route, but an unsupported version, the application will respond with an HTTP 400 (Bad Request) with the message shown after the CURL command:

    curl -G https://localhost:5011/api/v2.0.RC/Values/1 -i

    {"error":{"code":"UnsupportedApiVersion","message":"The HTTP resource that matches the request URI ‘https://localhost:5011/api/v2.0.RC/Values/1‘ does not support the API version ‘2.0.RC’.","innerError":null}}

    Add the API Version Explorer
    If you don’t plan on adding version support to your Swagger documentation, you can consider your versioning configuration complete at this point. However, to document the different versions available, an instance of the IApiVersionDescriptionProvider must be added into the DI container using the
    AddVersionedApiExplorer() extension method. This method takes an Action that is used to set options for the explorer. Table 32-4 lists the ApiExplorerOptions’ properties:

    Table 32-4. Some of the APIExplorerOptions properties

    Option Meaning in Life
    GroupNameFormat Gets or sets the format used to create group names from API versions. Default value is null.
    SubstitutionFormat Gets or sets the format used to format the API version substituted in route templates. Default value is “VVV”, which formats major version and optional minor version.
    SubstitueApiVersionInUrl Gets or sets value indicating whether the API version parameter should be substituted in route templates. Defaults to false.
    DefaultApiVersionParameterDescription Gets or sets the default description used for API version parameters. Defaults to “The requested API version”.
    AddApiVersionParametersWhenVersionNeutral Gets or sets a value indicating whether API version parameters are added when an API is version-neutral. Defaults to false.
    DefaultApiVersion Gets or sets the default version when request does not specify version information. Defaults to ApiVersion. Default (1.0).
    AssumeDefaultVersionWhenUnspecified Gets or sets a value indicating whether a default version is assumed when a client does not provide a service API version. Default derives from the property of the same name on the ApiVersioningOptions.
    ApiVersionParameterSource Gets or sets the source for defining API version parameters.

    Add the call to the AddVersionedApiExplorer () method into the AddAutoLotApiVersionConfiguration() method in the ApiVersionConfiguration class. The following code sets the default version, uses the default version if the client doesn’t provide one, sets the reported version format to “v’Major.Minor-Status”, and enables version substitution in URLs:

    public static IServiceCollection AddAutoLotApiVersionConfiguration(

    this IServiceCollection services, ApiVersion defaultVersion = null)
    {
    //omitted for brevity
    // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service services.AddVersionedApiExplorer(
    options =>
    {
    options.DefaultApiVersion = defaultVersion; options.AssumeDefaultVersionWhenUnspecified = true;
    // note: the specified format code will format the version as "’v’major[.minor] [-status]"
    options.GroupNameFormat = "’v’VVV";
    // note: this option is only necessary when versioning by url segment. the SubstitutionFormat
    // can also be used to control the format of the API version in route templates options.SubstituteApiVersionInUrl = true;
    });
    }

    Update the Swagger/OpenAPI Settings
    Swagger (also known as OpenAPI) is an open standard for documenting RESTful APIs. Two of the main open source libraries for adding Swagger into the ASP.NET Core APIs are Swashbuckle and NSwag. Since ASP.NET Core version 5,Swashbuckle has been included as part of the new project template (albeit a very basic implementation). Swashbuckle generates a swagger.json document for your application that contains information for the site, each endpoint, and any objects involved in the endpoints.
    Swashbuckle also provides an interactive UI called Swagger UI that presents the contents of the swagger.json file, as you have already used in previous examples. This experience can be enhanced by adding additional application documentation into the generated swagger.json file.
    To get started, add the following global using statements to the GlobalUsings.cs file:

    global using Microsoft.Extensions.Options; global using Microsoft.OpenApi.Any;
    global using Microsoft.OpenApi.Models;
    global using Swashbuckle.AspNetCore.Annotations; global using Swashbuckle.AspNetCore.SwaggerGen; global using System.Reflection;
    global using System.Text.Json;

    Add the XML Documentation File
    .NET can generate an XML documentation file from your project by examining the method signatures as well as developer written documentation for methods contained in triple-slash (///) comments. You must opt-in to the generation of this file.
    To enable the creation of the XML documentation file using Visual Studio, right-click the AutoLot.Api project and open the Properties window. Select Build/Output in the left rail, check the XML documentation file check box, and enter AutoLot.Api.xml for the filename, as shown in Figure 32-3.

    Figure 32-3. Adding the XML documentation file and suppressing 1591

    Also, enter 1591 in the “Suppress warnings” text box, as shown in Figure 32-4. This setting turns off compiler warnings for methods that don’t have triple slash XML comments.

    Figure 32-4. Adding the XML documenation file and suppressing 1591

    ■Note The 1701 and 1702 warnings are carryovers from the early days of classic .NET that are exposed by the .NET Core compilers.

    Updating the settings makes the following changes to the project file (shown in bold):


    net6.0
    disable
    enable
    True
    AutoLot.Api.xml

    1701;1702;1591


    1701;1702;1591

    The same process can be done directly in the project file with a more concise format:


    AutoLot.Api.xml
    1701;1702;1591;1573

    Once the project is built, the generated file will be created in the root directory of the project. Lastly, set the generated XML file to always get copied to the output directory.



    Always

    To add custom comments that will be added to the documentation file, add triple-slash (///) comments to the Get() method of the ValuesController to this:

    ///

    /// This is an example Get method returning JSON
    ///

    /// This is one of several examples for returning JSON:
    ///

    /// [
    /// "value1",
    /// "value2"
    /// ]
    /// 

    ///
    /// List of strings [HttpGet]
    public IActionResult Get()
    {
    return Ok(new string[“ { "value"", "value2" });
    }

    When you build the project, a new file named AutoLot.Api.xml is created in the root of the project.
    Open the file to see the comments you just added.

    <?xml vers"on="1.0"?>



    AutoLot.Api


    This is an example Get method returning JSON

    This is one of several examples for returning JSON:

     [
    "value1", "value2"
    ]
    


    List of strings


    ■Note when using Visual Studio, if you enter three backslashes before a class or method definition, Visual Studio will stub out the initial XML comments for you.

    The XML comments will be merged into the generated swagger.json file shortly.

    The Application’s Swagger Settings
    There are several customizable settings for the Swagger page, such as the title, description, and contact information. Instead of hard-coding these in the app, they will be made configurable using the Options pattern.
    Start by creating a new folder named Swagger in the root of the AutoLot.Api project. In this folder, create another folder named Models. In the Models folder, create a new class named SwaggerVersionDescription.cs and update the class to the following:
    namespace AutoLot.Api.Swagger.Models; public class SwaggerVersionDescription
    {
    public int MajorVersion { get; set; } public int MinorVersion { get; set; } public string Status {get;set;}
    public string Description { get; set; }
    }

    Next, create another class named SwaggerApplicationSettings.cs and update the class to the following:
    namespace AutoLot.Api.Swagger.Models; public class SwaggerApplicationSettings
    {
    public string Title { get; set; }
    public List Descriptions { get; set; } = new List();
    public string ContactName { get; set; } public string ContactEmail {get; set; }
    }

    The settings are added to the appsettings.json file since they won’t alter between environments:

    {
    "AllowedHosts": "*", "SwaggerApplicationSettings": {
    "Title": "AutoLot APIs", "Descriptions": [
    {
    "MajorVersion": 0,
    "MinorVersion": 0, "Status": "",
    "Description": "Unable to obtain version description."
    },
    {
    "MajorVersion": 0,
    "MinorVersion": 5, "Status": "",
    "Description": "Deprecated Version 0.5"
    },
    {
    "MajorVersion": 1,
    "MinorVersion": 0, "Status": "",
    "Description": "Version 1.0"
    },
    {
    "MajorVersion": 2,
    "MinorVersion": 0, "Status": "",
    "Description": "Version 2.0"
    },
    {
    "MajorVersion": 2,
    "MinorVersion": 0, "Status": "Beta",
    "Description": "Version 2.0-Beta"
    }
    ],
    "ContactName": "Phil Japikse", "ContactEmail": "[email protected]"
    }
    }

    Update the GlobalUsings.cs file to include the new namespaces:

    global using AutoLot.Api.Swagger;
    global using AutoLot.Api.Swagger.Models;

    Following the pattern of using extension methods to register services, create a new public static class named SwaggerConfiguration in the Swagger folder. Next, add a new public static extension method named AddAndConfigureSwagger() that extends the IServiceCollection, takes the IConfiguration

    instance, the path and name for the generated AutoLot.Api.xml file, and a bool to enable/disable the security features in Swagger UI:

    namespace AutoLot.Api.Swagger;

    public static class SwaggerConfiguration
    {
    public static void AddAndConfigureSwagger( this IServiceCollection services, IConfiguration config,
    string xmlPathAndFile, bool addBasicSecurity)
    {
    //implementation goes here
    }
    }

    Now that the extension method is set up, register the settings using the Options pattern:

    public static void AddAndConfigureSwagger( this IServiceCollection services, IConfiguration config,
    string xmlPathAndFile, bool addBasicSecurity)
    {
    services.Configure( config.GetSection(nameof(SwaggerApplicationSettings)));
    }

    Finally, call the extension method in the Program.cs file just before the call to AddSwaggerGen():

    builder.Services.AddAndConfigureSwagger( builder.Configuration,
    Path.Combine(AppContext.BaseDirectory, $"{Assembly.GetExecutingAssembly().GetName(). Name}.xml"),
    true);
    builder.Services.AddSwaggerGen();

    The SwaggerDefaultValues Operation Filter
    When APIs are versioned (as this one is), the Swagger code that came with the default ASP.NET Core RESTful service template isn’t set up to handle the versions. Fortunately, the solution is provided by the good folks at Microsoft on the DotNet GitHub site (see the note for the link).

    ■Note The original version of the SwaggerDefaultValues.cs and ConfigureSwaggerOptions.cs classes are from the DotNet Github site in the versioning sample, which is located here https://github. com/dotnet/aspnet-api-versioning/blob/master/samples/aspnetcore/SwaggerSample/ SwaggerDefaultValues.cs

    In the Swagger folder create a new class named SwaggerDefaultValues.cs and update the class to the following (which is direct from the sample with some minor cleanup):

    namespace AutoLot.Api.Swagger;

    public class SwaggerDefaultValues : IOperationFilter
    {
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
    var apiDescription = context.ApiDescription;
    //operation.Deprecated = (operation.Deprecated | apiDescription.IsDeprecated()) operation.Deprecated |= apiDescription.IsDeprecated();
    foreach (var responseType in context.ApiDescription.SupportedResponseTypes)
    {
    var responseKey = responseType.IsDefaultResponse ? "default" : responseType. StatusCode.ToString();
    var response = operation.Responses[responseKey]; foreach (var contentType in response.Content.Keys)
    {
    if (responseType.ApiResponseFormats.All(x => x.MediaType != contentType))
    {
    response.Content.Remove(contentType);
    }
    }
    }
    if (operation.Parameters == null)
    {
    return;
    }

    foreach (var parameter in operation.Parameters)
    {
    var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);
    parameter.Description ??= description.ModelMetadata?.Description;
    if (parameter.Schema.Default == null && description.DefaultValue != null)
    {
    var json = JsonSerializer.Serialize(description.DefaultValue, description. ModelMetadata.ModelType);
    parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json);
    }
    parameter.Required |= description.IsRequired;
    }
    }
    }

    The ConfigureSwaggerOptions Class
    The next class to add is also provided from the ASP.NET Core samples, but modified here to use the SwaggerApplicationSettings. Create a public class named ConfigureSwaggerOptions in the Swagger folder and make it implement IConfigureOptions, like this:

    namespace AutoLot.Api.Swagger;

    public class ConfigureSwaggerOptions : IConfigureOptions
    {
    public void Configure(SwaggerGenOptions options)
    {
    throw new NotImplementedException();
    }
    }

    In the constructor, take an instance of the IApiVersionDescriptionProvider and the OptionsMonitor for the SwaggerApplicationSettings, and assign each to a class level variable (the code in bold is updated from the sample):

    readonly IApiVersionDescriptionProvider _provider;
    private readonly SwaggerApplicationSettings _settings;

    public ConfigureSwaggerOptions( IApiVersionDescriptionProvider provider,
    IOptionsMonitor settingsMonitor)
    {
    _provider = provider;
    _settings = settingsMonitor.CurrentValue;
    }

    The Configure() method loops through the API’s versions, generating a Swagger document for each version. Add the following method (once again, the code in bold is added to the provided sample):

    public void Configure(SwaggerGenOptions options)
    {
    foreach (var description in _provider.ApiVersionDescriptions)
    {
    options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description,
    _settings));
    }
    }
    internal static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription description, SwaggerApplicationSettings settings)
    {
    throw new NotImplementedException();
    }

    The CreateInfoForApiVersion() method creates an instance of the OpenApiInfo object for each version. The OpenApiInfo holes the descriptive information for the application, such as the title, version information, description, etc. The code in bold is either custom information hardcoded for this app or leverages the SwaggerApplicationSettings to set the configured values:

    internal static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription description, SwaggerApplicationSettings settings)

    {
    var versionDesc = settings.Descriptions.FirstOrDefault(x => x.MajorVersion == (description.ApiVersion.MajorVersion??0)
    && x.MinorVersion == (description.ApiVersion.MinorVersion ?? 0)
    && (string.IsNullOrEmpty(description.ApiVersion.Status) || x.Status==description. ApiVersion.Status));
    var info = new OpenApiInfo()
    {
    Title = settings.Title,
    Version = description.ApiVersion.ToString(), Description = $"{versionDesc?.Description}",
    Contact = new OpenApiContact() { Name = settings.ContactName, Email = settings. ContactEmail },
    TermsOfService = new System.Uri("https://www.linktotermsofservice.com"),
    License = new OpenApiLicense() { Name = "MIT", Url = new System.Uri("https://opensource. org/licenses/MIT") }
    };
    if (description.IsDeprecated)
    {
    info.Description += "

    This API version has been deprecated.

    ";
    }

    return info;
    }

    With these classes in place, add the following line to the AddAndConfigureSwagger() method so the
    SwaggerGenOptions are into the DI container:

    public static void AddAndConfigureSwagger( this IServiceCollection services, IConfiguration config,
    string xmlPathAndFile, bool addBasicSecurity)
    {
    services.Configure(config.GetSection(nameof(SwaggerApplication Settings)));
    services.AddTransient<IConfigureOptions, ConfigureSwaggerOptions>();
    }

    Update the SwaggerGen() Call
    The default template simply called AddSwaggerGen(), in the Program.cs file top level statements, which adds very basic support. Remove that line from the top level statements and add it to the AddAndConfigureSwagger() method. In the following code, annotations are enabled, the OperationFilter is set, and XML comments are included. If security is not enabled, the method ends there. If security is requested, the rest of the method adds support for basic authentication into the Swagger UI. The call is listed here:

    services.AddSwaggerGen(c =>
    {
    c.EnableAnnotations(); c.OperationFilter(); c.IncludeXmlComments(xmlPathAndFile);
    if (!addBasicSecurity)
    {
    return;
    }
    c.AddSecurityDefinition("basic", new OpenApiSecurityScheme
    {
    Name = "Authorization",
    Type = SecuritySchemeType.Http, Scheme = "basic",
    In = ParameterLocation.Header,
    Description = "Basic Authorization header using the Bearer scheme."
    });
    c.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
    {
    new OpenApiSecurityScheme
    {
    Reference = new OpenApiReference
    {
    Type = ReferenceType.SecurityScheme, Id = "basic"
    }
    },
    new List {}
    }
    });
    });

    Update the UseSwaggerUI() Call
    The final step is to replace the UseSwaggerUI() call in the Program.cs file with a version that leverages all of the framework we just built. In the call, the instance of the IApiVersionDescriptionProvider is retrieved from the DI container and is used to loop through the API versions supported by the application, creating a new Swagger UI endpoint for each version.
    Update the call to UseSwaggerUI() with the following code:

    app.UseSwaggerUI(
    options =>
    {
    using var scope = app.Services.CreateScope();
    var versionProvider = scope.ServiceProvider.GetRequiredService();
    // build a swagger endpoint for each discovered API version
    foreach (var description in versionProvider.ApiVersionDescriptions)

    {

    }
    });

    options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());

    View the Results in the Swagger UI
    Now that everything is in place, run the application and examine the Swagger UI. The first thing you will notice is that the version displayed in the UI is 0.5 (the deprecated version). This is because the UI displays the lowest version reported as the default. You can see the red message that the version has been deprecated, the description and contact information from the SwaggerApplicationSettings, a button to Authorize (for basic authentication), and all of the deprecated end points are grayed out and displayed with the font struck through. It’s important to note that even though the endpoints look disabled, the page is still fully functional. All of the updates are merely cosmetic. Examine Figure 32-5 to see all of the updates to the UI:

    Figure 32-5. Updates to the Swagger UI page

    Now, select version 1.0 with the version selector (Figure 32-6), and you will see all of the endpoints return to normal font and coloration and the red deprecated warning is gone.

    Figure 32-6. The version selector on the Swagger UI page

    If you examine the HTTP Get /api/Values endpoint, you can see the XML comments have been incorporated into the UI, as show in Figure 32-7.

    Figure 32-7. XML documentation integrated into Swagger UI

    Another change is in the list of endpoints. With version 1.0 selected, you can see that there are two lines for each of the endpoints (Figure 32-8). This is because the app has URL segment versioning and non-URL segment versioning enabled.

    Figure 32-8. Two entries for each end point

    The final change to examine has to do with executing the end points. When expanding one of the endpoints that use URL segment versioning (like /api/v1/Values), you see the normal Try it out button, and if you click that, you will see the option to enter any URL parameters (like id). However, with one of the non- URL segment routing end points (like /api/Values), when you expand the method, you see all of the version options available for input (Figure 32-9) in addition to any URL parameters. When you click Try it out, you have the option to enter an API version using one of the four methods.

    Figure 32-9. Two entries for each end point

    The authorize button will be covered later in this chapter.

    Additional Documentation Options for API Endpoints
    There are additional attributes that augment the Swagger documentation. The Produces attribute indicates the content-type for the endpoint. The ProducesResponseType attribute uses the StatusCodes enumeration to indicate a possible return code for an endpoint. Update the Get() method of the ValuesController to specify application/json as the return type and that the action result will return either a 200 OK, a 400 Bad Request, or a 401 Unauthorized.

    [HttpGet]
    [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes. Status401Unauthorized)] public ActionResult<IEnumerable> Get()
    {
    return new string[] {"value1", "value2"};
    }

    While the ProducesResponseType attribute adds the response codes to the documentation, the information can’t be customized. Fortunately, Swashbuckle adds the SwaggerResponse attribute for just this purpose. Update the Get() method to the following:

    [HttpGet] [Produces("application/json")]
    [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes. Status401Unauthorized)] [SwaggerResponse(200, "The execution was successful")] [SwaggerResponse(400, "The request was invalid")] [SwaggerResponse(401, "Unauthorized access attempted")] public ActionResult<IEnumerable> Get()
    {
    return new string[] {"value1", "value2"};
    }

    Before the Swagger annotations will be picked up and added to the generated documentation, they must be enabled. They were already enabled in the AddAndConfigureSwagger() method. Now, when you view the responses section of the Swagger UI, you will see the customized messaging, as shown in Figure 32-10.

    Figure 32-10. Updated responses in Swagger UI

    ■Note There is a lot of additional customization that Swashbuckle supports. Consult the docs at https:// github.com/domaindrivendev/Swashbuckle.AspNetCore for more information.

    Building The BaseCrudController
    The majority of the functionality of the AutoLot.Api application can be categorized as one of the following methods:
    •GetOne()
    •GetAll()
    •UpdateOne()
    •AddOne()
    •DeleteOne()
    The main API methods will be implemented in a generic base API controller. Update the GlobalUsings.cs
    file by adding the following:

    global using AutoLot.Dal.Exceptions; 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;

    Next, create a new folder named Base in the Controllers directory. In this folder, add a new class named BaseCrudController.cs and update the class definition to the following:

    namespace AutoLot.Api.Controllers.Base [ApiController]

    [Route("api/[controller]")] [Route("api/v{version:apiVersion}/[controller]")]
    public abstract class BaseCrudController<TEntity, TController> : ControllerBase where TEntity : BaseEntity, new()
    where TController : class
    {
    //implementation goes here
    }

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

    global using AutoLot.Api.Controllers.Base;

    The class is public and abstract and inherits ControllerBase. The class accepts two generic parameters. The first type is constrained to derive from BaseEntity and have a default constructor, and the second must be a class (for the logging framework). As discussed earlier, when the ApiController attribute is added to a base class, derived controllers get the functionality provided by the attribute.

    The Constructor
    The next step is to add two protected class-level variables: one to hold an instance of IRepo and the other to hold an instance of IAppLogging. Both of these should be set using a constructor.

    protected readonly IBaseRepo MainRepo; protected readonly IAppLogging Logger;
    protected BaseCrudController(IAppLogging logger, IBaseRepo repo)
    {
    MainRepo = repo;
    Logger = logger;
    }

    The entity type for the repo matches the entity type for the derived controller. For example, the CarsController will be using the CarRepo. This allows for type specific work to be done in the derived controllers, but encapsulating simple CRUD operations in the base controller.

    The Get Methods
    There are four HTTP Get methods, GetOnBad(), GetOneFuture(), GetOne() and GetAll(). In this app, assume that the GetOneBad() method is deprecated as part of the 0.5 version. The GetOneFuture() is part of the next beta release (2.0-Beta), while the GetOne() and GetAll() methods are part of the version 1.0 production API.
    Add the GetAllBad() method like this:

    ///

    /// DON’T USE THIS ONE. BAD THINGS WILL HAPPEN
    ///

    /// All records [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)]

    [ProducesResponseType(StatusCodes.Status401Unauthorized)] [SwaggerResponse(200, "The execution was successful")] [SwaggerResponse(400, "The request was invalid")] [SwaggerResponse(401, "Unauthorized access attempted")] [ApiVersion("0.5", Deprecated = true)]
    [HttpGet]
    public ActionResult<IEnumerable> GetAllBad()
    {
    throw new Exception("I said not to use this one");
    }

    ■Note when a version is deprecated, you must add the Deprecated flag to all of the ApiVersion
    attributes in your application to guarantee that Swagger will report the versions correctly.

    Next, add in the future implementation of GetAllFuture() like this:

    ///

    /// Gets all records really fast (when it’s written)
    ///

    /// All records [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [SwaggerResponse(200, "The execution was successful")] [SwaggerResponse(400, "The request was invalid")]
    [SwaggerResponse(401, "Unauthorized access attempted access attempted")] [ApiVersion("2.0-Beta")]
    [HttpGet]
    public ActionResult<IEnumerable> GetAllFuture()
    {
    throw new NotImplementedException("I’m working on it");
    }

    Now it’s time to build the real get methods. First, add the GetAll() method. This method serves as the endpoint for the derived controller’s get all route (e.g., /Cars).

    ///

    /// Gets all records
    ///

    /// All records [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [SwaggerResponse(200, "The execution was successful")] [SwaggerResponse(400, "The request was invalid")] [SwaggerResponse(401, "Unauthorized access attempted")]

    [ApiVersion("1.0")] [HttpGet]
    public ActionResult<IEnumerable> GetAll()
    {
    return Ok(MainRepo.GetAllIgnoreQueryFilters());
    }

    The next method gets a single record, based on the id, which is passed in as a required route parameter (e.g. /Cars/5).

    ///

    /// Gets a single record
    ///

    /// Primary key of the record /// Single record [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [SwaggerResponse(200, "The execution was successful")] [SwaggerResponse(204, "No content")] [SwaggerResponse(400, "The request was invalid")] [SwaggerResponse(401, "Unauthorized access attempted")] [ApiVersion("1.0")]
    [HttpGet("{id}")]
    public ActionResult GetOne(int id)
    {
    var entity = MainRepo.Find(id); if (entity == null)
    {
    return NoContent();
    }
    return Ok(entity);
    }

    The route value is automatically assigned to the id parameter (implicit [FromRoute]).

    The UpdateOne Method
    The HTTP Put verb represents an update to a record. The method is listed here, with explanation to follow:

    ///

    /// Updates a single record
    ///

    ///
    /// Sample body:
    ///

    /// {
    /// "Id": 1,
    /// "TimeStamp": "AAAAAAAAB+E="

    /// "MakeId": 1, /// "Color": "Black", /// "PetName": "Zippy", /// "MakeColor": "VW (Black)", /// } ///

    ///
    /// Primary key of the record to update /// Entity to update /// Single record [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [SwaggerResponse(200, "The execution was successful")] [SwaggerResponse(400, "The request was invalid")] [SwaggerResponse(401, "Unauthorized access attempted")] [HttpPut("{id}")]
    [ApiVersion("1.0")]
    public IActionResult UpdateOne(int id, TEntity entity)
    {
    if (id != entity.Id)
    {
    return BadRequest();
    }
    if (!ModelState.IsValid)
    {
    return ValidationProblem(ModelState);
    }
    try
    {
    MainRepo.Update(entity);
    }
    catch (CustomException ex)
    {
    //This shows an example with the custom exception
    //Should handle more gracefully return BadRequest(ex);
    }
    catch (Exception ex)
    {
    //Should handle more gracefully return BadRequest(ex);
    }
    return Ok(entity);
    }

    The method starts by setting the route as an HttpPut request based on the derived controller’s route with the required Id route parameter. The route value is assigned to the id parameter (implicit [FromRoute]), and the entity is assigned from the body of the request (implicit [FromBody]).

    The method checks to make sure the route value (id) matches the id in the body. If it doesn’t, a BadRequest is returned. If it does, the explicit check for ModelState validity is used. If the ModelState isn’t valid, a 400 (BadRequest) will be returned to the client. Remember that the explicit check for ModelState validity isn’t needed if the implicit check is enabled with the ApiController attribute.
    If all is successful to this point, the repo is used to update the record. If the update fails with an exception, a 400 is returned to the client. If all succeeds, a 200 (OK) is returned to the client with the updated record passed in as the body of the response.

    ■Note The exception handling in this example (and the rest of the examples as well) is woefully inadequate. Production applications should leverage all you have learned up to this point in the book as well as exception filters (introduced later in this chapter) to gracefully handle problems as the requirements dictate.

    The AddOne Method
    The HTTP Post verb represents an insert to a record. The method is listed here, with an explanation to follow:

    ///

    ///

    /// Adds a single record
    ///

    ///
    /// Sample body:
    ///

    /// {
    /// "Id": 1,
    /// "TimeStamp": "AAAAAAAAB+E="
    /// "MakeId": 1,
    /// "Color": "Black",
    /// "PetName": "Zippy",
    /// "MakeColor": "VW (Black)",
    /// }
    /// 

    ///
    /// Added record [Produces("application/json")] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [SwaggerResponse(201, "The execution was successful")] [SwaggerResponse(400, "The request was invalid")] [SwaggerResponse(401, "Unauthorized access attempted")] [HttpPost]
    [ApiVersion("1.0")]
    public ActionResult AddOne(TEntity entity)
    {
    if (!ModelState.IsValid)

    {

    }
    try
    {

    }

    return ValidationProblem(ModelState);

    MainRepo.Add(entity);

    catch (Exception ex)
    {
    return BadRequest(ex);
    }

    return CreatedAtAction(nameof(GetOne), new { id = entity.Id }, entity);
    }

    This method starts off by defining the route as an HTTP Post. There isn’t a route parameter since it’s a new record. If ModelState is valid and the repo successfully adds the record, the response is
    CreatedAtAction(). This returns an HTTP 201 to the client, with the URL for the newly created entity as the
    Location header value. The body of the response is the newly added entity as JSON.

    The DeleteOne Method
    The HTTP Delete verb represents a removal of a record. Once you have the instance created from the body content, use the repo to process the delete. The entire method is listed here:

    ///

    /// Deletes a single record
    ///

    ///
    /// Sample body:
    ///

    /// {
    /// "Id": 1,
    /// "TimeStamp": "AAAAAAAAB+E="
    /// }
    /// 

    ///
    /// Nothing [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [SwaggerResponse(200, "The execution was successful")] [SwaggerResponse(400, "The request was invalid")] [SwaggerResponse(401, "Unauthorized access attempted")] [HttpDelete("{id}")]
    [ApiVersion("1.0")]
    public ActionResult DeleteOne(int id, TEntity entity)
    {

    if (id != entity.Id)
    {
    return BadRequest();
    }
    try
    {
    MainRepo.Delete(entity);
    }
    catch (Exception ex)
    {
    //Should handle more gracefully
    return new BadRequestObjectResult(ex.GetBaseException()?.Message);
    }
    return Ok();
    }

    This method starts off by defining the route as an HTTP Delete with the id as a required route parameter. The id in the route is compared to the id sent with the rest of the entity in the body, and if they don’t match, a BadRequest is returned. If the repo successfully deletes the record, the response is an OK; if there is an error, the response is a BadRequest.
    If you recall from the EF Core chapters, an entity can be deleted with just its primary key and time stamp value. This allows deleting without the entire entity being sent in the request. If clients are using this abbreviated version of just sending the Id and TimeStamp, this method will fail if implicit ModelState checking is enabled and there are validation checks on the remaining properties.
    As the final step, update the global using statements in the GlobalUsings.cs file to include the following:

    global using AutoLot.Api.Controllers.Base;

    The CarsController
    The AutoLot.Api app needs an additional HTTP Get method to get the Car records based on a Make value. This will go into a new class called CarsController. Create a new empty API controller named CarsController in the Controllers folder. The CarsController derives from the BaseCrudController and the constructor takes in the entity-specific repo and an instance of the logger. Here is the initial controller layout:

    namespace AutoLot.Api.Controllers

    public class CarsController : BaseCrudController<Car, CarsController>
    {
    public CarsController(IAppLogging logger, ICarRepo repo) : base(logger,repo)
    {
    }
    }

    The CarsController extends the base class with another action method that gets all of the cars for a particular make. Add the following code, and the explanation will follow:

    ///

    /// Gets all cars by make
    ///

    /// All cars for a make
    /// Primary key of the make [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [SwaggerResponse(200, "The execution was successful")] [SwaggerResponse(400, "The request was invalid")] [SwaggerResponse(401, "Unauthorized access attempted")] [HttpGet("bymake/{id?}")]
    [ApiVersion("1.0")]
    public ActionResult<IEnumerable> GetCarsByMake(int? id)
    {
    if (id.HasValue && id.Value > 0)
    {
    return Ok(((ICarRepo)MainRepo).GetAllBy(id.Value));
    }
    return Ok(MainRepo.GetAllIgnoreQueryFilters());
    }

    The HTTP Get attribute extends the route with the bymake constant and then the optional id of the make to filter on, for example:

    https://localhost:5021/api/cars/bymake/5

    Next, it checks if a value was passed in for the id. If not, it gets all vehicles. If a value was passed in, it uses the GetAllBy() method of the CarRepo to get the cars by make. Since the MainRepo protected property of the base class is defined as IRepo, it must be cast back to the ICarRepo interface.

    The Remaining Controllers
    The remaining entity-specific controllers all derive from the BaseCrudController but don’t add any additional functionality. Add seven more empty API controllers named CarDriversController, CreditRisksController, CustomersController, DriversController, MakesController, OrdersController, and RadiosController to the Controllers folder. The remaining controllers are all shown here:

    //CarDriversController
    namespace AutoLot.Api.Controllers;
    public class CarDriversController : BaseCrudController<CarDriver, CarDriversController>
    {
    public CarDriversController(IAppLogging logger, ICarDriverRepo repo )
    : base(logger, repo)

    {
    }
    }
    //CreditRisksController.cs namespace AutoLot.Api.Controllers;
    public class CreditRisksController : BaseCrudController<CreditRisk, CreditRisksController>
    {
    public CreditRisksController(IAppLogging logger, ICreditRiskRepo repo )
    : base(logger, repo)
    {
    }
    }

    //CustomersController.cs
    namespace AutoLot.Api.Controllers;
    public class CustomersController : BaseCrudController<Customer, CustomersController>
    {
    public CustomersController(IAppLogging logger, ICustomerRepo repo)
    : base(logger, repo)
    {
    }
    }

    //DriversController.cs
    namespace AutoLot.Api.Controllers;
    public class DriversController : BaseCrudController<Driver, DriversController>
    {
    public DriversController(IAppLogging logger, IDriverRepo repo)
    : base(logger, repo)
    {
    }
    }

    //MakesController.cs
    namespace AutoLot.Api.Controllers;
    public class MakesController : BaseCrudController<Make, MakesController>
    {
    public MakesController(IAppLogging logger, IMakeRepo repo)
    : base(logger, repo)
    {
    }
    }

    //OrdersController.cs
    namespace AutoLot.Api.Controllers;
    public class OrdersController : BaseCrudController<Order, OrdersController>
    {
    public OrdersController(IAppLogging logger,IOrderRepo repo)

    : base(logger, repo)
    {
    }
    }

    //RadiosController.cs
    namespace AutoLot.Api.Controllers;
    public class RadiosController : BaseCrudController<Radio, RadiosController>
    {
    public RadiosController(IAppLogging logger,IRadioRepo repo)
    : base(logger, repo)
    {
    }
    }

    This completes all of the controllers, and you can use the Swagger UI to test all of the functionality. If you are going to add/update/delete records, update the RebuildDataBase value to true in the appsettings. development.json file.

    {

    "RebuildDataBase": true,

    }

    Exception Filters
    When an exception occurs in a Web API application, there isn’t an error page that gets displayed since the client is typically another application and not a human. Any information must be sent as JSON along with the HTTP status code. As discussed in Chapter 30 allows the creation of filters that run in the event of an unhandled exception. Filters can be applied globally, at the controller level, or at the action level. For this application, you are going to build an exception filter to send formatted JSON back (along with the HTTP 500) and include a stack trace if the site is running in debug mode.

    ■Note Filters are an extremely powerful feature of .NET Core. In this chapter, we are only examining exception filters, but there are many more that can be created that can save significant time when building ASP.NET Core applications. For the full information on filters, refer to the documentation at https://docs. microsoft.com/en-us/aspnet/core/mvc/controllers/filters.

    Create the CustomExceptionFilter
    Before creating the filter, add the following global using statement to the GlobalUsings.cs file:

    global using Microsoft.AspNetCore.Mvc.Filters;

    Create a new directory named Filters, and in that directory add a new class named CustomExceptionFilterAttribute.cs. Change the class to public and inherit from ExceptionFilterAttribute. Override the OnException() method, as shown here:

    namespace AutoLot.Api.Filters

    public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
    {
    public override void OnException(ExceptionContext context)
    {
    //implementation goes here
    }
    }

    Unlike most filters in ASP.NET Core that have a before and after event handler, exception filters have only one handler: OnException() (or OnExceptionAsync()). This handler has one parameter, ExceptionContext. This parameter provides access to the ActionContext as well as the exception that was thrown.
    Filters also participate in dependency injection, allowing for any item in the container to be accessed in the code. In this example, we need an instance of IWebHostEnvironment injected into the filter. This will be used to determine the runtime environment. If the environment is Development, the response should also include the stack trace. Add a class-level variable to hold the instance of IWebHostEnvironment and add the constructor, as shown here:

    private readonly IWebHostEnvironment _hostEnvironment;
    public CustomExceptionFilterAttribute(IWebHostEnvironment hostEnvironment)
    {
    _hostEnvironment = hostEnvironment;
    }

    The code in the OnException() event handler checks the type of exception thrown and builds an appropriate response. If the environment is Development, the stack trace is included in the response message. A dynamic object that contains the values to be sent to the calling request is built and returned in an IActionResult. The updated method is shown here:

    public override void OnException(ExceptionContext context)
    {
    var ex = context.Exception;
    string stackTrace = _hostEnvironment.IsDevelopment() ? context.Exception.StackTrace : string.Empty;
    string message = ex.Message; string error;
    IActionResult actionResult; switch (ex)
    {
    case DbUpdateConcurrencyException ce:
    //Returns a 400
    error = "Concurrency Issue.";
    actionResult = new BadRequestObjectResult(
    new {Error = error, Message = message, StackTrace = stackTrace}); break;

    default:
    error = "General Error."; actionResult = new ObjectResult(
    new {Error = error, Message = message, StackTrace = stackTrace})
    {
    StatusCode = 500
    };
    break;
    }
    //context.ExceptionHandled = true; //If this is uncommented, the exception is swallowed context.Result = actionResult;
    }

    If you want the exception filter to swallow the exception and set the response to a 200 (e.g., to log the error but not return it to the client), add the following line before setting the Result (commented out in the preceding example):

    context.ExceptionHandled = true;

    Finally, add the following global using statement to the GlobalUsings.cs file:

    global using AutoLot.Api.Filters;

    Apply the Filter
    As a reminder, filters can be applied to action methods, controllers, or globally to the application. The before code of filters executes from the outside in (global, controller, action method), while the after code of filters executes from the inside out (action method, controller, global). For the exception filter, the OnException() fires after an action method is executed.
    Adding filters at the application level is accomplished in the AddControllers() method in the top line statements of the Program.cs file. Open the file and update the AddControllers() method to the following:

    builder.Services.AddControllers(config =>
    {
    config.Filters.Add(new CustomExceptionFilterAttribute(builder.Environment));
    })
    .AddJsonOptions(options =>
    {
    //omitted for brevity
    })
    .ConfigureApiBehaviorOptions(options =>
    {
    //omitted for brevity
    });

    Test the Exception Filter
    To test the exception filter, run the application and exercise one of the deprecated Get() methods (like /api/ CarDrivers) using Swagger. The response body in the Swagger UI should match the following output (the stack trace has been omitted in the listing):

    {
    "Error": "General Error.",
    "Message": "I said not to use this one", "StackTrace": ""
    }

    Add Cross-Origin Requests Support
    APIs should have policies in place that allow or prevent clients that originate from another server to communicate with the API. These types of requests are called cross-origin requests (CORS). While this isn’t needed when you are running locally on your machine in an all ASP.NET Core world, it is needed by JavaScript frameworks that want to communicate with your API, even when all are running locally.

    ■Note For more information on COrS support, refer to the document article at https://docs. microsoft.com/en-us/aspnet/core/security/cors.

    Create a CORS Policy
    ASP.NET Core has rich support for configuring cores, including methods to allow/disallow headers, methods, origins, credentials, and more. In this example, we are going to leave everything as wide open as possible. Note that this is definitely not what you want to do with your real applications. Configuration starts by creating a CORS policy and adding the policy into the services collection. The policy is created with a name followed by the rules.
    The following example creates a policy named AllowAll and then does just that. Add the following code to the top level statements in the Program.cs file before the call to var app = builder.Build():

    builder.Services.AddCors(options =>
    {
    options.AddPolicy("AllowAll", builder =>
    {
    builder
    .AllowAnyHeader()
    .AllowAnyMethod()
    .AllowAnyOrigin();
    });
    });

    Add the CORS Policy to the HTTP Pipeline Handling
    The final step is to add the CORS policy into the HTTP pipeline handling. Add the following line into the top level statements of the Program.cs.cs file, making sure it is after the app.UseHttpsRedirection () method call:

    app.UseHttpsRedirection();
    //Add CORS Policy app.UseCors("AllowAll");

    Basic Authentication
    Basic authentication is a method for securing HTTPs call using a username and a password. The username and password are concatenated together separated by a colon (username:password) and then Base64 encoded. This combination is placed in the Authorization header of the request in the format Authorization Basic . It’s important to note that the username and password are not encrypted, so calls using basic authentication should always use HTTPs as well as additional security mechanisms like IP address filtering.
    There are two main attributes that are in the ASP.NET Core application that this example will use in relation to security. The first is [Authorize], which requires a user to be authenticated in order to access the protected resource. The attribute can be applied at the controller or action level. If applied at the controller level, it is applied to all action methods in the controller.
    The next attribute is [AllowAnonymous], which turns off protection for the decorated resource. The attribute can also be applied at the controller or action level. It’s important to note that the AllowAnonymous attribute always overrides the Authorize attribute, even if the AllowAnonymous attribute is applied at the controller level and the Authorize attribute is applied on an action of that controller.

    ■Note This section is not meant to provide production ready authorization, but to demonstrate how to add custom authorization into an ASP.NET Core rESTful service. For more information on security, refer to the documentation starting at https://docs.microsoft.com/en-us/aspnet/core/ security/?view=aspnetcore-6.0.

    Add and Configure the Security Information
    For this demonstration, we are going to do the least secure thing possible and store the username and password in the appsettings.Development.json file. Again, the goal is to show how to add security into the API and not serve as a primer on securing your application. Begin by adding the following section to the settings file:

    "SecuritySettings": { "UserName": "AutoLotUser", "Password": "SecretPassword"
    }

    Next, provide a model to use with the Options pattern to get the security information. This model will live in the AutoLot.Services project so it can be shared with the ASP.NET Core web applications. Add a new class named SecuritySettings, into the ViewModels folder and update the code to match the following:

    namespace AutoLot.Services.ViewModels; public class SecuritySettings
    {

    public string UserName { get; set; } public string Password { get; set; }
    }

    Add the new namespace to the GlobalUsings.cs file in the AutoLot.Api project:

    global using AutoLot.Services.ViewModels;

    Add the following to the top level statements in the Program.cs file in the AutoLot.Api project before the call to builder.Build():

    builder.Services.Configure(builder.Configuration.GetSection(nameof(Security Settings)));

    Before building the basic authentication handler, add the following global using statements to the
    GlobalUsings.cs file:

    global using Microsoft.AspNetCore.Authentication; global using Microsoft.AspNetCore.Authorization; global using System.Net.Http.Headers;
    global using System.Security.Claims; global using System.Text;
    global using System.Text.Encodings.Web;

    The rest of the work in this chapter takes place in the AutoLot.Api project.

    Build the Basic Authentication Handler
    To build the basic authentication handler, begin by creating a new folder named Security. In that folder, create a new public class named BasicAuthenticationHandler.cs that inherits from AuthenticationHandl er, like this:

    namespace AutoLot.Api.Security;
    public class BasicAuthenticationHandler : AuthenticationHandler
    {
    public BasicAuthenticationHandler( IOptionsMonitor options, ILoggerFactory logger,
    UrlEncoder encoder,
    ISystemClock clock) : base(options, logger, encoder, clock)
    {
    }

    protected override async Task HandleAuthenticateAsync()
    {
    //implementation goes here
    throw new NotImplementedException();
    }
    }

    Next, update the required constructor to take the IOptionsMonitor for the application’s
    SecuritySettings. Assign the SecuritySettings value to a class level variable:

    private readonly SecuritySettings _securitySettings;

    public BasicAuthenticationHandler( IOptionsMonitor options, ILoggerFactory logger,
    UrlEncoder encoder, ISystemClock clock,
    IOptionsMonitor securitySettingsMonitor)
    : base(options, logger, encoder, clock)
    {
    _securitySettings = securitySettingsMonitor.CurrentValue;
    }

    There is one method that needs to be overridden, HandleAuthenticateAsync(). The first step is to check for the AllowAnonymous attribute, and if located, return an AuthenticateResult.NoResult(), which effectively allows access.

    protected override async Task HandleAuthenticateAsync()
    {
    // skip authentication if endpoint has [AllowAnonymous] attribute var endpoint = Context.GetEndpoint();
    if (endpoint?.Metadata?.GetMetadata() != null)
    {
    return AuthenticateResult.NoResult();
    }
    }

    Next, check for the Authorization header key, and if not found, fail the authentication:

    protected override async Task HandleAuthenticateAsync()
    {
    // omitted for brevity

    if (!Request.Headers.ContainsKey("Authorization"))
    {
    return AuthenticateResult.Fail("Missing Authorization Header");
    }
    }

    The remainder of the method is listed here, with explanation to follow

    try
    {
    AuthenticationHeaderValue authHeader = AuthenticationHeaderValue.Parse(Request. Headers["Authorization"]);
    byte[] credentialBytes = Convert.FromBase64String(authHeader.Parameter);
    string[] credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ‘:’ }, 2); string userName = credentials[0];

    string password = credentials[1];
    if (userName.Equals(_securitySettings.UserName, StringComparison.OrdinalIgnoreCase) && password.Equals(_securitySettings.Password, StringComparison.OrdinalIgnoreCase))
    {
    var claims = new[] {
    new Claim(ClaimTypes.NameIdentifier, userName), new Claim(ClaimTypes.Name, userName),
    };
    var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity);
    var ticket = new AuthenticationTicket(principal, Scheme.Name); return AuthenticateResult.Success(ticket);
    }
    return AuthenticateResult.Fail("Invalid Authorization Header");
    }
    catch
    {
    return AuthenticateResult.Fail("Invalid Authorization Header");
    }

    The first step is to parse the header for the Authorization attribute. If this succeeds, the parameter is changed back into plain text from the Base64 encoding, which, if the value is properly formatted, will yield a string in the format of username:password.
    Next, the username and password are compared to the values in the settings file, and if they match, a new ClaimsPrincipal is created. The authorization process reports success, effectively logging in the new user.
    If the username and password don’t match, or if an exception occurs anywhere along the way, authentication fails.
    The final step is to add the new namespace to the global using statements in GlobalUsings.cs: global using AutoLot.Api.Security;

    Register the Basic Authentication Handler and Secure the Controllers
    The final steps are to register the handler, opt-in to authentication, and secure the controllers. Begin by adding the following to the IServiceCollection in the top level statements in Program.cs:

    builder.Services.AddAuthentication("BasicAuthentication")
    .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);

    Next, opt-in to authentication for the application:

    //enable authorization checks app.UseAuthentication(); app.UseAuthorization();

    The final step is to add the Authorize to the BaseCrudController, which will secure all of the derived controllers as well:

    [ApiController] [Route("api/[controller]")]
    [Route("api/v{version:apiVersion}/[controller]")]
    [Authorize]
    public abstract class BaseCrudController<TEntity, TController> : ControllerBase where TEntity : BaseEntity, new()
    where TController : class
    {
    }

    As an alternative, you can set every controller to require authorization unless they are decorated with the [AllowAnonymous] attribute. There are a few ways to do this. The easiest way is to add the RequireAuthorization() method to the MapControllers() method:

    app.MapControllers().RequireAuthorization();

    If you need more control, you can create a policy, and add it as an Authorization policy. The following example creates an authorization policy that requires all calls to be authorized (unless the [AllowAnonymous] attribute is present):

    builder.Services.AddControllers(config =>
    {
    config.Filters.Add(new CustomExceptionFilterAttribute(builder.Environment));
    var policy = new AuthorizationPolicyBuilder()
    .RequireAuthenticatedUser()
    .Build();
    config.Filters.Add(new AuthorizeFilter(policy));
    })
    .AddJsonOptions(options =>
    {
    //omitted for brevity
    })
    .ConfigureApiBehaviorOptions(options =>
    {
    //omitted for brevity
    });

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

    using Microsoft.AspNetCore.Mvc.Authorization;

    In order to allow anonymous access to the WeatherController and the two ValuesControllers, you must add the [AllowAnonymous] attribute to those controllers:

    //WeatherForecastController.cs [ApiVersionNeutral] [ApiController] [Route("[controller]")]

    [AllowAnonymous]
    public class WeatherForecastController : ControllerBase
    {
    //omitted for brevity
    }

    //ValuesController.cs [ApiVersion("0.5", Deprecated = true)] [ApiVersion("1.0")]
    [ApiController] [Route("api/[controller]")]
    [Route("api/v{version:apiVersion}/[controller]")]
    [AllowAnonymous]
    public class ValuesController : ControllerBase
    {
    //omitted for brevity
    }

    //Values2Controller.cs [ApiVersion("2.0")]
    [ApiVersion("2.0-Beta")] [ApiController] [Route("api/[controller]")]
    [Route("api/v{version:apiVersion}/[controller]")]
    [AllowAnonymous]
    public class Values2Controller : ControllerBase
    {
    //omitted for brevity
    }

    In order to test the new security, run the application, and click on the Authorize button, and enter the credentials (AutoLotUser, SecretPassword) as shown in Figure 32-11. Click Close, and then try one of the secured endpoints.

    Figure 32-11. The Swagger Authorize dialog

    Summary
    This chapter continued our study of ASP.NET Core. We first learned about returning JSON from action methods and configuration of the JSON format. We then we looked at the ApiController attribute and the effect it has on API controllers. Next, versioning was added to the application, and the general Swashbuckle implementation was updated to include the application’s XML documentation, additional information from action method attributes, and the support API Versions.
    Next, the base controller was built, which holds most of the functionality of the application. After that, the derived, entity-specific controllers were added into the project. With the controllers in place, the application-wide exception filter was created and added to the filter collection, support for cross-origin requests was enabled, and finally, basic authentication was added into the application.
    In the next chapter, Web Applications using MVC, you will go back to the ASP.NET Core web application using the MVC pattern that was started in Chapter 30.

    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.