分类目录归档:C#

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 33 Web Applications with MVC

    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:

    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:

    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 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:

    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.