Tag Archives: Pro-csharp10-with-net6

Pro C#10 CHAPTER 34 Web Applications using Razor Pages

CHAPTER 34 Web Applications using Razor Pages

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

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

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

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

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

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

1559

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

namespace AutoLot.Web.Pages;

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

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

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

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

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

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

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

private readonly ICarDataService _carDataService;

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

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

public Car Entity { get; set; }

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

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

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

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

Razor Pages also support explicit binding:

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

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

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

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

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

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

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

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

Razor Syntax










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

Razor Syntax

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

  • one
  • two

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


@{
@:Straight Text

Value:@Model.Entity.Id


Lines without HTML tag

}


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


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


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


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

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

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

@{
Layout = "_Layout";
}

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

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

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

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

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

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

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

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

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

@model AutoLot.Models.Entities.Car

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

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

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

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

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

tag for Color to the following:

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

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

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

@model Car













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

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

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

h1 {
background-color: blue;
}

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

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

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

@{
Layout = "_MyCustomLayout";
}

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

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

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

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

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

For the menu partial, cut all the markup between the

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

The final step at this time is to cut out the






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






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

app.UseWebOptimizer();

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

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

});
*/
}

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

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

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

@addTagHelper *, WebOptimizer.Core

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

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

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

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

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

The Form Tag Helper
With Razor Pages, the

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


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


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

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

tag helper.

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

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

Razor Syntax

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

...

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

@Model.Entity.PetName

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

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

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

builder.Services.TryAddSingleton();

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

namespace AutoLot.Web.TagHelpers.Base;

public abstract class ItemLinkTagHelperBase : TagHelper
{
}

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

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

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

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

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

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

protected string ActionName { get; set; }

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

public int? ItemId { get; set; }

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

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

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

global using AutoLot.Web.TagHelpers.Base;

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

namespace AutoLot.Web.TagHelpers;

public class ItemDetailsTagHelper : ItemLinkTagHelperBase
{
}

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

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

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

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

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

Details

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

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

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

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

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

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

Delete

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

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

namespace AutoLot.Web.TagHelpers;

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

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

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

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

Edit

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

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

namespace AutoLot.Web.TagHelpers;

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

}

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

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

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

Create New

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

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

namespace AutoLot.Web.TagHelpers;

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

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

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

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

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

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

@addTagHelper *, AutoLot.Web

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

global using AutoLot.Models.Entities.Base;

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

namespace AutoLot.Web.Pages.Base;

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

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

protected readonly IAppLogging AppLoggingInstance; protected readonly IDataServiceBase DataService;

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

protected BasePageModel( IAppLogging appLogging,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

global using AutoLot.Web.Pages.Base;

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

_appLogging = appLogging;
_carService = carService;
}
}

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

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

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

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

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

@model IEnumerable< Car>

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

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

@if (showMake)
{

}

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

@foreach (var item in Model)
{

@if (showMake)
{

}

}

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

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

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

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

@{
if (Model.MakeId.HasValue)
{

Vehicle Inventory for @Model.MakeName

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

Vehicle Inventory

}
}

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

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

namespace AutoLot.Web.Pages.Cars;

public class DetailsModel : BasePageModel
{

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

{
await GetOneAsync(id);
}
}

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

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

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

Details for @Model.Entity.PetName

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

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

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

|
|

}

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

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

namespace AutoLot.Web.Pages.Cars;

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

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

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

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

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

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

tag helper:

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

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

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

Create a New Car


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

}
else
{
}

The

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

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

else
{

}

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

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

  |  

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

@section Scripts { }

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

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

namespace AutoLot.Web.Pages.Cars;

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

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

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

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

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

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

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

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

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

Edit @Model.Entity.PetName


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

}
else
{
}

The

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

else
{

}

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

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

& nbsp; |  

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

@section Scripts { }

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

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

namespace AutoLot.Web.Pages.Cars;

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

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

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

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

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

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

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

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

Delete @Model.Entity.PetName

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

}
else
{
}

The

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

else
{

}

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

Are you sure you want to delete this car?

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



  |  

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

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

global using Microsoft.AspNetCore.Mvc.ViewComponents;

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

public class MenuViewComponent : ViewComponent
{
private readonly IMakeDataService _dataService;

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

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

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

@model IEnumerable

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

@addTagHelper *, AutoLot.Web

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

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

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

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

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

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

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

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

    @model Make


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

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

    @model Make



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

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

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

    Vehicle Makes

    @foreach (var item in Model.MakeRecords) {

    }

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

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

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

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

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

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

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

    Details for @Model.Entity.Name

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

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

    |

    |

    }

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

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

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

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

    Create a New Car


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

    }
    else
    {

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

    @section Scripts { }
    }

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

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

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

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

    Edit @Model.Entity.Name


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

    }
    else
    {

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

      |  

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

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

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

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

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

    Delete @Model.Entity.Name

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

    }
    else
    {

    Are you sure you want to delete this car?

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



      |  

    }

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

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

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

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

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

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

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

    Validation

    Add To Cart










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

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

    tag):

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

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

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

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

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

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

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







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

    builder.Services.Configure(options =>
    {

    });

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

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

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

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

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

    global using Microsoft.AspNetCore.Http.Features;

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

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


    }

    Finally, add the partial to the _Layout partial:

    @RenderBody()

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

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

    namespace AutoLot.Web.Pages;

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

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

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

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

    @if (showBanner)
    {

    }
    else
    {

    }

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

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

    CHAPTER 32

    RESTful Services with ASP.NET Core

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

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

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

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

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

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

    1407

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

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

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

    [
    "value1", "value2"
    ]

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

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

    global using Microsoft.AspNetCore.Mvc;

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

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

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

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

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

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

    Figure 32-1. The Swagger documentation page

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

    Figure 32-2. The Swagger server response information

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

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

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

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

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

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

    global using System.Text.Json.Serialization;

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

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

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

    [ApiController]
    public abstract class BaseCrudController : ControllerBase
    {
    }

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

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

    [assembly: ApiController]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Table 32-1. Binding Source Inference Conventions

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

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

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

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

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

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

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

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

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

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

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

    This updates the return values to the following:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    builder.Services.AddApiVersioning();

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

    Add the new namespace to the GlobalUsings.cs file:

    global using AutoLot.Api.ApiVersionSupport;

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

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

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

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

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

    Table 32-2. The APIVersioningOptions properties

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

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

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

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

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

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

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

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

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

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

    Table 32-3. API Versioning Attributes

    Attribute Meaning in Life

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

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

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

    Now, update the call to this:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    }
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    [ "2.0", "2.0" ]

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

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

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

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

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

    //omitted for brevity
    }

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

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

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

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

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

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

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

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

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

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

    Table 32-4. Some of the APIExplorerOptions properties

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

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

    public static IServiceCollection AddAutoLotApiVersionConfiguration(

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

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

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

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

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

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

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

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

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


    net6.0
    disable
    enable
    True
    AutoLot.Api.xml

    1701;1702;1591


    1701;1702;1591

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


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

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



    Always

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

    ///

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

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

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

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

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

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



    AutoLot.Api


    This is an example Get method returning JSON

    This is one of several examples for returning JSON:

     [
    "value1", "value2"
    ]
    


    List of strings


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

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

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

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

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

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

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

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

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

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

    namespace AutoLot.Api.Swagger;

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

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

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

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

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

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

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

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

    namespace AutoLot.Api.Swagger;

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

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

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

    namespace AutoLot.Api.Swagger;

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

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

    readonly IApiVersionDescriptionProvider _provider;
    private readonly SwaggerApplicationSettings _settings;

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

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

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

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

    internal static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription description, SwaggerApplicationSettings settings)

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

    This API version has been deprecated.

    ";
    }

    return info;
    }

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

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

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

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

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

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

    {

    }
    });

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

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

    Figure 32-5. Updates to the Swagger UI page

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

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

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

    Figure 32-7. XML documentation integrated into Swagger UI

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

    Figure 32-8. Two entries for each end point

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

    Figure 32-9. Two entries for each end point

    The authorize button will be covered later in this chapter.

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

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

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

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

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

    Figure 32-10. Updated responses in Swagger UI

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

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

    global using AutoLot.Dal.Exceptions; global using AutoLot.Dal.Repos; global using AutoLot.Dal.Repos.Base;
    global using AutoLot.Dal.Repos.Interfaces; global using AutoLot.Models.Entities; global using AutoLot.Models.Entities.Base;

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

    namespace AutoLot.Api.Controllers.Base [ApiController]

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

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

    global using AutoLot.Api.Controllers.Base;

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

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

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

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

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

    ///

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

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

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

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

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

    ///

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

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

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

    ///

    /// Gets all records
    ///

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

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

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

    ///

    /// Gets a single record
    ///

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

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

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

    ///

    /// Updates a single record
    ///

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

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

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

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

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

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

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

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

    ///

    ///

    /// Adds a single record
    ///

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

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

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

    {

    }
    try
    {

    }

    return ValidationProblem(ModelState);

    MainRepo.Add(entity);

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

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

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

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

    ///

    /// Deletes a single record
    ///

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

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

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

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

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

    global using AutoLot.Api.Controllers.Base;

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

    namespace AutoLot.Api.Controllers

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

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

    ///

    /// Gets all cars by make
    ///

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

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

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

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

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

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

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

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

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

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

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

    : base(logger, repo)
    {
    }
    }

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

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

    {
    ...
    "RebuildDataBase": true,
    ...
    }

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

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

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

    global using Microsoft.AspNetCore.Mvc.Filters;

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

    namespace AutoLot.Api.Filters

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

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

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

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

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

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

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

    context.ExceptionHandled = true;

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

    global using AutoLot.Api.Filters;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    namespace AutoLot.Services.ViewModels; public class SecuritySettings
    {

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

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

    global using AutoLot.Services.ViewModels;

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

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

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

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

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

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

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

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

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

    private readonly SecuritySettings _securitySettings;

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

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

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

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

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

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

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

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

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

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

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

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

    Next, opt-in to authentication for the application:

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

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

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

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

    app.MapControllers().RequireAuthorization();

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

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

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

    using Microsoft.AspNetCore.Mvc.Authorization;

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

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

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

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

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

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

    Figure 32-11. The Swagger Authorize dialog

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

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

    CHAPTER 31

    Diving Into ASP.NET Core

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

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

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

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

    1355

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

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

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

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

    Helper Method Meaning in Life
    HttpContext Returns the HttpContext for the currently executing action.
    Request Returns the HttpRequest for the currently executing action.
    Response Returns the HttpResponse for the currently executing action.
    RouteData Returns the RouteData for the currently executing action (routing is covered later in this chapter).
    ModelState Returns the state of the model in regard to model binding and validation (both covered later in this chapter).
    Url Returns an instance of the IUrlHelper, providing access to building URLs for ASP.NET Core MVC applications and services.
    ViewData TempData Provide data to the view through the ViewDataDictionary and
    TempDataDictionary
    Page Returns a PageResult (derived from ActionResult) as the HTTP response.
    PartialView Returns a PartialViewResult to the response pipeline.
    ViewComponent Returns a ViewComponentResult to the response pipeline.
    OnPageHandlerSelected Executes when a page handler method is selected but before model binding.
    OnPageHandlerExecuting Executes before a page handler method executes.
    OnPageHandlerExecutionAsync Async version of OnPageHandlerExecuting.
    OnPageHandlerExecuted Executes after a page handler method executes.
    (continued)

    Table 31-1. (continued)

    Helper Method Meaning in Life
    OnPageHandlerSelectionAsync Async version of OnPageHandlerSelected.
    User Returns the ClaimsPrincipal user.
    Content Returns a ContentResult to the response. Overloads allow for adding a content type and encoding definition.
    File Returns a FileContentResult to the response.
    Redirect A series of methods that redirect the user to another URL by returning a
    RedirectResult.
    LocalRedirect A series of methods that redirect the user to another URL only if the URL is local. More secure than the generic Redirect methods.
    RedirectToAction RedirectToPage RedirectToRoute A series of methods that redirect to another action method, Razor Page, or named route. Routing is covered later in this chapter.
    TryUpdateModelAsync Used for explicit model binding.
    TryValidateModel Used for explicit model validation.

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

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

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

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

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

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

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

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

    Table 31-3. The IWebHostEnvironment Properties

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

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

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

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

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

    Table 31-4. The HostEnvironmentEnvExtensions Methods

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

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

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

    Update this block to flip it around:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    app.Run();

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

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

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

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

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

    var builder = WebApplication.CreateBuilder(args);

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

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

    app.UseRouting();

    app.UseAuthorization();

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

    app.Run();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    var config = builder.Configuration;

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

    var config = app.Configuration;

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

    config.GetConnectionString("AutoLot")

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

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

    Table 31-5. Lifetime Options for Services

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

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

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

    var app = builder.Build();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    services.AddScoped<ICarRepo, CarRepo>();

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

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

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

    global using Microsoft.AspNetCore.Builder;

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

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

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

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

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

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

    global using AutoLot.Services.DataServices;

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

    builder.Services.AddRepositories();

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

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

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

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

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

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

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

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

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

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

    @inject ICarRepo Repo

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

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

    "RebuildDatabase": true,

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

    "RebuildDatabase": false,

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

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

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

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

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

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

    namespace AutoLot.Services.DataServices.Interfaces;

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

    The IMakeDataService interface simply implements the IDataServiceBase interface:

    namespace AutoLot.Services.DataServices.Interfaces;

    public interface IMakeDataService : IDataServiceBase
    {
    }

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

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

    global using AutoLot.Services.DataServices.Interfaces;

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

    namespace AutoLot.Services.DataServices.Dal.Base;

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

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

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

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

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

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

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

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

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

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

    namespace AutoLot.Services.DataServices.Dal;

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

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

    namespace AutoLot.Services.DataServices.Dal;

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

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

    namespace AutoLot.Services.DataServices.Api.Base;

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

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

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

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

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

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

    namespace AutoLot.Services.DataServices.Api;

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

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

    namespace AutoLot.Services.DataServices.Api;

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

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

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

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

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

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

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

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

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

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

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

    Table 31-6. Some of the IOptions Interfaces

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

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

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

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

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

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

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

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

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

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

    Now that the DealerInfo is configured, instances are retrieved by injection one of the IOptions interfaces into the class constructor, controller action method, or Razor page handler method. Notice that what is injected in is not an instance of the DealerInfo class, but an instance of the IOptions interface. To get the configured instance, the CurrentValue (IOptionsMonitor) or Value
    (IOptionsSnapshot) must be used. The following example uses method injection to pass an instance if IOptionsMonitor into the HomeController’s Index method in the AutoLot.Mvc project, then gets the CurrentValue and passes the configured instance of the DealerInfo class to the view (although the view doesn’t do anything with it yet).

    public IActionResult Index([FromServices] IOptionsMonitor dealerMonitor)
    {
    var vm = dealerMonitor.CurrentValue;
    return View(vm);
    }

    The following example replicates the process for the Index Razor page in the Pages folder in the AutoLot.Web project. Instead of passing the instance to the view, it is assigned to a property on the page. Update the Index.cshtml.cs by adding the same injection into the OnGet() method:

    public DealerInfo DealerInfoInstance { get; set; }
    public void OnGet([FromServices]IOptionsMonitor dealerOptions)
    {
    DealerInfoInstance = dealerOptions.CurrentValue;
    }

    ■Note For more information on the options pattern in aSp.net Core, consult the documentation: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/ options?view=aspnetcore-6.0.

    The HTTP Client Factory
    ASP.NET Core 2.1 introduced the IHTTPClientFactory, which can be used to create and configure HttPClient instances. The factory manages the pooling and lifetime of the underlying HttpClientMessageHandler instance, abstracting that away from the developer. It provides four mechanisms of use:
    •Basic usage
    •Named clients,
    •Typed clients, and
    •Generated clients.
    After exploring the basic, named client, and typed client usages, we will build the API service wrappers that will be utilized by the data services built earlier in this chapter.

    ■Note For information on generating clients, consult the documentation: https://docs.microsoft. com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-6.0.

    Basic Usage
    The basic usage registers the IHttpClientFactory with the services collection, and then uses the injected factory instance to create HttPClient instances. This method is a convenient way to refactor an existing application that is creating HttPClient instances. To implement this basic usage, add the following line into the top level statements in Program.cs (no need to actually do this, as the projects will use Typed clients):

    builder.Services.AddHttpClient();

    Then in the class that needs an HttpClient, inject the IHttpClientFactory into the constructor, and then call CreateClient():
    namespace AutoLot.Services.DataServices.Api.Examples; public class BasicUsageWithIHttpClientFactory
    {
    private readonly IHttpClientFactory _clientFactory;
    public BasicUsageWithIHttpClientFactory(IHttpClientFactory clientFactory)
    {
    _clientFactory = clientFactory;
    }
    public async Task DoSomethingAsync()
    {
    var client = _clientFactory.CreateClient();
    //do something interesting with the client
    }
    }

    Named Clients
    Named clients are useful when your application uses distinct HttpClient instances, especially when they are configured differently. When registering the IHttpClientFactory, a name is provided along with any specific configuration for that HttpClient usage. To create a client named AutoLotApi, add the following into the top level statements in Program.cs (no need to actually do this, as the projects will use Typed clients):

    using AutoLot.Services.DataServices.Api.Examples; builder.Services.AddHttpClient(NamedUsageWithIHttpClientFactory.API_NAME, client =>
    {
    //add any configuration here
    });

    Then in the class that needs an HttpClient, inject the IHttpClientFactory into the constructor, and then call CreateClient() passing in the name of the client to be created. The configuration from the AddHttpClient() call is used to create the new instance:
    namespace AutoLot.Services.DataServices.Api.Examples; public class NamedUsageWithIHttpClientFactory
    {
    public const string API_NAME = "AutoLotApi"; private readonly IHttpClientFactory _clientFactory;
    public NamedUsageWithIHttpClientFactory(IHttpClientFactory clientFactory)
    {
    _clientFactory = clientFactory;
    }
    public async Task DoSomethingAsync()
    {
    var client = _clientFactory.CreateClient(API_NAME);
    //do something interesting with the client
    }
    }

    Typed Clients
    Typed clients are classes that accept an HttpClient instance through injection into its constructor. Since the typed client is a class, it can be added into the service collection and injected into other classes,
    fully encapsulating the calls using an HttpClient. As an example, suppose you have a class named
    ApiServiceWrapper that takes in an HttpClient and implements IApiServiceWrapper as follows:
    namespace AutoLot.Services.DataServices.Api.Examples; public interface IApiServiceWrapperExample
    {
    //interesting methods places here
    }

    public class ApiServiceWrapperExample : IApiServiceWrapperExample
    {
    protected readonly HttpClient Client;
    public ApiServiceWrapperExample(HttpClient client)
    {
    Client = client;
    //common client configuration goes here
    }
    //interesting methods implemented here
    }

    With the interface and the class in place, they can be added to the service collection as follows:

    builder.Services.AddHttpClient<IApiServiceWrapperExample,ApiServiceWrapperExample>();

    Inject the IApiServiceWrapper interface into the class that needs to make calls to the API and use the methods on the class instance to call to the API. This pattern completely abstracts the HttpClient away from the calling code:
    namespace AutoLot.Services.DataServices.Api.Examples; public class TypedUsageWithIHttpClientFactory
    {
    private readonly IApiServiceWrapperExample _serviceWrapper;

    public TypedUsageWithIHttpClientFactory(IApiServiceWrapperExample serviceWrapper)
    {
    _serviceWrapper = serviceWrapper;
    }
    public async Task DoSomethingAsync()
    {
    //do something interesting with the service wrapper
    }
    }

    In addition to the configuration options in the constructor of the class, the call to AddHttpClient() can also configure the client:

    builder.Services.AddHttpClient<IApiServiceWrapperExample,ApiServiceWrapperExample>(client=>
    {
    //configuration goes here
    });

    The AutoLot API Service Wrapper
    The AutoLot API service wrapper uses a typed base client and entity specific typed clients to encapsulate all of the calls to the AutoLot.Api project. Both the AutoLot.Mvc and AutoLot.Web projects will use the service wrapper through the data services classes stubbed out earlier in this chapter and completed at the end of this section. The AutoLot.Api project will be finished in the next chapter.

    Update the Application Configuration
    The AutoLot.Api application endpoints will vary based on the environment. For example, when developing on your workstation, the base URI is https://localhost:5011. In your integration environment, the URI might be https://mytestserver.com. The environmental awareness, in conjunction with the updated configuration system, will be used to add these different values.
    The appsettings.Development.json file will add the service information for the local machine. As code moves through different environments, the settings would be updated in each environment’s specific file to match the base URI and endpoints for that environment. In this example, you only update the settings for the development environment. In the appsettings.Development.json files in the AutoLot.Mvc and AutoLot.Web projects, add the following after the ConnectionStrings entry (changes in bold):

    "ConnectionStrings": {
    "AutoLot": "Server=.,5433;Database=AutoLot;User ID=sa;Password=P@ssw0rd;"
    },
    "ApiServiceSettings": {
    "Uri": "https://localhost:5011/", "UserName": "AutoLotUser", "Password": "SecretPassword", "CarBaseUri": "api/v1/Cars", "MakeBaseUri": "api/v1/Makes", "MajorVersion": 1,
    "MinorVersion": 0, "Status": ""
    }

    ■Note Make sure the port number matches your configuration for autoLot.api.

    Create the ApiServiceSettings Class
    The service settings will be populated from the settings the same way the dealer information was populated. In the AutoLot.Service project, create a new folder named ApiWrapper and in that folder, create a new folder named Models. In this folder, add a class named ApiServiceSettings.cs. The property names of the class need to match the property names in the JSON ApiServiceSettings section. The class is listed here:
    namespace AutoLot.Services.ApiWrapper.Models; public class ApiServiceSettings
    {
    public ApiServiceSettings() { } public string UserName { get; set; } public string Password { get; set; } public string Uri { get; set; }
    public string CarBaseUri { get; set; } public string MakeBaseUri { get; set; } public int MajorVersion { get; set; } public int MinorVersion { get; set; } public string Status { get; set; }

    public string ApiVersion
    => $"{MajorVersion}.{MinorVersion}" + (!string.IsNullOrEmpty(Status)?$"-
    {Status}":string.Empty);
    }

    ■Note api versioning will be covered in depth in Chapter 32.

    Add the following global using statement to the GlobalUsings.cs file in the AutoLot.Service project:

    global using AutoLot.Services.ApiWrapper.Models;

    Register the ApiServiceSettings Class
    We are once again going to use an extension method to register everything needed for the API service wrapper, including configuring the ApiServiceSettings.cs using the Options pattern. Create a new folder named Configuration under the ApiWrapper folder, and in that folder create a new public static class named ServiceConfiguration, as shown here:
    namespace AutoLot.Services.ApiWrapper.Configuration; public static class ServiceConfiguration
    {
    public static IServiceCollection ConfigureApiServiceWrapper(this IServiceCollection services, IConfiguration config)
    {
    services.Configure(config.GetSection(nameof(ApiServiceSettings))); return services;
    }
    }

    Add the following global using statement to the GlobalUsings.cs file for both the AutoLot.Mvc and AutoLot.Web projects:

    global using AutoLot.Services.ApiWrapper.Configuration;

    Add the following to the top level statements in Program.cs (in both AutoLot.Mvc and AutoLot.Web) before the call to builder.Build():

    builder.Services.ConfigureApiServiceWrapper(builder.Configuration);

    The API Service Wrapper Base Class and Interface
    The ApiServiceWrapperBase class is a generic base class that performs create, read, update, and delete (CRUD) operations against the AutoLot.Api RESTful service. This centralizes communication with the service, configuration of the HTTP client, error handling, and so on. Entity specific classes will inherit from this base class, and those will be added into the services collection.

    The IApiServiceWrapperBase Interface
    The AutoLot service wrapper interface contains the common methods to call into the AutoLot.Api service. Create a directory named Interfaces in the ApiWrapper directory. Add a new interface named IApiServiceWrapper.cs and update the interface to the code shown here:

    namespace AutoLot.Services.ApiWrapper.Interfaces;

    public interface IApiServiceWrapperBase where TEntity : BaseEntity, new()
    {
    Task<IList> GetAllEntitiesAsync(); Task GetEntityAsync(int id); Task AddEntityAsync(TEntity entity); Task UpdateEntityAsync(TEntity entity); Task DeleteEntityAsync(TEntity entity);
    }

    Add the following global using statement to the GlobalUsings.cs file for the AutoLot.Services project:

    global using AutoLot.Services.ApiWrapper.Interfaces;

    The ApiServiceWrapperBase Class
    Before building the base class, add the following global using statements to the GlobalUsings.cs file:

    global using AutoLot.Services.ApiWrapper.Base; global using Microsoft.Extensions.Options; global using System.Net.Http.Headers;
    global using System.Net.Http.Json; global using System.Text;
    global using System.Text.Json;

    Create a new folder named Base in the ApiWrapper directory of the AutoLot.Services project and add a class named ApiServiceWrapperBase. Make the class public and abstract and implement the
    IApiServiceWrapperBase interface. Add a protected constructor that takes an instance of HttpClient and IOptionsMonitor and a string for the entity specific endpoint. Add a protected readonly string to hold the ApiVersion from the settings and initialize them all from the constructor
    like this:

    namespace AutoLot.Services.ApiWrapper.Base;

    public abstract class ApiServiceWrapperBase
    : IApiServiceWrapperBase where TEntity : BaseEntity, new()
    {
    protected readonly HttpClient Client; private readonly string _endPoint;
    protected readonly ApiServiceSettings ApiSettings; protected readonly string ApiVersion;

    protected ApiServiceWrapperBase(
    HttpClient client, IOptionsMonitor apiSettingsMonitor, string endPoint)
    {
    Client = client;
    _endPoint = endPoint;
    ApiSettings = apiSettingsMonitor.CurrentValue; ApiVersion = ApiSettings.ApiVersion;
    }
    }

    Configuring the HttpClient in the Constructor
    The constructor adds the standard configuration to the HttpClient that will be used by all methods, including an AuthorizationHeader that uses basic authentication. Basic authentication will be covered in depth in the next chapter, Restful Services with ASP.NET Core, so for now just understand that basic authentication takes a username and password, concatenates them together (separated by a colon) and Base64 encodes them. The rest of the code sets the BaseAddress for the HttpClient and specifies that the client is expecting JSON.

    public ApiServiceWrapperBase(
    HttpClient client, IOptionsMonitor apiSettingsMonitor, string endPoint)
    {
    Client = client;
    _endPoint = endPoint;
    ApiSettings = apiSettingsMonitor.CurrentValue; ApiVersion = ApiSettings.ApiVersion; Client.BaseAddress = new Uri(ApiSettings.Uri);
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/ json"));
    var authToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{apiSettingsMonitor. CurrentValue.UserName}:{apiSettingsMonitor.CurrentValue.Password}")); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("basic", authToken);
    }

    The Internal Support Methods
    The class contains four support methods that are used by the public methods.

    The Post and Put Helper Methods
    These methods wrap the related HttpClient methods.

    internal async Task PostAsJsonAsync(string uri, string json)
    {
    return await Client.PostAsync(uri, new StringContent(json, Encoding.UTF8, "application/ json"));
    }

    internal async Task PutAsJsonAsync(string uri, string json)
    {
    return await Client.PutAsync(uri, new StringContent(json, Encoding.UTF8, "application/ json"));
    }

    The HTTP Delete Helper Method Call
    The final helper method is used for executing an HTTP delete. The HTTP 1.1 specification (and later) allows for passing a body in a delete statement, but there isn’t yet an extension method of the HttpClient for doing this. The HttpRequestMessage must be built up from scratch.
    The first step is to then create a request message using object initialization to set Content, Method, and RequestUri. Once this is complete, the message is sent, and the response is returned to the calling code. The method is shown here:

    internal async Task DeleteAsJsonAsync(string uri, string json)
    {
    HttpRequestMessage request = new HttpRequestMessage
    {
    Content = new StringContent(json, Encoding.UTF8, "application/json"), Method = HttpMethod.Delete,
    RequestUri = new Uri(uri)
    };
    return await Client.SendAsync(request);
    }

    The HTTP Get Calls
    There are two Get calls: one to get all records and one to get a single record. They both follow the same pattern. The GetAsync() method is called to return an HttpResponseMessage. The success or failure of the call is checked with the EnsureSuccessStatusCode() method, which throws an exception if the call did not return a successful status code. Then the body of the response is serialized back into the property type
    (either as an entity or a list of entities) and returned to the calling code. The endpoint is constructed from the settings, and the ApiVersion is appended as a query string value. Each of these methods is shown here:

    public async Task<IList> GetAllEntitiesAsync()
    {
    var response = await Client.GetAsync($"{ApiSettings.Uri}{_endPoint}?v={ApiVersion}"); response.EnsureSuccessStatusCode();
    var result = await response.Content.ReadFromJsonAsync<IList>(); return result;
    }

    public async Task GetEntityAsync(int id)
    {
    var response = await Client.GetAsync($"{ApiSettings.Uri}{_endPoint}/{id}?v={ApiVersion}"); response.EnsureSuccessStatusCode();
    var result = await response.Content.ReadFromJsonAsync(); return result;
    }

    Notice the improvement in serializing the response body into an item (or list of items). Prior versions of ASP.NET Core would have to use the following code instead of the shorter ReadFromJsonAsync() method:

    var result = await JsonSerializer.DeserializeAsync<IList>(await response.Content. ReadAsStreamAsync());

    The HTTP Post Call
    The method to add a record uses an HTTP Post request. It uses the helper method to post the entity as JSON and then returns the record from the response body. The method is listed here:

    public async Task AddEntityAsync(TEntity entity)
    {
    var response = await PostAsJsonAsync($"{ApiSettings.Uri}{_endPoint}?v={ApiVersion}", JsonSerializer.Serialize(entity));
    if (response == null)
    {
    throw new Exception("Unable to communicate with the service");
    }

    var location = response.Headers?.Location?.OriginalString;
    return await response.Content.ReadFromJsonAsync() ?? await GetEntityAsync(entity.Id);
    }

    There are two lines of note. The first is the line getting the location. Typically, when an HTTP Post call is successful, the service returns an HTTP 201 (Created at) status code. The service will also add the URI of the newly created resource in the Location header. The preceding code demonstrates getting the Location header but isn’t using the location in the code process.
    The second line of note is reading the response content and creating an instance of the updated record. The service wrapper method needs to get the updated instance from the service to guarantee server generated values (like Id and TimeStamp) and are updated in the client app. Returning the updated entity in the response from HTTP post calls is not a guaranteed function of a service. If the service doesn’t return the entity, then the method uses the GetEntityAsync() method. It could also use the URI from the location if
    necessary, but since the GetEntityAsync() supplies everything needed, getting the location value is merely
    for demo purposes.

    The HTTP Put Call
    The method to update a record uses an HTTP Put request by way of the PutAsJsonAsync() helper method. This method also assumes that the updated entity is in the body of the response, and if it isn’t, calls the GetEntityAsync() to refresh the server generated values.

    public async Task UpdateEntityAsync(TEntity entity)
    {
    var response = await PutAsJsonAsync($"{ApiSettings.Uri}{_endPoint}/{entity. Id}?v={ApiVersion}",
    JsonSerializer.Serialize(entity)); response.EnsureSuccessStatusCode();
    return await response.Content.ReadFromJsonAsync() ?? await GetEntityAsync(entity.Id);
    }

    The HTTP Delete Call
    The final method to add, is for executing an HTTP Delete. The pattern follows the rest of the methods: use the helper method and check the response for success. There isn’t anything to return to the calling code since the entity was deleted. The method is shown here:

    public async Task DeleteEntityAsync(TEntity entity)
    {
    var response = await DeleteAsJsonAsync($"{ApiSettings.Uri}{_endPoint}/{entity. Id}?v={ApiVersion}",
    JsonSerializer.Serialize(entity)); response.EnsureSuccessStatusCode();
    }

    The Entity Specific Interfaces
    In the Interfaces directory, create two new interfaces named ICarApiServiceWrapper.cs and
    IMakeApiServiceWrapper.cs. Update the interfaces to the following:

    //ICarApiServiceWrapper.cs
    namespace AutoLot.Services.ApiWrapper.Interfaces;

    public interface ICarApiServiceWrapper : IApiServiceWrapperBase
    {
    Task<IList> GetCarsByMakeAsync(int id);
    }

    //IMakeApiServiceWrapper.cs
    namespace AutoLot.Services.ApiWrapper.Interfaces;

    public interface IMakeApiServiceWrapper : IApiServiceWrapperBase
    {
    }

    The Entity Specific Classes
    In the ApiWrapper directory, create two new classes named CarApiServiceWrapper.cs and MakeApiServiceWrapper.cs. The constructor for each class takes an instance of HttpClient and IOptions and passes them into the base class along with the entity specific end point:

    //CarApiServiceWrapper.cs
    namespace AutoLot.Services.ApiWrapper;

    public class CarApiServiceWrapper : ApiServiceWrapperBase, ICarApiServiceWrapper
    {
    public CarApiServiceWrapper(HttpClient client, IOptionsMonitor apiSettingsMonitor)
    : base(client, apiSettingsMonitor, apiSettingsMonitor.CurrentValue.CarBaseUri) { }
    }

    //MakeApiServiceWrapper.cs
    namespace AutoLot.Services.ApiWrapper;

    public class MakeApiServiceWrapper : ApiServiceWrapperBase, IMakeApiServiceWrapper
    {
    public MakeApiServiceWrapper(HttpClient client, IOptionsMonitor apiSettingsMonitor)
    : base(client, apiSettingsMonitor, apiSettingsMonitor.CurrentValue.MakeBaseUri) { }
    }

    The MakeApiServiceWrapper only needs the methods exposed in the ApiServiceWrapperBase to do its job. The CarApiServiceWrapper class has one additional method to get the list of Car records by Make. The method follows the same pattern as the base class’s HTTP Get methods but uses a unique end point:

    public async Task<IList> GetCarsByMakeAsync(int id)
    {
    var response = await Client.GetAsync($"{ApiSettings.Uri}{ApiSettings.CarBaseUri}/bymake/
    {id}?v={ApiVersion}"); response.EnsureSuccessStatusCode();
    var result = await response.Content.ReadFromJsonAsync<IList>(); return result;
    }

    As a last step, register the two typed clients by updating the ConfigureApiServiceWrapper method in the ServiceConfiguration class to the following:

    public static IServiceCollection ConfigureApiServiceWrapper(this IServiceCollection services, IConfiguration config)
    {
    services.Configure(config.GetSection(nameof(ApiServiceSettings))); services.AddHttpClient<ICarApiServiceWrapper, CarApiServiceWrapper>(); services.AddHttpClient<IMakeApiServiceWrapper, MakeApiServiceWrapper>();
    return services;
    }

    Complete the API Data Services
    Now that the API service wrappers are complete, it’s time to revisit the API data services and complete the class implementations.

    Complete the ApiDataServiceBase Class
    The first step to complete the base class is to update the constructor to receive an instance of the IApiServic eWrapperBase interface and assign it to a protected field:

    protected readonly IApiServiceWrapperBase ServiceWrapper;
    protected ApiDataServiceBase(IApiServiceWrapperBase serviceWrapperBase)
    {
    ServiceWrapper = serviceWrapperBase;
    }

    The implementation for each of the CRUD methods calls the related method on the ServiceWrapper:

    public async Task<IEnumerable> GetAllAsync()
    => await ServiceWrapper.GetAllEntitiesAsync();
    public async Task FindAsync(int id)
    => await ServiceWrapper.GetEntityAsync(id);
    public async Task UpdateAsync(TEntity entity, bool persist = true)
    {
    await ServiceWrapper.UpdateEntityAsync(entity); return entity;
    }

    public async Task DeleteAsync(TEntity entity, bool persist = true)
    => await ServiceWrapper.DeleteEntityAsync(entity);

    public async Task AddAsync(TEntity entity, bool persist = true)
    {
    await ServiceWrapper.AddEntityAsync(entity); return entity;
    }

    Complete the Entity Specific Classes
    The CarApiDataService and MakeApiDataService classes need their constructors updated to receive their entity specific derived instance of the IApiServiceWrapperBase interface and pass it to the
    base class:

    public class CarApiDataService : ApiDataServiceBase, ICarDataService
    {
    public CarApiDataService(ICarApiServiceWrapper serviceWrapper) : base(serviceWrapper) { }
    }

    public class MakeApiDataService : ApiDataServiceBase, IMakeDataService
    {
    public MakeApiDataService(IMakeApiServiceWrapper serviceWrapper):base(serviceWrapper) { }
    }

    The GetAllByMakeIdIdAsync() method determines if a value was passed in for the makeId parameter.
    If there is a value, the relevant method on the ICarApiServiceWrapper is called. Otherwise, the base
    GetAllAsync() is called:

    public async Task<IEnumerable> GetAllByMakeIdAsync(int? makeId)
    => makeId.HasValue
    ? await ((ICarApiServiceWrapper)ServiceWrapper). GetCarsByMakeAsync(makeId.Value)
    : await GetAllAsync();

    Deploying ASP.NET Core Applications
    Prior versions of ASP.NET applications could only be deployed to Windows servers using IIS as the web server. ASP.NET Core can be deployed to multiple operating systems in multiple ways, using a variety of web servers. ASP.NET Core applications can also be deployed outside of a web server. The high-level options are as follows:
    •On a Windows server (including Azure) using IIS
    •On a Windows server (including Azure app services) outside of IIS
    •On a Linux server using Apache or NGINX
    •On Windows or Linux in a container
    This flexibility allows organizations to decide the deployment platform that makes the most sense for them, including popular container-based deployment models (such as using Docker), as opposed to being locked into Windows servers.

    Lightweight and Modular HTTP Request Pipeline
    Following along with the principles of .NET, you must opt in for everything in ASP.NET Core. By default, nothing is loaded into an application. This enables applications to be as lightweight as possible, improving performance, and minimizing the surface area and potential risk.

    Logging
    Logging in ASP.NET Core is based on an ILoggerFactory. This enables different logging providers to hook into the logging system to send log messages to different locations, such as the Console. The ILoggerFactory is used to create an instance of ILogger, which provides the following methods for logging by way of the LoggerExtensions class:

    public static class LoggerExtensions
    {
    public static void LogDebug(this ILogger logger, EventId eventId, Exception exception, string message, params object[] args)
    public static void LogDebug(this ILogger logger, EventId eventId, string message, params object[] args)
    public static void LogDebug(this ILogger logger, Exception exception, string message, params object[] args)
    public static void LogDebug(this ILogger logger, string message, params object[] args)

    public static void LogTrace(this ILogger logger, EventId eventId, Exception exception, string message, params object[] args)
    public static void LogTrace(this ILogger logger, EventId eventId, string message, params object[] args)
    public static void LogTrace(this ILogger logger, Exception exception, string message, params object[] args)
    public static void LogTrace(this ILogger logger, string message, params object[] args)

    public static void LogInformation(this ILogger logger, EventId eventId, Exception exception, string message, params object[] args)
    public static void LogInformation(this ILogger logger, EventId eventId, string message, params object[] args)
    public static void LogInformation(this ILogger logger, Exception exception, string message, params object[] args)
    public static void LogInformation(this ILogger logger, string message, params object[] args)

    public static void LogWarning(this ILogger logger, EventId eventId, Exception exception, string message, params object[] args)
    public static void LogWarning(this ILogger logger, EventId eventId, string message, params object[] args)
    public static void LogWarning(this ILogger logger, Exception exception, string message, params object[] args)
    public static void LogWarning(this ILogger logger, string message, params object[] args)

    public static void LogError(this ILogger logger, EventId eventId, Exception exception, string message, params object[] args)
    public static void LogError(this ILogger logger, EventId eventId, string message, params object[] args)
    public static void LogError(this ILogger logger, Exception exception, string message, params object[] args)
    public static void LogError(this ILogger logger, string message, params object[] args)

    public static void LogCritical(this ILogger logger, EventId eventId, Exception exception, string message, params object[] args)
    public static void LogCritical(this ILogger logger, EventId eventId, string message, params object[] args)
    public static void LogCritical(this ILogger logger, Exception exception, string message, params object[] args)
    public static void LogCritical(this ILogger logger, string message, params object[] args)

    public static void Log(this ILogger logger, LogLevel logLevel, string message, params object[] args)
    public static void Log(this ILogger logger, LogLevel logLevel, EventId eventId, string message, params object[] args)
    public static void Log(this ILogger logger, LogLevel logLevel, Exception exception, string message, params object[] args)
    public static void Log(this ILogger logger, LogLevel logLevel, EventId eventId, Exception exception, string message, params object[] args)
    }

    Add Logging with Serilog
    Any provider that supplies an ILoggerFactory extension can be used for logging in ASP.NET Core, and Serilog is one such logging framework. The next sections cover creating a logging infrastructure based on Serilog and configuring the ASP.NET Core applications to use the new logging code.

    The Logging Settings
    To configure Serilog, we are going to use the application configuration files in conjunction with a C# class. Start by adding a new folder named Logging to the AutoLot.Service project. Create a new folder named Settings in the Logging folder, and in that new folder, add a class named AppLoggingSettings. Update the code to the following:
    namespace AutoLot.Services.Logging.Settings; public class AppLoggingSettings
    {
    public GeneralSettings General { get; set; } public FileSettings File { get; set; }
    public SqlServerSettings MSSqlServer { get; set; }

    public class GeneralSettings
    {
    public string RestrictedToMinimumLevel { get; set; }
    }
    public class SqlServerSettings
    {
    public string TableName { get; set; } public string Schema { get; set; }
    public string ConnectionStringName { get; set; }
    }

    public class FileSettings
    {
    public string Drive { get; set; } public string FilePath { get; set; } public string FileName { get; set; } public string FullLogPathAndFileName =>

    $"{Drive}{Path.VolumeSeparatorChar}{Path.DirectorySeparatorChar}{FilePath}{Path. DirectorySeparatorChar}{FileName}";
    }
    }

    Add the following global using statement to the GlobalUsings.cs file in the AutoLot.Service project.

    global using AutoLot.Services.Logging.Settings;

    Next, use the following JSON to replace the scaffolded Logging details in the appsettings.
    Development.json files for the AutoLot.Api, AutoLot.Mvc, and AutoLot.Web projects:

    "AppLoggingSettings": { "MSSqlServer": {
    "TableName": "SeriLogs", "Schema": "Logging", "ConnectionStringName": "AutoLot"
    },

    "File": { "Drive": "c",
    "FilePath": "temp", "FileName": "log_AutoLot.txt"
    },
    "General": {
    "RestrictedToMinimumLevel": "Information"
    }
    },

    Next, add the following AppName node to each of the files, customized for each app:

    //AutoLot.Api "AppName":"AutoLot.Api - Dev"

    //AutoLot.Mvc "AppName":"AutoLot.Mvc - Dev"

    //AutoLot.Web "AppName":"AutoLot.Web - Dev"

    For reference, here is the complete listing for each project (notice the extra line at the beginning of the AutoLot.Web file – that will be covered in chapter 34):

    //AutoLot.Api
    {
    "AppLoggingSettings": { "MSSqlServer": {
    "TableName": "SeriLogs", "Schema": "Logging", "ConnectionStringName": "AutoLot"
    },
    "File": { "Drive": "c",
    "FilePath": "temp", "FileName": "log_AutoLot.txt"
    },
    "General": {
    "RestrictedToMinimumLevel": "Information"
    }
    },
    "ConnectionStrings": {
    "AutoLot": "Server=.,5433;Database=AutoLot;User ID=sa;Password=P@ssw0rd;"
    },
    "RebuildDatabase": true, "AppName": "AutoLot.Api - Dev"
    }

    //AutoLot.Mvc
    {
    "AppLoggingSettings": { "MSSqlServer": {

    "TableName": "SeriLogs", "Schema": "Logging", "ConnectionStringName": "AutoLot"
    },
    "File": { "Drive": "c",
    "FilePath": "temp", "FileName": "log_AutoLot.txt"
    },
    "General": {
    "RestrictedToMinimumLevel": "Information"
    }
    },
    "ConnectionStrings": {
    "AutoLot": "Server=.,5433;Database=AutoLot;User ID=sa;Password=P@ssw0rd;"
    },
    "RebuildDatabase": true, "UseApi": false, "ApiServiceSettings": {
    "Uri": "https://localhost:5011/", "UserName": "AutoLotUser", "Password": "SecretPassword", "CarBaseUri": "api/v1/Cars", "MakeBaseUri": "api/v1/Makes"
    },
    "AppName": "AutoLot.Mvc - Dev"
    }

    //AutoLot.Web
    {
    "DetailedErrors": true, "AppLoggingSettings": {
    "MSSqlServer": { "TableName": "SeriLogs", "Schema": "Logging",
    "ConnectionStringName": "AutoLot"
    },
    "File": { "Drive": "c",
    "FilePath": "temp", "FileName": "log_AutoLot.txt"
    },
    "General": {
    "RestrictedToMinimumLevel": "Information"
    }
    },
    "ConnectionStrings": {
    "AutoLot": "Server=.,5433;Database=AutoLot;User ID=sa;Password=P@ssw0rd;"
    },
    "RebuildDatabase": true, "UseApi": false,

    "ApiServiceSettings": {
    "Uri": "https://localhost:5011/", "UserName": "AutoLotUser", "Password": "SecretPassword", "CarBaseUri": "api/v1/Cars", "MakeBaseUri": "api/v1/Makes"
    },
    "AppName": "AutoLot.Web - Dev"
    }

    The final step is to clear out the Logging section of each of the appsettings.json files, leaving just the AllowedHosts entry in the AutoLot.Api project, and the AllowedHosts and the DealerInfo in the AutoLot. Mvc and AutoLot.Web projects:

    //AutoLot.Api
    {
    "AllowedHosts": "*"
    }

    //AutoLot.Mvc
    {
    "AllowedHosts": "*", "DealerInfo": {
    "DealerName": "Skimedic's Used Cars", "City": "West Chester",
    "State": "Ohio"
    }
    }

    //AutoLot.Web
    {
    "AllowedHosts": "*", "DealerInfo": {
    "DealerName": "Skimedic's Used Cars", "City": "West Chester",
    "State": "Ohio"
    }
    }

    The Logging Configuration
    The next step is to configure Serilog. Get started by adding a new folder named Configuration in the Logging folder of the AutoLot.Service project. In this folder add a new class named LoggingConfiguration. Make the class public and static, as shown here:
    namespace AutoLot.Services.Logging.Configuration; public static class LoggingConfiguration
    {
    }

    Serilog uses sinks to write to different logging targets. With this mechanism, a single call to SeriLog will write to many places. The targets we will use for logging in the ASP.NET Core apps are a text file, the
    database, and the console. The text file and database sinks require configuration, an output template for the text file sink, and a list of fields for the database sink.
    To set up the file template, create the following static readonly string:

    internal static readonly string OutputTemplate =
    @"[{Timestamp:yy-MM-dd HH:mm:ss} {Level}]{ApplicationName}:{SourceContext}{NewLine} Message:{Message}{NewLine}in method {MemberName} at {FilePath}:{LineNumber}{NewLine}
    {Exception}{NewLine}";

    The SQL Server sink needs a list of columns identified using the SqlColumn type. Add the following code to configure the database columns:

    internal static readonly ColumnOptions ColumnOptions = new ColumnOptions
    {
    AdditionalColumns = new List
    {
    new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "ApplicationName"}, new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "MachineName"},
    new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "MemberName"}, new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "FilePath"}, new SqlColumn {DataType = SqlDbType.Int, ColumnName = "LineNumber"},
    new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "SourceContext"}, new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "RequestPath"}, new SqlColumn {DataType = SqlDbType.VarChar, ColumnName = "ActionName"},
    }
    };

    Swapping the default logger with Serilog is a three-step process. The first is to clear the existing provider, the second is to add Serilog into the WebApplicationBuilder, and the third is to finish configuring Serilog. Add a new method named ConfigureSerilog(), which is an extension method for the WebApplicationBuilder. The first line clears out the default loggers, and the last line adds the fully configured Serilog framework into the WebApplicationBuilder‘s logging framework.

    public static void ConfigureSerilog(this WebApplicationBuilder builder)
    {
    builder.Logging.ClearProviders(); var config = builder.Configuration;
    var settings = config.GetSection(nameof(AppLoggingSettings)).Get(); var connectionStringName = settings.MSSqlServer.ConnectionStringName;
    var connectionString = config.GetConnectionString(connectionStringName); var tableName = settings.MSSqlServer.TableName;
    var schema = settings.MSSqlServer.Schema;
    string restrictedToMinimumLevel = settings.General.RestrictedToMinimumLevel; if (!Enum.TryParse(restrictedToMinimumLevel, out var logLevel))
    {
    logLevel = LogEventLevel.Debug;
    }
    var sqlOptions = new MSSqlServerSinkOptions
    {
    AutoCreateSqlTable = false,

    SchemaName = schema, TableName = tableName,
    };
    if (builder.Environment.IsDevelopment())
    {
    sqlOptions.BatchPeriod = new TimeSpan(0, 0, 0, 1);
    sqlOptions.BatchPostingLimit = 1;
    }

    var log = new LoggerConfiguration()
    .MinimumLevel.Is(logLevel)
    .Enrich.FromLogContext()
    .Enrich.With(new PropertyEnricher("ApplicationName", config.GetValue("Applicati onName")))
    .Enrich.WithMachineName()
    .WriteTo.File(
    path: builder.Environment.IsDevelopment()? settings.File.FileName : settings.File. FullLogPathAndFileName,
    rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: logLevel, outputTemplate: OutputTemplate)
    .WriteTo.Console(restrictedToMinimumLevel: logLevel)
    .WriteTo.MSSqlServer( connectionString: connectionString, sqlOptions, restrictedToMinimumLevel: logLevel, columnOptions: ColumnOptions);
    builder.Logging.AddSerilog(log.CreateLogger(),false);
    }

    With everything in place, it’s time to create the logging framework that will use Serilog.

    The AutoLot Logging Framework
    The AutoLot logging framework leverages the built-in logging capabilities of ASP.NET Core to simplify using Serilog. It starts with the IAppLogging interface.

    The IAppLogging Interface
    The IApplogging interface holds the logging methods for the custom logging system. Add new directory named Interfaces in the Logging directory in the AutoLot.Service project. In this directory, add a new interface named IAppLogging. Update the code in this interface to match the following:
    namespace AutoLot.Services.Logging.Interfaces; public interface IAppLogging
    {
    void LogAppError(Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0);

    void LogAppError(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0);

    void LogAppCritical(Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0);

    void LogAppCritical(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0);

    void LogAppDebug(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0);

    void LogAppTrace(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0);

    void LogAppInformation(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0);

    void LogAppWarning(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0);
    }

    The attributes CallerMemberName, CallerFilePath, and CallerLineNumber inspect the call stack to get the values they are named for from the calling code. For example, if the line that calls LogAppWarning() is in the DoWork() function, in a file named MyClassFile.cs, and resides on line number 36, then the call:

    _appLogger.LogAppError(ex, "ERROR!");

    is converted into the equivalent of this:

    _appLogger.LogAppError(ex,"ERROR","DoWork ","c:/myfilepath/MyClassFile.cs",36);

    If values are passed into the method call for any of the attributed parameters, the values passed in are used instead of the values from the attributes.
    Add the following global using statement to the GlobalUsings.cs file in the AutoLot.Services project:

    global using AutoLot.Services.Logging.Interfaces;

    The AppLogging Class
    The AppLogging class implements the IAppLogging interface. Add a new class named AppLogging to the Logging directory. Make the class public and implement IAppLogging and add a constructor that takes an instance of ILogger and stores it in a class level variable.

    namespace AutoLot.Services.Logging;

    public class AppLogging : IAppLogging
    {
    private readonly ILogger _logger;

    public AppLogging(ILogger logger)
    {
    _logger = logger;
    }
    }

    Serilog enables adding additional properties into the standard logging process by pushing them onto the LogContext. Add an internal method to log an event with an exception and push the MemberName, FilePath, and LineNumber properties. The PushProperty() method returns an IDisposable, so the method disposes everything before leaving the method.

    internal static void LogWithException(string memberName,
    string sourceFilePath, int sourceLineNumber, Exception ex, string message, Action<Exception, string, object[]> logAction)
    {
    var list = new List
    {
    LogContext.PushProperty("MemberName", memberName), LogContext.PushProperty("FilePath", sourceFilePath), LogContext.PushProperty("LineNumber", sourceLineNumber),
    };
    logAction(ex,message,null); foreach (var item in list)
    {
    item.Dispose();
    }
    }

    Repeat the process for log events that don’t include an exception:

    internal static void LogWithoutException(string memberName, string sourceFilePath, int sourceLineNumber, string message, Action<string, object[]> logAction)
    {
    var list = new List
    {
    LogContext.PushProperty("MemberName", memberName), LogContext.PushProperty("FilePath", sourceFilePath), LogContext.PushProperty("LineNumber", sourceLineNumber),
    };

    logAction(message, null); foreach (var item in list)
    {
    item.Dispose();
    }
    }

    For each type of logging event, call the appropriate help method to write to the logs:

    public void LogAppError(Exception exception, string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
    {
    LogWithException(memberName, sourceFilePath, sourceLineNumber, exception, message, _ logger.LogError);
    }

    public void LogAppError(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
    {
    LogWithoutException(memberName, sourceFilePath, sourceLineNumber, message, _logger. LogError);
    }

    public void LogAppCritical(Exception exception, string message, [CallerMemberName] string memberName = "",
    [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
    {
    LogWithException(memberName, sourceFilePath, sourceLineNumber, exception, message, _ logger.LogCritical);
    }

    public void LogAppCritical(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
    {
    LogWithoutException(memberName, sourceFilePath, sourceLineNumber, message, _logger. LogCritical);
    }

    public void LogAppDebug(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
    {
    LogWithoutException(memberName, sourceFilePath, sourceLineNumber, message, _logger.LogDebug);
    }

    public void LogAppTrace(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
    {
    LogWithoutException(memberName, sourceFilePath, sourceLineNumber, message, _logger. LogTrace);
    }

    public void LogAppInformation(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
    {
    LogWithoutException(memberName, sourceFilePath, sourceLineNumber, message, _logger. LogInformation);
    }

    public void LogAppWarning(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
    {
    LogWithoutException(memberName, sourceFilePath, sourceLineNumber, message, _logger. LogWarning);
    }

    Final Configuration
    The final configuration is to add the IAppLogging<> interface into the DI container and call the extension method to add SeriLog into the WebApplicationBuilder. Start by creating a new extension method in the LoggingConfiguration class:

    public static IServiceCollection RegisterLoggingInterfaces(this IServiceCollection services)
    {
    services.AddScoped(typeof(IAppLogging<>), typeof(AppLogging<>)); return services;
    }

    Next, add the following global using statement to the GlobalUsings.cs file in each web project:

    global using AutoLot.Services.Logging.Configuration; global using AutoLot.Services.Logging.Interfaces;

    Next, add both of the extension methods to the top level statements in the Program.cs file. Note that the ConfigureSerilog() method extends off of the WebAppBuilder (the builder variable) and the RegisterLoggingInterfaces() method extends off of the IServiceCollection:

    //Configure logging builder.ConfigureSerilog(); builder.Services.RegisterLoggingInterfaces();

    Add Logging to the Data Services
    With Serilog and the AutoLot logging system in place, it’s time to update the data services to add logging capabilities.

    Update the Base Classes
    Starting with the ApiDataServiceBase and DalDataServiceBase classes, update the generic definition to also accept a class that implements IDataServiceBase. This is to allow for strongly typing the IAppLogging interface to each of the derived class. Here are the updated classes definitions:

    //ApiDataServiceBase.cs
    public abstract class ApiDataServiceBase<TEntity, TDataService> : IDataServiceBase where TEntity : BaseEntity, new()
    where TDataService : IDataServiceBase
    {
    //omitted for brevity
    }

    //DalDataServiceBase.cs
    public abstract class DalDataServiceBase<TEntity, TDataService> : IDataServiceBase where TEntity : BaseEntity, new()
    where TDataService : IDataServiceBase
    {
    //omitted for brevity
    }

    Next, update each of the constructors to take an instance of IAppLogging and assign it to a protected class field:

    //CarApiDataService.cs
    protected readonly IApiServiceWrapperBase ServiceWrapper;
    protected readonly IAppLogging AppLoggingInstance;

    protected ApiDataServiceBase(IAppLogging appLogging,
    IApiServiceWrapperBase serviceWrapperBase)
    {
    ServiceWrapper = serviceWrapperBase;
    AppLoggingInstance = appLogging;
    }

    //MakeApiDataService.cs
    protected readonly IBaseRepo MainRepo;
    protected readonly IAppLogging AppLoggingInstance;

    protected DalDataServiceBase(IAppLogging appLogging, IBaseRepo mainRepo)
    {
    MainRepo = mainRepo;
    AppLoggingInstance = appLogging;
    }

    Update the Entity Specific Data Services Classes
    Each of the entity specific classes need to change their inheritance signature to use the new generic parameter and also take an instance of IAppLogging in the constructor and pass it to the base class. Here is the code for the updated classes:

    //CarApiDataService.cs
    public class CarApiDataService : ApiDataServiceBase<Car,CarApiDataService>, ICarDataService
    {
    public CarApiDataService( IAppLogging appLogging, ICarApiServiceWrapper serviceWrapper)
    : base(appLogging, serviceWrapper) { }
    //omitted for brevity
    }

    //MakeApiDataService.cs
    public class MakeApiDataService
    : ApiDataServiceBase<Make,MakeApiDataService>, IMakeDataService
    {
    public MakeApiDataService( IAppLogging appLogging, IMakeApiServiceWrapper serviceWrapper)
    : base(appLogging,serviceWrapper) { }
    }

    //CarDalDataService.cs
    public class CarDalDataService : DalDataServiceBase<Car,CarDalDataService>,ICarDataService
    {
    private readonly ICarRepo _repo;
    public CarDalDataService(IAppLogging appLogging, ICarRepo repo) : base(appLogging, repo)
    {
    _repo = repo;
    }
    //omitted for brevity
    }

    //MakeApiDataService.cs
    public class MakeDalDataService : DalDataServiceBase<Make,MakeDalDataService>,IMakeD ataService
    {
    public MakeDalDataService(IAppLogging appLogging,IMakeRepo repo)
    : base(appLogging, repo) { }
    }

    Next, update the constructors to take the instance of IAppLogging and assign it to a protected class field:

    Test-Drive the Logging Framework
    To close out the chapter, let’s test the logging framework. The first step is to update the HomeController in the AutoLot.Mvc project to use the new logging framework. Replace the ILogger parameter with IAppLogging and update the type of the field, like this:

    public class HomeController : Controller
    {
    private readonly IAppLogging _logger; public HomeController(IAppLogging logger)
    {
    _logger = logger;
    }
    //omitted for brevity
    }

    With this in place, log a test error in the Index() method:

    public IActionResult Index([FromServices]IOptionsMonitor dealerMoitor)
    {
    _logger.LogAppError("Test error"); var vm = dealerMonitor.CurrentValue; return View(vm);
    }

    Run the AutoLot.Mvc project. When the app starts, a record will be logged in the SeriLogs table as well as written to a file named log_AutoLotYYYYMMDD.txt.
    When you open the log file, you might be surprised to see that there are a lot of additional entries that didn’t come from the one call to the logger. That is because EF Core and ASP.NET Core emit very verbose logging when the log level is set to Information. To eliminate the noise, update the appsettings. Development.json files in the AutoLot.Api, AutoLot,Mvc, and AutoLot.Web projects so that the log level is Warning, like this:

    "RestrictedToMinimumLevel": "Warning"

    String Utilities
    Recall that one of the conventions in ASP.NET Core removes the Controller suffix from controllers and the Async suffix from action methods when routing controllers and actions. When manually building up routes, it’s common to have to remove the suffix through code. While the code is simple to write, it can be repetitive. To cut down on repeating the string manipulation code, the next step will create two string extension methods.
    Add a new directory named Utilities in the AutoLot.Services project, and in that directory, create a new public static class named StringExtensions. In that class, add the following two extension methods:
    namespace AutoLot.Services.Utilities public static class StringExtensions
    {
    public static string RemoveControllerSuffix(this string original)
    => original.Replace("Controller", "", StringComparison.OrdinalIgnoreCase);

    public static string RemoveAsyncSuffix(this string original)
    => original.Replace("Async", "", StringComparison.OrdinalIgnoreCase); public static string RemovePageModelSuffix(this string original)
    => original.Replace("PageModel", "", StringComparison.OrdinalIgnoreCase);
    }

    Next, add the following global using statement to the GlobalUsings.cs file in the AutoLot.Services project and all three web applications:

    global using AutoLot.Services.Utilities;

    Summary
    This chapter dove into the new features introduced in ASP.NET Core and began the process of updating the three ASP.NET Core applications. In the next chapter, you will finish the AutoLot.Api application.

    Pro C#10 CHAPTER 30 Introducing ASP.NET Core

    PART IX

    ASP.NET Core

    CHAPTER 30

    Introducing ASP.NET Core

    The final section of this book covers ASP.NET Core, C# and the .NET web development framework. The chapter begins with an introduction of ASP.NET MVC and the basics of the MVC pattern as implemented in ASP.NET Core. Next, you will create the solution and the three ASP.NET Core projects that will be developed over the course of the rest of the book. The first application, AutoLot.Api is an ASP.NET Core RESTful service, the second is an ASP.NET Core web application using the Model-View-Controller pattern and the final application is an ASP.NET Core web application using Razor pages. The RESTFul services serves as
    an optional back end to the MVC and Razor Page applications, and the AutoLot.Dal and AutoLot.Models projects that you built earlier in this book will serve as the data access layer for all of the applications.
    After building the projects and solution, the next section demonstrates the many ways to run and debug ASP.NET Core projects using Visual Studio or Visual Studio Code. The rest of this chapter explores the many features from ASP.NET that were carried forward into ASP.NET Core. This includes controllers and actions, routing, model binding and validation, and finally filters.

    A Quick Look Back at ASP.NET MVC
    The ASP.NET MVC framework is based on the Model-View-Controller pattern and provided an answer to developers who were frustrated by WebForms, which was essentially a leaky abstraction over
    HTTP. WebForms was created to help client-server developers move to the Web, and it was pretty successful in that respect. However, as developers became more accustomed to web development, many wanted more control over the rendered output, elimination of view state, and adherence to a proven web application design pattern. With those goals in mind, ASP.NET MVC was created.

    Introducing the MVC Pattern
    The Model-View-Controller (MVC) pattern has been around since the 1970s, originally created as a pattern for use in Smalltalk. The pattern has made a resurgence recently, with implementations in many different and varied languages, including Java (Spring Framework), Ruby (Ruby on Rails), and .NET(ASP.NET MVC).

    The Model
    The model is the data of your application. The data is typically represented by plain old CLR objects (POCOs). View models are composed of one or more models and shaped specifically for the consumer of the data. One way to think about models and view models is to relate them to database tables and database views.
    Academically, models should be extremely clean and not contain validation or any other business rules. Pragmatically, whether or not models contain validation logic or other business rules depends

    © Andrew Troelsen, Phil Japikse 2022
    A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_30

    1313

    entirely on the language and frameworks used, as well as specific application needs. For example, EF Core contains many data annotations that double as a mechanism for shaping the database tables and a means for validation in ASP.NET Core web applications. In this book (and in my professional work), the examples focus on reducing duplication of code, which places data annotations and validations where they make the most sense.

    The View
    The view is the user interface of the application. Views accept commands and render the results of those commands to the user. The view should be as lightweight as possible and not actually process any of the work, but hand off all work to the controller. Views are typically strongly typed to a model, although that is not required.

    The Controller
    The controller is the brains of the application. Controllers take commands/requests from the user (via the view) or client (through API calls) through action methods, and handle them appropriately. The results of the operation are then returned to the user or client. Controllers should be lightweight and leverage other components or services to handle the details of the requests. This promotes separation of concerns and increases testability and maintainability.

    ASP.NET Core and the MVC Pattern
    ASP.NET Core is capable of creating many types of web applications and services. Two of the options are web applications using the MVC pattern and RESTful services. If you have worked with ASP.NET “classic,” these are analogous to ASP.NET MVC and ASP.NET Web API, respectively. The MVC web application and API application types share the “model” and the “controller” portion of the pattern, while MVC web applications also implement the “view” to complete the MVC pattern.

    ASP.NET Core and .NET Core
    Just as Entity Framework Core is a complete rewrite of Entity Framework 6, ASP.NET Core is a rewrite of the popular ASP.NET Framework. Rewriting ASP.NET was no small task, but it was necessary in order to remove the dependency on System.Web. Removing this dependency enabled ASP.NET applications to run on operating systems other than Windows and other web servers besides Internet Information Services (IIS), including self-hosted. This opened the door for ASP.NET Core applications to use a cross-platform, lightweight, fast, and open source web server called Kestrel. Kestrel presents a uniform development experience across all platforms.

    ■Note Kestrel was originally based on LibUV, but since ASP.NET Core 2.1, it is now based on managed sockets.

    Like EF Core, ASP.NET Core is being developed on GitHub as a completely open source project (https://github.com/aspnet). It is also designed as a modular system of NuGet packages. Developers only install the features that are needed for a particular application, minimizing the application footprint,
    reducing the overhead, and decreasing security risks. Additional improvements include a simplified startup, built-in dependency injection, a cleaner configuration system, and pluggable middleware.

    One Framework, Many Uses
    There are lots of changes and improvements in ASP.NET Core, as you will see throughout the rest of the chapters in this section. Besides the cross-platform capabilities, another significant change is the unification of the web application frameworks. ASP.NET Core encompasses ASP.NET MVC, ASP.NET Web API, and Razor Pages into a single development framework. Developing web applications and services with the ASP. NET (not ASP.NET Core) Framework presented several choices, including WebForms, MVC, Web API, Windows Communication Foundation (WCF), and WebMatrix. They all had their positives and negatives; some were closely related, and others were quite different. All of the choices available meant developers had to know each of them in order to select the proper one for the task at hand or just select one and hope for the best.
    With ASP.NET Core, you can build applications that use Razor Pages, the Model-View-Controller pattern, RESTful services, and SPA applications using Blazor WebAssembly or JavaScript frameworks like Angular and React. While the UI rendering varies with choices between MVC, Razor Pages, and the
    JavaScript frameworks, the underlying server side development framework is the same across all choices. Blazor WebAssembly is a client side development framework, and doesn’t have a server side component like the other ASP.NET Core application types. Two prior choices that have not been carried forward into ASP. NET Core are WebForms and WCF.

    ■Note With all of the separate frameworks brought under the same roof, the former names of ASP.NET MVC and ASP.NET Web API have been officially retired. In this book, I still refer to ASP.NET Core web applications using the Model-View-Controller pattern as MVC or MVC based applications and refer to ASP.NET rESTful services as API or rESTful services for simplicity.

    Create and Configure the Solution and Projects
    Before diving into the some of the major concepts in ASP.NET Core, let’s build the solution and projects that will be used through the rest of the chapters. The ASP.NET Core projects can be created using either Visual Studio or the command line. Both options will be covered in the next two sections.

    Using Visual Studio 2022
    Visual Studio has the advantage of a GUI to step you through the process of creating a solution and projects, adding NuGet packages, and creating references between projects.

    Create the Solution and Projects
    Start by creating a new project in Visual Studio. Select the C# template ASP.NET Core Web API from the “Create a new project” dialog. In the “Configure your new project” dialog, enter AutoLot.Api for the project name and AutoLot for the solution name, as shown in Figure 30-1.

    Figure 30-1. Creating the AutoLot.Api project and AutoLot solution

    On the Additional information screen, select .NET 6.0 (Long-term support) for the Framework and leave the “Configure for HTTPS” and “Enable OpenAPI” check boxes checked, as shown in Figure 30-2. Then click Create.

    ■Note Minimal APIs are a new feature in .NET 6 for creation without the traditional Controllers and action methods. These will not be covered in this text.

    Figure 30-2. Selecting the ASP.NET Core Web API template

    ■Note hot reload is a new feature in .NET 6 that reloads your app while it is running when non-destructive changes are made, reducing the need to restart your application while developing to see changes. This has replaced razor runtime compilation, which was used in .NET 5 to accomplish the same goal.

    Now add another ASP.NET Core web application to the solution. Select the “ASP.NET Core Web App (Model-View-Controller)” template. Name the project AutoLot.Mvc, then make sure that .NET Core 6.0 is selected, and the “Configure for HTTPS” option is checked as shown in Figure 30-3.

    Figure 30-3. Configuring the ASP.NET Core MVC based Web Application template

    Next, add the last ASP.NET Core web application to the solution. Select the “ASP.NET Core Web App” template. Name the project AutoLot.Web, then make sure that .NET Core 6.0 (Long-term support) is selected, and the “Configure for HTTPS” option is checked as shown in Figure 30-4.

    Figure 30-4. Configuring the ASP.NET Core Razor page MVC Web Application template

    Finally, add a C# Class Library to the project and name it AutoLot.Services.

    Add in AutoLot.Models and AutoLot.Dal
    The solution requires the completed data access layer which was completed in Chapter 23. You can also use the versions from Chapter 24, but that chapter was all about testing the finished data access layer and didn’t make any changes to the data access layer projects. You can either copy the files into the current solution directory or leave them in the place where you built them. Either way, you need to right-click your solution name in Solution Explorer, select Add ➤ Existing Project, and navigate to the AutoLot.Models.csproj file and select it. Repeat for the AutoLot.Dal project by selecting the AutoLot.Dal.csproj file.

    Add the Project References
    Add the following project references by right-clicking the project name in Solution Explorer and selecting Add ➤ Project Reference for each project.
    AutoLot.Api, AutoLot.Web, and AutoLot.Mvc reference the following:
    •AutoLot.Models
    •AutoLot.Dal
    •AutoLot.Services
    AutoLot.Services references the following:
    •AutoLot.Models
    •AutoLot.Dal

    Add the NuGet Packages
    Additional NuGet packages are needed to complete the applications.
    To the AutoLot.Api project, add the following packages:
    •AutoMapper
    •Microsoft.AspNetCore.Mvc.Versioning
    •Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
    •Microsoft.EntityFrameworkCore.Design
    •Microsoft.EntityFrameworkCore.SqlServer
    •Microsoft.VisualStudio.Web.CodeGeneration.Design
    •Microsoft.VisualStudio.Threading.Analyzers
    •System.Text.Json
    •Swashbuckle.AspNetCore
    •Swashbuckle.AspNetCore.Annotations
    •Swashbuckle.AspNetCore.Swagger
    •Swashbuckle.AspNetCore.SwaggerGen
    •Swashbuckle.AspNetCore.SwaggerUI

    ■Note With the ASP.NET Core 6.0 API templates, Swashbuckle.AspNetCore is already referenced. The additional Swashbuckle packages add capabilities beyond the basic implementation.

    To the AutoLot.Mvc project, add the following packages:
    •AutoMapper
    •System.Text.Json
    •LigerShark.WebOptimizer.Core
    •Microsoft.Web.LibraryManager.Build
    •Microsoft.VisualStudio.Web.CodeGeneration.Design
    •Microsoft.EntityFrameworkCore.Design
    •Microsoft.EntityFrameworkCore.SqlServer
    •Microsoft.VisualStudio.Threading.Analyzers
    To the AutoLot.Web project, add the following packages:
    •AutoMapper
    •System.Text.Json
    •LigerShark.WebOptimizer.Core
    •Microsoft.Web.LibraryManager.Build

    •Microsoft.VisualStudio.Web.CodeGeneration.Design
    •Microsoft.EntityFrameworkCore.Design
    •Microsoft.EntityFrameworkCore.SqlServer
    •Microsoft.VisualStudio.Threading.Analyzers
    To the AutoLot.Services project, add the following packages:
    •Microsoft.Extensions.Hosting.Abstractions
    •Microsoft.Extensions.Options
    •Serilog.AspNetCore
    •Serilog.Enrichers.Environment
    •Serilog.Settings.Configuration
    •Serlog.Sinks.Console
    •Serilog.Sinks.File
    •Serilog.Sinks.MSSqlServer
    •System.Text.Json
    •Microsoft.VisualStudio.Threading.Analyzers

    Using the Command Line
    As shown earlier in this book, .NET Core projects and solutions can be created using the command line. Open a prompt and navigate to the directory where you want the solution located.

    ■Note The commands listed use the Windows directory separator. If you are using a non-Windows operating system, adjust the separator characters as needed. They also use a specific directory path when adding the AutoLot. dal and AutoLot.Models projects to the solution, which will need to be updated based on the location of your projects.

    The following commands create the AutoLot solution and add the existing AutoLot.Models and AutoLot.
    Dal projects into the solution using the same options that were shown when using Visual Studio 2022:

    rem create the solution dotnet new sln -n AutoLot
    rem add autolot dal to solution update the path references as needed dotnet sln AutoLot.sln add ..\Chapter_23\AutoLot.Models
    dotnet sln AutoLot.sln add ..\Chapter_23\AutoLot.Dal

    Create the AutoLot.Service project, add it to the solution, add the NuGet packages and the project references.

    rem create the class library for the application services and add it to the solution dotnet new classlib -lang c# -n AutoLot.Services -o .\AutoLot.Services -f net6.0 dotnet sln AutoLot.sln add AutoLot.Services

    dotnet add AutoLot.Services package Microsoft.Extensions.Hosting.Abstractions dotnet add AutoLot.Services package Microsoft.Extensions.Options
    dotnet add AutoLot.Services package Serilog.AspNetCore
    dotnet add AutoLot.Services package Serilog.Enrichers.Environment dotnet add AutoLot.Services package Serilog.Settings.Configuration dotnet add AutoLot.Services package Serilog.Sinks.Console
    dotnet add AutoLot.Services package Serilog.Sinks.File
    dotnet add AutoLot.Services package Serilog.Sinks.MSSqlServer dotnet add AutoLot.Services package System.Text.Json
    dotnet add AutoLot.Services package Microsoft.VisualStudio.Threading.Analyzers
    rem update the path references as needed
    dotnet add AutoLot.Services reference ..\Chapter_23\AutoLot.Models dotnet add AutoLot.Services reference ..\Chapter_23\AutoLot.Dal

    Create the AutoLot.Api project, add it to the solution, add the NuGet packages and the project references.

    dotnet new webapi -lang c# -n AutoLot.Api -au none -o .\AutoLot.Api -f net6.0 dotnet sln AutoLot.sln add AutoLot.Api
    dotnet add AutoLot.Api package AutoMapper
    dotnet add AutoLot.Api package Swashbuckle.AspNetCore
    dotnet add AutoLot.Api package Swashbuckle.AspNetCore.Annotations dotnet add AutoLot.Api package Swashbuckle.AspNetCore.Swagger dotnet add AutoLot.Api package Swashbuckle.AspNetCore.SwaggerGen dotnet add AutoLot.Api package Swashbuckle.AspNetCore.SwaggerUI
    dotnet add AutoLot.Api package Microsoft.VisualStudio.Web.CodeGeneration.Design dotnet add AutoLot.Api package Microsoft.EntityFrameworkCore.Design
    dotnet add AutoLot.Api package Microsoft.EntityFrameworkCore.SqlServer dotnet add AutoLot.Api package Microsoft.VisualStudio.Threading.Analyzers dotnet add AutoLot.Api package System.Text.Json
    dotnet add AutoLot.Api package Microsoft.AspNetCore.Mvc.Versioning
    dotnet add AutoLot.Api package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
    rem add project references
    rem update the path references as needed
    dotnet add AutoLot.Api reference ..\Chapter_23\AutoLot.Dal dotnet add AutoLot.Api reference ..\Chapter_23\AutoLot.Models dotnet add AutoLot.Api reference AutoLot.Services

    Create the AutoLot.Mvc project, add it to the solution, add the NuGet packages and the project references.

    dotnet new mvc -lang c# -n AutoLot.Mvc -au none -o .\AutoLot.Mvc -f net6.0 dotnet sln AutoLot.sln add AutoLot.Mvc
    rem add packages
    dotnet add AutoLot.Mvc package AutoMapper dotnet add AutoLot.Mvc package System.Text.Json
    dotnet add AutoLot.Mvc package LigerShark.WebOptimizer.Core dotnet add AutoLot.Mvc package Microsoft.Web.LibraryManager.Build
    dotnet add AutoLot.Mvc package Microsoft.EntityFrameworkCore.Design

    dotnet add AutoLot.Mvc package Microsoft.EntityFrameworkCore.SqlServer dotnet add AutoLot.Mvc package Microsoft.VisualStudio.Threading.Analyzers
    dotnet add AutoLot.Mvc package Microsoft.VisualStudio.Web.CodeGeneration.Design
    rem add project references
    rem update the path references as needed
    dotnet add AutoLot.Mvc reference ..\Chapter_23\AutoLot.Models dotnet add AutoLot.Mvc reference ..\Chapter_23\AutoLot.Dal dotnet add AutoLot.Mvc reference AutoLot.Services

    Finally, create the AutoLot.Web project, add it to the solution, add the NuGet packages, and add the project references.

    dotnet new webapp -lang c# -n AutoLot.Web -au none -o .\AutoLot.Web -f net6.0 dotnet sln AutoLot.sln add AutoLot.Web
    rem add packages
    dotnet add AutoLot.Web package AutoMapper dotnet add AutoLot.Web package System.Text.Json
    dotnet add AutoLot.Web package LigerShark.WebOptimizer.Core dotnet add AutoLot.Web package Microsoft.Web.LibraryManager.Build
    dotnet add AutoLot.Web package Microsoft.EntityFrameworkCore.SqlServer
    dotnet add AutoLot.Web package Microsoft.EntityFrameworkCore.SqlServer.Design dotnet add AutoLot.Web package Microsoft.VisualStudio.Web.CodeGeneration.Design dotnet add AutoLot.Web package Microsoft.VisualStudio.Threading.Analyzers
    rem add project references
    rem update the path references as needed
    dotnet add AutoLot.Web reference ..\Chapter_23\AutoLot.Models dotnet add AutoLot.Web reference ..\Chapter_23\AutoLot.Dal dotnet add AutoLot.Web reference AutoLot.Services

    That completes the setup using the command line. As you can probably see, it is much more efficient provided you don’t need the Visual Studio GUI to help you.

    ■Note At the time of this writing, Visual Studio created projects with files that use nested namespaces, while the CLI tooling creates files that use file scoped namespaces.

    Update the Entity Framework Core Package Reference
    Recall from the EF Core chapters that to clear out temporal tables, the Microsoft.EntityFrameworkCore. Design package reference must be modified. Update the reference in the project files for the AutoLot.Api, AutoLot.Mvc, and AutoLot.Web projects to the following:

    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0>

    all

    Disable Nullable Reference Types For All Projects
    At this time, disable nullable reference types by updating each of the project files’ PropertyGroup to the following:


    net6.0
    disable
    enable

    Create a GlobalUsing.cs Class in Each Project
    The final setup step is to add a file named GlobalUsings.cs to the root folder of each project and clear out any scaffolded code. These will be used to hold the using statements for each project.

    Running ASP.NET Core Applications
    Previous versions of ASP.NET web applications always ran using IIS (or IIS Express). With ASP.NET Core, applications typically run using the Kestrel web server with an option to use IIS, Apache, Nginx, etc., by way of a reverse proxy between Kestrel and the other web server. This shift from requiring IIS to allowing other web servers not only changes the deployment model, but also changes the development possibilities. During development, you can now run your applications in these ways:
    •From Visual Studio, using Kestrel or IIS Express
    •From a command prompt with the .NET CLI, using Kestrel
    •From Visual Studio Code, using Kestrel, from the Run menu
    •From Visual Studio Code’s Terminal window using the .NET CLI and Kestrel
    When using any of these options, the application’s ports and environment are configured with a file named launchsettings.json located under the Properties folder. The launchsettings.json file for the AutoLot.Mvc project is listed here for reference (your ports for both the IIS Express and AutoLot.Mvc profiles will be different):

    {
    "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": {
    "applicationUrl": "http://localhost:42788", "sslPort": 44375
    }
    },
    "profiles": {
    "AutoLot.Mvc": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true,
    "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": {
    "ASPNETCORE_ENVIRONMENT": "Development",

    }
    },
    "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": {
    "ASPNETCORE_ENVIRONMENT": "Development",
    }
    }
    }
    }

    Using Visual Studio
    The first profile defines the settings when using Kestrel as the web server. The Kestrel profile is always named after the project (e.g., AutoLot.Mvc) when it is created, but can be changed to any other name. The iisSettings section and the IIS Express profile together define the settings when running the application using IIS Express as the web server. The most important settings to note are the applicationUrl, which also defines the port, and the environmentVariables block, which defines the environment variables to
    use when debugging. Any environment variables defined in this section supersede any user or machine environment settings with the same name.
    The Run command in Visual Studio allows for choosing either the Kestrel or IIS Express profile, as shown in Figure 30-5. Once a profile is selected, you can run the project by pressing F5 (debug mode), pressing Ctrl+F5 (the same as “Start Without Debugging” in the Debug menu), or clicking the green run arrow (the same as “Start Debugging” in the Debug menu). New in .NET 6 and Visual Studio 2022, the Kestrel profile is set as the default.

    Figure 30-5. The available Visual Studio debugging profiles

    ■Note There is also an option to create additional profiles, such as running using the WSL (Windows Subsystem for Linux). This feature isn’t covered in this book, but if you are curious to learn more, there is a book on the topic titled Pro Windows Subsystem for Linux (https://link.springer.com/ book/10.1007/978-1-4842-6873-5z)

    Using Visual Studio Code
    To run the projects from Visual Studio Code, open the folder where the solution is located. When you press F5 (or click Run), VS Code will prompt you to select the runtime to use (select .NET 6+ and .NET Core) and which project to run (AutoLot.Api, AutoLot.Web, or AutoLot.Mvc). It will then create a run configuration and place it in a file named launch.json. The launch settings only have to be configured the first time. Once the files exists, you can freely run/debug your application without having to make the selections again. Visual Studio Code uses the Kestrel profile when running your application.

    Using the Command Line or Terminal Window
    Running from the command line for ASP.NET Core apps is the same as any other .NET application that you have seen in this book. Simply navigate to the directory where the csproj file for your application is located and enter the following command:

    dotnet run

    This starts your application using the Kestrel profile. To end the process, press Ctrl+C.

    Changing Code While Debugging
    When running from the command line using dotnet run, the code in your application’s projects can be changed, but the changes won’t be reflected in the running app. To have the changes reflected in the running app, enter the following command:

    dotnet watch

    This command runs with Hot Reload enabled. Hot Reload is a new feature in .NET 6 that attempts to reload your app in real time when changes are made. This is a vast improvement over the .NET 5 dotnet watch run command, which restarted your entire app when a file watcher noticed changes.
    Not all changes can be reloaded in real time, as some do require a restart of your app. If this is needed, you will be prompted to restart your application. If the prompt doesn’t appear, you can force a reload by using Ctrl+R in the terminal window. To cancel the application, hit Ctrl+C.
    Hot Reload is also available when debugging with Visual Studio 2022 or Visual Studio Code.

    Debugging ASP.NET Core Applications
    When running your application from Visual Studio or Visual Studio Code, debugging works as expected. When running from the command line, you have to attach to the running process before you can debug your application.

    Attaching with Visual Studio
    After launching your app (with dotnet run or dotnet watch run), select Debug ➤ Attach to Process in Visual Studio. When the Attach to Process dialog appears, filter the process by your application name, as shown in Figure 30-6.

    Figure 30-6. Attaching to the running applications for debugging in Visual Studio

    Once attached to the running process, you can set breakpoints in Visual Studio, and debugging works as expected. Hot reload is enabled when you attach to the running process, so the edit and continue experience is getting better, although not yet to the level it was in prior versions of the .NET framework.

    Attaching with Visual Studio Code
    After launching your app (with dotnet run or dotnet watch run), select .NET Core Attach instead of .NET Core Launch (web) by clicking the green run arrow in VS Code, as shown in Figure 30-7.

    Figure 30-7. Attaching to the running applications for debugging in Visual Studio Code

    When you click the Run button, you will be prompted to select which process to attach. Select your application. You can now set breakpoints as expected.

    Update the AutoLot.Api and AutoLot.Web Kestrel Ports
    You might have noticed that AutoLot.Api, AutoLot.Web, and AutoLot.Mvc have different ports specified for the IIS Express and Kestrel profiles. Instead of remembering each of the randomly assigned ports, update all of the projects settings as follows:

    //AutoLot.Mvc launchSettins.json "AutoLot.Mvc": {
    "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development"
    },
    "applicationUrl": "https://localhost:5001;http://localhost:5000",
    "dotnetRunMessages": true
    },
    //AutoLot.Api launchSettins.json "AutoLot.API": {
    "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger",
    "applicationUrl": "https://localhost:5011;http://localhost:5010",
    "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development"
    }
    },
    //AutoLot.Web launchSettins.json "AutoLot.Web": {
    "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true,
    "applicationUrl": "https://localhost:5021;http://localhost:5020",
    "environmentVariables": {

    "ASPNETCORE_ENVIRONMENT": "Development"
    }
    },

    ASP.NET Core Concepts from MVC/Web API
    Many of the design goals and features that brought developers to use ASP.NET MVC and ASP.NET Web API are still supported (and improved) in ASP.NET Core. Some of these (but not all) are listed here:
    •Convention over configuration
    •Controllers and actions
    •Cleaner directory structure
    •Model binding
    •Model validation
    •Routing
    •Filters
    •Layouts and Razor Views
    These items are all covered in the next sections, except for layouts and Razor views, which are covered in a later chapter.

    ■Note razor page based applications are new in ASP.NET Core, however many of the concepts covered in this section also apply to this new application type. razor page based applications will be formally introduced in the next chapter.

    Convention over Configuration
    ASP.NET MVC and ASP.NET Web API reduced the amount of configuration necessary by introducing certain conventions. When followed, these conventions reduce the amount of manual (or templated) configuration, but also require the developers to know the conventions in order to take advantage of them. Two of the main conventions include naming conventions and directory structure.

    Naming Conventions
    There are multiple naming conventions in ASP.NET Core, for MVC style and RESTful service applications as well as Razor page based applications. For example, controllers are typically named with the Controller suffix (e.g., HomeController) in addition to deriving from Controller (or ControllerBase). When accessed through routing, the Controller suffix is dropped. When routing to async action methods named with the suffix Async, the Async suffix is dropped. Razor pages’ code behind files are named with the Model suffix
    (ErrorModel), which is dropped just like the Controller and Async suffixes. This convention of dropping the suffix is repeated throughout ASP.NET Core.
    Another naming convention is used in locating the views for a controller’s action methods. When looking for a controller’s views, the controller name minus the suffix is the starting search location. By default, an action method will render the view of the same name as the method.
    There will be many examples of ASP.NET Core conventions covered in the following chapters.

    Controllers and Actions (MVC Based Web Apps and RESTful Services)
    Just like ASP.NET MVC and ASP.NET Web API, controllers and action methods are the workhorses of an ASP. NET Core MVC style web application or RESTful service application.

    ■Note razor pages derive from the PageModel class, which will be covered in the next chapter.

    The Controller Class
    As mentioned already, ASP.NET Core unified ASP.NET MVC5 and ASP.NET Web API. This unification also combines the Controller, ApiController, and AsyncController base classes from MVC5 and Web API
    2.1into one new class, Controller, which has a base class of its own named ControllerBase. ASP.NET Core web application controllers inherit from the Controller class, while ASP.NET Core service controllers inherit from the ControllerBase class (covered next).
    The Controller class provides a host of helper methods for MVC style web applications. Table 30-1 lists the most commonly used methods.

    Table 30-1. Some of the Helper Methods Provided by the Controller Class

    Helper Method Meaning in Life
    ViewDataTempDataViewBag Provide data to the view through the ViewDataDictionary, TempDataDictionary, and dynamic ViewBag transport.
    View Returns a ViewResult (derived from ActionResult) as the HTTP response. Defaults to a view of the same name as the action method, with the option of specifying a specific view. All options allow specifying a view model that is strongly typed and sent to the View.
    PartialView Returns a PartialViewResult to the response pipeline.
    ViewComponent Returns a ViewComponentResult to the response pipeline.
    Json Returns a JsonResult containing an object serialized as JSON as the response.
    OnActionExecuting Executes before an action method executes.
    OnActionExecutionAsync Async version of OnActionExecuting.
    OnActionExecuted Executes after an action method executes.

    The ControllerBase Class
    The ControllerBase class provides the core functionality for both ASP.NET Core MVC style web applications and RESTful services, in addition to helper methods for returning HTTP status codes. Table 30-2 lists some of the core functionality in ControllerBase.

    Table 30-2. Some of the Helper Methods Provided by the ControllerBase Class

    Helper Method Meaning in Life
    HttpContext Returns the HttpContext for the currently executing action.
    Request Returns the HttpRequest for the currently executing action.
    Response Returns the HttpResponse for the currently executing action.
    RouteData Returns the RouteData for the currently executing action (routing is covered later in this chapter).
    ModelState Returns the state of the model in regard to model binding and validation (both covered later in this chapter).
    Url Returns an instance of the IUrlHelper, providing access to building URLs for ASP.NET Core MVC applications and services.
    User Returns the ClaimsPrincipal user.
    Content Returns a ContentResult to the response. Overloads allow for adding a content type and encoding definition.
    File Returns a FileContentResult to the response.
    Redirect A series of methods that redirect the user to another URL by returning a
    RedirectResult.
    LocalRedirect A series of methods that redirect the user to another URL only if the URL is local. More secure than the generic Redirect methods.
    RedirectToActionRedirect ToPageRedirectToRoute A series of methods that redirect to another action method, Razor Page, or named route. Routing is covered later in this chapter.
    TryUpdateModelAsync Used for explicit model binding (covered later in this chapter).
    TryValidateModel Used for explicit model validation (covered later in this chapter).

    And Table 30-3 covers some of the helper methods for returning HTTP status codes.

    Table 30-3. Some of the HTTP Status Code Helper Methods Provided by the ControllerBase Class

    Helper Method HTTP Status Code Action Result Status Code
    NoContent NoContentResult 204
    Ok OkResult 200
    NotFound NotFoundResult 404
    BadRequest BadRequestResult 400
    CreatedCreatedAt ActionCreatedAtRoute CreatedResultCreatedAtAction ResultCreateAtRouteResult 201
    AcceptedAcceptedAt ActionAcceptedAtRoute AcceptedResultAccepted AtActionResultAcceptedAtRouteResult 202

    Actions
    Actions are methods on a controller that return an IActionResult (or Task for async operations) or a class that implements IActionResult, such as ActionResult or ViewResult.
    This example returns a ViewResult in an MVC based application:

    public async Task Index()
    => View(await _serviceWrapper.GetCarsAsync());

    This example returns an HTTP status code of 200 with a list of Car records as JSON:

    [Produces("application/json")]
    public ActionResult<IEnumerable> GetCarsByMake(int? id)
    {
    return Ok(MainRepo.GetAll());
    }

    Actions and their return values will be covered more in the following chapters.

    Antiforgery Tokens
    ASP.NET Core uses antiforgery middleware to help combat against cross-site request forgery attacks. ASP. NET Core uses the Synchronizer Token Pattern (STP) which creates a server side token that is unique and unpredictable. That token is sent to the browser with the response. Any request that comes back to the server must include the correct token, or the request is refused.

    Opting In (MVC Style Web Apps)
    Receiving the token and validating it is handled in the action methods for MVC style web applications(MVC). To validate the token, simply add the ValidateAntiForgeryToken attribute to every HTTP Post method in your application, like this:

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task Create (Car entity)
    {
    //do important stuff
    }

    To opt out of the check, simply leave the ValidateAntiForgeryToken attribute off of the method.

    ■Note Adding the antiforgery token to the response will be covered along with tag helpers and views.

    Opting Out (Razor Page Web Apps)
    Razor page base applications automatically participate in the antiforgery pattern. To opt out of participating, add the IgnoreAntiforgeryToken to the PageModel class:

    [IgnoreAntiforgeryToken]
    public class ErrorModel : PageModel
    {
    //Omitted for brevity
    }

    Directory Structure Conventions
    There are several folder conventions that you must understand to successfully build ASP.NET Core web applications and services. Many of the directories from Web API/MVC are the same (Controllers, Views, Areas) while there are some new ones (e.g., Pages, wwwroot). You will find the directory structure improved from MVC/WebAPI.

    The Controllers Folder
    By convention, the Controllers folder is where the ASP.NET Core MVC and API implementations (and the routing engine) expect that the controllers for your application are placed.

    The Views Folder
    The Views folder is where the views for an MVC style application are stored. Each controller gets its own folder under the main Views folder named after the controller name (minus the Controller suffix). The action methods will render views in their controller’s folder by default. For example, the Views/Home folder holds all the views for the HomeController controller class.

    The Shared Folder
    A special folder under Views is named Shared. This folder is accessible to all controllers and their action methods. After searching the folder named for the controller, if the view can’t be found, then the Shared folder is searched for the view.

    The Pages Folder
    The Pages folder is where the pages for the application are stored when building web applications using Razor pages. The directory structure under the Pages folder sets the base route for each page (more on routing later).

    The Shared Folder
    A special folder under Pages is named Shared. This folder is accessible to all pages in a Razor page based web application.

    The Areas Folder
    Areas are a feature that is used to organize related functionality into a group as a separate namespace for routing and folder structure for views and Razor pages. Each area gets its own set of controllers (API
    applications), controllers and views (MVC style applications), and pages (Razor page based applications). An application can have zero to many areas, and each area goes under the parent Areas folder.

    The wwwroot Folder
    An improvement in ASP.NET Core web applications over the previous framework versions is the creation of a special folder named wwwroot. In ASP.NET MVC, the JavaScript files, images, CSS, and other client-side content were intermingled with all the other folders. In ASP.NET Core, the client side files are all contained under the wwwroot folder. This separation of compiled files from client-side files significantly cleans up the project structure when working with ASP.NET Core.
    There is an exception to this conventions that relates to view and page specific CSS files. Those are stored alongside the views/pages they target. They will be covered in later chapters.

    Routing
    Routing is how ASP.NET Core matches HTTP requests to the proper code (the executable endpoints) to handle those requests as well as create URLs from the executable end points. This is accomplished with routing middleware registered in the Startup class (pre-C# 10) or the top level statements in the Program.cs file (C# 10 and later).
    A route in an MVC based web application consists of an (optional) area, a controller, an action method, an HTTP verb (POST or GET), and (optional) additional values (called route values). Routes in ASP.NET Core RESTful services consists of an (optional) area, a controller, an (optional) action method, an HTTP Verb (POST, PUT, GET, DELETE, etc.), and (optional) additional route values. When defining routes for ASP.NET RESTful services, an action method is not usually specified. Instead, once the controller is located, the action method to execute is based on the HTTP verb of the request. In MVC style web applications and RESTful services, routes can be configured along in the middleware or through route attributes on controllers and action methods.
    A route in a Razor page based application is the directory structure of the application, an HTTP Verb, the page itself, and (optional) additional route values. In addition to the default constructs, all ASP.NET style applications can create a route that ignores the standard templates. In Razor page based applications, routes are based on the directory structure of the application, with additional tokens available as part of the @page directive. All of this is covered in more detail shortly.

    URL Patterns and Route Tokens
    Route definitions are composed of URL patterns that contain variable placeholders (called tokens) and (optional) literals placed into an ordered collection known as the route table. Each entry in the route table must define a different URL pattern to match. Table 30-4 lists the reserved token names and their definitions.

    Table 30-4. Reserved Route Tokens for MVC Styled and RESTful Service Applications

    Token Meaning in Life
    Area Defines the area for the route
    Controller Defines the controller (minus the controller suffix)
    Action Defines the action method name

    • or Catch-all parameter. When used as a prefix to a route parameter, it binds the rest of the URI. For example, car/{slug} matches any URI that starts with /car and has any value following it. The following value is assigned to the slug route value. Using a double * in the path preserves path separator characters while a single does not.

    Handling Duplicate C# Method Signatures
    The action token by default matches the C# method name on a controller. The ActionName attribute can be used to change in relation to routing. For example, the following code specifies the HTTP Get and HTTP Post action methods with the name and same signature. C# doesn’t allow two methods with the same signature with the same name, so a compiler error occurs:

    [HttpGet]
    public async Task Create()
    {
    //do something
    }

    [HttpPost] [ValidateAntiForgeryToken]
    //Compiler error
    public async Task Create()
    {
    //Do something else
    }

    The solution is to rename one of the methods and add the ActionName attribute to change the name used by the routing engine:

    [HttpPost] [ActionName("Create")] [ValidateAntiForgeryToken]
    public async Task CreateCar()
    {
    //Do something else
    }

    Custom Route Tokens
    In addition to the reserved tokens, routes can contain custom tokens that are mapped (model bound) to a controller action method’s or Razor page handler method’s parameters. When defining routes using
    tokens, there must be a literal value separating the tokens. While {controller}/{action}/{id?} is valid,
    {controller}{action}{id?} is not.

    Route Token Constraints
    Route tokens can also be constrained to disambiguate similar routes. Table 30-5 shows the available route token constraints. Note that the constraints are not meant to be used for validation. If a route value is invalid based on a constraint (e.g., too big for the int constraint), the routing engine will not find a match and returns a 404 (Not Found). However, business logic would probably require that a 400 (Bad Request) be returned instead.

    Table 30-5. Route Tokens Constraints for MVC Styled and RESTful Service Applications

    Constraint Example Meaning in Life
    Int {id:int} Matches any integer.
    Bool {active:bool} Matches true or false. Case insensitive.
    datetime {dob:datetime} Matches a valid DateTime value in the invariant culture.
    decimal {price:decimal} Matches a valid decimal value in the invariant culture.
    double {weight:double} Matches a valid double value in the invariant culture.
    Float {weight:float} Matches a valid float value in the invariant culture.
    Guid {id:guid} Matches a valid GUID value.
    Long {ticks:long} Matches a valid long value in the invariant culture.
    minlength(value) {name:minlength(4)} String must be at least value characters long.
    maxlength(value) {name:maxlength(12)} String must be at most value characters long.
    length(value) {name:length(12)} String must be exactly value characters long.
    min(value) {age:min(18)} Integer must be at least value
    max(value) {age:max(65)} Integer must be at most value
    range(min,max) {age:range(12,65)} Integer must be between min and max
    alpha {name:alpha} String must only contain letters a-z, case insensitive.
    regex(expression) {ssn:regex(^\d{{3}}-
    \d{{2}}-\d{{4}}$)} String must match regular expression.
    required {name:required} Value is required.

    Conventional Routing (MVC Style Web Apps)
    Conventional routing builds the route table in the Startup class (pre-.NET6) or in the Program.cs file’s top level statements (.NET6+). The MapControllerRoute() method adds an endpoint into the route table. The method specifies a name, URL pattern, and any default values for the variables in the URL pattern. In the following code sample, the predefined {controller} and {action} placeholders refer to a controller (minus the Controller suffix) and an action method contained in that controller. The placeholder {id} is custom and is translated into a parameter (named id) for the action method. Adding a question mark to a
    route token indicates that it is an optional route value and is represented in the action method as a nullable parameter.

    app.UseRouting(); app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}"
    );

    When a URL is requested, it is checked against the route table. If there is a match, the HTTP Verb (Post, Get, etc.) of the request is also checked to make sure the code located at that application endpoint accepts the request’s verb. For example, an HTTP Post with the URL (minus the scheme and domain) Car/Delete/5 comes into the application. From the following two actions, the second delete action method would be targeted (since it is marked with the [HttpPost] attribute), passing in 5 as the value for the id parameter:

    public class CarController : ControllerBase
    {
    [HttpGet]
    public async Task Delete(int? id)
    {
    //return a view
    }

    [HttpPost] [ValidateAntiForgeryToken]
    public async Task Delete(int id, Car car)
    {
    //delete the record
    }
    }

    The defaults specify how to fill in the blanks for URLs that don’t contain all of the defined components. In the previous code, if no additional route values were specified in the URL (such as http:// localhost:5001), then the routing engine would call the Index() action method of the HomeController class, without an id parameter. The defaults are progressive, meaning that they can be excluded from right
    to left. However, route parts can’t be skipped. Entering a URL like http://localhost:5001/Delete/5 will fail the {controller}/{action}/{id} pattern.
    Conventional routing is order dependent. The routing engine starts at the top of the route table and will attempt to find the first matching route based on the (optional) area, controller, action, custom tokens, and HTTP verb. If the routing engine finds more than one end point that matches the route plus HTTP Verb it will throw an AmbiguousMatchException. If the routing engine can’t find a matching route, it will return a 404.
    Notice that the route template doesn’t contain a protocol or hostname. The routing engine automatically prepends the correct information when creating routes and uses the HTTP verb, path, and parameters to determine the correct application endpoint. For example, if your site is running on https:// www.skimedic.com, the protocol (HTTPS) and hostname (www.skimedic.com) is automatically prepended
    to the route when created (e.g., https://www.skimedic.com/Car/Delete/5). For an incoming request, the routing engine uses the Car/Delete/5 portion of the URL.

    Area Routes
    If your application contains an area, there is an additional route pattern that needs to be mapped. Instead of using MapControllerRoute(), call MapAreaControllerRoute(), like this:

    app.UseRouting(); app.MapAreaControllerRoute(
    name:"areaRoute", areaName:"Admin",
    pattern:"Admin/{controller}/{action}/{id?}");

    Since area routes are more specific than the non-area routes, they are typically added to the route table first, like this:

    app.UseRouting(); app.MapAreaControllerRoute(
    name:"areaRoute", areaName:"Admin",
    pattern:"Admin/{controller}/{action}/{id?}"); app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

    Named Routes
    Route names can be used as a shorthand to generate URLs from within the application. In the preceding conventional route, the endpoint is assigned the name default.

    Attribute Routing (MVC Style Web Apps and RESTful Services)
    Attribute routing works the same way as conventional routing when matching routes with application endpoints and HTTP verbs. The difference is how the routes are configured. In attribute routing, routes are defined using C# attributes on controllers and their action methods. This can lead to more precise routing, but can also increase the amount of configuration, since every controller and action needs to have routing information specified. Before we look at examples of attribute routing, it must be enabled in the Startup class (pre .NET 6) or the Program.cs file’s top level statements (.NET6+) by calling MapControllers():

    app.MapControllers();

    For an example of attribute routing, take the following code snippet. The four Route attributes on the Index() action method equate to the same default route defined earlier. The Index() action method is the application endpoint for
    •mysite.com ([Route("/")]),
    •mysite.com/Home ([Route("/Home")]),
    •mysite.com/Home/Index ([Route("/Home/Index")]), or
    •mysite.com/Home/Index/5 ([Route("/Home/Index/{id?}")]).

    public class HomeController : Controller
    {
    [Route("/")]
    [Route("/Home")] [Route("/Home/Index")] [Route("/Home/Index/{id?}")]
    public IActionResult Index(int? id)
    {
    ...
    }
    }

    In the previous example, the only route token used was for the optional id parameter. The same set of routes can be created using the controller and action route tokens, like this:

    public class HomeController : Controller
    {
    [Route("/")]
    [Route("/[controller]")]
    [Route("/[controller]/[action]")]
    [Route("/[controller]/[action]/{id?}")]
    public IActionResult Index(int? id)
    {
    ...
    }
    }

    The major difference between conventional routing and attribute routing is that conventional routing covers the application, while attribute routing covers just the controller and its action methods with the Route attribute. If conventional routing is not used, every controller will need to have their route defined with attribute routing or they will not be able to be accessed. For example, if there wasn’t a default route defined in the route table (using conventional routing), the following code is not discoverable since the controller doesn’t have any routing configured:

    public class CarController : Controller
    {
    public IActionResult Delete(int id)
    {
    ...
    }
    }

    ■Note Conventional and attribute routing can be used together. If the default controller route was set as in the conventional routing example, the preceding controller would be located by the route table.

    When routes are added at the controller level, the action methods derive from that base route. For example, the following controller route covers the Delete() (and any other) action method:

    [Route("[controller]/[action]/{id?}")]
    public class CarController : Controller
    {
    public IActionResult Delete(int id)
    {
    ...
    }
    }

    ■Note The built-in tokens are distinguished with square brackets ([]) in attribute routing instead of the curly braces ({}) used in conventional routing. Custom tokens still use curly braces.

    If an action method needs to restart the route pattern, prefix the route with a forward slash (/). For example, if the delete method should follow the URL pattern mysite.com/Delete/Car/5, configure the action as follows:

    [Route("[controller]/[action]/{id?}")] public class CarController : Controller
    {
    [Route("/[action]/[controller]/{id}")]
    public IActionResult Delete(int id)
    {
    ...
    }
    }

    As shown with the default attribute route example, route definitions can use literal route values instead of using token replacement. The following code will produce the same result for the Delete() action method as the previous code sample:

    [Route("[controller]/[action]/{id?}")] public class CarController : Controller
    {
    [Route("/Delete/Car/{id}")]
    public IActionResult Delete(int id)
    {
    ...
    }
    }

    Razor Page Routing
    As mentioned already, Razor page routing is based on the folder structure of the application. To enable routing, call MapRazorPages() in the Startup class (pre .NET 6) or the Program.cs file’s top level statements (.NET6+):

    app.UseRouting(); app.MapRazorPages();

    For Razor page based web applications, the Index page is the default page for a directory. This means that if an Index.cshtml page is located at Pages/Cars/Index.cshtml, then both the routes that map to that page include /Cars and /Cars/Index.
    Additional route tokens can be added after the @page directive to refine the route. Suppose you have a Car folder under the Pages folder, and in the Cars folder, there is a page named Delete.cshtml. The default route for this page is /Cars/Delete. To add an optional id token to accept a URI like /Cars/Delete/5, update the @page directive to the following:

    @page "{id?}"

    Just like routing for MVC style applications, the route can be reset using a forward slash (/). Once reset, you can add literals and tokens to completely change the route. For example, the route for the Delete. cshtml page can be updated to /Delete/Vehicle/5 by using the following:

    @page "/Delete/Vehicle/{id?}

    Routing and HTTP Verbs
    As we have already discussed, none of the route templates define an HTTP verb. In MVC based applications and RESTful services, the action methods are decorated with attributes to indicate which HTTP verb it should handle. For Razor page based applications, the different HTTP verbs are handled with specific page handler methods.

    HTTP Verbs in MVC Styled Web Application Attribute Routing
    As discussed earlier, a common pattern in web applications using the MVC pattern, there will be two application endpoints that match a particular route template. The discriminator in these instances is the HTTP verb, as we saw with the CarController‘s two Delete() action methods.
    Routes can also be modified using the HTTP verb attributes. For example, the following shows the optional id route token added to the route template for both Delete() methods:

    [Route("[controller]/[action]")]
    public class CarController : Controller
    {
    [HttpGet("{id?}")]
    public IActionResult Delete(int? id)
    {
    ...
    }
    [HttpPost("{id}")] [ValidateAntiForgeryToken]
    public IActionResult Delete(int id, Car recordToDelete)
    {
    ...
    }
    }

    ■Note Browsers only support get and post requests, so while you can decorate your MVC web application’s methods with additional hTTP Verb attributes (like HttpPut and HttpDelete), browsers will not be able to make the appropriate requests to leverage those endpoints.

    Routes can also be restarted using the HTTP verbs; just preface the route templated with a forward slash (/), as the following example demonstrates:

    [HttpGet("/[controller]/[action]/{makeId}/{makeName}")] public IActionResult ByMake(int makeId, string makeName)
    {
    ViewBag.MakeName = makeName;
    return View(_repo.GetAllBy(makeId));
    }

    If an action method isn’t decorated with an HTTP verb attribute, it defaults to accepting HTTP get requests. However, in MVC styled web applications, unmarked action methods can also respond to HTTP post requests, which might cause unexpected results. For this reason, it’s considered a best practice to mark all action methods explicitly with the correct verb attribute.

    HTTP Verbs in RESTful Service Routing
    Route definitions used for RESTful services typically do not specify action methods. When action methods are not part of the route template, the action methods are selected based on the HTTP verb of the request (and optionally the content type). The following code shows an API controller with four methods that all match the same route template. Notice that the HTTP verb attributes are different for each of the action methods:

    [Route("api/[controller]")] [ApiController]
    public class CarController : ControllerBase
    {
    [HttpGet("{id}")]
    public IActionResult GetCarsById(int id)
    {
    ...
    }
    [HttpPost("{id}")]
    public IActionResult CreateANewCar(int id, Car entity)
    {
    ...
    }
    [HttpPut("{id}")]
    public IActionResult UpdateAnExistingCar(int id, Car entity)
    {
    ...
    }

    [HttpDelete("{id}")]
    public IActionResult DeleteACar(int id, Car entity)
    {
    ...
    }
    }

    If an action method doesn’t have an HTTP verb attribute, it is treated as the application endpoint for HTTP get requests. Just as with MVC style applications, if the route requested is matched but there isn’t an action method with the correct verb attribute, the server will return a 404 (not found).

    ■Note ASP.NET Web API allowed you to omit the hTTP verb for a method if the name started with Get, Put, Delete, or Post. This convention has been removed in ASP.NET Core. If an action method does not have an hTTP verb specified, it will be called using an hTTP get.

    The final endpoint selector for API controllers is the optional Consumes attribute, which specifies the content type that is accepted by the endpoint. The request must use the matching content-type header, or a 415 Unsupported Media Type error will be returned. The following two example endpoints, both in the same controller, differentiate between JSON and XML:

    [HttpPost] [Consumes("application/json")]
    public IActionResult PostJson(IEnumerable values)
    => Ok(new { Consumes = "application/json", Values = values });
    [HttpPost]
    [Consumes("application/x-www-form-urlencoded")]
    public IActionResult PostForm([FromForm] IEnumerable values)
    => Ok(new { Consumes = "application/x-www-form-urlencoded", Values = values });

    ■Note There is one additional (and optional) route selector for API controllers, and that involves versioning. Versioning API applications is covered in Chapter 32.

    HTTP Verbs in Razor Page Routing
    Once a Razor page is discovered as the endpoint for a route, the HTTP verb is used to determine the correct page handler method to execute. The following code sample for the Delete.cshtml page will execute the OnGet() method on get requests and the OnPost() method on post requests.

    public class DeleteModel : PageModel
    {
    public IActionResult OnGet(int? id)
    {
    //handle the get request here
    }

    public IActionResult OnPost(int? id)
    {
    //handle the post request here
    }
    }

    This will be covered in more depth in the next chapter.

    Redirecting Using Routing
    Another advantage of routing is that you no longer have to hard-code URLs for other pages in your site. The routing entries are used to match incoming requests as well as build URLs. When building URLs, the scheme, host, and port are added based on the values of the current request.
    When redirecting in server side code (e.g., in a controller’s action method or in a Razor page), there are several redirect methods that can be used to redirect the execution path to another end point. Table 30-6 shows three of the redirect methods and their most commonly used overloads.

    Table 30-6. Methods for Server-Side Redirecting of Requests

    Method Meaning in Life
    RedirectToAction() Redirects to an action. Overloaded parameter options include: actionName, controllerName, routeValues
    If any of the parameters are not supplied, the values will be supplied by the current HTTP request.
    RedirectToRoute() Redirects to a named route. Optional route values can be supplied.
    RedirectToPage() Redirects to a Razor Page. Optional route values can be supplied.

    For an example, the following code redirects the request from the Delete method to the Index method in the same controller (since a controller name wasn’t provided):

    [HttpPost("{id}")]
    public async Task Delete(int id, Car car)
    {
    //interesting code here
    return RedirectToAction(nameof(Index));
    }

    Model Binding
    Model binding is the process where ASP.NET Core uses the name-value pairs submitted in an HTTP Post call to assign values to models. The values are submitted using form fields, request body (for API style controllers), route data, query string parameters, or uploaded files. To bind to a reference type, the name- value pairs come from the form values or the request body, the reference types must have a public default constructor, and the properties to be bound must be public and writable.
    When assigning values, implicit type conversions (such as setting a string property value using an int) are used where applicable. If type conversion doesn’t succeed, that property is flagged in error. Before discussing binding in greater detail, it’s important to understand the ModelState dictionary and its role in the binding (and validation) process.

    The ModelState Dictionary
    The ModelState dictionary contains an entry for every property being bound and an entry for the model itself. If an error occurs during model binding, the binding engine adds the errors to the dictionary entry for the property and sets ModelState.IsValid = false. If all matched properties are successfully assigned, the binding engine sets ModelState.IsValid = true.

    ■Note Model validation, which also sets the ModelState dictionary entries, happens after model binding. Both implicit and explicit model binding automatically call validation for the model. Validation is covered shortly.

    How you handle the binding and/or validation errors varies based on the needs of your application. The following code sample from an API endpoint shows how to get all of the errors from the ModelState, create an anonymous object, and then return a BadRequestObject (HTTP status code 400) with the resulting object sent back in the body of the response as JSON:

    [HttpPost] [ValidateAntiForgeryToken]
    public async Task Update(Car entity)
    {
    if (!ModelState.IsValid)
    {
    IEnumerable errorList = ModelState.Values.SelectMany(v => v.Errors).Select(e=>e. ErrorMessage);
    var responseContent =
    new { Message = "One or more field validation errors occurred", Errors = errorList }; apiLogEntry.ResponseContentBody = JsonSerializer.Serialize(responseContent);
    return new BadRequestObjectResult(responseContent);
    }
    //binding and validation was successful, execute the rest of the method
    }

    Adding Custom Errors to the ModelState Dictionary
    In addition to the properties and errors added by the binding and validation engines, custom errors can be added to the ModelState dictionary. Errors can be added at the property level or for the entire model.
    To add a specific error for a property (e.g., the PetName property of the Car entity), use the following:

    ModelState.AddModelError("PetName","Name is required");

    To add an error for the entire model, use string.Empty for the property name, like this:

    ModelState.AddModelError(string.Empty, $"Unable to create record: {ex.Message}");

    Clearing the ModelState Dictionary
    There are times where you might need to clear the ModelState of all values and errors. To reset the ModelState, simply call the Clear() method, like this:

    ModelState.Clear();

    This is commonly used with explicit validation or when required properties are intentionally left out because of over posting concerns.

    Implicit Model Binding
    Implicit model binding occurs when the model to be bound is a parameter for an action method (MVC/API applications) or a handler method (Razor page applications). It uses reflection (and recursion for complex types) to match the model’s writable property names with the names contained in the name-value pairs posted to the action method. If there is a name match, the binder uses the value from the name-value pair to attempt to set the property value. If multiple names from the name-value pairs match, the first matching name’s value is used. If a property isn’t found in the name-value pairs, the property is set to its default value. The order the name-value pairs are searched is as follows:
    •Form values from an HTTP Post method (including JavaScript AJAX posts)
    •Request body (for API controllers)
    •Route values provided through ASP.NET Core routing (for simple types)
    •Query string values (for simple types)
    •Uploaded files (for IFormFile types)
    For example, the following method will attempt to set all of the properties on the Car type implicitly. If the binding process completes without error, the ModelState.IsValid property returns true.

    [HttpPost] [ValidateAntiForgeryToken]
    public ActionResult Create(Car entity)
    {
    if (ModelState.IsValid)
    {
    //Save the data;
    }
    }

    Here an example OnPostAsync() method in a Razor page PageModel class that takes in an optional integer (from the route or query string) and an implicitly bound Car entity:

    public async Task OnPostAsync(int? id, Car entity)
    {
    if (ModelState.IsValid)
    {
    //Save the data;
    }
    }

    Explicit Model Binding
    Explicit model binding is executed with a call to TryUpdateModelAsync(), passing in an instance of the type being bound and the list of properties to bind. The method then uses reflection to find matches between the property names and the names in the name-value pairs in the request. If the model binding fails, the method returns false and sets the ModelState errors in the same manner as implicit model binding.

    When using explicit model binding, the type being bound isn’t a parameter of the action method (MVC/ API) or handler method (Razor pages). For example, you could write the previous Create() method this way and use explicit model binding:

    [HttpPost] [ValidateAntiForgeryToken]
    public async Task Create()
    {
    var newCar = new Car();
    if (await TryUpdateModelAsync(newCar ,"", c=>c.Color,c=>c.PetName,c=>c.MakeId))
    {
    //do something important
    }
    }

    Explicit model binding also works in Razor page handler methods:

    public async Task OnPostAsync()
    {
    var newCar = new Car();
    if (await TryUpdateModelAsync(newCar ,"", c=>c.Color,c=>c.PetName,c=>c.MakeId))
    {
    //do something important
    }
    }

    ■Note The second parameter (set to an empty string in these examples) will be covered shortly in the handling Property Name Prefixes section.

    With implicit model binding, an instance is getting created for you. However, with explicit model binding, you must first create the instance then call TryUpdateModelAsync(), which attempts to update the values of that instance from the name value pairs sent in the request. Like explicit model binding, the binding engine ignores any properties without a matching name in the name-value pairs in the request.
    Since you have to create the instance first with explicit model binding, you can set properties on your instance before calling TryUpdateModelAsync(), like this:

    var newCar = new Car
    {
    Color = "Purple", MakeId = 1, PetName = "Prince"
    };
    if (await TryUpdateModelAsync(newCar,"", c=>c.Color,c=>c.PetName,c=>c.MakeId))
    {
    //do something important
    }

    In the previous example, any of the values set with the initial object initialization will retain their values if the property does not have a matching name in the name-value pairs in the request.

    Property Model Binding
    A property on a MVC style controller or a Razor page PageModel can be marked as the binding target for HTTP Post requests. This is accomplished by adding the public property to the class and marking it with the BindProperty attribute. When using property binding, the controller’s action methods or PageModel handler methods do not take the property as a parameter. Here are two examples that uses property binding on a Car property for an MVC style application and a Razor page PageModel:

    //CarsController - MVC
    public class CarsController : Controller
    {
    [BindProperty] public Car Entity { get; set; }

    [HttpPost("{id}")] [ValidateAntiForgeryToken] [ActionName("Edit")]
    public async Task Edit(int id)
    {
    //Handle the post request
    } //omitted for brevity
    }

    //EditPage -- Razor
    public class EditModel : PageModel
    {
    [BindProperty] public Car Entity { get; set; }

    public async Task OnGet(int? id)
    {
    //Handle the HTTP Get request
    }
    public async Task OnPost(int id)
    {
    //Handle the HTTP Get request
    }
    }

    By default, property binding only works with HTTP Post requests. If you need HTTP Get requests to also bind to the property, update the BindProperty attribute as follows:

    [BindProperty(Name="car",SupportsGet = true)] public Car Entity { get; set; }

    Handling Property Name Prefixes
    Sometimes the data will come into your action method from a table, a parent-child construct, or a complex object that adds a prefix to the names in the name-value pairs. Recall that both implicit and explicit model binding uses reflection to match property names with names in the name-value pairs of the request, and prefixes to the names will prevent the matches from being made.

    With implicit model binding, the Bind attribute is used to specify a prefix for the property names. The following example sets a prefix for the names:

    [HttpPost] [ValidateAntiForgeryToken]
    public ActionResult Create([Bind(Prefix="CarList")]Car car)
    {
    if (ModelState.IsValid)
    {
    //Save the data;
    }
    //handle the binding errors
    }

    With explicit model binding, the prefix is set in the TryUpdateModelAsync() method using the second parameter (which was just an empty string in the previous examples):

    [HttpPost] [ValidateAntiForgeryToken]
    public async Task Create()
    {
    var newCar = new Car();
    if (await TryUpdateModelAsync(newCar,"CarList", c=>c.Color,c=>c.PetName,c=>c.MakeId))
    {
    //Save the data
    }
    //handle the binding errors
    }

    Preventing Over Posting
    Over posting is when the request submits more values than you are expecting (or wanting). This can be accidental (the developer left too many fields in the form), or malicious (someone used browser dev tools to modify the form before submitting it). For example, presume you want the application to allow changing colors, names, or makes, but not the prices on Car records.
    The Bind attribute in HTTP Post methods allows you to limit the properties that participate in implicit model binding. If a Bind attribute is placed on a reference parameter, the fields listed in the Include list are the only fields that will be assigned through model binding. If the Bind attribute is not used, all fields are bindable.
    The following example uses the Bind attribute to only allow updating the PetName and Color fields in the Update() method:

    [HttpPost] [ValidateAntiForgeryToken]
    public ActionResult Update([Bind(nameof(Car.PetName),nameof(Car.Color))]Car car)
    {
    //body omitted for brevity
    }

    Here is the same example in a Razor page:

    public async Task OnPostAsync(int? id, [Bind(nameof(Car.PetName),nameof(Car. Color))]Car car)
    {
    //body omitted for brevity
    }

    To prevent over posting when using explicit model binding, you remove the properties in the body of the TryUpdateModel() method’s body that shouldn’t be accepted. The following example only allows updating the PetName and Color fields in the Update() method:

    [HttpPost] [ValidateAntiForgeryToken]
    public async Task Update()
    {
    var newCar = new Car();
    if (await TryUpdateModelAsync(newCar,"", c=>c.Color,c=>c.PetName))
    {
    //save the data
    }
    //Handle the binding errors
    }

    When using this method of implicit method of model binding, the Bind attribute can be used to prevent over posting:

    public async Task OnPostAsync(int? id, [Bind(nameof(Car.PetName),nameof(Car. Color))]Car car)
    {
    //body omitted for brevity
    }

    You can also specify the binding source in Razor pages. The following instructs the binding engine to get the data from the request’s Form data:

    public async Task OnPostAsync(int? id, [FromForm]Car car)
    {
    //body omitted for brevity
    }

    Controlling Model Binding Sources in ASP.NET Core
    Binding sources can be controlled through a set of attributes on the action parameters. Custom model binders can also be created; however, that is beyond the scope of this book. Table 30-7 lists the attributes that can be used to control model binding.

    Table 30-7. Controlling Model Binding Sources

    Attribute Meaning in Life
    BindingRequired A model state error will be added if binding cannot occur instead of just setting the property to its default value.
    BindNever Tells the model binder to never bind to this parameter.
    FromHeaderFromQuery FromRouteFromForm Used to specify the exact binding source to apply (header, query string, route parameters, or form values).
    FromServices Binds the type using dependency injection (covered later in this chapter).
    FromBody Binds data from the request body. The formatter is selected based on the content of the request (e.g., JSON, XML, etc.). There can be at most one parameter decorated with the FromBody attribute.
    ModelBinder Used to override the default model binder (for custom model binding).

    Here are two examples that show using the FromForm attribute. The first is in a RESTful service controller, and the second is from a Razor page handler method:

    //API ActionMethod [HttpPost]
    public ActionResult Create([FromForm] Car entity)
    {
    //body omitted for brevity
    }

    //Razor page
    public async Task OnPostAsync(int? id, [FromForm]Car car)
    {
    //body omitted for brevity
    }

    Model Validation
    Model validation occurs immediately after model binding (both explicit and implicit). While model binding adds errors to the ModelState data dictionary due to conversion issues, validation failures add errors to the ModelState data dictionary due to broken validation rules. Examples of validation rules include required fields, strings that have a maximum allowed length, properly formatted phone numbers, or dates being within a certain allowed range.
    Validation rules are set through validation attributes, either built-in or custom. Table 30-8 lists some of the built-in validation attributes. Note that several also double as data annotations for shaping the EF Core entities.

    Table 30-8. Some of the Built-in Validation Attributes

    Attribute Meaning in Life
    CreditCard Performs a Luhn-10 check on the credit card number
    Compare Validates the two properties in a model match
    EmailAddress Validates the property has a valid email format
    Phone Validates the property has a valid phone number format
    Range Validates the property falls within a specified range
    RegularExpression Validates the property matches a specified regular expression
    Required Validates the property has a value
    StringLength Validates the property doesn’t exceed a maximum length
    Url Validates the property has a valid URL format
    Remote Validates input on the client by calling an action method on the server

    Custom validation attributes can also be developed and are covered later in this book.

    Explicit Model Validation
    Explicit model validation is executed with a call to TryValidateModel(), passing in an instance of the type being validated. The result of this call is a populated ModelState instance with any invalid properties. Valid properties do not get written to the ModelState object like they do with the binding/validation combination.
    Take the following code as an example. This method uses explicit binding, leaving out the Color property. Since the color property is required, the ModelState reports as invalid. The Color is updated, explicit validation is called, and the ModelState is still invalid:

    [HttpPost] [ValidateAntiForgeryToken]
    public async Task CreateCar()
    {
    var newCar = new Car();
    if (await TryUpdateModelAsync(newCar, "",c => c.PetName, c => c.MakeId))
    {
    //car is bound and valid - save it
    }

    var isValid = ModelState.IsValid; //false newCar.Color = "Purple"; TryValidateModel(newCar);
    isValid = ModelState.IsValid; //still false
    //rest of the method
    }
    }

    In order for an object that has been through a validation pass to be revalidated, the ModelState must be cleared, as in the following update to the method:

    [HttpPost] [ValidateAntiForgeryToken]
    public async Task CreateCar()
    {
    var newCar = new Car();
    if (await TryUpdateModelAsync(newCar, "",c => c.PetName, c => c.MakeId))
    {
    //car is bound and valid - save it
    }

    var isValid = ModelState.IsValid; //false newCar.Color = "Purple"; TryValidateModel(newCar);
    isValid = ModelState.IsValid; //still false ModelState.Clear(); TryValidateModel(newCar);
    isValid = ModelState.IsValid; //true
    //rest of the method
    }
    }

    Do know that calling Clear() clears out all ModelState data, including the binding information.

    Filters
    Filters in ASP.NET Core run code before or after specific stages of the request processing pipeline. There are built-in filters for authorization and caching, as well as options for assigning customer filters. Table 30-9 lists the types of filters that can be added into the pipeline, listed in their order of execution.

    Table 30-9. Filters Available in ASP.NET Core

    Filter Meaning in Life
    Authorization filters Run first and determine if the user is authorized for the current request.
    Resource filters Run immediately after the authorization filter and can run after the rest of the pipeline has completed. Run before model binding.
    Action filters Run immediately before an action is executed and/or immediately after an action is executed. Can alter values passed into an action and the result returned from an action. Applies to MVC style web applications and RESTful services.
    Page Filters Can be built to run code after a handler method has been selected but before model binding, after the handler method executes after model binding is complete or immediately after the handler executed. Similar to Action filters but apply to Razor Pages.
    Exception filters Used to apply global policies to unhandled exceptions that occur before writing to the response body.
    Result filters Run code immediately after the successful execution of action results. Useful for logic that surrounds view or formatter execution.

    Authorization Filters
    Authorization filters work with the ASP.NET Core Identity system to prevent access to controllers or actions that the user doesn’t have permission to use. It’s not recommended to build custom authorization filters since the built-in AuthorizeAttribute and AllowAnonymousAttribute usually provide enough coverage when using ASP.NET Core Identity.

    Resource Filters
    Resource filters have two methods. The OnResourceExecuting() method executes after authorization filters and prior to any other filters, and the OnResourceExecuted() method executes after all other filters. This enables resource filters to short-circuit the entire response pipeline. A common use for resource filters is for caching. If the response is in the cache, the filter can skip the rest of the pipeline.

    Action Filters
    The OnActionExecuting() method executes immediately before the execution of the action method, and the OnActionExecuted() method executes immediately after the execution of the action method. Action filters can short-circuit the action method and any filters that are wrapped by the action filter (order of execution and wrapping are covered shortly).

    Page Filters
    The OnHandlerSelected() method executes after a handler method has been selected but before model binding occurs. The OnPageHandlerExecuting() method executes after model binding is complete and the OnPageHandlerExecuted() method executes after the handler method executes.

    Exception Filters
    Exception filters enable implementation of consistent error handling in an application. The OnException() method executes when unhandled exceptions are thrown in controller creation, model binding, action filters, or action methods, page filters, or page handler methods.

    Result Filters
    Result filters wrap the execution of the IActionResult for an action method. A common scenario is to add header information into the HTTP response message using a result filter. The OnResultExecuting() method executes before the response and OnResultExecuted() executes after the response has started.

    Summary
    This chapter introduced ASP.NET Core and is the first of a set of chapters covering ASP.NET Core. This chapter began with a brief look back at the history of ASP.NET and then looked at the features from classic ASP.NET MVC and ASP.NET Web API that also exist in ASP.NET Core.
    The next section created the solution and the projects, updated the ports, and examined running and debugging ASP.NET Core web applications and services.
    In the next chapter, you will dive into many of the new features added in ASP.NET Core.

    Pro C#10 CHAPTER 29 WPF Notifications, Validations, Commands, and MVVM

    CHAPTER 29

    WPF Notifications, Validations, Commands, and MVVM

    This chapter will conclude your investigation of the WPF programming model by covering the capabilities that support the Model-View-ViewModel (MVVM) pattern. The first section covers the Model-View- ViewModel pattern. Next, you learn about the WPF notification system and its implementation of the Observable pattern through observable models and observable collections. Having the data in the UI accurately portray the current state of the data automatically improves the user experience significantly and reduces the manual coding required in older technologies (such as WinForms) to achieve the same result.
    Building on the Observable pattern, you will examine the mechanisms to add validation into your application. Validation is a vital part of any application—not only letting the user know that something is wrong but also letting them know what is wrong. To inform the user what the error is, you will also learn how to incorporate validation into the view markup.
    Next, you will take a deeper dive into the WPF command system and create custom commands to encapsulate program logic, much as you did in Chapter 25 with the built-in commands. There are several advantages to creating custom commands, including (but not limited to) enabling code reuse, logic encapsulation, and separation of concerns.
    Finally, you will bring all of this together in a sample MVVM application.

    Introducing Model-View-ViewModel
    Before you dive into notifications, validations, and commands in WPF, it would be good to understand the end goal of this chapter, which is the Model-View-ViewModel pattern (MVVM). Derived from Martin
    Fowler’s Presentation Model pattern, MVVM leverages XAML-specific capabilities, discussed in this chapter, to make your WPF development faster and cleaner. The name itself describes the main components of the pattern: model, view, view model.

    The Model
    The model is the object representation of your data. In MVVM, models are conceptually the same as the models from your data access layer (DAL). Sometimes they are the same physical class, but there is no requirement for this. As you read this chapter, you will learn how to decide whether you can use your DAL models or whether you need to create new ones.
    Models typically take advantage of the built-in (or custom) validations through data annotations and the INotifyDataErrorInfo interface and are configured as observable to tie into the WPF notification system. You will see all of this later in this chapter.

    © Andrew Troelsen, Phil Japikse 2022
    A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_29

    1273

    The View
    The view is the UI of the application, and it is designed to be very lightweight. Think of the menu board at a drive-thru restaurant. The board displays menu items and prices, and it has a mechanism so the user can communicate with the back-end systems. However, there isn’t any intelligence built into the board, unless it is specifically user interface logic, such as turning on the lights if it gets dark.
    MVVM views should be developed with the same goals in mind. Any intelligence should be built into the application elsewhere. The only code in the code-behind file (e.g., MainWindow.xaml.cs) should be directly related to manipulating the UI. It should not be based on business rules or anything that needs to be persisted for future use. While not a main goal of MVVM, well-developed MVVM applications typically have very little code in the code-behind.

    The View Model
    In WPF and other XAML technologies, the view model serves two purposes.
    •The view model provides a single stop for all the data needed by the view. This doesn’t mean the view model is responsible for getting the actual data; instead, it is merely a transport mechanism to move the data from the data store to the view. Usually, there is a one-to-one correlation between views and view models, but architectural differences exist, and your mileage may vary.
    •The second job is to act as the controller for the view. Just like the menu board, the view model takes direction from the user and relays that call to the relevant code to make sure the proper actions are taken. Quite often this code is in the form of custom commands.

    Anemic Models or Anemic View Models
    In the early days of WPF, when developers were still working out how best to implement the MVVM pattern, there were significant (and sometimes heated) discussions about where to implement items like validation and the Observable pattern. One camp (the anemic model camp) argued that it all should be in the view model since adding those capabilities to the model broke separation of concerns. The other camp (the anemic view model camp) argued it should all be in the models since that reduced duplication of code.
    The real answer is, of course, it depends. When INotifyPropertyChanged, IDataErrorInfo, and INotifyDataErrorInfo are implemented on the model classes, this ensures that the relevant code is close to the target of the code (as you will see in this chapter) and is implemented only once for each model.
    That being said, there are times when your view model classes will need to be developed as observables themselves. At the end of the day, you need to determine what makes the most sense for your application, without over-complicating your code or sacrificing the benefits of MVVM.

    ■Note There are multiple MVVM frameworks available for WPF, such as MVVMLite, Caliburn.Micro, and Prism (although Prism is much more than just an MVVM framework). This chapter discusses the MVVM pattern and the features in WPF that support implementing the pattern. I leave it to you, the reader, to examine the different frameworks and select the one that best matches your app’s needs.

    The WPF Binding Notification System
    A significant shortcoming in the binding system for WinForms is a lack of notifications. If the data represented in the view is updated programmatically, the UI must also be refreshed programmatically to keep them in sync. This leads to a lot of calls to Refresh() on controls, typically more than are absolutely necessary in order to be safe. While usually not a significant performance issue to include too many calls to Refresh(), if you don’t include enough, the experience for the user could be affected negatively.
    The binding system built into XAML-based applications corrects this problem by enabling you to hook your data objects and collections into a notification system by developing them as observables. Whenever a property’s value changes on an observable model or the collection changes (e.g., items are added, removed, or reordered) on an observable collection, an event is raised (either NotifyPropertyChanged
    or NotifyCollectionChanged). The binding framework automatically listens for those events to occur
    and updates the bound controls when they fire. Even better, as a developer, you have control over which properties raise the notifications. Sounds perfect, right? Well, it’s not quite perfect. There can be a fair amount of code involved in setting this up for observable models if you are doing it all by hand. Fortunately, there is an open source framework that makes it much simpler, as you shall soon see.

    Observable Models and Collections
    In this section, you will create an application that uses observable models and collections. To get started, create a new WPF application named WpfNotifications. The application will be a master-detail form, allowing the user to select a specific car using a ComboBox, and then the details for that car will be displayed in the following TextBox controls. Update MainWindow.xaml by replacing the default Grid with the following markup:























    Pro C#10 CHAPTER 28 WPF Resources, Animations, Styles, and Templates

    CHAPTER 28

    WPF Resources, Animations, Styles, and Templates

    This chapter introduces you to three important (and interrelated) topics that will deepen your understanding of the Windows Presentation Foundation (WPF) API. The first order of business is to learn the role of logical resources. As you will see, the logical resource (also known as an object resource) system is a way to name and refer to commonly used objects within a WPF application. While logical resources are often authored in XAML, they can also be defined in procedural code.
    Next, you will learn how to define, execute, and control an animation sequence. Despite what you might think, WPF animations are not limited to video game or multimedia applications. Under the WPF API, animations can be as subtle as making a button appear to glow when it receives focus or expanding the size of a selected row in a DataGrid. Understanding animations is a key aspect of building custom control templates (as you will see later in this chapter).
    You will then explore the role of WPF styles and templates. Much like a web page that uses CSS or the ASP.NET theme engine, a WPF application can define a common look and feel for a set of controls. You can define these styles in markup and store them as object resources for later use, and you can also apply them dynamically at runtime. The final example will teach you how to build custom control templates.

    Understanding the WPF Resource System
    Your first task is to examine the topic of embedding and accessing application resources. WPF supports two flavors of resources. The first is a binary resource, and this category typically includes items most programmers consider to be resources in the traditional sense (embedded image files or sound clips, icons used by the application, etc.).
    The second flavor, termed object resources or logical resources, represents a named .NET object that can be packaged and reused throughout the application. While any .NET object can be packaged as an object resource, logical resources are particularly helpful when working with graphical data of any sort, given that you can define commonly used graphic primitives (brushes, pens, animations, etc.) and refer to them when required.

    © 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_28

    1233

    Working with Binary Resources
    Before getting to the topic of object resources, let’s quickly examine how to package up binary resources such as icons or image files (e.g., company logos or images for an animation) into your applications. If you would like to follow along, create a new WPF application named BinaryResourcesApp. Update the markup for your initial window to handle the Window Loaded event and to use a DockPanel as the layout root, like so:

    <Window x:Class="BinaryResourcesApp.MainWindow"

    Title="Fun with Binary Resources" Height="500" Width="649" Loaded="MainWindow_OnLoaded">


    Now, let’s say your application needs to display one of three image files inside part of the window, based on user input. The WPF Image control can be used to display not only a typical image file (.bmp,
    .gif, .ico, .jpg, .png, .wdp, or *.tiff) but also data in a DrawingImage (as you saw in Chapter 27). You might build a UI for your window that supports a DockPanel containing a simple toolbar with Next and Previous buttons. Below this toolbar you can place an Image control, which currently does not have a value set to the Source property, like so:



    tags, as shown here:

    To allow the Cancel button to use this brush as well, you should promote the scope of your

    to a parent element’s resource dictionary. For example, if you move it to the
    , both buttons can use the same brush because they are child elements of the layout manager. Even better, you could package the brush into the resource dictionary of the Window itself so the window’s content can use it.
    When you need to define a resource, you use the property-element syntax to set the Resources property of the owner. You also give the resource item an x:Key value, which will be used by other parts of the window when they want to refer to the object resource. Be aware that x:Key and x:Name are not the same! The x:Name attribute allows you to gain access to the object as a member variable in your code file, while the x:Key attribute allows you to refer to an item in a resource dictionary.
    Visual Studio allows you to promote a resource to a higher scope using its respective Properties window.
    To do so, first identify the property that has the complex object you want to package as a resource (the Background property, in this example). To the right of the property is a small square that, when clicked, will open a pop-up menu. From it, select the Convert to New Resource option (see Figure 28-3).

    Figure 28-3. Moving a complex object into a resource container

    You are asked to name your resource (myBrush) and specify where to place it. For this example, leave the default selection of the current document (see Figure 28-4).

    Figure 28-4. Naming the object resource

    When you are done, you will see the brush has been moved inside the Window.Resources tag.







    And the Button control’s Background has been updated to use a new resource.



    First, notice that you have defined an event trigger for your button to ensure that your storyboard executes when the button has loaded into memory. The StringAnimationUsingKeyFrames class oversees changing the content of the button, via the Storyboard.TargetProperty value.
    Within the scope of the element, you define four DiscreteStringKeyFrame elements, which change the button’s Content property over the course of two seconds (note that the duration established by StringAnimationUsingKeyFrames is a total of three seconds, so you will see a slight pause between the final ! and looping O).
    Now that you have a better feel for how to build animations in C# code and XAML, let’s look at the role of WPF styles, which make heavy use of graphics, object resources, and animations.

    Understanding the Role of WPF Styles
    When you are building the UI of a WPF application, it is not uncommon for a family of controls to require a shared look and feel. For example, you might want all button types to have the same height, width, background color, and font size for their string content. Although you could handle this by setting each button’s individual properties to identical values, such an approach makes it difficult to implement changes down the road because you would need to reset the same set of properties on multiple objects for every change.
    Thankfully, WPF offers a simple way to constrain the look and feel of related controls using styles.
    Simply put, a WPF style is an object that maintains a collection of property-value pairs. Programmatically speaking, an individual style is represented using the System.Windows.Style class. This class has a property named Setters, which exposes a strongly typed collection of Setter objects. It is the Setter object that allows you to define the property-value pairs.
    In addition to the Setters collection, the Style class also defines a few other important members that allow you to incorporate triggers, restrict where a style can be applied, and even create a new style based on an existing style (think of it as “style inheritance”). Be aware of the following members of the Style class:
    •Triggers: Exposes a collection of trigger objects, which allow you to capture various event conditions within a style
    •BasedOn: Allows you to build a new style based on an existing style
    •TargetType: Allows you to constrain where a style can be applied

    Defining and Applying a Style
    In almost every case, a Style object will be packaged as an object resource. Like any object resource, you can package it at the window or application level, as well as within a dedicated resource dictionary (this is great because it makes the Style object easily accessible throughout your application). Now recall that the goal is to define a Style object that fills (at minimum) the Setters collection with a set of property-value pairs.
    Let’s build a style that captures the basic font characteristics of a control in your application. Start by creating a new WPF application named WpfStyles. Open your App.xaml file and define the following named style:



    Notice that your BasicControlStyle adds three Setter objects to the internal collection. Now, let’s apply this style to a few controls in your main window. Because this style is an object resource, the controls that want to use it still need to use the {StaticResource} or {DynamicResource} markup extension to locate the style. When they find the style, they will set the resource item to the identically named Style property. Replace the default Grid control with the following markup:


    Here, you have defined a template that consists of a named Grid control containing a named Ellipse and a Label. Because your Grid has no defined rows or columns, each child stacks on top of the previous control, which centers the content. If you run your application now, you will notice that the Click event will fire only when the mouse cursor is within the bounds of the Ellipse! This is a great feature of the WPF template architecture: you do not need to recalculate hit-testing, bounds checking, or any other low-level
    detail. So, if your template used a Polygon object to render some oddball geometry, you can rest assured that the mouse hit-testing details are relative to the shape of the control, not the larger bounding rectangle.

    Templates as Resources
    Currently, your template is embedded to a specific Button control, which limits reuse. Ideally, you would place your template into a resource dictionary so you can reuse your round button template between projects or, at minimum, move it into the application resource container for reuse within this project. Let’s move the local Button resource to the application level by cutting the template definition from the Button and pasting it into the Application.Resources tag in the App.xaml file. Add in a Key and a TargetType, as follows:







    Update the Button markup to the following:

    Now, because this resource is available for the entire application, you can define any number of round buttons just by simply applying the template. Create two additional Button controls that use this template for testing purposes (no need to handle the Click event for these new items).





    Incorporating Visual Cues Using Triggers
    When you define a custom template, the visual cues of the default template are removed as well. For example, the default button template contains markup that informs the control how to look when certain UI events occur, such as when it receives focus, when it is clicked with the mouse, when it is enabled (or disabled), and so on. Users are quite accustomed to these sorts of visual cues because it gives the control somewhat of a tactile response. However, your RoundButtonTemplate does not define any such markup, so the look of the control is identical regardless of the mouse activity. Ideally, your control should look slightly different when clicked (maybe via a color change or drop shadow) to let the user know the visual state has changed.
    This can be done using triggers, as you have already learned. For simple operations, triggers work perfectly well. There are additional ways to do this that are beyond the scope of this book, but there is more information available at https://docs.microsoft.com/en-us/dotnet/desktop-wpf/themes/how-to- create-apply-template.
    By way of example, update your RoundButtonTemplate with the following markup, which adds two
    triggers. The first will change the color of the control to blue and the foreground color to yellow when the mouse is over the surface. The second shrinks the size of the Grid (and, therefore, all child elements) when the control is pressed via the mouse.


















    The Role of the {TemplateBinding} Markup Extension
    The problem with the control template is that each of the buttons looks and says the same thing. Updating the markup to the following has no effect:

    Pro C#10 CHAPTER 27 WPF Graphics Rendering Services

    CHAPTER 27

    WPF Graphics Rendering Services

    In this chapter, you’ll examine the graphical rendering capabilities of WPF. As you’ll see, WPF provides three separate ways to render graphical data: shapes, drawings, and visuals. After you understand the pros and cons of each approach, you will start learning about the world of interactive 2D graphics using the classes within System.Windows.Shapes. After this, you’ll see how drawings and geometries allow you to render 2D data in a more lightweight manner. Finally, you’ll learn how the visual layer gives you the greatest level of power and performance.
    Along the way, you will explore several related topics, such as how to create custom brushes and pens, how to apply graphical transformations to your renderings, and how to perform hit-test operations. You’ll see how the integrated tools of Visual Studio and an additional tool named Inkscape can simplify your graphical coding endeavors.

    ■Note Graphics are a key aspect of WPF development. Even if you are not building a graphics-heavy application (such as a video game or multimedia application), the topics in this chapter are critical when you work with services such as control templates, animations, and data-binding customization.

    Understanding WPF’s Graphical Rendering Services
    WPF uses a particular flavor of graphical rendering that goes by the term retained-mode graphics. Simply put, this means that since you are using XAML or procedural code to generate graphical renderings, it is the responsibility of WPF to persist these visual items and ensure that they are correctly redrawn and refreshed in an optimal manner. Thus, when you render graphical data, it is always present, even when the end user hides the image by resizing or minimizing the window, by covering the window with another, and so forth.
    In stark contrast, previous Microsoft graphical rendering APIs (including Windows Forms’ GDI+) were immediate-mode graphical systems. In this model, it was up to the programmer to ensure that rendered visuals were correctly “remembered” and updated during the life of the application. For example, in a Windows Forms application, rendering a shape such as a rectangle involved handling the Paint event (or overriding the virtual OnPaint() method), obtaining a Graphics object to draw the rectangle, and, most important, adding the infrastructure to ensure that the image was persisted when the user resized the window (e.g., creating member variables to represent the position of the rectangle and calling Invalidate() throughout your program).
    The shift from immediate-mode to retained-mode graphics is indeed a good thing, as programmers have far less grungy graphics code to author and maintain. However, I’m not suggesting that the WPF graphics API is completely different from earlier rendering toolkits. For example, like GDI+, WPF supports various brush types and pen objects, techniques for hit-testing, clipping regions, graphical transformations,

    © 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_27

    1197

    and so on. So, if you currently have a background in GDI+ (or C/C++-based GDI), you already know a good deal about how to perform basic renderings under WPF.

    WPF Graphical Rendering Options
    As with other aspects of WPF development, you have a number of choices regarding how to perform your graphical rendering, beyond the decision to do so via XAML or procedural C# code (or perhaps a
    combination of both). Specifically, WPF provides the following three distinct ways to render graphical data:
    •Shapes: WPF provides the System.Windows.Shapes namespace, which defines a small number of classes for rendering 2D geometric objects (rectangles, ellipses, polygons, etc.). While these types are simple to use and powerful, they do come with a fair amount of memory overhead if used with reckless abandon.
    •Drawings and geometries: The WPF API provides a second way to render graphical data, using descendants from the System.Windows.Media.Drawing abstract class. Using classes such as GeometryDrawing or ImageDrawing (in addition to various geometry objects), you can render graphical data in a more lightweight (but less feature-rich) manner.
    •Visuals: The fastest and most lightweight way to render graphical data under WPF is using the visual layer, which is accessible only through C# code. Using descendants of System.Windows.Media.Visual, you can speak directly to the WPF graphical subsystem.
    The reason for offering different ways to do the same thing (i.e., render graphical data) has to do with memory use and, ultimately, application performance. Because WPF is such a graphically intensive
    system, it is not unreasonable for an application to render hundreds or even thousands of different images on a window’s surface, and the choice of implementation (shapes, drawings, or visuals) could have a
    huge impact.
    Do understand that when you build a WPF application, chances are good you’ll use all three options. As a rule of thumb, if you need a modest amount of interactive graphical data that can be manipulated by the user (receive mouse input, display tooltips, etc.), you’ll want to use members in the System.Windows.Shapes namespace.
    In contrast, drawings and geometries are more appropriate when you need to model complex, generally noninteractive, vector-based graphical data using XAML or C#. While drawings and geometries can still respond to mouse events, hit-testing, and drag-and-drop operations, you will typically need to author more code to do so.
    Last but not least, if you require the fastest possible way to render massive amounts of graphical data, the visual layer is the way to go. For example, let’s say you are using WPF to build a scientific application that can plot out thousands of points of data. Using the visual layer, you can render the plot points in the most optimal way possible. As you will see later in this chapter, the visual layer is accessible only via C# code and is not XAML-friendly.
    No matter which approach you take (shapes, drawings and geometries, or visuals), you will make use of common graphical primitives such as brushes (which fill interiors), pens (which draw exteriors), and transformation objects (which, well, transform the data). To begin the journey, you will start working with the classes of System.Windows.Shapes.

    ■Note WPF also ships with a full-blown API that can be used to render and manipulate 3D graphics, which is not addressed in this text.

    Rendering Graphical Data Using Shapes
    Members of the System.Windows.Shapes namespace provide the most straightforward, most interactive, yet most memory-intensive way to render a two-dimensional image. This namespace (defined in the PresentationFramework.dll assembly) is quite small and consists of only six sealed classes that extend the abstract Shape base class: Ellipse, Rectangle, Line, Polygon, Polyline, and Path.
    The abstract Shape class inherits from FrameworkElement, which inherits from UIElement. These classes define members to deal with sizing, tooltips, mouse cursors, and whatnot. Given this inheritance chain, when you render graphical data using Shape-derived classes, the objects are just about as functional (as far as user interactivity is concerned) as a WPF control!
    For example, determining whether the user has clicked your rendered image is no more complicated than handling the MouseDown event. By way of a simple example, if you authored this XAML of a Rectangle object in the Grid of your initial Window:

    you could implement a C# event handler for the MouseDown event that changes the rectangle’s background color when clicked, like so:

    private void myRect_MouseDown(object sender, MouseButtonEventArgs e)
    {
    // Change color of Rectangle when clicked. myRect.Fill = Brushes.Pink;
    }

    Unlike with other graphical toolkits you may have used, you do not need to author a ton of infrastructure code that manually maps mouse coordinates to the geometry, manually calculates hit-testing, renders to an off-screen buffer, and so forth. The members of System.Windows.Shapes simply respond to the events you register with, just like a typical WPF control (e.g., Button, etc.).
    The downside of all this out-of-the-box functionality is that the shapes do take up a fair amount of memory. If you’re building a scientific application that plots thousands of points on the screen, using shapes would be a poor choice (essentially, it would be about as memory intensive as rendering thousands of Button objects!). However, when you need to generate an interactive 2D vector image, shapes are a wonderful choice.
    Beyond the functionality inherited from the UIElement and FrameworkElement parent classes, Shape
    defines a number of members for each of the children; Table 27-1 shows some of the more useful ones.

    Table 27-1. Key Properties of the Shape Base Class

    Properties Meaning in Life
    DefiningGeometry Returns a Geometry object that represents the overall dimensions of the current shape. This object contains only the plot points that are used to render the data and has no trace of the functionality from UIElement or FrameworkElement.
    Fill Allows you to specify a brush object to fill the interior portion of a shape.
    GeometryTransform Allows you to apply transformations to a shape before it is rendered on the screen. The inherited RenderTransform property (from UIElement) applies the transformation after it has been rendered on the screen.
    (continued)

    Table 27-1. (continued)

    Properties Meaning in Life
    Stretch Describes how to fill a shape within its allocated space, such as its position within a layout manager. This is controlled using the corresponding System. Windows.Media.Stretch enumeration.
    Stroke Defines a brush object or, in some cases, a pen object (which is really a brush in disguise) that is used to paint the border of a shape.
    StrokeDashArray, StrokeEndLineCap, StrokeStartLineCap, StrokeThickness These (and other) stroke-related properties control how lines are configured when drawing the border of a shape. In a majority of cases, these properties will configure the brush used to draw a border or line.

    ■Note If you forget to set the Fill and Stroke properties, WPF will give you “invisible” brushes, and, therefore, the shape will not be visible on the screen!

    Adding Rectangles, Ellipses, and Lines to a Canvas
    You will build a WPF application that can render shapes using XAML and C# and, while doing so, learn a bit about the process of hit-testing. Create a new WPF application named RenderingWithShapes and change the title of MainWindow.xaml to “Fun with Shapes!” Then update the initial XAML of the , replacing the Grid with a containing a (now empty) and a . Note that each contained item has a fitting name via the Name property.





    Now, populate the with a set of objects, each of which contains a specific Shape-derived class as content. Notice that each is assigned to the same GroupName (to ensure mutual exclusivity) and is also given a fitting name.











    As you can see, declaring Rectangle, Ellipse, and Line objects in XAML is quite straightforward and requires little comment. Recall that the Fill property is used to specify a brush to paint the interior of a shape. When you require a solid-colored brush, just specify a hard-coded string of known values, and the underlying type converter will generate the correct object. One interesting feature of the Rectangle type is that it defines RadiusX and RadiusY properties to allow you to render curved corners.
    Line represents its starting and ending points using the X1, X2, Y1, and Y2 properties (given that height and width make little sense when describing a line). Here you are setting up a few additional properties that control how to render the starting and ending points of the Line, as well as how to configure the stroke settings. Figure 27-1 shows the rendered toolbar, as seen through the Visual Studio WPF designer.

    Figure 27-1. Using Shapes as content for a set of RadioButtons

    Now, using the Properties window of Visual Studio, handle the MouseLeftButtonDown event for the Canvas, and handle the Click event for each RadioButton. In your C# file, your goal is to render the selected shape (a circle, square, or line) when the user clicks within the Canvas. First, define the following nested enum (and corresponding member variable) within your Window-derived class:

    public partial class MainWindow : Window
    {
    private enum SelectedShape
    { Circle, Rectangle, Line }
    private SelectedShape _currentShape;
    }

    Within each Click event handler, set the currentShape member variable to the correct SelectedShape
    value, as follows:

    private void CircleOption_Click(object sender, RoutedEventArgs e)
    {
    _currentShape = SelectedShape.Circle;
    }

    private void RectOption_Click(object sender, RoutedEventArgs e)
    {
    _currentShape = SelectedShape.Rectangle;
    }

    private void LineOption_Click(object sender, RoutedEventArgs e)
    {
    _currentShape = SelectedShape.Line;
    }

    With the MouseLeftButtonDown event handler of the Canvas, you will render out the correct shape (of a predefined size), using the X,Y position of the mouse cursor as a starting point. Here is the complete implementation, with analysis to follow:

    private void CanvasDrawingArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
    Shape shapeToRender = null;
    // Configure the correct shape to draw. switch (_currentShape)
    {
    case SelectedShape.Circle:
    shapeToRender = new Ellipse() { Fill = Brushes.Green, Height = 35, Width = 35 }; break;
    case SelectedShape.Rectangle:
    shapeToRender = new Rectangle()
    { Fill = Brushes.Red, Height = 35, Width = 35, RadiusX = 10, RadiusY = 10 }; break;
    case SelectedShape.Line: shapeToRender = new Line()
    {
    Stroke = Brushes.Blue, StrokeThickness = 10,
    X1 = 0, X2 = 50, Y1 = 0, Y2 = 50,
    StrokeStartLineCap= PenLineCap.Triangle, StrokeEndLineCap = PenLineCap.Round
    };
    break; default:
    return;
    }
    // Set top/left position to draw in the canvas. Canvas.SetLeft(shapeToRender, e.GetPosition(canvasDrawingArea).X); Canvas.SetTop(shapeToRender, e.GetPosition(canvasDrawingArea).Y);
    // Draw shape!
    canvasDrawingArea.Children.Add(shapeToRender);
    }

    ■Note You might notice that the Ellipse, Rectangle, and Line objects being created in this method have the same property settings as the corresponding XAML definitions! As you might hope, you can streamline this code, but that requires an understanding of the WPF object resources, which you will examine in Chapter 28.

    As you can see, you are testing the currentShape member variable to create the correct Shape- derived object. After this point, you set the top-left value within the Canvas using the incoming MouseButtonEventArgs. Last but not least, you add the new Shape-derived type to the collection of UIElement objects maintained by the Canvas. If you run your program now, you should be able to click anywhere in the canvas and see the selected shape rendered at the location of the left mouse-click.

    Removing Rectangles, Ellipses, and Lines from a Canvas
    With the Canvas maintaining a collection of objects, you might wonder how you can dynamically remove an item, perhaps in response to the user right-clicking a shape. You can certainly do this using a class in the
    System.Windows.Media namespace called the VisualTreeHelper. Chapter 28 will explain the roles of “visual trees” and “logical trees” in some detail. Until then, you can handle the MouseRightButtonDown event on your Canvas object and implement the corresponding event handler like so:

    private void CanvasDrawingArea_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
    {
    // First, get the X,Y location of where the user clicked. Point pt = e.GetPosition((Canvas)sender);
    // Use the HitTest() method of VisualTreeHelper to see if the user clicked
    // on an item in the canvas.
    HitTestResult result = VisualTreeHelper.HitTest(canvasDrawingArea, pt);
    // If the result is not null, they DID click on a shape! if (result != null)
    {
    // Get the underlying shape clicked on, and remove it from
    // the canvas. canvasDrawingArea.Children.Remove(result.VisualHit as Shape);
    }
    }

    This method begins by obtaining the exact X,Y location the user clicked in the Canvas and performs a hit-test operation via the static VisualTreeHelper.HitTest() method. The return value, a HitTestResult object, will be set to null if the user does not click a UIElement within the Canvas. If HitTestResult is not null, you can obtain the underlying UIElement that was clicked via the VisualHit property, which you are casting into a Shape-derived object (remember, a Canvas can hold any UIElement, not just shapes!). Again, you’ll get more details on exactly what a “visual tree” is in the next chapter.

    ■Note By default, VisualTreeHelper.HitTest() returns the topmost UIElement clicked and does not provide information on other objects below that item (e.g., objects overlapping by Z-order).

    With this modification, you should be able to add a shape to the canvas with a left mouse-click and delete an item from the canvas with a right mouse-click!
    So far, so good. At this point, you have used Shape-derived objects to render content on RadioButtons using XAML and populated a Canvas using C#. You will add a bit more functionality to this example when you examine the role of brushes and graphical transformations. On a related note, a different example in this chapter will illustrate drag-and-drop techniques on UIElement objects. Until then, let’s examine the remaining members of System.Windows.Shapes.

    Working with Polylines and Polygons
    The current example used only three of the Shape-derived classes. The remaining child classes (Polyline, Polygon, and Path) are extremely tedious to render correctly without tool support (such as Microsoft Blend, the companion tool for Visual Studio designed for WPF developers, or other tools that can create vector graphics) simply because they require a large number of plot points to represent their output. Here is an overview of the remaining Shapes types.

    The Polyline type lets you define a collection of (x, y) coordinates (via the Points property) to draw a series of line segments that do not require connecting ends. The Polygon type is similar; however, it is
    programmed so that it will always close the starting and ending points and fill the interior with the specified brush. Assume you have authored the following in the Kaxaml editor:




    Figure 27-2 shows the rendered output in Kaxaml.

    Figure 27-2. Polygons and polylines

    Working with Paths
    Using the Rectangle, Ellipse, Polygon, Polyline, and Line types alone to draw a detailed 2D vector image would be extremely complex, as these primitives do not allow you to easily capture graphical data such as curves, unions of overlapping data, and so forth. The final Shape-derived class, Path, provides the ability
    to define complex 2D graphical data represented as a collection of independent geometries. After you have defined a collection of such geometries, you can assign them to the Data property of the Path class, where this information will be used to render your complex 2D image.
    The Data property takes a System.Windows.Media.Geometry-derived class, which contains the key members described in Table 27-2.

    Table 27-2. Select Members of the System.Windows.Media.Geometry Type

    Member Meaning in Life
    Bounds Establishes the current bounding rectangle containing the geometry.
    FillContains() Determines whether a given Point (or other Geometry object) is within the bounds of a particular Geometry-derived class. This is useful for hit-testing calculations.
    GetArea() Returns the entire area that a Geometry-derived type occupies.
    GetRenderBounds() Returns a Rect that contains the smallest possible rectangle that could be used to render the Geometry-derived class.
    Transform Assigns a Transform object to the geometry to alter the rendering.

    The classes that extend Geometry (see Table 27-3) look very much like their Shape-derived counterparts.
    For example, EllipseGeometry has similar members to Ellipse. The big distinction is that Geometry- derived classes do not know how to render themselves directly because they are not UIElements. Rather, Geometry-derived classes represent little more than a collection of plot-point data, which say in effect “If a Path uses my data, this is how I would render myself.”

    Table 27-3. Geometry-Derived Classes

    Geometry Class Meaning in Life
    LineGeometry Represents a straight line
    RectangleGeometry Represents a rectangle
    EllipseGeometry Represents an ellipse
    GeometryGroup Allows you to group several Geometry objects
    CombinedGeometry Allows you to merge two different Geometry objects into a single shape
    PathGeometry Represents a figure composed of lines and curves

    ■Note Path is not the only class in WPF that can use a collection of geometries. For example, DoubleAnimationUsingPath, DrawingGroup, GeometryDrawing, and even UIElement can all use geometries for rendering, using the PathGeometry, ClipGeometry, Geometry, and Clip properties, respectively.

    The following is a Path that makes use of a few Geometry-derived types. Notice that you are setting the Data property of Path to a GeometryGroup object that contains other Geometry-derived objects such as EllipseGeometry, RectangleGeometry, and LineGeometry. Figure 27-3 shows the output.











    Figure 27-3. A Path containing various Geometry objects

    The image in Figure 27-3 could have been rendered using the Line, Ellipse, and Rectangle classes shown earlier. However, this would have put various UIElement objects in memory. When you use geometries to model the plot points of what to draw and then place the geometry collection into a container that can render the data (Path, in this case), you reduce the memory overhead.
    Now recall that Path has the same inheritance chain as any other member of System.Windows.Shapes and therefore can send the same event notifications as other UIElement objects. Thus, if you were to define this same element in a Visual Studio project, you could determine whether the user clicked
    anywhere in the sweeping line simply by handling a mouse event (remember, Kaxaml does not allow you to handle events for the markup you have authored).

    The Path Modeling “Mini-Language”
    Of all the classes listed in Table 27-3, PathGeometry is the most complex to configure in terms of XAML or code. This has to do with the fact that each segment of the PathGeometry is composed of objects that
    contain various segments and figures (e.g., ArcSegment, BezierSegment, LineSegment, PolyBezierSegment, PolyLineSegment, PolyQuadraticBezierSegment, etc.). Here is an example of a Path object whose Data property has been set to a composed of various figures and segments:















    Now, to be perfectly honest, few programmers will ever need to manually build complex 2D images by directly describing Geometry- or PathSegment-derived classes. Later in this chapter, you will learn how to convert vector graphics into path statements that can be used in XAML.
    Even with the assistance of these tools, the amount of XAML required to define a complex Path object would be ghastly, as the data consists of full descriptions of various Geometry- or PathSegment-derived classes. To produce more concise and compact markup, the Path class has been designed to understand a specialized “mini-language.”
    For example, rather than setting the Data property of Path to a collection of Geometry- and
    PathSegment-derived types, you can set the Data property to a single string literal containing a number of

    known symbols and various values that define the shape to be rendered. Here is a simple example, and the resulting output is shown in Figure 27-4:

    Figure 27-4. The Path mini-language allows you to compactly describe a Geometry/PathSegment object model

    The M command (short for move) takes an X,Y position that represents the starting point of the drawing.
    The C command takes a series of plot points to render a curve (a cubic Bézier curve to be exact), while H
    draws a horizontal line.
    Now, to be perfectly honest, the chances that you will ever need to manually build or parse a string literal containing Path mini-language instructions are slim to none. However, at the least, you will no longer be surprised when you view XAML-generated dedicated tools.

    WPF Brushes and Pens
    Each of the WPF graphical rendering options (shape, drawing and geometries, and visuals) makes extensive use of brushes, which allow you to control how the interior of a 2D surface is filled. WPF provides six different brush types, all of which extend System.Windows.Media.Brush. While Brush is abstract, the descendants described in Table 27-4 can be used to fill a region with just about any conceivable option.

    Table 27-4. WPF Brush-Derived Types

    Brush Type Meaning in Life

    DrawingBrush Paints an area with a Drawing-derived object (GeometryDrawing, ImageDrawing, or VideoDrawing)
    ImageBrush Paints an area with an image (represented by an ImageSource object)
    LinearGradientBrush Paints an area with a linear gradient RadialGradientBrush Paints an area with a radial gradient SolidColorBrush Paints a single color, set with the Color property
    VisualBrush Paints an area with a Visual-derived object (DrawingVisual, Viewport3DVisual, and ContainerVisual)

    The DrawingBrush and VisualBrush classes allow you to build a brush based on an existing Drawing- or Visual-derived class. These brush classes are used when you are working with the other two graphical options of WPF (drawings or visuals) and will be examined later in this chapter.
    ImageBrush, as the name suggests, lets you build a brush that displays image data from an external file or embedded application resource, by setting the ImageSource property. The remaining brush types (LinearGradientBrush and RadialGradientBrush) are quite straightforward to use, though typing in the
    required XAML can be a tad verbose. Thankfully, Visual Studio supports integrated brush editors that make it simple to generate stylized brushes.

    Configuring Brushes Using Visual Studio
    Let’s update your WPF drawing program, RenderingWithShapes, to use some more interesting brushes. The three shapes you have employed so far to render data on your toolbar use simple, solid colors, so you can capture their values using simple string literals. To spice things up a tad, you will now use the integrated brush editor. Ensure that the XAML editor of your initial window is the open window within the IDE and select the Ellipse element. Now, in the Properties window, locate the Brush category and then click Fill property listed on the top (see Figure 27-5).

    Figure 27-5. Any property that requires a brush can be configured with the integrated brush editor

    At the top of the Brushes editor, you will see a set of properties that are all “brush compatible” for the selected item (i.e., Fill, Stroke, and OpacityMask). Below this, you will see a series of tabs that allow you to configure different types of brushes, including the current solid color brush. You can use the color
    selector tool, as well as the ARGB (alpha, red, green, and blue, where “alpha” controls transparency) editors to control the color of the current brush. Using these sliders and the related color selection area, you can create any sort of solid color. Use these tools to change the Fill color of your Ellipse and view the resulting XAML. You will notice the color is stored as a hexadecimal value, as follows:

    More interestingly, this same editor allows you to configure gradient brushes, which are used to define a series of colors and transition points. Recall that this Brushes editor provides you with a set of tabs, the first of which lets you set a null brush for no rendered output. The other four allow you to set up a solid color brush (what you just examined), gradient brush, tile brush, or image brush.
    Click the gradient brush button, and the editor will display a few new options (see Figure 27-6). The three buttons on the lower left allow you to pick a linear gradient, pick a radial gradient, or reverse the gradient stops. The bottommost strip will show you the current color of each gradient stop, each of which is marked by a “thumb” on the strip. As you drag these thumbs around the gradient strip, you can
    control the gradient offset. Furthermore, when you click a given thumb, you can change the color for that particular gradient stop via the color selector. Finally, if you click directly on the gradient strip, you can add gradient stops.

    Figure 27-6. The Visual Studio brush editor allows you to build basic gradient brushes

    Take a few minutes to play around with this editor to build a radial gradient brush containing three gradient stops, set to your colors of choice. Figure 27-6 shows the brush you just constructed, using three different shades of green.
    When you are done, the IDE will update your XAML with a custom brush, set to a brush-compatible property (the Fill property of the Ellipse in this example) using property-element syntax, as follows:








    Configuring Brushes in Code
    Now that you have built a custom brush for the XAML definition of your Ellipse, the corresponding C# code is out-of-date, in that it will still render a solid green circle. To sync things back up, update the correct case statement to use the same brush you just created. The following is the necessary update, which looks more complex than you might expect, just because you are converting the hexadecimal value to a proper Color object via the System.Windows.Media.ColorConverter class (see Figure 27-7 for the modified output):

    case SelectedShape.Circle:
    shapeToRender = new Ellipse() { Height = 35, Width = 35 };
    // Make a RadialGradientBrush in code! RadialGradientBrush brush = new RadialGradientBrush(); brush.GradientStops.Add(new GradientStop(
    (Color)ColorConverter.ConvertFromString("#FF77F177"), 0)); brush.GradientStops.Add(new GradientStop(
    (Color)ColorConverter.ConvertFromString("#FF11E611"), 1)); brush.GradientStops.Add(new GradientStop(
    (Color)ColorConverter.ConvertFromString("#FF5A8E5A"), 0.545)); shapeToRender.Fill = brush;
    break;

    Figure 27-7. Drawing circles with a bit more pizzazz

    By the way, you can build GradientStop objects by specifying a simple color as the first constructor parameter using the Colors enumeration, which returns a configured Color object.

    GradientStop g = new GradientStop(Colors.Aquamarine, 1);

    Or, if you require even finer control, you can pass in a configured Color object, like so:

    Color myColor = new Color() { R = 200, G = 100, B = 20, A = 40 }; GradientStop g = new GradientStop(myColor, 34);

    Of course, the Colors enum and Color class are not limited to gradient brushes. You can use them anytime you need to represent a color value in code.

    Configuring Pens
    In comparison with brushes, a pen is an object for drawing borders of geometries or, in the case of the Line or PolyLine class, the line geometry itself. Specifically, the Pen class allows you to draw a specified thickness, represented by a double value. In addition, a Pen can be configured with the same sort of properties seen in the Shape class, such as starting and stopping pen caps, dot-dash patterns, and so forth. For example, you can add the following markup to a shape to define the pen attributes:

    In many cases, you won’t need to directly create a Pen object because this will be done indirectly when you assign a value to properties, such as StrokeThickness to a Shape-derived type (as well as other UIElements). However, building a custom Pen object is handy when working with Drawing-derived types (described later in the chapter). Visual Studio does not have a pen editor per se, but it does allow you to configure all the stroke-centric properties of a selected item using the Properties window.

    Applying Graphical Transformations
    To wrap up the discussion of using shapes, let’s address the topic of transformations. WPF ships with numerous classes that extend the System.Windows.Media.Transform abstract base class. Table 27-5 documents many of the key out-of-the-box Transform-derived classes.

    Table 27-5. Key Descendants of the System.Windows.Media.Transform Type

    Type Meaning in Life
    MatrixTransform Creates an arbitrary matrix transformation that is used to manipulate objects or coordinate systems in a 2D plane
    RotateTransform Rotates an object clockwise about a specified point in a 2D (x, y) coordinate system
    ScaleTransform Scales an object in the 2D (x, y) coordinate system
    SkewTransform Skews an object in the 2D (x, y) coordinate system
    TranslateTransform Translates (moves) an object in the 2D (x, y) coordinate system
    TransformGroup Represents a composite Transform composed of other Transform objects

    Transformations can be applied to any UIElement (e.g., descendants of Shape as well as controls such as Button controls, TextBox controls, and the like). Using these transformation classes, you can render graphical data at a given angle, skew the image across a surface, and expand, shrink, or flip the target item in a variety of ways.

    ■Note While transformation objects can be used anywhere, you will find them most useful when working with WPF animations and custom control templates. As you will see later in the chapter, you can use WPF animations to incorporate visual cues to the end user for a custom control.

    Transformations (or a whole set of them) can be assigned to a target object (e.g., Button, Path, etc.) using two common properties, LayoutTransform and RenderTransform.
    The LayoutTransform property is helpful, in that the transformation occurs before elements are rendered into a layout manager, and therefore the transformation will not affect Z-ordering operations (in other words, the transformed image data will not overlap).
    The RenderTransform property, on the other hand, occurs after the items are in their container, and therefore it is quite possible that elements can be transformed in such a way that they could overlap each other, based on how they were arranged in the container.

    A First Look at Transformations
    You will add some transformational logic to your RenderingWithShapes project in just a moment. However, to see transformation objects in action, open Kaxaml and define a simple StackPanel in the root Page or Window and set the Orientation property to Horizontal. Now, add the following Rectangle, which will be drawn at a 45-degree angle using a RotateTransform object:






    Here is a

    And for good measure, here is an Ellipse that is scaled by 20 degrees with a ScaleTransform (note the values set to the initial Height and Width), as well as a TextBox that has a group of transformation objects applied to it:














    Note that when a transformation is applied, you are not required to perform any manual calculations to correctly respond to hit-testing, input focus, or whatnot. The WPF graphics engine handles such tasks on your behalf. For example, in Figure 27-8, you can see that the TextBox is still responsive to keyboard input.

    Figure 27-8. The results of graphical transformation objects

    Transforming Your Canvas Data
    Now, let’s incorporate some transformational logic into your RenderingWithShapes example. In addition to applying a transformation object to a single item (e.g., Rectangle, TextBox, etc.), you can also apply transformation objects to a layout manager to transform all of the internal data. You could, for example, render the entire DockPanel of the main window at an angle.





    ...

    This is a bit extreme for this example, so let’s add a final (less aggressive) feature that allows the user to flip the entire Canvas and all contained graphics. Begin by adding a final ToggleButton to your ToolBar, defined as follows:

    Within the Click event handler, create a RotateTransform object and connect it to the Canvas object via the LayoutTransform property if this new ToggleButton is clicked. If the ToggleButton is not clicked, remove the transformation by setting the same property to null.

    private void FlipCanvas_Click(object sender, RoutedEventArgs e)
    {
    if (flipCanvas.IsChecked == true)
    {
    RotateTransform rotate = new RotateTransform(-180); canvasDrawingArea.LayoutTransform = rotate;
    }
    else
    {
    canvasDrawingArea.LayoutTransform = null;
    }
    }

    Run your application and add a bunch of graphics throughout the canvas area, making sure to go edge to edge with them. If you click your new button, you will find that the shape data flows outside of the boundaries of the canvas! This is because you have not defined a clipping region (see Figure 27-9).

    Figure 27-9. Oops! Your data is flowing outside of the canvas after the transformation!

    Fixing this is trivial. Rather than manually authoring complex clipping-logic code, simply set the ClipToBounds property of the Canvas to true, which prevents child elements from being rendered outside the parent’s boundaries. If you run your program again, you’ll find the data will not bleed off the canvas boundary.

    The last tiny modification to make has to do with the fact that when you flip the canvas by pressing your toggle button and then click the canvas to draw a new shape, the point at which you click is not the point where the graphical data is applied. Rather, the data is rendered above the mouse cursor.

    To resolve this issue, apply the same transformation object to the shape being drawn before the rendering occurs (via RenderTransform). Here is the crux of the code:

    private void CanvasDrawingArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
    //omitted for brevity
    if (flipCanvas.IsChecked == true)
    {
    RotateTransform rotate = new RotateTransform(-180); shapeToRender.RenderTransform = rotate;
    }
    // Set top/left to draw in the canvas. Canvas.SetLeft(shapeToRender,
    e.GetPosition(canvasDrawingArea).X); Canvas.SetTop(shapeToRender,
    e.GetPosition(canvasDrawingArea).Y);

    // Draw shape! canvasDrawingArea.Children.Add(shapeToRender);
    }

    This wraps up your examination of System.Windows.Shapes, brushes, and transformations. Before looking at the role of rendering graphics using drawings and geometries, let’s see how Visual Studio can be used to simplify how you work with primitive graphics.

    Working with the Visual Studio Transform Editor
    In the previous example, you applied various transformations by manually entering markup and authoring some C# code. While this is certainly useful, you will be happy to know that the latest version of Visual Studio ships with an integrated transformation editor. Recall that any UI element can be the recipient of transformational services, including a layout system containing various UI elements. To illustrate the use of Visual Studio’s transform editor, create a new WPF application named FunWithTransforms.

    Building the Initial Layout
    First, split your initial Grid into two columns using the integrated grid editor (the exact size does not matter). Now, locate the StackPanel control within your Toolbox and add it to take up the entire space of the first column of the Grid; then add three Button controls to the StackPanel, like so:








    No matter which brush-compatible property you set with your custom , the bottom line is you are rendering a 2D vector image with much less overhead than the same 2D image rendered with shapes.

    Containing Drawing Types in a DrawingImage
    The DrawingImage type allows you to plug your drawing geometry into a WPF control. Consider the following:









    In this case, your has been placed into a , rather than a
    . Using this , you can set the Source property of the Image control.

    Working with Vector Images
    As you might agree, it would be quite challenging for a graphic artist to create a complex vector-based image using the tools and techniques provided by Visual Studio. Graphic artists have their own set of tools that
    can produce amazing vector graphics. Neither Visual Studio nor its companion Expression Blend for Visual

    Studio has that type of design power. Before you can import vector images into WPF application, they must be converted into Path expressions. At that point, you can program against the generated object model using Visual Studio.

    ■Note You can find the image being used (LaserSign.svg) as well as the exported path (LaserSign. xaml) data in the Chapter 27 folder of the download files. the image is originally from Wikipedia, located at https://en.wikipedia.org/wiki/Hazard_symbol.

    Converting a Sample Vector Graphic File into XAML
    Before you can import complex graphical data (such as vector graphics) into a WPF application, you need to convert the graphics into path data. As an example of how to do this, start with a sample .svg image file, such as the laser sign referenced in the preceding note. Then download and install an open source tool called Inkscape (located at www.inkscape.org). Using Inkscape, open the LaserSign.svg file from the chapter download. You might be prompted to upgrade the format. Fill in the selections as shown in Figure 27-12.

    Figure 27-12. Upgrading the SVG file to the latest format in Inkscape

    The next steps will seem a bit odd at first, but once you get over the oddity, it is a simple way to convert vector images to the correct XAML. When you have the image the way you want it, select the File ➤ Print menu option. Next, select the Microsoft XPS Document Writer as the printer target and then click Print. On the next screen, enter a filename and select where the file should be saved; then click Save. Now you have a complete *.xps (or *.oxps) file.

    ■Note Depending on a number of variables with your system configuration, the generated file will have either the .xps or .oxps extension. Either way, the process works the same.

    The *.xps and *.oxps formats are actually .zip files. Rename the extension of the file to .zip, and you can open the file in File Explorer (or 7-Zip, or your favorite archive tool). You will see that it contains the hierarchy shown in Figure 27-13.

    Figure 27-13. The folder hierarchy of the printed XPS file

    The file that you need is in the Pages directory (Documents/1/Pages) and is named 1.fpage. Open the file with a text editor and copy everything except the open and closing tags. The path data can then be copied into the Kaxaml and placed inside a Canvas in the main Window. Your image will show in the XAML window.

    ■Note the latest version of Inkscape has an option to save the file as Microsoft XAML. Unfortunately, at the time of this writing, it is not compatible with WPF.

    Importing the Graphical Data into a WPF Project
    At this point, create a new WPF application named InteractiveLaserSign. Resize the Window to a Height of 600 and Width of 650, and replace the Grid with a Canvas.




    Copy the entire XAML from the 1.fpage file (excluding the outer FixedPage tag) and paste it into the
    Canvas control. View the Window in design mode, and you will see the sign reproduced in your application. If you view the Document Outline, you will see that each part of the image is represented as a XAML
    Path element. If you resize your Window, the image quality stays the same, regardless of how big you make the window. This is because images represented by Path elements are rendered using the drawing engine and math instead of flipping pixels.

    Interacting with the Sign
    Recall that routed event tunnel and bubble, so any Path clicked inside the Canvas can be handled by a click event handler on the canvas. Update the Canvas markup to the following:

    Add the event handler with the following code:

    private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
    if (e.OriginalSource is Path p)
    {
    p.Fill = new SolidColorBrush(Colors.Red);
    }
    }

    Now, run your application. Click the lines to see the effects.
    You now understand the process of generating Path data for complex graphics and how to interact with the graphical data in code. As you might agree, the ability for professional graphic artists to generate complex graphical data and export the data as XAML is extremely powerful. Once the graphical data has been generated, developers can import the markup and program against the object model.

    Rendering Graphical Data Using the Visual Layer
    The final option for rendering graphical data with WPF is termed the visual layer. As mentioned, you can gain access to this layer only through code (it is not XAML-friendly). While a vast majority of your WPF applications will work just fine using shapes, drawings, and geometries, the visual layer does provide the fastest possible way to render huge amounts of graphical data. This low-level graphical layer can also
    be useful when you need to render a single image over a large area. For example, if you need to fill the background of a window with a plain, static image, the visual layer is the fastest way to do so. It can also be useful if you need to change between window backgrounds quickly, based on user input or whatnot.
    I won’t spend too much time delving into the details of this aspect of WPF programming, but let’s build a small sample program to illustrate the basics.

    The Visual Base Class and Derived Child Classes
    The abstract System.Windows.Media.Visual class type supplies a minimal set of services (rendering, hit-testing, transformations) to render graphics, but it does not provide support for additional nonvisual
    services, which can lead to code bloat (input events, layout services, styles, and data binding). The Visual class is an abstract base class. You need to use one of the derived types to perform actual rendering operations. WPF provides a handful of subclasses, including DrawingVisual, Viewport3DVisual, and ContainerVisual.
    In this example, you will focus only on DrawingVisual, a lightweight drawing class that is used to render shapes, images, or text.

    A First Look at Using the DrawingVisual Class
    To render data onto a surface using DrawingVisual, you need to take the following basic steps:
    1.Obtain a DrawingContext object from the DrawingVisual class.
    2.Use the DrawingContext to render the graphical data.
    These two steps represent the bare minimum necessary for rendering some data to a surface. However, if you want the graphical data you’ve rendered to be responsive to hit-testing calculations (which would be important for adding user interactivity), you will also need to perform these additional steps:
    1.Update the logical and visual trees maintained by the container upon which you are rendering.
    2.Override two virtual methods from the FrameworkElement class, allowing the container to obtain the visual data you have created.
    You will examine these final two steps in a bit. First, to illustrate how you can use the DrawingVisual class to render 2D data, create a new WPF application named RenderingWithVisuals. Your first goal is to use a DrawingVisual to dynamically assign data to a WPF Image control. Begin by updating the XAML of your window to handle the Loaded event, like so:


    Title="Fun With Visual Layer" Height="450" Width="800" Loaded="MainWindow_Loaded">

    Next, replace the Grid with a StackPanel and add an Image in the StackPanel, like this:



    Your control does not yet have a Source value because that will happen at runtime. The Loaded event will do the work of building the in-memory graphical data, using a DrawingBrush object. Make sure the following namespaces are at the top of MainWindow.cs:

    using System;
    using System.Windows;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;

    Here is the implementation of the Loaded event handler:

    private void MainWindow_Loaded( object sender, RoutedEventArgs e)
    {
    const int TextFontSize = 30;
    // Make a System.Windows.Media.FormattedText object.
    FormattedText text = new FormattedText( "Hello Visual Layer!",
    new System.Globalization.CultureInfo("en-us"), FlowDirection.LeftToRight,

    new Typeface(this.FontFamily, FontStyles.Italic, FontWeights.DemiBold, FontStretches.UltraExpanded),
    TextFontSize, Brushes.Green, null,
    VisualTreeHelper.GetDpi(this).PixelsPerDip);
    // Create a DrawingVisual, and obtain the DrawingContext. DrawingVisual drawingVisual = new DrawingVisual(); using(DrawingContext drawingContext =
    drawingVisual.RenderOpen())
    {
    // Now, call any of the methods of DrawingContext to render data.
    drawingContext.DrawRoundedRectangle( Brushes.Yellow, new Pen(Brushes.Black, 5), new Rect(5, 5, 450, 100), 20, 20);
    drawingContext.DrawText(text, new Point(20, 20));
    }
    // Dynamically make a bitmap, using the data in the DrawingVisual.
    RenderTargetBitmap bmp = new RenderTargetBitmap( 500, 100, 100, 90, PixelFormats.Pbgra32);
    bmp.Render(drawingVisual);
    // Set the source of the Image control!
    myImage.Source = bmp;
    }

    This code introduces a number of new WPF classes, which I will briefly comment on here. The method begins by creating a new FormattedText object that represents the textual portion of the in-memory image you are constructing. As you can see, the constructor allows you to specify numerous attributes such as font size, font family, foreground color, and the text itself.
    Next, you obtain the necessary DrawingContext object via a call to RenderOpen() on the DrawingVisual instance. Here, you are rendering a colored, rounded rectangle into the DrawingVisual, followed by your formatted text. In both cases, you are placing the graphical data into the DrawingVisual using hard-coded values, which is not necessarily a great idea for production but is fine for this simple test.
    The last few statements map the DrawingVisual into a RenderTargetBitmap object, which is a member of the System.Windows.Media.Imaging namespace. This class will take a visual object and transform it into an in-memory bitmap image. After this point, you set the Source property of the Image control, and sure enough, you will see the output in Figure 27-14.

    Figure 27-14. Using the visual layer to render an in-memory bitmap

    ■Note the System.Windows.Media.Imaging namespace contains a number of additional encoding classes that let you save the in-memory RenderTargetBitmap object to a physical file in a variety of formats. Check out the JpegBitmapEncoder class (and friends) for more information.

    Rendering Visual Data to a Custom Layout Manager
    While it is interesting to use DrawingVisual to paint onto the background of a WPF control, it is perhaps more common to build a custom layout manager (Grid, StackPanel, Canvas, etc.) that uses the visual layer internally to render its content. After you have created such a custom layout manager, you can plug it into a normal Window (or Page or UserControl) and have a part of the UI using a highly optimized rendering agent while the noncritical aspects of the hosting Window are using shapes and drawings for the remainder of the graphical data.
    If you don’t require the extra functionality provided by a dedicated layout manager, you could opt to simply extend FrameworkElement, which does have the necessary infrastructure to also contain visual items. To illustrate how this could be done, insert a new class to your project named CustomVisualFrameworkElement. Extend this class from FrameworkElement and import the System,
    System.Windows, System.Windows.Input, System.Windows.Media, and System.Windows.Media.Imaging
    namespaces.
    This class will maintain a member variable of type VisualCollection, which contains two fixed DrawingVisual objects (of course, you could add new members to this collection via a mouse operation, but this example will keep it simple). Update your class with the following new functionality:

    public class CustomVisualFrameworkElement : FrameworkElement
    {
    // A collection of all the visuals we are building.
    VisualCollection theVisuals;
    public CustomVisualFrameworkElement()
    {
    // Fill the VisualCollection with a few DrawingVisual objects.
    // The ctor arg represents the owner of the visuals.
    theVisuals = new VisualCollection(this)
    {AddRect(),AddCircle()};
    }
    private Visual AddCircle()
    {
    DrawingVisual drawingVisual = new DrawingVisual();
    // Retrieve the DrawingContext in order to create new drawing content.
    using DrawingContext drawingContext = drawingVisual.RenderOpen()
    // Create a circle and draw it in the DrawingContext.
    drawingContext.DrawEllipse(Brushes.DarkBlue, null, new Point(70, 90), 40, 50);
    return drawingVisual;
    }
    private Visual AddRect()
    {
    DrawingVisual drawingVisual = new DrawingVisual();

    using DrawingContext drawingContext = drawingVisual.RenderOpen()
    Rect rect =
    new Rect(new Point(160, 100), new Size(320, 80)); drawingContext.DrawRectangle(Brushes.Tomato, null, rect); return drawingVisual;
    }
    }

    Now, before you can use this custom FrameworkElement in your Window, you must override two key virtual methods mentioned previously, both of which are called internally by WPF during the rendering process. The GetVisualChild() method returns a child at the specified index from the collection of child elements. The read-only VisualChildrenCount property returns the number of visual child elements within this visual collection. Both methods are easy to implement because you can delegate the real work to the VisualCollection member variable.

    protected override int VisualChildrenCount
    => theVisuals.Count;

    protected override Visual GetVisualChild(int index)
    {
    // Value must be greater than zero, so do a sanity check. if (index < 0 || index >= theVisuals.Count)
    {
    throw new ArgumentOutOfRangeException();
    }
    return theVisuals[index];
    }

    You now have just enough functionality to test your custom class. Update the XAML description of the Window to add one of your CustomVisualFrameworkElement objects to the existing StackPanel. Doing so will require you to add a custom XML namespace that maps to your .NET namespace.






    When you run the program, you will see the result shown in Figure 27-15.

    Figure 27-15. Using the visual layer to render data to a custom FrameworkElement

    Responding to Hit-Test Operations
    Because DrawingVisual does not have any of the infrastructures of UIElement or FrameworkElement, you will need to programmatically add the ability to calculate hit-test operations. Thankfully, this is fairly easy to do in the visual layer because of the concept of logical and visual trees. As it turns out, when you author a blob of XAML, you are essentially building a logical tree of elements. However, behind every logical tree is a much richer description known as the visual tree, which contains lower-level rendering instructions.
    Chapter 28 will delve into these trees in more detail, but for now, just understand that until you register your custom visuals with these data structures, you will not be able to perform hit-testing operations.
    Luckily, the VisualCollection container does this on your behalf (which explains why you needed to pass in a reference to the custom FrameworkElement as a constructor argument).
    First, update the CustomVisualFrameworkElement class to handle the MouseDown event in the class constructor using standard C# syntax, like so:
    this.MouseDown += CustomVisualFrameworkElement_MouseDown;
    The implementation of this handler will call the VisualTreeHelper.HitTest() method to see whether the mouse is within the boundaries of one of the rendered visuals. To do this, you specify as a parameter to HitTest() a HitTestResultCallback delegate that will perform the calculations. If you click a visual, you will toggle between a skewed rendering of the visual and the original rendering. Add the following methods to your CustomVisualFrameworkElement class:
    void CustomVisualFrameworkElement_MouseDown(object sender, MouseButtonEventArgs e)
    {
    // Figure out where the user clicked.
    Point pt = e.GetPosition((UIElement)sender);
    // Call helper function via delegate to see if we clicked on a visual.
    VisualTreeHelper.HitTest(this, null,
    new HitTestResultCallback(myCallback), new PointHitTestParameters(pt));
    }

    public HitTestResultBehavior myCallback(HitTestResult result)
    {
    // Toggle between a skewed rendering and normal rendering,
    // if a visual was clicked.
    if (result.VisualHit.GetType() == typeof(DrawingVisual))
    {
    if (((DrawingVisual)result.VisualHit).Transform == null)
    {
    ((DrawingVisual)result.VisualHit).Transform = new SkewTransform(7, 7);
    }
    else
    {
    ((DrawingVisual)result.VisualHit).Transform = null;
    }
    }
    // Tell HitTest() to stop drilling into the visual tree.
    return HitTestResultBehavior.Stop;
    }

    Now, run your program once again. You should now be able to click either rendered visual and see the transformation in action! While this is just a simple example of working with the visual layer of WPF,
    remember that you make use of the same brushes, transformations, pens, and layout managers as you would when working with XAML. As a result, you already know quite a bit about working with this Visual-derived classes.
    That wraps up your investigation of the graphical rendering services of Windows Presentation Foundation. While you learned a number of interesting topics, the reality is that you have only scratched the surface of WPF’s graphical capabilities. I will leave it in your hands to dig deeper into the topics of shapes, drawings, brushes, transformations, and visuals (and, to be sure, you will see some additional details of these topics in the remaining WPF chapters).

    Summary
    Because Windows Presentation Foundation is such a graphically intensive GUI API, it comes as no surprise that we are given a number of ways to render graphical output. This chapter began by examining each of three ways a WPF application can do so (shapes, drawings, and visuals) and discussed various rendering primitives such as brushes, pens, and transformations.
    Remember that when you need to build interactive 2D renderings, shapes make the process very simple. However, static, noninteractive renderings can be rendered in a more optimal manner by using drawings and geometries, while the visual layer (accessible only in code) gives you maximum control and performance.

    Pro C#10 CHAPTER 26 WPF Controls, Layouts, Events, and Data Binding

    CHAPTER 26

    WPF Controls, Layouts, Events, and Data Binding

    Chapter 25 provided a foundation for the WPF programming model, including an examination of the Window and Application classes, the grammar of XAML, and the use of code files. Chapter 25 also introduced you to the process of building WPF applications using the designers of Visual Studio. In this chapter, you will dig into the construction of more sophisticated graphical user interfaces using several new controls and layout managers, learning about additional features of the WPF Visual Designer for XAML of Visual Studio along the way.
    This chapter will also examine some important related WPF control topics such as the data-binding programming model and the use of control commands. You will also learn how to use the Ink and Documents APIs, which allow you to capture stylus (or mouse) input and build rich text documents using the XML Paper Specification, respectively.

    A Survey of the Core WPF Controls
    Unless you are new to the concept of building graphical user interfaces (which is fine), the general purpose of the major WPF controls should not raise too many issues. Regardless of which GUI toolkit you might have used in the past (e.g., VB6, MFC, Java AWT/Swing, Windows Forms, macOS, or GTK+/GTK# [among others]), the core WPF controls listed in Table 26-1 are likely to look familiar.

    Table 26-1. The Core WPF Controls

    WPF Control
    Category Example Members Meaning in Life
    Core user input controls Button, RadioButton, ComboBox, CheckBox, Calendar, DatePicker, Expander, DataGrid, ListBox, ListView, ToggleButton, TreeView, ContextMenu, ScrollBar, Slider, TabControl, TextBlock, TextBox, RepeatButton, RichTextBox, Label WPF provides an entire family of controls you can use to build the crux of a user interface.
    Window and control adornments Menu, ToolBar, StatusBar, ToolTip, ProgressBar You use these UI elements to decorate the frame of a Window object with input devices (such as the Menu) and user informational elements (e.g., StatusBar and ToolTip).
    (continued)

    © 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_26

    1137

    Table 26-1. (continued)

    WPF Control
    Category Example Members Meaning in Life
    Media controls Image, MediaElement, SoundPlayerAction These controls provide support for audio/video playback and image display.
    Layout controls Border, Canvas, DockPanel, Grid, GridView, GridSplitter, GroupBox, Panel, TabControl, StackPanel, Viewbox, WrapPanel WPF provides numerous controls that allow you to group and organize other controls for the purpose of layout management.

    ■Note The intent of this chapter is not to walk through each and every member of each and every WPF control. Rather, you will receive an overview of the various controls with an emphasis on the underlying programming model and key services common to most WPF controls.

    The WPF Ink Controls
    In addition to the common WPF controls listed in Table 26-1, WPF defines additional controls for working with the digital Ink API. This aspect of WPF development is useful during tablet PC development because it lets you capture input from the stylus. However, this is not to say a standard desktop application cannot leverage the Ink API because the same controls can capture input using the mouse.
    The System.Windows.Ink namespace of PresentationCore.dll contains various Ink API support types (e.g., Stroke and StrokeCollection); however, a majority of the Ink API controls (e.g., InkCanvas and InkPresenter) are packaged up with the common WPF controls under the System.Windows.Controls namespace in the PresentationFramework.dll assembly. You’ll work with the Ink API later in this chapter.

    The WPF Document Controls
    WPF also provides controls for advanced document processing, allowing you to build applications that incorporate Adobe PDF–style functionality. Using the types within the System.Windows.Documents namespace (also in the PresentationFramework.dll assembly), you can create print-ready documents that support zooming, searching, user annotations (sticky notes), and other rich text services.
    Under the covers, however, the document controls do not use Adobe PDF APIs; rather, they use the XML Paper Specification (XPS) API. To the end user, there will really appear to be no difference because PDF documents and XPS documents have an almost identical look and feel. In fact, you can find many free utilities that allow you to convert between the two file formats on the fly. Because of space limitation, these controls won’t be covered in this edition.

    WPF Common Dialog Boxes
    WPF also provides you with a few common dialog boxes such as OpenFileDialog and SaveFileDialog. These dialog boxes are defined within the Microsoft.Win32 namespace of the PresentationFramework. dll assembly. Working with either of these dialog boxes is a matter of creating an object and invoking the ShowDialog() method, like so:

    using Microsoft.Win32;
    //omitted for brevity
    private void btnShowDlg_Click(object sender, RoutedEventArgs e)
    {
    // Show a file save dialog.
    SaveFileDialog saveDlg = new SaveFileDialog(); saveDlg.ShowDialog();
    }

    As you would hope, these classes support various members that allow you to establish file filters and directory paths and gain access to user-selected files. You will put these file dialogs to use in later examples; you will also learn how to build custom dialog boxes to gather user input.

    A Brief Review of the Visual Studio WPF Designer
    A majority of these standard WPF controls have been packaged up in the System.Windows.Controls namespace of the PresentationFramework.dll assembly. When you build a WPF application using Visual Studio, you will find most of these common controls contained in the toolbox, provided you have a WPF designer open as the active window.
    Similar to other UI frameworks created with Visual Studio, you can drag these controls onto the WPF window designer and configure them using the Properties window (which you learned about in Chapter 25). While Visual Studio will generate a good amount of the XAML on your behalf, it is not uncommon to edit the markup yourself manually. Let’s review the basics.

    Working with WPF Controls Using Visual Studio
    You might recall from Chapter 25 that when you place a WPF control onto the Visual Studio designer, you want to set the x:Name property through the Properties window (or through XAML directly) because this allows you to access the object in your related C# code file. You might also recall that you can use the Events tab of the Properties window to generate event handlers for a selected control. Thus, you could use Visual Studio to generate the following markup for a simple Button control:

    You might also recall that the immediate child element of a ContentControl-derived class is the implied content; therefore, you do not need to define a Button.Content scope explicitly when specifying complex content. You could simply author the following:

    In either case, you set the button’s Content property to a StackPanel of related items. You can also author this sort of complex content using the Visual Studio designer. After you define the layout manager for a content control, you can select it on the designer to serve as a drop target for the internal controls. At this point, you can edit each using the Properties window. If you were to use the Properties window to handle the Click event for the Button control (as shown in the previous XAML declarations), the IDE would generate an empty event handler, to which you could add your own custom code, like so:

    private void btnMyButton_Click(object sender, RoutedEventArgs e)
    {
    MessageBox.Show("You clicked the button!");
    }

    Working with the Document Outline Editor
    You should recall from the previous chapter that the Document Outline window of Visual Studio (which you can open using the View ➤ Other Windows menu) is useful when designing a WPF control that has complex content. The logical tree of XAML is displayed for the Window you are building, and if you click any of these nodes, it is automatically selected in the visual designer and the XAML editor for editing.
    With the current edition of Visual Studio, the Document Outline window has a few additional features that you might find useful. To the right of any node you will find an icon that looks similar to an eyeball.
    When you toggle this button, you can opt to hide or show an item on the designer, which can be helpful when you want to focus in on a particular segment to edit (note that this will not hide the item at runtime; this only hides items on the designer surface).
    Right next to the “eyeball icon” is a second toggle that allows you to lock an item on the designer. As you might guess, this can be helpful when you want to make sure you (or your co-workers) do not accidentally change the XAML for a given item. In effect, locking an item makes it read-only at design time (however, you can change the object’s state at runtime).

    Controlling Content Layout Using Panels
    A WPF application invariably contains a good number of UI elements (e.g., user input controls, graphical content, menu systems, and status bars) that need to be well organized within various windows. After you place the UI elements, you need to make sure they behave as intended when the end user resizes the window or possibly a portion of the window (as in the case of a splitter window). To ensure your WPF
    controls retain their position within the hosting window, you can take advantage of a good number of panel types (also known as layout managers).
    By default, a new WPF Window created with Visual Studio will use a layout manager of type Grid (more details in just a bit). However, for now, assume a Window with no declared layout manager, like so:

    <Window x:Class="MyWPFApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Fun with Panels!" Height="285" Width="325">

    When you declare a control directly inside a window that doesn’t use panels, the control is positioned dead center in the container. Consider the following simple window declaration, which contains a single Button control. Regardless of how you resize the window, the UI widget is always equidistant from all four sides of the client area. The Button’s size is determined by the assigned Height and Width properties of the Button.

    <!- This button is in the center of the window at all times ->
    <Window x:Class="MyWPFApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Fun with Panels!" Height="285" Width="325">

    Notice in the Button’s opening definition that you have handled the Click event by specifying the name of a method to be called when the event is raised. The Click event works with the RoutedEventHandler delegate, which expects an event handler that takes an object as the first parameter and a System.Windows. RoutedEventArgs as the second. Implement this handler as so:

    public void btnClickMe_Clicked(object sender, RoutedEventArgs e)
    {
    // Do something when button is clicked.
    MessageBox.Show("Clicked the button");
    }

    If you run your application, you will see this message box display, regardless of which part of the button’s content you click (the green Ellipse, the yellow Ellipse, the Label, or the Button’s surface). This is a good thing. Imagine how tedious WPF event handling would be if you were forced to handle a Click event for every one of these subelements. Not only would the creation of separate event handlers for each aspect of the Button be labor intensive, you would end up with some mighty nasty code to maintain down the road.
    Thankfully, WPF routed events take care of ensuring that your single Click event handler will be called regardless of which part of the button is clicked automatically. Simply put, the routed events model automatically propagates an event up (or down) a tree of objects, looking for an appropriate handler.
    Specifically speaking, a routed event can make use of three routing strategies. If an event is moving from the point of origin up to other defining scopes within the object tree, the event is said to be a bubbling event. Conversely, if an event is moving from the outermost element (e.g., a Window) down to the point of origin, the event is said to be a tunneling event. Finally, if an event is raised and handled only by the originating element (which is what could be described as a normal CLR event), it is said to be a direct event.

    The Role of Routed Bubbling Events
    In the current example, if the user clicks the inner yellow oval, the Click event bubbles out to the next level of scope (the Canvas), then to the StackPanel, and finally to the Button where the Click event handler is handled. In a similar way, if the user clicks the Label, the event is bubbled to the StackPanel and then finally to the Button element.
    Given this bubbling routed event pattern, you have no need to worry about registering specific Click event handlers for all members of a composite control. However, if you want to perform custom clicking logic for multiple elements within the same object tree, you can do so.

    By way of illustration, assume you need to handle the clicking of the outerEllipse control in a unique manner. First, handle the MouseDown event for this subelement (graphically rendered types such as the Ellipse do not support a Click event; however, they can monitor mouse button activity via MouseDown, MouseUp, etc.).

    Then implement an appropriate event handler, which for illustrative purposes will simply change the
    Title property of the main window, like so:

    public void outerEllipse_MouseDown(object sender, MouseButtonEventArgs e)
    {
    // Change title of window.
    this.Title = "You clicked the outer ellipse!";
    }

    With this, you can now take different courses of action depending on where the end user has clicked (which boils down to the outer ellipse and everywhere else within the button’s scope).

    ■Note Routed bubbling events always move from the point of origin to the next defining scope. Thus, in this example, if you click the innerEllipse object, the event will be bubbled to the Canvas, not to the outerEllipse because they are both Ellipse types within the scope of Canvas.

    Continuing or Halting Bubbling
    Currently, if the user clicks the outerEllipse object, it will trigger the registered MouseDown event handler for this Ellipse object, at which point the event bubbles to the button’s Click event. If you want to inform WPF to stop bubbling up the tree of objects, you can set the Handled property of the EventArgs parameter to true, as follows:

    public void outerEllipse_MouseDown(object sender, MouseButtonEventArgs e)
    {
    // Change title of window.
    this.Title = "You clicked the outer ellipse!";
    // Stop bubbling!
    e.Handled = true;
    }

    In this case, you would find that the title of the window is changed, but you will not see the MessageBox displayed by the Click event handler of the Button. In a nutshell, routed bubbling events make it possible to allow a complex group of content to act either as a single logical element (e.g., a Button) or as discrete items (e.g., an Ellipse within the Button).

    The Role of Routed Tunneling Events
    Strictly speaking, routed events can be bubbling (as just described) or tunneling in nature. Tunneling events (which all begin with the Preview suffix; e.g., PreviewMouseDown) drill down from the topmost element into the inner scopes of the object tree. By and large, each bubbling event in the WPF base class libraries is paired with a related tunneling event that fires before the bubbling counterpart. For example, before the bubbling MouseDown event fires, the tunneling PreviewMouseDown event fires first.
    Handling a tunneling event looks just like the processing of handling any other events; simply assign the event handler name in XAML (or, if needed, use the corresponding C# event-handling syntax in your code file) and implement the handler in the code file. Just to illustrate the interplay of tunneling and bubbling events, begin by handling the PreviewMouseDown event for the outerEllipse object, like so:

    Next, retrofit the current C# class definition by updating each event handler (for all objects) to append data about the current event into a string member variable named mouseActivity, using the incoming event args object. This will allow you to observe the flow of events firing in the background.

    public partial class MainWindow : Window
    {
    string _mouseActivity = string.Empty; public MainWindow()
    {
    InitializeComponent();
    }
    public void btnClickMe_Clicked(object sender, RoutedEventArgs e)
    {
    AddEventInfo(sender, e); MessageBox.Show(_mouseActivity, "Your Event Info");
    // Clear string for next round.
    _mouseActivity = "";
    }
    private void AddEventInfo(object sender, RoutedEventArgs e)
    {
    _mouseActivity += string.Format(
    "{0} sent a {1} event named {2}.\n", sender, e.RoutedEvent.RoutingStrategy, e.RoutedEvent.Name);
    }
    private void outerEllipse_MouseDown(object sender, MouseButtonEventArgs e)
    {
    AddEventInfo(sender, e);
    }

    private void outerEllipse_PreviewMouseDown(object sender, MouseButtonEventArgs e)
    {
    AddEventInfo(sender, e);
    }
    }

    Notice that you are not halting the bubbling of an event for any event handler. If you run this application, you will see a unique message box display based on where you click the button. Figure 26-15 shows the result of clicking the outer Ellipse object.

    Figure 26-15. Tunneling first, bubbling second

    So, why do WPF events typically tend to come in pairs (one tunneling and one bubbling)? The answer is that by previewing events, you have the power to perform any special logic (data validation, disable bubbling action, etc.) before the bubbling counterpart fires. By way of an example, assume you have a TextBox that should contain only numerical data. You could handle the PreviewKeyDown event, and if you see the user has entered non-numerical data, you could cancel the bubbling event by setting the Handled property to true.
    As you would guess, when you are building a custom control that contains custom events, you could author the event in such a way that it can bubble (or tunnel) through a tree of XAML. For the purpose of this chapter, I will not be examining how to build custom routed events (however, the process is not that different from building a custom dependency property). If you are interested, check out the topic “Routed Events Overview” within the .NET Framework 4.7 SDK documentation. In it you will find a number of tutorials that will help you on your way.

    A Deeper Look at WPF APIs and Controls
    The remainder of this chapter will give you a chance to build a new WPF application using Visual Studio. The goal is to create a UI that consists of a TabControl widget containing a set of tabs. Each tab will illustrate some new WPF controls and interesting APIs you might want to make use of in your software projects. Along the way, you will also learn additional features of the Visual Studio WPF designers. To get started, create a new WPF application named WpfControlsAndAPIs.

    Working with the TabControl
    As mentioned, your initial window will contain a TabControl with three different tabs, each of which shows off a set of related controls and/or WPF APIs. Update the window’s Width to 800 and Height to 350. Locate the TabControl control in the Visual Studio Toolbox, drop one onto your designer, and update the markup to the following:








    You will notice that you are given two tab items automatically. To add additional tabs, you simply need to right-click the TabControl node in the Document Outline window and select the Add TabItem menu option (you can also right-click the TabControl on the designer to activate the same menu option) or just start typing in the XAML editor. Add one additional tab using either approach.
    Now, update each TabItem control through the XAML editor and change the Header property for each tab, naming them Ink API, Data Binding, and DataGrid. At this point, your window designer should look like what you see in Figure 26-16.

    Figure 26-16. The initial layout of the tab system

    Be aware that when you select a tab for editing, that tab becomes the active tab, and you can design that tab by dragging controls from the Toolbox window. Now that you have the core TabControl defined, you can work out the details tab by tab and learn more features of the WPF API along the way.

    Building the Ink API Tab
    The first tab will be used to show the overall role of WPF’s digital Ink API, which allows you to incorporate painting functionality into a program easily. Of course, the application does not literally need to
    be a painting application; you can use this API for a wide variety of purposes, including capturing handwriting input.

    ■Note For most of the rest of this chapter (and the next WPF chapters as well), i will be editing the XaMl directly instead of using the various designer windows. While the dragging and dropping of controls works, more often than not the layout isn’t what you want (visual studio adds margins and padding based on where you drop the control), and you spend a significant amount of time cleaning up the XaMl anyway.

    Begin by changing the Grid tag under the Ink API TabItem to a StackPanel and add a closing tag (make sure to remove "/" from the opening tag). Your markup should look like this:



    Designing the Toolbar
    Add a new ToolBar control into the StackPanel (using the XAML editor) named InkToolbar with a height of 60.

    Add three RadioButton controls inside a WrapPanel, inside a Border control, to the ToolBar as follows:







    When a RadioButton control is not placed inside of a parent panel control, it will take on a UI identical to a Button control! That’s why I wrapped the RadioButton controls in the WrapPanel.
    Next, add a Separator and then a ComboBox with a Width of 175 and a Margin of 10,0,0,0. Add three ComboBoxItem tags with content of Red, Green, and Blue, and follow the entire ComboBox with another Separator control, as follows:







    The RadioButton Control
    In this example, you want these three RadioButton controls to be mutually exclusive. In other GUI frameworks, ensuring that a group of related controls (such as radio buttons) were mutually exclusive required that you place them in the same group box. You don’t need to do this under WPF. Instead, you can

    simply assign them all to the same group name. This is helpful because the related items do not need to be physically collected in the same area but can be anywhere in the window.
    The RadioButton class includes an IsChecked property, which toggles between true and false when the end user clicks the UI element. Furthermore, RadioButton provides two events (Checked and Unchecked) that you can use to intercept this state change.

    Add the Save, Load, and Delete Buttons
    The final controls in the ToolBar control will be a Grid holding three Button controls. Add the following markup after the last Separator control:






    Pro C#10 CHAPTER 25 Introducing Windows Presentation Foundation and XAML

    PART VIII

    Windows Client Development

    CHAPTER 25

    Introducing Windows Presentation Foundation and XAML

    When version 1.0 of the .NET platform was released, programmers who needed to build graphical desktop applications made use of two APIs named Windows Forms and GDI+, packaged up primarily in the System. Windows.Forms.dll and System.Drawing.dll assemblies. While Windows Forms and GDI+ are still viable APIs for building traditional desktop GUIs, Microsoft shipped an alternative GUI desktop API named Windows Presentation Foundation (WPF) beginning with the release of .NET 3.0. WPF and Windows Forms joined the .NET Core family with the release of .NET Core 3.0.
    This initial WPF chapter begins by examining the motivation behind this new GUI framework, which will help you see the differences between the Windows Forms/GDI+ and WPF programming models.
    Next, you will come to know the role of several important classes, including Application, Window, ContentControl, Control, UIElement, and FrameworkElement.
    This chapter will then introduce you to an XML-based grammar named Extensible Application Markup Language (XAML; pronounced “zammel”). Here, you will learn the syntax and semantics of XAML (including attached property syntax and the role of type converters and markup extensions).
    This chapter wraps up by investigating the integrated WPF designers of Visual Studio by building your first WPF application. During this time, you will learn to intercept keyboard and mouse activities, define application-wide data, and perform other common WPF tasks.

    The Motivation Behind WPF
    Over the years, Microsoft has created numerous graphical user interface toolkits (raw C/C++/Windows API development, VB6, MFC, etc.) to build desktop executables. Each of these APIs provided a code base to represent the basic aspects of a GUI application, including main windows, dialog boxes, controls, menu systems, etc. With the initial release of the .NET platform, the Windows Forms API quickly became the preferred model for UI development, given its simple yet powerful object model.
    While many full-featured desktop applications have been successfully created using Windows Forms, the fact of the matter is that this programming model is rather asymmetrical. Simply put, System.Windows. Forms.dll and System.Drawing.dll do not provide direct support for many additional technologies required to build a feature-rich desktop application. To illustrate this point, consider the ad hoc nature of GUI desktop development before the release of WPF (see Table 25-1).

    © 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_25

    1099

    Table 25-1. Pre-WPF Solutions to Desired Functionalities

    Desired Functionality Technology
    Building windows with controls Windows Forms
    2D graphics support GDI+ (System.Drawing.dll)
    3D graphics support DirectX APIs
    Support for streaming video Windows Media Player APIs
    Support for flow-style documents Programmatic manipulation of PDF files

    As you can see, a Windows Forms developer must pull in types from several unrelated APIs and object models. While it is true that making use of these diverse APIs might look similar syntactically (it is just
    C# code, after all), you might also agree that each technology requires a radically different mindset. For example, the skills required to create a 3D rendered animation using DirectX are completely different from those used to bind data to a grid. To be sure, it is difficult for a Windows Forms programmer to master the diverse nature of each API.

    Unifying Diverse APIs
    WPF was purposely created to merge these previously unrelated programming tasks into a single unified object model. Thus, if you need to author a 3D animation, you have no need to manually program against the DirectX API (although you could) because 3D functionality is baked directly into WPF. To see how well things have cleaned up, consider Table 25-2, which illustrates the desktop development model ushered in as of .NET 3.0.

    Table 25-2. .NET 3.0+ Solutions to Desired Functionalities

    Desired Functionality Technology
    Building forms with controls WPF
    2D graphics support WPF
    3D graphics support WPF
    Support for streaming video WPF
    Support for flow-style documents WPF

    The obvious benefit here is that .NET programmers now have a single, symmetrical API for all common GUI desktop programming needs. After you become comfortable with the functionality of the key WPF assemblies and the grammar of XAML, you will be amazed how quickly you can create sophisticated UIs.

    Providing a Separation of Concerns via XAML
    Perhaps one of the most compelling benefits is that WPF provides a way to cleanly separate the look and feel of a GUI application from the programming logic that drives it. Using XAML, it is possible to define the UI of an application via XML markup. This markup (ideally generated using tools such as Microsoft Visual Studio or Blend for Visual Studio) can then be connected to a related C# code file to provide the guts of the program’s functionality.

    ■Note XaML is not limited to WpF applications. any application can use XaML to describe a tree of .net objects, even if they have nothing to do with a visible user interface.

    As you dig into WPF, you might be surprised how much flexibility this “desktop markup” provides. XAML allows you to define not only simple UI elements (buttons, grids, list boxes, etc.) in markup but also interactive 2D and 3D graphics, animations, data-binding logic, and multimedia functionality (such as video playback).
    XAML also makes it easy to customize how a control should render its visual appearance. For example, defining a circular button control that animates your company logo requires just a few lines of markup.
    As shown in Chapter 27, WPF controls can be modified through styles and templates, which allow you to change the overall look and feel of an application with minimum fuss and bother. Unlike Windows Forms development, the only compelling reason to build a custom WPF control from the ground up is if you need to change the behaviors of a control (e.g., add custom methods, properties, or events; subclass an existing control to override virtual members). If you simply need to change the look and feel of a control (again, such as a circular animated button), you can do so entirely through markup.

    Providing an Optimized Rendering Model
    GUI toolkits such as Windows Forms, MFC, or VB6 performed all graphical rendering requests (including the rendering of UI elements such as buttons and list boxes) using a low-level, C-based API (GDI), which has been part of the Windows OS for years. GDI provides adequate performance for typical business
    applications or simple graphical programs; however, if a UI application needed to tap into high-performance graphics, DirectX was required.
    The WPF programming model is quite different, in that GDI is not used when rendering graphical data. All rendering operations (e.g., 2D graphics, 3D graphics, animations, control rendering, etc.) now make use of the DirectX API. The first obvious benefit is that your WPF applications will automatically take advantage of hardware and software optimizations. As well, WPF applications can tap into rich graphical services (blur effects, anti- aliasing, transparency, etc.) without the complexity of programming directly against the DirectX API.

    ■Note although WpF does push all rendering requests to the directX layer, I don’t want to suggest that a WpF application will perform as fast as building an application using unmanaged C++ and directX directly. although significant performance advances have been made in WpF with every release, if you are intending to build a desktop application that requires the fastest possible execution speed (such as a 3d video game), unmanaged C++ and directX are still the best approach.

    Simplifying Complex UI Programming
    To recap the story thus far, Windows Presentation Foundation (WPF) is an API for building desktop applications that integrates various desktop APIs into a single object model and provides a clean separation of concerns via XAML. In addition to these major points, WPF applications also benefit from a simple way to integrate services into your programs, which historically were quite complex to account for. The following is a quick rundown of the core WPF features:
    •Multiple layout managers (far more than Windows Forms) to provide extremely flexible control over the placement and repositioning of content.
    •Use of an enhanced data-binding engine to bind content to UI elements in a variety of ways.

    •A built-in style engine, which allows you to define “themes” for a WPF application.
    •Use of vector graphics, which allows content to be automatically resized to fit the size and resolution of the screen hosting the application.
    •Support for 2D and 3D graphics, animations, and video and audio playback.
    •A rich typography API, such as support for XML Paper Specification (XPS) documents, fixed documents (WYSIWYG), flow documents, and document annotations (e.g., a Sticky Notes API).
    •Support for interoperating with legacy GUI models (e.g., Windows Forms, ActiveX, and Win32 HWNDs). For example, you can incorporate custom Windows Forms controls into a WPF application and vice versa.
    Now that you have some idea of what WPF brings to the table, let’s look at the various types of applications that can be created using this API. Many of these features will be explored in detail in the chapters to come.

    Investigating the WPF Assemblies
    WPF is ultimately little more than a collection of types bundled within .NET assemblies. Table 25-3 describes the key assemblies used to build WPF applications, each of which must be referenced when creating a new project. As you would hope, Visual Studio WPF projects reference these required assemblies automatically.

    Table 25-3. Core WPF Assemblies

    Assembly Meaning in Life
    PresentationCore This assembly defines numerous namespaces that constitute the foundation of the WPF GUI layer. For example, this assembly contains support for the WPF Ink API, animation primitives, and numerous graphical rendering types.
    PresentationFramework This assembly contains a majority of the WPF controls, the Application and Window classes, support for interactive 2D graphics, and numerous types used in data binding.
    System.Xaml.dll This assembly provides namespaces that allow you to program against a XAML document at runtime. By and large, this library is useful only if you are authoring WPF support tools or need absolute control over XAML at runtime.
    WindowsBase.dll This assembly defines types that constitute the infrastructure of the WPF API, including those representing WPF threading types, security types, various type converters, and support for dependency properties and routed events (described in Chapter 27).

    Collectively, these four assemblies define new namespaces and .NET classes, interfaces, structures, enumerations, and delegates. Table 25-4 describes the role of some (but certainly not all) of the important namespaces.

    Table 25-4. Core WPF Namespaces

    Namespace Meaning in Life
    System.Windows This is the root namespace of WPF. Here, you will find core classes (such as
    Application and Window) that are required by any WPF desktop project.
    System.Windows.Controls This contains all the expected WPF widgets, including types to build menu systems, tooltips, and numerous layout managers.
    System.Windows.Data This contains types to work with the WPF data-binding engine, as well as support for data-binding templates.
    System.Windows.Documents This contains types to work with the documents API, which allows you to integrate PDF-style functionality into your WPF applications, via the XML Paper Specification (XPS) protocol.
    System.Windows.Ink This provides support for the Ink API, which allows you to capture input from a stylus or mouse, respond to input gestures, and so forth. This is useful for Tablet PC programming; however, any WPF can make use of this API.
    System.Windows.Markup This namespace defines several types that allow XAML markup (and the equivalent binary format, BAML) to be parsed and processed programmatically.
    System.Windows.Media This is the root namespace to several media-centric namespaces. Within these namespaces you will find types to work with animations, 3D rendering, text rendering, and other multimedia primitives.
    System.Windows.Navigation This namespace provides types to account for the navigation logic employed by XAML browser applications (XBAPs) as well as standard desktop applications that require a navigational page model.
    System.Windows.Shapes This defines classes that allow you to render interactive 2D graphics that automatically respond to mouse input.

    To begin your journey into the WPF programming model, you will examine two members of the System.Windows namespace that are commonplace to any traditional desktop development effort: Application and Window.

    ■Note If you have created desktop uIs using the Windows Forms apI, be aware that the System.Windows. Forms. and System.Drawing. assemblies are not related to WpF. these libraries represent the original
    .net guI toolkit, Windows Forms/gdI+.

    The Role of the Application Class
    The System.Windows.Application class represents a global instance of a running WPF application. This class supplies a Run() method (to start the application), a series of events that you can handle in order to interact with the application’s lifetime (such as Startup and Exit). Table 25-5 details some of the key properties.

    Table 25-5. Key Properties of the Application Type

    Property Meaning in Life
    Current This static property allows you to gain access to the running Application object from anywhere in your code. This can be helpful when a window or dialog box needs to gain access to the Application object that created it, typically to access application-wide variables and functionality.
    MainWindow This property allows you to programmatically get or set the main window of the application.
    Properties This property allows you to establish and obtain data that is accessible throughout all aspects of a WPF application (windows, dialog boxes, etc.).
    StartupUri This property gets or sets a URI that specifies a window or page to open automatically when the application starts.
    Windows This property returns a WindowCollection type, which provides access to each window created from the thread that created the Application object. This can be helpful when you want to iterate over each open window of an application and alter its state (such as minimizing all windows).

    Constructing an Application Class
    Any WPF application will need to define a class that extends Application. Within this class, you will define your program’s entry point (the Main() method), which creates an instance of this subclass and typically handles the Startup and Exit events (as necessary). Here is an example:

    // Define the global application object
    // for this WPF program.
    class MyApp : Application
    {
    [STAThread]
    static void Main(string[] args)
    {
    // Create the application object.
    MyApp app = new MyApp();

    // Register the Startup/Exit events.
    app.Startup += (s, e) => { / Start up the app / }; app.Exit += (s, e) => { / Exit the app / };
    }
    }

    Within the Startup handler, you will most often process any incoming command-line arguments and launch the main window of the program. The Exit handler, as you would expect, is where you can author any necessary shutdown logic for the program (e.g., save user preferences, write to the Windows registry).

    ■Note the Main() method of a WpF application must be attributed with the [STAThread] attribute, which ensures any legacy CoM objects used by your application are thread-safe. If you do not annotate Main() in this way, you will encounter a runtime exception. even with the introduction of top-level statements in C# 9.0, you will still want to use the more traditional Main() method in your WpF applications. In fact, the Main() method is autogenerated for you.

    Enumerating the Windows Collection
    Another interesting property exposed by Application is Windows, which provides access to a collection representing each window loaded into memory for the current WPF application. As you create new Window objects, they are automatically added into the Application.Windows collection. Here is an example method that will minimize each window of the application (perhaps in response to a given keyboard gesture or menu option triggered by the end user):

    static void MinimizeAllWindows()
    {
    foreach (Window wnd in Application.Current.Windows)
    {
    wnd.WindowState = WindowState.Minimized;
    }
    }

    You will build some WPF applications shortly, but until then, let’s check out the core functionality of the
    Window type and learn about a number of important WPF base classes in the process.

    The Role of the Window Class
    The System.Windows.Window class (located in the PresentationFramework.dll assembly) represents a single window owned by the Application-derived class, including any dialog boxes displayed by the main window. Not surprisingly, Window has a series of parent classes, each of which brings more functionality
    to the table. Consider Figure 25-1, which shows the inheritance chain (and implemented interfaces) for
    System.Windows.Window as seen through the Visual Studio Object Browser.

    Figure 25-1. The hierarchy of the Window class

    You will come to understand the functionality provided by many of these base classes as you progress through this chapter and the chapters to come. However, to whet your appetite, the following sections present a breakdown of the functionality provided by each base class (consult the .NET 6 documentation for full details).

    The Role of System.Windows.Controls.ContentControl
    The direct parent of Window is ContentControl, which is quite possibly the most enticing of all WPF classes. This base class provides derived types with the ability to host a single piece of content, which, simply put, refers to the visual data placed within the interior of the control’s surface area via the Content property. The WPF content model makes it quite simple to customize the basic look and feel of a content control.
    For example, when you think of a typical “button” control, you tend to assume that the content is a simple string literal (OK, Cancel, Abort, etc.). If you are using XAML to describe a WPF control and the value you want to assign to the Content property can be captured as a simple string, you may set the Content property within the element’s opening definition as so (don’t fret over the exact markup at this point):


    You can also make use of XAML’s property-element syntax to set complex content. Consider the following functionally equivalent

    Do be aware that not every WPF element derives from ContentControl and, therefore, not all controls support this unique content model (however, most do). As well, some WPF controls add a few refinements to the basic content model you have just examined. Chapter 26 will examine the role of WPF content in much more detail.

    The Role of System.Windows.Controls.Control
    Unlike ContentControl, all WPF controls share the Control base class as a common parent. This base class provides numerous core members that account for basic UI functionality. For example, Control defines properties to establish the control’s size, opacity, tab order logic, the display cursor, background color, and so forth. Furthermore, this parent class provides support for templating services. As explained in Chapter 27, WPF controls can completely change the way they render their appearance using templates and styles.
    Table 25-6 documents some key members of the Control type, grouped by related functionality.

    Table 25-6. Key Members of the Control Type

    Members Meaning in Life
    Background, Foreground, BorderBrush, BorderThickness, Padding, HorizontalContentAlignment, VerticalContentAlignment These properties allow you to set basic settings regarding how the control will be rendered and positioned.
    FontFamily, FontSize, FontStretch, FontWeight These properties control various font-centric settings.
    IsTabStop, TabIndex These properties are used to establish tab order among controls on a window.
    MouseDoubleClick, PreviewMouseDoubleClick These events handle the act of double-clicking a widget.
    Template This property allows you to get and set the control’s template, which can be used to change the rendering output of the widget.

    The Role of System.Windows.FrameworkElement
    This base class provides a number of members that are used throughout the WPF framework, such as support for storyboarding (used within animations) and support for data binding, as well as the ability to name a member (via the Name property), obtain any resources defined by the derived type, and establish the overall dimensions of the derived type. Table 25-7 hits the highlights.

    Table 25-7. Key Members of the FrameworkElement Type

    Members Meaning in Life
    ActualHeight, ActualWidth, MaxHeight, MaxWidth, MinHeight, MinWidth, Height, Width These properties control the size of the derived type.
    ContextMenu Gets or sets the pop-up menu associated with the derived type.
    Cursor Gets or sets the mouse cursor associated with the derived type.
    HorizontalAlignment, VerticalAlignment Gets or sets how the type is positioned within a container (such as a panel or list box).
    Name Allows to you assign a name to the type in order to access its functionality in a code file.
    Resources Provides access to any resources defined by the type (see Chapter 29 for an examination of the WPF resource system).
    ToolTip Gets or sets the tooltip associated with the derived type.

    The Role of System.Windows.UIElement
    Of all the types within a Window’s inheritance chain, the UIElement base class provides the greatest amount of functionality. The key task of UIElement is to provide the derived type with numerous events to allow the derived type to receive focus and process input requests. For example, this class provides numerous events to account for drag-and-drop operations, mouse movement, keyboard input, stylus input, and touch.
    Chapter 25 digs into the WPF event model in detail; however, many of the core events will look quite familiar (MouseMove, KeyUp, MouseDown, MouseEnter, MouseLeave, etc.). In addition to defining dozens of events, this parent class provides several properties to account for control focus, enabled state, visibility, and hit-testing logic, as shown in Table 25-8.

    Table 25-8. Key Members of the UIElement Type

    Members Meaning in Life
    Focusable, IsFocused These properties allow you to set focus on a given derived type.
    IsEnabled This property allows you to control whether a given derived type is enabled or disabled.
    IsMouseDirectlyOver, IsMouseOver These properties provide a simple way to perform hit-testing logic.
    IsVisible, Visibility These properties allow you to work with the visibility setting of a derived type.
    RenderTransform This property allows you to establish a transformation that will be used to render the derived type.

    The Role of System.Windows.Media.Visual
    The Visual class type provides core rendering support in WPF, which includes hit-testing of graphical data, coordinate transformation, and bounding box calculations. In fact, the Visual class interacts with the underlying DirectX subsystem to draw data on the screen. As you will examine in Chapter 26, WPF provides three possible manners in which you can render graphical data, each of which differs in terms of functionality and performance. Use of the Visual type (and its children, such as DrawingVisual) provides
    the most lightweight way to render graphical data, but it also entails the greatest amount of manual code to account for all the required services. Again, more details to come in Chapter 28.

    The Role of System.Windows.DependencyObject
    WPF supports a particular flavor of .NET properties termed dependency properties. Simply put, this style of property provides extra code to allow the property to respond to several WPF technologies such as styles, data binding, animations, and so forth. For a type to support this new property scheme, it will need to derive from the DependencyObject base class. While dependency properties are a key aspect of WPF development, much of the time their details are hidden from view. Chapter 26 dives further into the details of dependency properties.

    The Role of System.Windows.Threading.DispatcherObject
    The final base class of the Window type (beyond System.Object, which I assume needs no further explanation at this point in the book) is DispatcherObject. This type provides one property of interest, Dispatcher, which returns the associated System.Windows.Threading.Dispatcher object. The Dispatcher class is the entry point to the event queue of the WPF application, and it provides the basic constructs for dealing with concurrency and threading. The Dispatcher class was explored in Chapter 15.

    Understanding the Syntax of WPF XAML
    Production-level WPF applications will typically make use of dedicated tools to generate the necessary XAML. As helpful as these tools are, it is a good idea to understand the overall structure of XAML markup. To help in your learning process, allow me to introduce a popular (and free) tool that allows you to easily experiment with XAML.

    Introducing Kaxaml
    When you are first learning the grammar of XAML, it can be helpful to use a free tool named Kaxaml. You can obtain this popular XAML editor/parser from https://github.com/punker76/kaxaml.

    ■Note For many editions of this book, I’ve pointed users to www.kaxaml.com, but unfortunately, that site has been retired. Jan Karger (https://github.com/punker76) has forked the old code and has done some work on improving it. You can find his version of the tool on github https://github.com/punker76/ kaxaml/releases. Much respect and thanks to the original developers of Kaxaml and to Jan for keeping it alive; it is a great tool and has helped countless developers learn XaML.

    Kaxaml is helpful, in that it has no clue about C# source code, event handlers, or implementation logic.
    It is a much more straightforward way to test XAML snippets than using a full-blown Visual Studio WPF project template. As well, Kaxaml has several integrated tools, such as a color chooser, a XAML snippet manager, and even an “XAML scrubber” option that will format your XAML based on your settings. When you first open Kaxaml, you will find simple markup for a control, as follows:

    <Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    Like a Window, a Page contains various layout managers and controls. However, unlike a Window, Page objects cannot run as stand-alone entities. Rather, they must be placed inside a suitable host such as a NavigationWindow or a Frame. The good news is that you can type identical markup within a or

    scope.

    ■Note If you change the and elements in the Kaxaml markup window to and
    , you can press the F5 key to load a new window onto the screen.

    As an initial test, enter the following markup into the XAML pane at the bottom of the tool:






    You should now see your page render at the upper part of the Kaxaml editor (see Figure 25-2).

    Figure 25-2. Kaxaml is a helpful (and free) tool used to learn the grammar of XAML

    As you work with Kaxaml, remember that this tool does not allow you to author any markup that entails code compilation (however, using x:Name is allowed). This includes defining an x:Class attribute (for specifying a code file), entering event handler names in markup, or using any XAML keywords that also entail code compilation (such as FieldModifier or ClassModifier). Any attempt to do so will result in a markup error.

    XAML XML Namespaces and XAML “Keywords”
    The root element of a WPF XAML document (such as a , , , or
    definition) will almost always reference the following two predefined XML namespaces:



    The first XML namespace, http://schemas.microsoft.com/winfx/2006/xaml/presentation, maps a slew of WPF .NET namespaces for use by the current *.xaml file (System.Windows, System.Windows.
    Controls, System.Windows.Data, System.Windows.Ink, System.Windows.Media, System.Windows. Navigation, etc.).
    This one-to-many mapping is hard-coded within the WPF assemblies (WindowsBase.dll, PresentationCore.dll, and PresentationFramework.dll) using the assembly-level [XmlnsDefinition] attribute. For example, if you open the Visual Studio Object Browser and select the PresentationCore.dll assembly, you will see listings such as the following, which essentially imports System.Windows:

    [assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation",
    "System.Windows")]

    The second XML namespace, http://schemas.microsoft.com/winfx/2006/xaml, is used to include XAML-specific “keywords” (for lack of a better term) as well as the inclusion of the System.Windows.Markup namespace, as follows:

    [assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml",
    "System.Windows.Markup")]

    One rule of any well-formed XML document (remember, XAML is an XML-based grammar) is that the opening root element designates one XML namespace as the primary namespace, which is the namespace that contains the most common items. If a root element requires the inclusion of additional secondary namespaces (as seen here), they must be defined using a unique tag prefix (to resolve any possible name clashes). As a convention, the prefix is simply x; however, this can be any unique token you require, such as XamlSpecificStuff.





    The obvious downside of defining wordy XML namespace prefixes is you are required to type XamlSpecificStuff each time your XAML file needs to refer to one of the items defined within this XAML-centric XML namespace. Given that XamlSpecificStuff requires many additional keystrokes, just stick with x.
    In any case, beyond the x:Name, x:Class, and x:Code keywords, the http://schemas.microsoft.com/ winfx/2006/xaml XML namespace also provides access to additional XAML keywords, the most common of which are shown in Table 25-9.

    Table 25-9. XAML Keywords

    XAML Keyword Meaning in Life
    x:Array Represents a .NET array type in XAML.
    x:ClassModifier Allows you to define the visibility of the C# class (internal or public) denoted by the
    Class keyword.
    x:FieldModifier Allows you to define the visibility of a type member (internal, public, private, or protected) for any named subelement of the root (e.g., a

    Notice that within the scope of the tags, you have defined a subscope named

    . Within this scope, you have defined a custom . (Do not worry about the exact code for the brush; you’ll learn about WPF graphics in Chapter 28.)
    Any property can be set using property-element syntax, which always breaks down to the following pattern:





    While any property could be set using this syntax, if you can capture a value as a simple string, you will save yourself typing time. For example, here is a much more verbose way to set the Width of your Button:

    Understanding XAML Attached Properties
    In addition to property-element syntax, XAML defines a special syntax used to set a value to an attached property. Essentially, an attached property allows a child element to set the value for a property that is defined in a parent element. The general template to follow looks like this:



    The most common use of attached property syntax is to position UI elements within one of the WPF layout manager classes (Grid, DockPanel, etc.). The next chapter dives into these panels in some detail; for now, enter the following in Kaxaml:





    Here, you have defined a Canvas layout manager that contains an Ellipse. Notice that the Ellipse can inform its parent (the Canvas) where to position its top-left position using attached property syntax.
    There are a few items to be aware of regarding attached properties. First and foremost, this is not an all- purpose syntax that can be applied to any property of any parent. For example, the following XAML cannot be parsed without error:




    Attached properties are a specialized form of a WPF-specific concept termed a dependency property. Unless a property was implemented in a specific manner, you cannot set its value using attached property syntax. You will explore dependency properties in detail in Chapter 25.

    ■Note Visual studio has Intellisense, which will show you valid attached properties that can be set by a given element.

    Understanding XAML Markup Extensions
    As explained, property values are most often represented using a simple string or via property-element syntax. There is, however, another way to specify the value of a XAML attribute, using markup extensions. Markup extensions allow a XAML parser to obtain the value for a property from a dedicated, external class. This can be beneficial given that some property values require several code statements to execute to figure out the value.
    Markup extensions provide a way to cleanly extend the grammar of XAML with new functionality. A markup extension is represented internally as a class that derives from MarkupExtension. Note that the chances of you ever needing to build a custom markup extension will be slim to none. However, a subset of XAML keywords (such as x:Array, x:Null, x:Static, and x:Type) are markup extensions in disguise!
    A markup extension is sandwiched between curly brackets, like so:

    To see some markup extensions in action, author the following into Kaxaml:








    Sun Kil Moon
    Red House Painters
    Besnard Lakes




    First, notice that the definition has a new XML namespace declaration, which allows you to gain access to the System namespace of mscorlib.dll. With this XML namespace established, you first make
    use of the x:Static markup extension and grab values from OSVersion and ProcessorCount of the System. Environment class.
    The x:Type markup extension allows you to gain access to the metadata description of the specified item. Here, you are simply assigning the fully qualified names of the WPF Button and System.Boolean types.
    The most interesting part of this markup is the ListBox. Here, you are setting the ItemsSource property to an array of strings declared entirely in markup! Notice here how the x:Array markup extension allows you to specify a set of subitems within its scope:


    Sun Kil Moon
    Red House Painters
    Besnard Lakes

    ■Note the previous XaML example is used only to illustrate a markup extension in action. as you will see in Chapter 26, there are much easier ways to populate ListBox controls!

    Figure 25-3 shows the markup of this in Kaxaml.

    Figure 25-3. Markup extensions allow you to set values via the functionality of a dedicated class

    You have now seen numerous examples that showcase each of the core aspects of the XAML syntax. As you might agree, XAML is interesting, in that it allows you to describe a tree of .NET objects in a declarative manner. While this is extremely helpful when configuring graphical user interfaces, do remember that XAML can describe any type from any assembly, provided it is a nonabstract type containing a default constructor.

    Building WPF Applications Using Visual Studio
    Let’s examine how Visual Studio can simplify the construction of WPF programs. While you can build WPF applications using Visual Studio Code, Visual Studio Code does not have any designer support for building WPF applications. Visual Studio, with its rich XAML support, is a more productive IDE when building WPF applications.

    ■Note here, I will point out some key features of using Visual studio to build WpF applications. Forthcoming chapters will illustrate additional aspects of the Ide where necessary.

    The WPF Project Templates
    The New Project dialog box of Visual Studio defines a set of WPF project templates, including WPF Application, WPF Class Library, WPF Custom Control Library, and WPF User Control Library. Create a new WPF Application project named WpfTesterApp.

    ■Note When selecting WpF projects from the Visual studio “add a new project” screen, be sure to select the WpF project templates that do not have “(.net Framework)” in the title. If you select a template with “(.net Framework)” in the title, you will be building your app using .net Framework 4.x.

    Examining the project file, you can see that the SDK is set to Microsoft.NET.Sdk. The TargetFramework
    value is set to net6.0-windows, and the project will build an executable that uses WPF:



    WinExe
    net6.0-windows
    true
    disable
    enable

    At the time of this writing, the WPF template doesn’t set the flags for either implicit using statements or nullable reference types. Update the project file to enable implicit using statements and disables nullable reference types:



    WinExe
    net6.0-windows
    true
    enable
    disable

    Beyond the project file, the template also provides an initial Window- and the Application-derived class, each represented using a XAML and C# code file.

    The Toolbox and XAML Designer/Editor
    Visual Studio provides a Toolbox (which you can open via the View menu) that contains numerous WPF controls. The top part of the panel holds the most common controls, and the bottom contains all controls (see Figure 25-4).

    Figure 25-4. The Toolbox contains the WPF controls that can be placed on the designer surface

    Using a standard drag-and-drop operation, you can place any of these controls onto the window’s designer surface or drag the control into the XAML markup editor at the bottom of the designer. When you do, the initial XAML will be authored on your behalf. Use your mouse to drag a Button control and a
    Calendar control onto the designer surface. After you have done so, notice how you can relocate and resize
    your controls (and be sure to examine the resulting XAML generated based on your edits).
    In addition to building the UI via the mouse and toolbox, you can manually enter your markup using the integrated XAML editor. As you can see in Figure 25-5, you do get IntelliSense support, which can help simplify the authoring of the markup. For example, try to add the Margin property to the element.

    Figure 25-5. The WPF Window designer

    Take a few moments to add some property values directly in the XAML editor. Be sure you take the time to become comfortable using this aspect of the WPF designer.

    Setting Properties Using the Properties Window
    After you have placed some controls onto your designer (or manually defined them in the editor), you can then make use of the Properties window to set property values for the selected control, as well as rig up event handlers for the selected control. By way of a simple test, select your Button control on the designer. Now, use the Properties window to change the Background color of the Button using the integrated Brushes editor (see Figure 25-6; you will learn more about the Brushes editor in Chapter 27, during your examination of WPF graphics).

    Figure 25-6. The Properties window can be used to configure the UI of a WPF control

    ■Note the properties window provides a search text area at the top. type in the name of a property you would like to set to quickly find the item in question.

    After you have finished tinkering with the Brushes editor, check out the generated markup. It might look something like this:

    Handling Events Using the Properties Window
    If you want to handle events for a given control, you can also make use of the Properties window, but this time you need to click the Events button at the upper right of the Properties window (look for the lightning bolt icon). Ensure that the button is selected on your designer and locate the Click event. Once you do, double-click directly on the Click event entry. This will cause Visual Studio to automatically build an event handler that takes the following general form:

    NameOfControl_NameOfEvent

    Since you did not rename your button, the Properties window shows that it generated an event handler named Button_Click (see Figure 25-7).

    Figure 25-7. Handling events using the Properties window

    As well, Visual Studio generated the corresponding C# event handler in your window’s code file. Here, you can add any sort of code that must execute when the button is clicked. For a quick test, just enter the following code statement:

    private void Button_Click(object sender, RoutedEventArgs e)
    {
    MessageBox.Show("You clicked the button!");
    }

    Handling Events in the XAML Editor
    You can also handle events directly in the XAML editor. By way of an example, place your mouse within the
    element and type in the MouseMove event, followed by the equal sign. Once you do, you will see that Visual Studio displays any compatible handlers in your code file (if they exist), as well as the option to create a new event handler (see Figure 25-8).

    Figure 25-8. Handling events using the XAML editor

    Let the IDE create the MouseMove event handler, enter the following code, and then run the application to see the result:

    private void MainWindow_MouseMove (object sender, MouseEventArgs e)
    {
    this.Title = e.GetPosition(this).ToString();
    }

    ■Note Chapter 29 covers MVVM and the Command pattern, which is a much better way to handle click events in enterprise applications. But if you need only a simple app, handling click events with a straight event handler is perfectly acceptable.

    The Document Outline Window
    When you work with any XAML-based project, you will certainly make use of a healthy amount of markup to represent your UIs. When you begin to work with more complex XAML, it can be useful to visualize the markup to quickly select an item to edit on the Visual Studio designer.
    Currently, your markup is quite tame because you have defined only a few controls within the initial
    . Nevertheless, locate the Document Outline window in your IDE, mounted by default on the left side of Visual Studio (if you cannot locate it, simply activate it using the View ➤ Other Windows menu option). Now, make sure your XAML designer is the active window in the IDE (rather than the C# code file), and you will notice the Document Outline window displays the nested elements (see Figure 25-9).

    Figure 25-9. Visualizing your XAML via the Document Outline window

    This tool also provides a way to temporarily hide a given item (or set of items) on the designer as well as lock items to prevent additional edits from taking place. In the next chapter, you will see how the Document Outline window also provides many other features to group selected items into new layout managers (among other features).

    Enable or Disable the XAML Debugger
    When you run the application, you will see the MainWindow on the screen. You will also see the interactive debugger, as shown in Figure 25-10 (minimized) and Figure 25-11 (expanded).

    Figure 25-10. XAML UI debugger (minimized)

    Figure 25-11. XAML UI debugger

    If you want to turn this off, you will find the entries for XAML debugging under Tools ➤ Options ➤ Debugging ➤ XAML Hot Reload. Deselect the top box to prevent the debugger window from overlaying your windows. Figure 25-12 shows the entries.

    Figure 25-12. Enabling/disabling XAML UI debugging

    Examining the App.xaml File
    How did the project know what window to launch? Even more intriguing, if you examine the code files in your application, you will also see that there is not a Main() method anywhere to be found. You have learned throughout this book that applications must have an entry point, so how does .NET know how to launch your app? Fortunately, both plumbing items are handled for you through the Visual Studio templates and the WPF framework.
    To solve the riddle of which window to launch, the App.xaml file defines an application class through markup. In addition to the namespace definitions, it defines application properties such as the StartupUri, application-wide resources (covered in Chapter 28), and specific handlers for application events such as Startup and Exit. The StartupUri indicates which window to load on startup. Open the App.xaml file and examine the markup, shown here:




    Using the XAML designer and using Visual Studio code completion, add handlers for the Startup and
    Exit events. Your updated XAML should look like this (notice the change in bold):




    If you look at the App.xaml.cs file, it should look like this:

    public partial class App : Application
    {
    private void App_OnStartup(object sender, StartupEventArgs e)
    {
    }
    private void App_OnExit(object sender, ExitEventArgs e)
    {
    }
    }

    Note that the class is marked as partial. In fact, all the code-behind windows for XAML files are marked partial. That is key to solving the riddle of where the Main() method lives. But first, you need to examine what happens when msbuild.exe processes XAML files.

    Mapping the Window XAML Markup to C# Code
    When msbuild.exe processed your *.csproj file, it produced three files for each XAML file in your project with the form of *.g.cs (where g denotes autogenerated), *.g.i.cs (where i denotes IntelliSense), and
    *.baml (for Binary Application Markup Language). These are saved into the \obj\Debug directory (and can be viewed in Solution Explorer by clicking the Show All Files button). You might have to hit the Refresh button in Solution Explorer to see them since they are not part of the actual project but build artifacts.
    To make the most sense of the process, it is helpful to provide names for your controls. Go ahead and provide names for the Button and Calendar controls, as follows:


    Now rebuild your solution (or project) and refresh the files in Solution Explorer. If you open the MainWindow.g.cs file into a text editor, you will find a class named MainWindow, which extends the Window base class. The name of this class is a direct result of the x:Class attribute in the start tag.
    This class defines a private member variable of type bool (named _contentLoaded), which was not directly accounted for in the XAML markup. This data member is used to determine (and ensure) the content of the window is assigned only once. This class also contains a member variable of type System. Windows.Controls.Button, named ClickMe. The name of the control is based on the x:Name (or the shorthand form Name) attribute value within the opening

    Pro C#10 CHAPTER 24 Test-Driving AutoLot

    CHAPTER 24

    Test-Driving AutoLot

    Now that you have the finished AutoLot data access layer, it’s time to take it for a test-drive. Integration testing in an integral part of software development and is a great way to make sure your data access code behaves as expected. In this chapter, we will be using xUnit, a testing framework for .NET Core.
    After creating the solution and test project, the AutoLot.Dal and AutoLot.Models projects are added to the solution. Next, xUnit will be explored to show how to run automated tests. Then the rest of the chapter is dedicated to creating and executing integration tests.

    Setting Up the Test-Drives
    Instead of creating a client application to test-drive the completed AutoLot data access layer, we are going to use automated integration tests. The tests will demonstrate create, read, update, and delete calls to the database. This allows us to examine the code without the overhead of creating another application. Each of the tests in this section will execute a query (either create, read, update, or delete) and then have one or more Assert statements to validate that the result is what was expected.

    Create the Project
    To get started, we are going to set up an integration test platform using xUnit, a .NET Core–compatible testing framework. Start by adding a new xUnit test project named AutoLot.Dal.Tests. In Visual Studio, this project type is named xUnit Test Project.

    ■Note Unit tests are designed to test a single unit of code. What we will be doing throughout this chapter is technically creating integration tests, since we are testing the C# code and EF Core all the way to the database and back.

    From the command-line interface, execute the following commands using the command-line interface (CLI):

    dotnet new sln -n Chapter24_AllProjects
    dotnet new xunit -lang c# -n AutoLot.Dal.Tests -o .\AutoLot.Dal.Tests -f net6.0 dotnet sln .\Chapter23_AllProjects.sln add AutoLot.Dal.Tests

    © 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_24

    1051

    Add the Microsoft.EntityFrameworkCore, Microsoft.EntityFrameworkCore.Design, Microsoft. EntityFrameworkCore.SqlServer, and Microsoft.Extensions.Configuration.Json NuGet packages to the AutoLot.Dal.Tests project. If using the CLI, use the following commands:

    dotnet add AutoLot.Dal.Tests package Microsoft.EntityFrameworkCore
    dotnet add AutoLot.Dal.Tests package Microsoft.EntityFrameworkCore.Design dotnet add AutoLot.Dal.Tests package Microsoft.EntityFrameworkCore.SqlServer dotnet add AutoLot.Dal.Tests package Microsoft.Extensions.Configuration.Json

    The tests will use the data initializer code that clears the temporal data, so the same adjustment to the project file must be made regarding the Microsoft.EntityFrameworkCore.Design package. Update the package to remove (or comment out) the IncludeAssets tag:



    all

    The versions of the Microsoft.NET.Test.Sdk and coverlet.collector packages that ship with the xUnit project template typically lag behind the currently available versions. To update them, either use the NuGet Package Manager in Visual Studio to update all NuGet packages or use the CLI. To update them with the CLI, then add them again since adding packages from the command line will always retrieve the latest non-prerelease version. Here are the commands:

    dotnet add AutoLot.Dal.Tests package Microsoft.NET.Test.Sdk dotnet add AutoLot.Dal.Tests package coverlet.collector

    Next, add project references to AutoLot.Models and AutoLot.Dal. To do this from the command line, execute the following (update the path and directory separator to your projects from Chapter 23):

    dotnet add AutoLot.Dal.Tests reference ..\Chapter_23\AutoLot.Dal dotnet add AutoLot.Dal.Tests reference ..\Chapter_23\AutoLot.Models

    Finally, turn off nullable reference types and enable global implicit using statements in the project file:


    net6.0
    false
    disable
    enable

    ■Note At the time of this writing, the xUnit project template does add the node for ImplicitUsings into the project file.

    Make the AutoLot.Dal Internals Visible to AutoLot.Dal.Tests
    To test (or use) the methods and classes in the AutoLot.Dal project that are marked internal, the internals need to be made visible to the AutoLot.Dal.Tests project. Open the AutoLot.Dal.csproj file and add the following:



    <_Parameter1>AutoLot.Dal.Tests

    Add the GlobalUsings File
    Create a new file named GlobalUsings.cs in the root of the AutoLot.Dal.Tests project. This will be the central location for all the using statements needed in this project. Update the code to match the following:

    global using System.Data;
    global using System.Linq.Expressions;

    global using AutoLot.Dal.EfStructures; global using AutoLot.Dal.Exceptions; global using AutoLot.Dal.Initialization; global using AutoLot.Dal.Repos;
    global using AutoLot.Dal.Repos.Interfaces;
    global using AutoLot.Dal.Tests.Base; global using AutoLot.Models.Entities;
    global using AutoLot.Models.Entities.Owned;

    global using Microsoft.EntityFrameworkCore;
    global using Microsoft.EntityFrameworkCore.ChangeTracking; global using Microsoft.EntityFrameworkCore.Storage;
    global using Microsoft.EntityFrameworkCore.Query; global using Microsoft.Extensions.Configuration;
    global using Xunit;
    global using Xunit.Abstractions;

    A First Look at xUnit
    There are two types of tests that will be used in this chapter. Parameterless test methods are referred to as facts (and use the Fact attribute). Tests that take parameters are referred to as theories (and use the Theory attribute). Theory tests run multiple iterations of the test method passing in different values for each run. To demonstrate these test types, create a new class named SampleTests.cs in the AutoLot.Dal.Tests project and update the code to the following:

    namespace AutoLot.Dal.Tests;

    public class SampleTests
    {
    //tests go here
    }

    Fact Test Methods
    The first test to create is a Fact test. With Fact tests, all values are contained in the test method. The following (trivial) example tests 3+2=5:

    [Fact]
    public void SimpleFactTest()
    {
    Assert.Equal(5,3+2);
    }

    There are a variety of Assert types available. In this example, the test is asserting the actual result (3+2) equals the expected result (5).

    Theory Test Methods
    When using Theory type tests, the values for the tests are passed into the test method. The previous test tested only one case, 3+2. Theories permit testing multiple use cases without having to repeat the test code multiple times. The values can come from the InlineData attribute, methods, or classes. For our purpose, we will only use the InlineData attribute. Create the following test that provided different addends and expected results to the test:

    [Theory] [InlineData(3,2,5)]
    [InlineData(1,-1,0)]
    public void SimpleTheoryTest(int addend1, int addend2, int expectedResult)
    {
    Assert.Equal(expectedResult,addend1+addend2);
    }

    ■Note there will be many examples of Fact and Theory tests throughout this chapter, as well as additional capabilities for the xUnit testing framework. For more information in the xUnit test framework, consult the documentation located at https://xunit.net/.

    Executing Tests
    While the xUnit tests can be executed from the command line (using dotnet test), it is a better developer experience (in my opinion) to use Visual Studio to execute the tests. Launch the Test Explorer from the Test menu to have access to running and debugging all or selected tests.

    Configure the Project and DbContext Instances
    To retrieve the connection string at runtime, we are going to use the .NET Core configuration capabilities using a JSON file. Add a JSON file, named appsettings.testing.json, to the project and add your connection string information into the file in the following format (update your connection string from what is listed here as necessary):

    {
    "ConnectionStrings": {
    "AutoLot": "server=.,5433;Database=AutoLot;User Id=sa;Password=P@ssw0rd;"
    }
    }

    Update the project file to have the settings file copied to the output folder on every build. Do that by adding the following ItemGroup to the AutoLot.Dal.Tests.csproj file:



    Always

    Create the Integration Test Helper
    The TestHelpers class will handle the application configuration as well as create new instances of
    ApplicationDbContext. Add a new public static class named TestHelpers.cs in the root of the project:
    namespace AutoLot.Dal.Tests; public static class TestHelpers
    {
    //helper code goes here
    }

    Add a public static method to create an instance of the IConfiguration interface using the appsettings. testing.json file. Add the following code to the class:

    public static IConfiguration GetConfiguration() => new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.testing.json", true, true)
    .Build();

    ■Note Configuration of assemblies is covered in Chapter 16.

    Add another public static method to create instances of the ApplicationDbContext class using the
    IConfiguration instance. Add the following code to the class:

    public static ApplicationDbContext GetContext(IConfiguration configuration)
    {
    var optionsBuilder = new DbContextOptionsBuilder(); var connectionString = configuration.GetConnectionString("AutoLot"); optionsBuilder.UseSqlServer(connectionString);
    return new ApplicationDbContext(optionsBuilder.Options);
    }

    Add another static method that will create a new instance of ApplicationDbContext. This demonstrates creating an instance of the ApplicationDbContext class from an existing instance to share the connection and transaction.public static ApplicationDbContext GetSecondContext(

    ApplicationDbContext oldContext, IDbContextTransaction trans)
    {
    var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseSqlServer(oldContext.Database.GetDbConnection());
    var context = new ApplicationDbContext(optionsBuilder.Options); context.Database.UseTransaction(trans.GetDbTransaction()); return context;
    }

    Add the BaseTest Class
    The BaseTest class handles the infrastructure for the tests in this chapter. Add a new folder named Base to the test project and add a new class file named BaseTest.cs to that folder. Make the class abstract and implement IDisposable. Add two protected readonly properties to hold the IConfiguration and ApplicationDbContext instances and dispose of the ApplicationDbContext instance in the virtual Dispose() method.

    namespace AutoLot.Dal.Tests.Base;

    public abstract class BaseTest : IDisposable
    {
    protected readonly IConfiguration Configuration; protected readonly ApplicationDbContext Context;

    public virtual void Dispose()
    {
    Context.Dispose();
    }
    }

    The xUnit test framework provides a mechanism to run code before and after each test is executed. Test classes (called fixtures) that implement the IDisposable interface will execute the code in the class constructor in the inheritance chain before each test is run. This is commonly referred to as the test setup. After each test is executed, the code in the Dispose methods (through the inheritance chain) is executed. This is referred to as test teardown.

    Add a protected constructor that creates an instance of IConfiguration and assigns it to the protected class variable. Use the configuration to create an instance of ApplicationDbContext using the TestHelper class and also assign it to the protected class variable.

    protected BaseTest()
    {
    Configuration = TestHelpers.GetConfiguration(); Context = TestHelpers.GetContext(Configuration);
    }

    The ITestOutputHelper interface allows for content to be written to the test output window. When using the IDisposable pattern with xUnit test fixtures, the instance for this interface can be injected into the constructor. Add a protected readonly variable to hold the instance and update the constructor to the following:

    protected readonly ITestOutputHelper OutputHelper;

    protected BaseTest(ITestOutputHelper outputHelper)
    {
    Configuration = TestHelpers.GetConfiguration(); Context = TestHelpers.GetContext(Configuration); OutputHelper = outputHelper;
    }

    Add the Transacted Test Execution Helpers
    The final two methods in the BaseTest class enable running test methods in a transaction. The methods will take an Action delegate as a single parameter, create an explicit transaction (or enlist an existing transaction), execute the Action delegate, and then roll back the transaction. We do this so any create/ update/delete tests leave the database in the state it was in before the test was run. The transactions are executed inside an execution strategy in case the ApplicationDbContext is configured to enable retry on transient errors.
    The ExecuteInATransaction() method executes the action delegate using a single instance of the
    ApplicationDbContext. Add the following code into your BaseTest class:

    protected void ExecuteInATransaction(Action actionToExecute)
    {
    var strategy = Context.Database.CreateExecutionStrategy(); strategy.Execute(() =>
    {
    using var trans = Context.Database.BeginTransaction(); actionToExecute();
    trans.Rollback();
    });
    }

    The ExecuteInASharedTransaction() method allows for multiple ApplicationDbContext instances to share a single transaction. Add the following code into your BaseTest class:

    protected void ExecuteInASharedTransaction(Action actionToExecute)
    {
    var strategy = Context.Database.CreateExecutionStrategy(); strategy.Execute(() =>
    {
    using IDbContextTransaction trans = Context.Database.BeginTransaction(IsolationLevel.ReadUncommitted);
    actionToExecute(trans); trans.Rollback();
    });
    }

    Add the EnsureAutoLotDatabase Test Fixture Class
    The xUnit test framework provides a mechanism for running code before any of the tests are run (referred to as fixture setup) and after all the tests are run (referred to as fixture teardown). This practice is generally not recommended, but in our case, we want to ensure that the database is created and loaded with data before any tests are run instead of before each test is run. Test classes that implement IClassFixture where T:
    TestFixtureClass will have the constructor code of T (the TestFixtureClass) executed before any tests are run, and the Dispose() code will run after all tests are completed.
    Add a new class named EnsureAutoLotDatabaseTestFixture.cs to the Base directory and implement
    IDisposable. Make the class public and sealed, and add the following using statements:

    namespace AutoLot.Dal.Tests.Base;

    public sealed class EnsureAutoLotDatabaseTestFixture : IDisposable
    {
    //add implementation code here

    public void Dispose()
    {
    }
    }

    The constructor code uses the TestHelpers class to get the instance of IConfiguration and then gets an instance of the ApplicationDbContext. Next, it calls the ClearAndReseedDatabase() method from the SampleDataInitializer. The final line disposes of the context instance. In our examples, the Dispose() method doesn’t have any code but needs to be implemented to satisfy the IDisposable interface. The following listing shows the updated constructor:

    public EnsureAutoLotDatabaseTestFixture()
    {
    var configuration = TestHelpers.GetConfiguration(); var context = TestHelpers.GetContext(configuration); SampleDataInitializer.ClearAndReseedDatabase(context); context.Dispose();
    }

    Add the Integration Test Classes
    The next step is to add the classes that will hold the automated tests. These classes are referred to as test fixtures. Add a new folder named IntegrationTests in the AutoLot.Dal.Tests folder and add five files named CarTests.cs, CustomerOrderViewModelTests.cs, CustomerTests.cs, MakeTests.cs, and OrderTests.cs to this folder.
    Depending on the capabilities of the test runner, xUnit tests are run in serial within a test fixture (class), but in parallel across test fixtures. This can be problematic when executing integration tests that interact with a database. Parallel database tests using a single database instance can cause blocks, produce erroneous results, and are generally problematic.
    xUnit test execution can be changed to serial across test fixtures by adding them into the same test collection. Test collections are defined by name using the Collection attribute on the class. Add the following Collection attribute to the top of all four classes:

    [Collection("Integration Tests")]

    Next, inherit from BaseTest and implement the IClassFixture interface in both classes. Add a constructor to receive the ITestOutputHelper instance and pass it to the base class. Update each class to match the following:

    //CarTests.cs
    namespace AutoLot.Dal.Tests.IntegrationTests;

    [Collection("Integation Tests")] public class CarTests
    : BaseTest, IClassFixture
    {
    public CarTests(ITestOutputHelper outputHelper) : base(outputHelper)
    {
    }
    }

    //CustomerOrderViewModelTests.cs
    namespace AutoLot.Dal.Tests.IntegrationTests;

    [Collection("Integation Tests")]
    public class CustomerOrderViewModelTests
    : BaseTest, IClassFixture
    {
    public CustomerOrderViewModelTests(ITestOutputHelper outputHelper)
    : base(outputHelper)
    {
    }
    }

    //CustomerTests.cs
    namespace AutoLot.Dal.Tests.IntegrationTests;

    [Collection("Integation Tests")] public class CustomerTests
    : BaseTest, IClassFixture

    {
    public CustomerTests(ITestOutputHelper outputHelper) : base(outputHelper)
    {
    }
    }

    //MakeTests.cs
    namespace AutoLot.Dal.Tests.IntegrationTests;

    [Collection("Integation Tests")] public class MakeTests
    : BaseTest, IClassFixture
    {
    public MakeTests(ITestOutputHelper outputHelper) : base(outputHelper)
    {
    }
    }

    //OrderTests.cs
    namespace AutoLot.Dal.Tests.IntegrationTests;

    [Collection("Integation Tests")] public class OrderTests
    : BaseTest, IClassFixture
    {
    public OrderTests(ITestOutputHelper outputHelper):base(outputHelper)
    {
    }
    }

    For the CarTests class, update the constructor to create an instance of the CarRepo and assign the instance to a private readonly class-level variable. Override the Dispose() method, and in that method, dispose of the repo.

    [Collection("Integration Tests")]
    public class CarTests : BaseTest, IClassFixture
    {
    private readonly ICarRepo _carRepo;
    public CarTests(ITestOutputHelper outputHelper) : base(outputHelper)
    {
    _carRepo = new CarRepo(Context);
    }

    public override void Dispose()
    {
    _carRepo.Dispose(); base.Dispose();
    }
    }

    Repeat the process for the CustomerOrderViewModelTests class, using the
    CustomerOrderViewModelRepo instead:

    [Collection("Integration Tests")]
    public class CustomerOrderViewModelTests : BaseTest, IClassFixture
    {
    private readonly ICustomerOrderViewModelRepo _repo;
    public CustomerOrderViewModelTests(ITestOutputHelper outputHelper) : base(outputHelper)
    {
    _repo = new CustomerOrderViewModelRepo(Context);
    }

    public override void Dispose()
    {
    _repo.Dispose(); base.Dispose();
    }
    }

    The setup for the CustomerTests class is simpler, since it does not use the CustomerRepo:

    [Collection("Integration Tests")]
    public class CustomerTests : BaseTest, IClassFixture
    {
    public CustomerOrderViewModelTests(ITestOutputHelper outputHelper) : base(outputHelper)
    {
    }
    }

    The same process is needed for the MakeTests class, using MakeRepo:

    [Collection("Integration Tests")]
    public class MakeTests : BaseTest, IClassFixture
    {
    private readonly IMakeRepo _repo;
    public MakeTests(ITestOutputHelper outputHelper) : base(outputHelper)
    {
    _repo = new MakeRepo(Context);
    }

    public override void Dispose()
    {
    _repo.Dispose(); base.Dispose();
    }
    }

    The final repository update is in the OrderTests class, using OrderRepo:

    [Collection("Integration Tests")]
    public class OrderTests : BaseTest, IClassFixture
    {
    private readonly IOrderRepo _repo;
    public OrderTests(ITestOutputHelper outputHelper) : base(outputHelper)
    {
    _repo = new OrderRepo(Context);
    }

    public override void Dispose()
    {
    _repo.Dispose(); base.Dispose();
    }
    }

    Querying the Database
    Recall that creating entity instances from a database data involves executing a LINQ statement or a SQL statement (using FromSqlRaw()/FromSqlInterpolated()) against the DbSet properties. When using LINQ, the statements are converted to SQL by the database provider and the LINQ translation engine, and the appropriate data is read from the database. Data can also be loaded using the FromSqlRaw() or
    FromSqlInterpolated() method using raw SQL strings and, optionally, additional LINQ statements. Entities loaded into the DbSet collections are added to the ChangeTracker by default but can be added without tracking. Data loaded in keyless DbSet collections is never tracked.
    If related entities are already loaded into the DbSet, EF Core will wire up the new instances along the navigation properties. For example, if the Cars are loaded into the DbSet collection and then the related Orders are loaded into the DbSet of the same ApplicationDbContext instance, the Car. Orders navigation property will return the related Order entities without requerying the database.
    Many of the methods demonstrated here have async versions available. The syntax of the LINQ queries are structurally the same, so I will only demonstrate the nonasync version.

    LINQ Queries
    The DbSet collection type implements (among other interfaces) IQueryable. This allows C# LINQ commands to be used to create queries to get data from the database. While all C# LINQ statements are available for use with the DbSet collection type, some LINQ statements might not be supported by the database provider, and additional LINQ statements are added by EF Core. Unsupported LINQ statements that cannot be translated into the database provider’s query language will throw a runtime exception. Some nontranslatable LINQ statements will execute on the client side if they are the last statement in the LINQ chain; however, others (like the update to the Take() method that works with ranges) will still throw an error unless the query is first executed using ToList() or a similar construct.

    LINQ Execution
    Where ToQueryString() is available, the tests in this next section set a variable (qs) to this value and are output to the test results using the ITestOutputHelper so you can examine the query while running the tests.

    Get All Records
    To get all the records for a table, simply use the DbSet property directly without any LINQ statements. Add the following Fact to the CustomerTests.cs class:

    [Fact]
    public void ShouldGetAllOfTheCustomers()
    {
    var qs = Context.Customers.ToQueryString(); var customers = Context.Customers.ToList(); Assert.Equal(5, customers.Count);
    }

    The statement gets translated into the following SQL:

    SELECT .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [Dbo].[Customers] AS

    The same process is used for Keyless entities, like the CustomerOrderViewModel, which is configured to get its data from the CustomerOrderView. Add the following test to the CustomerOrderViewModelTest.cs class to show getting data from the view:

    [Fact]
    public void ShouldGetAllViewModels()
    {
    var qs = Context.CustomerOrderViewModels.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}"); List list =
    Context.CustomerOrderViewModels.ToList(); Assert.NotEmpty(list);
    Assert.Equal(5, list.Count);
    }

    The statement gets translated into the following SQL:

    SELECT .[Color], .[DateBuilt], .[Display], .[FirstName],
    .[IsDrivable], .[LastName], .[Make], .[PetName], .[Price] FROM [CustomerOrderView] AS

    Filter Records
    The following test in the CustomerTests class shows querying for customers where the last name starts with a W (case insensitive):

    [Fact]
    public void ShouldGetCustomersWithLastNameW()
    {
    IQueryable query = Context.Customers
    .Where(x => x.PersonInformation.LastName.StartsWith("W")); var qs = query.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}");

    List customers = query.ToList(); Assert.Equal(2, customers.Count);
    foreach (var customer in customers)
    {
    var pi = customer.PersonInformation;
    Assert.StartsWith("W", pi.LastName, StringComparison.OrdinalIgnoreCase);
    }
    }

    The LINQ query gets translated into the following SQL:

    SELECT .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [dbo].[Customers] AS
    WHERE .[LastName] IS NOT NULL AND (.[LastName] LIKE N'W%')

    The next test in the CustomerTests class demonstrates chaining Where() methods in a LINQ query to find the customers where the last name starts with a W and the first name starts with an M. Note that since SQL Server is case insensitive, these queries are also case insensitive:

    [Fact]
    public void ShouldGetCustomersWithLastNameWAndFirstNameM()
    {
    IQueryable query = Context.Customers
    .Where(x => x.PersonInformation.LastName.StartsWith("W"))
    .Where(x => x.PersonInformation.FirstName.StartsWith("M")); var qs = query.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}");
    List customers = query.ToList(); Assert.Single(customers);
    foreach (var customer in customers)
    {
    var pi = customer.PersonInformation;
    Assert.StartsWith("W", pi.LastName, StringComparison.OrdinalIgnoreCase); Assert.StartsWith("M", pi.FirstName, StringComparison.OrdinalIgnoreCase);
    }
    }

    This test in the CustomerTests class repeats the same filter using a single Where() method instead of two chained methods:

    [Fact]
    public void ShouldGetCustomersWithLastNameWAndFirstNameM()
    {
    IQueryable query = Context.Customers
    .Where(x => x.PersonInformation.LastName.StartsWith("W") &&
    x.PersonInformation.FirstName.StartsWith("M"));
    var qs = query.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}"); List customers = query.ToList(); Assert.Single(customers);
    foreach (var customer in customers)

    {
    var pi = customer.PersonInformation;
    Assert.StartsWith("W", pi.LastName, StringComparison.OrdinalIgnoreCase); Assert.StartsWith("M", pi.FirstName, StringComparison.OrdinalIgnoreCase);
    }
    }

    Both queries get translated into the following SQL:

    SELECT .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [dbo].[Customers] AS
    WHERE (.[LastName] IS NOT NULL AND (.[LastName] LIKE N'W%')) AND (.[FirstName] IS NOT NULL AND (.[FirstName] LIKE N'M%'))

    The next test in the CustomerTests class demonstrates querying for customers where the last name starts with a W (case insensitive) or the last name starts with an H (case insensitive):

    [Fact]
    public void ShouldGetCustomersWithLastNameWOrH()
    {
    IQueryable query = Context.Customers
    .Where(x => x.PersonInformation.LastName.StartsWith("W") ||
    x.PersonInformation.LastName.StartsWith("H"));
    var qs = query.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}"); List customers = query.ToList(); Assert.Equal(3, customers.Count);
    foreach (var customer in customers)
    {
    var pi = customer.PersonInformation; Assert.True(
    pi.LastName.StartsWith("W",StringComparison.OrdinalIgnoreCase) || pi.LastName.StartsWith("H",StringComparison.OrdinalIgnoreCase));
    }
    }

    This gets translated into the following SQL:

    SELECT .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [dbo].[Customers] AS
    WHERE (.[LastName] IS NOT NULL AND (.[LastName] LIKE N'W%')) OR (.[LastName] IS NOT NULL AND (.[LastName] LIKE N'H%'))

    The following in the CustomerTests class also queries for customers where the last name starts with a W (case insensitive) or the last name starts with an H (case insensitive). This test demonstrates using the EF.Functions.Like() method. Note that you must include the wildcard (%) yourself.

    [Fact]
    public void ShouldGetCustomersWithLastNameWOrH()
    {
    IQueryable query = Context.Customers

    .Where(x => EF.Functions.Like(x.PersonInformation.LastName, "W%") ||
    EF.Functions.Like(x.PersonInformation.LastName, "H%"));
    var qs = query.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}"); List customers = query.ToList(); Assert.Equal(3, customers.Count);
    }

    This gets translated into the following SQL (notice it does not check for null):

    SELECT .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [Dbo].[Customers] AS
    WHERE (.[LastName] LIKE N'W%') OR (.[LastName] LIKE N'H%')

    The following test in the CarTests.cs class uses a Theory to query for the number of Car records (drivable or not) in the Inventory table based on a specified MakeId:

    [Theory] [InlineData(1, 2)]
    [InlineData(2, 1)]
    [InlineData(3, 1)]
    [InlineData(4, 2)]
    [InlineData(5, 3)]
    [InlineData(6, 1)]
    public void ShouldGetTheCarsByMake(int makeId, int expectedCount)
    {
    IQueryable query =
    Context.Cars.IgnoreQueryFilters().Where(x => x.MakeId == makeId);
    var qs = query.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}"); var cars = query.ToList(); Assert.Equal(expectedCount, cars.Count);
    }

    Each InlineData row becomes a unique test in the test runner. For this example, six tests are processed, and six queries are executed against the database. Here is the SQL from one of the tests (the only difference in the queries from the other tests in the Theory is the value for MakeId):

    DECLARE @ makeId_0 int = 1;

    SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],
    [i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp] FROM [dbo].[Inventory] AS [i]
    WHERE [i].[MakeId] = @ makeId_0

    The next test uses the Car repository to get the number of records for each Make. Since the GetCarsBy()
    method leaves the query filters in place, there is one less record when the MakeId is one.

    [Theory] [InlineData(1, 1)]
    [InlineData(2, 1)]
    [InlineData(3, 1)]

    [InlineData(4, 2)]
    [InlineData(5, 3)]
    [InlineData(6, 1)]
    public void ShouldGetTheCarsByMakeUsingTheRepo(int makeId, int expectedCount)
    {
    var qs = _carRepo.GetAllBy(makeId).AsQueryable().ToQueryString(); OutputHelper.WriteLine($"Query: {qs}");
    var cars = _carRepo.GetAllBy(makeId).ToList(); Assert.Equal(expectedCount, cars.Count);
    }

    When examining the generated query, you can see the query filter that excluded records that aren’t drivable:

    DECLARE @ makeId_0 int = 1;

    SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],
    [i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp], [m].[Id], [m].[Name], [m].[TimeStamp]
    FROM [dbo].[Inventory] AS [i]
    INNER JOIN [dbo].[Makes] AS [m] ON [i].[MakeId] = [m].[Id]
    WHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND ([i].[MakeId] = @ makeId_0) ORDER BY [i].[PetName]

    Sort Records
    Recall that sorting is achieved by using OrderBy()/OrderByDescending(). If more than one story level is required, add ThenBy()/ThenByDescending() for each subsequent property. Ascending and descending sorts for different properties can be used together, as shown in the following test in the CustomerTests.cs file:

    [Fact]
    public void ShouldSortByLastNameThenFirstName()
    {
    //Sort by Last name then first name var query = Context.Customers
    .OrderBy(x => x.PersonInformation.LastName)
    .ThenByDescending(x => x.PersonInformation.FirstName); var qs = query.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}");
    var customers = query.ToList();
    for (int x = 0; x < customers.Count - 1; x++)
    {
    Compare(customers[x].PersonInformation, customers[x + 1].PersonInformation);
    }
    static void Compare(Person p1, Person p2)
    {
    var compareValue = string.Compare(p1.LastName, p2.LastName, StringComparison.CurrentCultureIgnoreCase);
    Assert.True(compareValue <=0); if (compareValue == 0)

    {
    //Descending first name sort Assert.True(string.Compare(p1.FirstName,p2.FirstName,
    StringComparison.CurrentCultureIgnoreCase) >= 0);
    }
    }
    }

    The preceding LINQ query gets translated into the following:

    SELECT .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [dbo].[Customers] AS
    ORDER BY .[LastName], .[FirstName] DESC

    Reverse Sort Records
    The Reverse() method reverses the entire sort order, as demonstrated in the next test:

    [Fact]
    public void ShouldSortByFirstNameThenLastNameUsingReverse()
    {
    //Sort by Last name then first name descending then reverse the sort var query = Context.Customers
    .OrderBy(x => x.PersonInformation.LastName)
    .ThenByDescending(x => x.PersonInformation.FirstName)
    .Reverse();
    var qs = query.ToQueryString(); var customers = query.ToList();
    //if only one customer, nothing to test if (customers.Count <= 1) { return; }

    for (int x = 0; x < customers.Count - 1; x++)
    {
    var pi1 = customers[x].PersonInformation;
    var pi2 = customers[x + 1].PersonInformation;
    var compareLastName = string.Compare(pi1.LastName, pi2.LastName, StringComparison.CurrentCultureIgnoreCase); Assert.True(compareLastName >= 0);
    if (compareLastName != 0) continue;
    var compareFirstName = string.Compare(pi1.FirstName, pi2.FirstName, StringComparison.CurrentCultureIgnoreCase); Assert.True(compareFirstName <= 0);
    }
    }

    The preceding LINQ query gets translated into the following, inverting the original
    OrderBy()/ThenByDescending() query:

    SELECT .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [dbo].[Customers] AS
    ORDER BY .[LastName] DESC, .[FirstName]

    Single-Record Queries
    Because of the immediate execution of the single-record LINQ statements, the ToQueryString() method isn’t available. The listed query translations are provided by using SQL Server Profiler. All of the single- record tests are in the CustomerTests.cs file.

    Using First
    When using the parameterless form of First() and FirstOrDefault(), the first record (based on database order or any preceding ordering clauses) will be returned. The following test shows querying for the first record based on database order:

    [Fact]
    public void GetFirstMatchingRecordDatabaseOrder()
    {
    //Gets the first record, database order var customer = Context.Customers.First(); Assert.Equal(1, customer.Id);
    }

    The preceding LINQ query gets translated into the following:

    SELECT TOP(1) .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [Dbo].[Customers] AS
    The following test demonstrates getting the first record based on “last name, first name” order:

    [Fact]
    public void GetFirstMatchingRecordNameOrder()
    {
    //Gets the first record, lastname, first name order var customer = Context.Customers
    .OrderBy(x => x.PersonInformation.LastName)
    .ThenBy(x => x.PersonInformation.FirstName)
    .First();
    Assert.Equal(1, customer.Id);
    }

    The preceding LINQ query gets translated into the following:

    SELECT TOP(1) .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [Dbo].[Customers] AS
    ORDER BY .[LastName], .[FirstName]
    The following test asserts that an exception is thrown if there isn’t a match when using First(): [Fact]
    public void FirstShouldThrowExceptionIfNoneMatch()
    {

    //Filters based on Id. Throws due to no match
    Assert.Throws(() => Context.Customers.First(x => x.Id == 10));
    }

    ■Note Assert.Throws() is a special type of assert statement. it is expecting an exception to the thrown by the code in the expression. if an exception doesn’t get thrown, the assertion fails.

    When using FirstOrDefault(), instead of an exception, the result is a null record when no data is returned. This test shows creating an expression variable:

    [Fact]
    public void FirstOrDefaultShouldReturnDefaultIfNoneMatch()
    {
    //Expression<Func> is a lambda expression Expression<Func<Customer, bool>> expression = x => x.Id == 10;
    //Returns null when nothing is found
    var customer = Context.Customers.FirstOrDefault(expression);
    Assert.Null(customer);
    }

    The preceding LINQ query gets translated into the same as the previous:

    SELECT TOP(1) .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [Dbo].[Customers] AS
    WHERE .[Id] = 10

    Using Last
    When using the parameterless form of Last() and LastOrDefault(), the last record (based on any preceding ordering clauses) will be returned. As a reminder, EF Core will throw an exception if no sort is specified. The following test gets the last record based on “last name, first name” order:

    [Fact]
    public void GetLastMatchingRecordNameOrder()
    {
    //Gets the last record, lastname desc, first name desc order var customer = Context.Customers
    .OrderBy(x => x.PersonInformation.LastName)
    .ThenBy(x => x.PersonInformation.FirstName)
    .Last();
    Assert.Equal(4, customer.Id);
    }

    EF Core reverses the order by statements and then takes top(1) to get the result. Here is the executed query:

    SELECT TOP(1) .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [Dbo].[Customers] AS
    ORDER BY .[LastName] DESC, .[FirstName] DESC

    This test confirms that EF Core throws an exception when Last() is used without an
    OrderBy()/OrderByDescending():

    [Fact]
    public void LastShouldThrowIfNoSortSpecified()
    {
    Assert.Throws(() => Context.Customers.Last());
    }

    Using Single
    Conceptually, Single()/SingleOrDefault() works the same as First()/FirstOrDefault(). The main difference is that Single()/SingleOrDefault() returns Top(2) instead of Top(1) and throws an exception if two records are returned from the database. The following test retrieves the single record where Id == 1:

    [Fact]
    public void GetOneMatchingRecordWithSingle()
    {
    //Gets the first record, database order
    var customer = Context.Customers.Single(x => x.Id == 1);
    Assert.Equal(1, customer.Id);
    }

    The preceding LINQ query gets translated into the following:

    SELECT TOP(2) .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [Dbo].[Customers] AS
    WHERE .[Id] = 1

    Single() throws an exception if no records are returned.[Fact] public void SingleShouldThrowExceptionIfNoneMatch()
    {
    //Filters based on Id. Throws due to no match
    Assert.Throws(() => Context.Customers.Single(x => x.Id == 10));
    }

    When using Single() or SingleOrDefault() and more than one record is returned, an exception is thrown.

    [Fact]
    public void SingleShouldThrowExceptionIfMoreThenOneMatch()
    {
    // Throws due to more than one match
    Assert.Throws(() => Context.Customers.Single());
    }
    [Fact]
    public void SingleOrDefaultShouldThrowExceptionIfMoreThenOneMatch()
    {
    // Throws due to more than one match
    Assert.Throws(() => Context.Customers.SingleOrDefault());
    }

    When using SingleOrDefault(), instead of an exception, the result is a null record when no data is returned.

    [Fact]
    public void SingleOrDefaultShouldReturnDefaultIfNoneMatch()
    {
    //Expression<Func> is a lambda expression Expression<Func<Customer, bool>> expression = x => x.Id == 10;
    //Returns null when nothing is found
    var customer = Context.Customers.SingleOrDefault(expression);
    Assert.Null(customer);
    }

    The preceding LINQ query gets translated into the following:
    SELECT TOP(2) .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [Dbo].[Customers] AS
    WHERE .[Id] = 10

    Global Query Filters
    Recall that there is a global query filter on the Car entity to filter out any cars where IsDrivable is false. Open the CarTests.cs class and add the following test that gets all the records that pass the query filter:

    [Fact]
    public void ShouldReturnDrivableCarsWithQueryFilterSet()
    {
    IQueryable query = Context.Cars; var qs = query.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}"); var cars = query.ToList(); Assert.NotEmpty(cars);
    Assert.Equal(9, cars.Count);
    }

    Recall that we create 10 cars in the data initialization process, and one of them is set to be nondrivable.
    When the query is executed, the global query filter is applied, and the following SQL is executed:

    SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],
    [i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp] FROM [dbo].[Inventory] AS [i]
    WHERE [i].[IsDrivable] = CAST(1 AS bit)

    Disable the Query Filters
    To disable global query filters for the entities in a query, add the IgnoreQueryFilters() method to the LINQ query. If there is more than one entity with a global query filter and some of the entities’ filters are required, they must be added to the LINQ statement’s Where() methods. Add the following test to the CarTests.cs class, which disables the query filter and returns all records:

    [Fact]
    public void ShouldGetAllOfTheCars()
    {
    IQueryable query = Context.Cars.IgnoreQueryFilters(); var qs = query.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}");
    var cars = query.ToList(); Assert.Equal(10, cars.Count);
    }

    As one would expect, the where clause eliminating nondrivable cars is no longer on the generated SQL.

    SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable], [i]. [MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp]
    FROM [dbo].[Inventory] AS [i]

    Query Filters on Navigation Properties
    In addition to the global query filter on the Car entity, we added a query filter to the CarNavigation property of the Order entity. To see this in action, add the following test to the OrderTests.cs class:

    [Fact]
    public void ShouldGetAllOrdersExceptFiltered()
    {
    var query = Context.Orders.AsQueryable(); var qs = query.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}"); var orders = query.ToList(); Assert.NotEmpty(orders); Assert.Equal(4,orders.Count);
    }

    The generated SQL is listed here:
    SELECT [o].[Id], [o].[CarId], [o].[CustomerId], [o].[TimeStamp] FROM [dbo].[Orders] AS [o]
    INNER JOIN (
    SELECT [i].[Id], [i].[IsDrivable] FROM [dbo].[Inventory] AS [i]
    WHERE [i].[IsDrivable] = CAST(1 AS bit)
    ) AS [t] ON [o].[CarId] = [t].[Id]
    WHERE [t].[IsDrivable] = CAST(1 AS bit)

    Because the CarNavigation navigation property is a required navigation property, the query translation engine uses an INNER JOIN, eliminating the Order records where the Car is nondrivable.

    To return all records, add IgnoreQueryFilters() to your LINQ query.[Fact] public void ShouldGetAllOrders()
    {
    var query = Context.Orders.IgnoreQueryFilters(); var qs = query.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}");

    var orders = query.ToList(); Assert.NotEmpty(orders); Assert.Equal(5, orders.Count);
    }

    You can see from the generated SQL that the where clause has been removed, and the query has been simplified:

    SELECT [o].[Id], [o].[CarId], [o].[CustomerId], [o].[TimeStamp] FROM [dbo].[Orders] AS [o]

    Load Related Data Eagerly
    Entities that are linked through navigation properties can be instantiated in one query using eager loading. The Include() method indicates a join to the related entity, and the ThenInclude() method is used for subsequent joins to other entities. Both of these methods will be demonstrated in these tests. When the Include()/ThenInclude() methods are translated into SQL, required relationships use an inner join, and optional relationships use a left join.
    Add the following test to the CarTests.cs class to show a single Include(): [Fact]
    public void ShouldGetAllOfTheCarsWithMakes()
    {
    IIncludableQueryable<Car, Make> query = Context.Cars.Include(c => c.MakeNavigation); var qs = query.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}");
    var cars = query.ToList(); Assert.Equal(9, cars.Count);
    }

    The query adds the MakeNavigation property to the results, performing an inner join with the following SQL being executed. The query returns all the columns from both tables, and then EF Core created Car and Make instances from the returned data. Notice the global query filter is in effect.

    SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],
    [i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp], [m].[Id], [m].[Name], [m].[TimeStamp]
    FROM [dbo].[Inventory] AS [i]
    INNER JOIN [dbo].[Makes] AS [m] ON [i].[MakeId] = [m].[Id] WHERE [i].[IsDrivable] = CAST(1 AS bit)

    The next test demonstrates using two sets of related data. The first is getting the Make information (same as the previous test), while the second is getting the Orders and then the Customers attached to the Orders. The entire test is also filtering out the Car records that don’t have any orders.

    [Fact]
    public void ShouldGetCarsOnOrderWithRelatedProperties()
    {

    IIncludableQueryable<Car, Customer?> query = Context.Cars
    .Where(c => c.Orders.Any())
    .Include(c => c.MakeNavigation)
    .Include(c => c.Orders).ThenInclude(o => o.CustomerNavigation);
    var qs = query.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}"); var cars = query.ToList(); Assert.Equal(4, cars.Count); cars.ForEach(c =>
    {
    Assert.NotNull(c.MakeNavigation); Assert.NotNull(c.Orders.ToList()[0].CustomerNavigation);
    });
    }

    The generated query is rather lengthy. Here is the generated query:

    SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],
    [i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp], [m].[Id],
    [m].[Name], [m].[TimeStamp], [t0].[Id], [t0].[CarId], [t0].[CustomerId],
    [t0].[TimeStamp], [t0].[Id0], [t0].[TimeStamp0], [t0].[FirstName],
    [t0].[FullName], [t0].[LastName], [t0].[Id1] FROM [dbo].[Inventory] AS [i]
    INNER JOIN [dbo].[Makes] AS [m] ON [i].[MakeId] = [m].[Id] LEFT JOIN (
    SELECT [o0].[Id], [o0].[CarId], [o0].[CustomerId], [o0].[TimeStamp],
    .[Id] AS [Id0], .[TimeStamp] AS [TimeStamp0], .[FirstName],
    .[FullName], .[LastName], [t1].[Id] AS [Id1] FROM [dbo].[Orders] AS [o0]
    INNER JOIN (
    SELECT [i1].[Id], [i1].[IsDrivable] FROM [dbo].[Inventory] AS [i1]
    WHERE [i1].[IsDrivable] = CAST(1 AS bit)
    ) AS [t1] ON [o0].[CarId] = [t1].[Id]
    INNER JOIN [dbo].[Customers] AS ON [o0].[CustomerId] = .[Id] WHERE [t1].[IsDrivable] = CAST(1 AS bit)
    ) AS [t0] ON [i].[Id] = [t0].[CarId]
    WHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND EXISTS ( SELECT 1
    FROM [dbo].[Orders] AS [o] INNER JOIN (
    SELECT [i0].[Id], [i0].[Color], [i0].[DateBuilt], [i0].[Display],
    [i0].[IsDrivable], [i0].[MakeId], [i0].[PetName], [i0].[Price], [i0].[TimeStamp] FROM [dbo].[Inventory] AS [i0]
    WHERE [i0].[IsDrivable] = CAST(1 AS bit)
    ) AS [t] ON [o].[CarId] = [t].[Id]
    WHERE ([t].[IsDrivable] = CAST(1 AS bit)) AND ([i].[Id] = [o].[CarId]))
    ORDER BY [i].[Id], [m].[Id], [t0].[Id], [t0].[Id1], [t0].[Id0]

    If you run the same query without the query filters, the query becomes much simpler. Here is the updated test that removes the query filters:

    [Fact]
    public void ShouldGetCarsOnOrderWithRelatedPropertiesIgnoreFilters()
    {
    IIncludableQueryable<Car, Customer> query = Context.Cars.IgnoreQueryFilters().Where(c => c.Orders.Any())
    .Include(c => c.MakeNavigation)
    .Include(c => c.Orders).ThenInclude(o => o.CustomerNavigati on) ;
    var qs = query.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}"); var cars = query.ToList(); Assert.Equal(5, cars.Count); cars.ForEach(c =>
    {
    Assert.NotNull(c.MakeNavigation); Assert.NotNull(c.Orders.ToList()[0].CustomerNavigation);
    });
    }

    The generated query is rather lengthy. Here is the generated query:

    SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable], [i].
    [MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp], [m].[Id], [m].[Name], [m].
    [TimeStamp], [t].[Id], [t].[CarId], [t].[CustomerId], [t].[TimeStamp], [t].[Id0], [t]. [TimeStamp0], [t].[FirstName], [t].[FullName], [t].[LastName]
    FROM [dbo].[Inventory] AS [i]
    INNER JOIN [dbo].[Makes] AS [m] ON [i].[MakeId] = [m].[Id] LEFT JOIN (
    SELECT [o0].[Id], [o0].[CarId], [o0].[CustomerId], [o0].[TimeStamp], .[Id] AS
    [Id0], .[TimeStamp] AS [TimeStamp0], .[FirstName], .[FullName], .[LastName] FROM [dbo].[Orders] AS [o0]
    INNER JOIN [dbo].[Customers] AS ON [o0].[CustomerId] = .[Id]
    ) AS [t] ON [i].[Id] = [t].[CarId] WHERE EXISTS (
    SELECT 1
    FROM [dbo].[Orders] AS [o] WHERE [i].[Id] = [o].[CarId])
    ORDER BY [i].[Id], [m].[Id], [t].[Id], [t].[Id0]

    Splitting Queries on Related Data
    The more joins added into a LINQ query, the more complex the resulting query becomes. As the previous examples demonstrated, query filters can make the queries even more complex. EF Core 5 introduced the ability to run complicated joins as split queries by adding the AsSplitQuery() method into the LINQ query. As discussed in the previous chapters, this can gain efficiency at the risk of data inconsistency. The following test demonstrates the same query just exercised, but as a split query:

    [Fact]
    public void ShouldGetCarsOnOrderWithRelatedPropertiesAsSplitQuery()
    {
    IQueryable query = Context.Cars.Where(c => c.Orders.Any())

    .Include(c => c.MakeNavigation)
    .Include(c => c.Orders).ThenInclude(o => o.CustomerNavigation)
    .AsSplitQuery();
    var qs = query.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}"); var cars = query.ToList(); Assert.Equal(4, cars.Count); cars.ForEach(c =>
    {
    Assert.NotNull(c.MakeNavigation); Assert.NotNull(c.Orders.ToList()[0].CustomerNavigation);
    });
    }

    The ToQueryString() method returns only the first query, so the following queries were captured using SQL Server Profiler:

    SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable], [i]. [MakeId],
    [i].[PetName], [i].[Price], [i].[TimeStamp], [m].[Id], [m].[Name], [m].[TimeStamp] FROM [dbo].[Inventory] AS [i]
    INNER JOIN [dbo].[Makes] AS [m] ON [i].[MakeId] = [m].[Id] WHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND EXISTS (
    SELECT 1
    FROM [dbo].[Orders] AS [o] INNER JOIN (
    SELECT [i0].[Id], [i0].[Color], [i0].[DateBuilt], [i0].[Display], [i0].[IsDrivable],
    [i0].[MakeId], [i0].[PetName], [i0].[Price], [i0].[TimeStamp] FROM [dbo].[Inventory] AS [i0]
    WHERE [i0].[IsDrivable] = CAST(1 AS bit)
    ) AS [t] ON [o].[CarId] = [t].[Id]
    WHERE ([t].[IsDrivable] = CAST(1 AS bit)) AND ([i].[Id] = [o].[CarId])) ORDER BY [i].[Id], [m].[Id]

    SELECT [t0].[Id], [t0].[CarId], [t0].[CustomerId], [t0].[TimeStamp], [t0].[Id0],
    [t0].[TimeStamp0], [t0].[FirstName], [t0].[FullName], [t0].[LastName], [i].[Id], [m].[Id]
    FROM [dbo].[Inventory] AS [i]
    INNER JOIN [dbo].[Makes] AS [m] ON [i].[MakeId] = [m].[Id] INNER JOIN (
    SELECT [o0].[Id], [o0].[CarId], [o0].[CustomerId], [o0].[TimeStamp],
    .[Id] AS [Id0], .[TimeStamp] AS [TimeStamp0], .[FirstName], .[FullName], .[LastName]
    FROM [dbo].[Orders] AS [o0] INNER JOIN (
    SELECT [i1].[Id], [i1].[IsDrivable] FROM [dbo].[Inventory] AS [i1]
    WHERE [i1].[IsDrivable] = CAST(1 AS bit)
    ) AS [t1] ON [o0].[CarId] = [t1].[Id]
    INNER JOIN [dbo].[Customers] AS ON [o0].[CustomerId] = .[Id] WHERE [t1].[IsDrivable] = CAST(1 AS bit)
    ) AS [t0] ON [i].[Id] = [t0].[CarId]

    WHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND EXISTS ( SELECT 1
    FROM [dbo].[Orders] AS [o] INNER JOIN (
    SELECT [i0].[Id], [i0].[Color], [i0].[DateBuilt], [i0].[Display], [i0].[IsDrivable],
    [i0].[MakeId], [i0].[PetName], [i0].[Price], [i0].[TimeStamp] FROM [dbo].[Inventory] AS [i0]
    WHERE [i0].[IsDrivable] = CAST(1 AS bit)
    ) AS [t] ON [o].[CarId] = [t].[Id]
    WHERE ([t].[IsDrivable] = CAST(1 AS bit)) AND ([i].[Id] = [o].[CarId])) ORDER BY [i].[Id], [m].[Id]

    Once again, removing the query filters greatly simplifies the queries generated. Here is the updated test:

    [Fact]
    public void ShouldGetCarsOnOrderWithRelatedPropertiesAsSplitQueryIgnoreQueryFilters()
    {
    IQueryable query = Context.Cars.IgnoreQueryFilters()
    .Where(c => c.Orders.Any())
    .Include(c => c.MakeNavigation)
    .Include(c => c.Orders).ThenInclude(o => o.CustomerNavigation)
    .AsSplitQuery(); var qs = query.ToQueryString();
    OutputHelper.WriteLine($"Query: {qs}"); var cars = query.ToList(); Assert.Equal(5, cars.Count); cars.ForEach(c =>
    {
    Assert.NotNull(c.MakeNavigation); Assert.NotNull(c.Orders.ToList()[0].CustomerNavigation);
    });
    }

    Here are the generated queries:

    SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],
    [i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp], [m].[Id], [m].[Name], [m]. [TimeStamp]
    FROM [dbo].[Inventory] AS [i]
    INNER JOIN [dbo].[Makes] AS [m] ON [i].[MakeId] = [m].[Id] WHERE EXISTS (
    SELECT 1
    FROM [dbo].[Orders] AS [o] WHERE [i].[Id] = [o].[CarId])
    ORDER BY [i].[Id], [m].[Id]

    SELECT [t].[Id], [t].[CarId], [t].[CustomerId], [t].[TimeStamp], [t].[Id0],
    [t].[TimeStamp0], [t].[FirstName], [t].[FullName], [t].[LastName], [i].[Id], [m].[Id] FROM [dbo].[Inventory] AS [i]
    INNER JOIN [dbo].[Makes] AS [m] ON [i].[MakeId] = [m].[Id]

    INNER JOIN (
    SELECT [o0].[Id], [o0].[CarId], [o0].[CustomerId], [o0].[TimeStamp],
    .[Id] AS [Id0], .[TimeStamp] AS [TimeStamp0], .[FirstName], .[FullName], .[LastName]
    FROM [dbo].[Orders] AS [o0]
    INNER JOIN [dbo].[Customers] AS ON [o0].[CustomerId] = .[Id]
    ) AS [t] ON [i].[Id] = [t].[CarId] WHERE EXISTS (
    SELECT 1
    FROM [dbo].[Orders] AS [o] WHERE [i].[Id] = [o].[CarId])
    ORDER BY [i].[Id], [m].[Id]

    Filtering Related Data
    EF Core 5 introduces the ability to filter when including collection properties. Prior to EF Core 5, the only way to get a filtered list for a collection navigation property was to use explicit loading. Add the following test into the MakeTests.cs class, which demonstrates getting all the Make records and those cars that are yellow:

    [Fact]
    public void ShouldGetAllMakesAndCarsThatAreYellow()
    {
    var query = Context.Makes.IgnoreQueryFilters()
    .Include(x => x.Cars.Where(x => x.Color == "Yellow")); var qs = query.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}");
    var makes = query.ToList(); Assert.NotNull(makes); Assert.NotEmpty(makes);
    Assert.NotEmpty(makes.Where(x => x.Cars.Any())); Assert.Empty(makes.First(m => m.Id == 1).Cars); Assert.Empty(makes.First(m => m.Id == 2).Cars); Assert.Empty(makes.First(m => m.Id == 3).Cars); Assert.Single(makes.First(m => m.Id == 4).Cars); Assert.Empty(makes.First(m => m.Id == 5).Cars);
    }

    The generated SQL is as follows:

    SELECT [m].[Id], [m].[Name], [m].[TimeStamp], [t].[Id], [t].[Color],
    [t].[DateBuilt], [t].[Display], [t].[IsDrivable], [t].[MakeId],
    [t].[PetName], [t].[Price], [t].[TimeStamp] FROM [dbo].[Makes] AS [m]
    LEFT JOIN (
    SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],
    [i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp] FROM [dbo].[Inventory] AS [i]
    WHERE [i].[Color] = N'Yellow'
    ) AS [t] ON [m].[Id] = [t].[MakeId]
    ORDER BY [m].[Id], [t].[Id]

    Changing the query to a split query yields this SQL (collection from SQL Server Profiler): SELECT [m].[Id], [m].[Name], [m].[TimeStamp]
    FROM [dbo].[Makes] AS [m]
    ORDER BY [m].[Id]

    SELECT [t].[Id], [t].[Color], [t].[DateBuilt], [t].[Display], [t].[IsDrivable],
    [t].[MakeId], [t].[PetName], [t].[Price], [t].[TimeStamp], [m].[Id] FROM [dbo].[Makes] AS [m]
    INNER JOIN (
    SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],
    [i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp] FROM [dbo].[Inventory] AS [i]
    WHERE [i].[Color] = N'Yellow'
    ) AS [t] ON [m].[Id] = [t].[MakeId] ORDER BY [m].[Id]

    Load Related Data Explicitly
    If the related data needs to be loaded after the principal entity was queried into memory, the related entities can be retrieved from the database with subsequent database calls. This is triggered using the Entry() method on the derived DbContext. When loading entities on the many end of a one-to-many relationship, use the Collection() method on the Entry result. To load entities on the one end of a one-to-many (or
    in a one-to-one relationship), use the Reference() method. Calling Query() on the Collection() or Reference() method returns an IQueryable that can be used to get the query string (as shown in the following tests) and to manage query filters (as shown in the next section). To execute the query and load the record(s), call the Load() method on the Collection(), Reference(), or Query() method. Query execution happens immediately when Load() is called.
    The following test (back in the CarTests.cs class) shows how to load a reference navigation property on the Car entity:

    [Fact]
    public void ShouldGetReferenceRelatedInformationExplicitly()
    {
    var car = Context.Cars.First(x => x.Id == 1); Assert.Null(car.MakeNavigation);
    var query = Context.Entry(car).Reference(c => c.MakeNavigation).Query(); var qs = query.ToQueryString();
    OutputHelper.WriteLine($"Query: {qs}"); query.Load(); Assert.NotNull(car.MakeNavigation);
    }

    The generated SQL to get the Make information is as follows (the Car record was already queried):

    DECLARE @ p_0 int = 1;

    SELECT [m].[Id], [m].[Name], [m].[TimeStamp] FROM [dbo].[Makes] AS [m]
    WHERE [m].[Id] = @ p_0

    This test shows how to load a collection navigation property on the Car entity:

    [Fact]
    public void ShouldGetCollectionRelatedInformationExplicitly()
    {
    var car = Context.Cars.First(x => x.Id == 1); Assert.Empty(car.Orders);
    var query = Context.Entry(car).Collection(c => c.Orders).Query(); var qs = query.ToQueryString();
    OutputHelper.WriteLine($"Query: {qs}"); query.Load(); Assert.Single(car.Orders);
    }

    The generated SQL is as follows:

    DECLARE @ p_0 int = 1;

    SELECT [o].[Id], [o].[CarId], [o].[CustomerId], [o].[TimeStamp] FROM [dbo].[Orders] AS [o]
    INNER JOIN (
    SELECT [i].[Id], [i].[IsDrivable] FROM [dbo].[Inventory] AS [i]
    WHERE [i].[IsDrivable] = CAST(1 AS bit)
    ) AS [t] ON [o].[CarId] = [t].[Id]
    WHERE ([t].[IsDrivable] = CAST(1 AS bit)) AND ([o].[CarId] = @ p_0)

    Load Related Data Explicitly with Query Filters
    In addition to shaping queries generated when eagerly loading related data, global query filters are active when explicitly loading related data. Take the following test (in the MakeTests.cs class):

    [Theory] [InlineData(1,1)] [InlineData(2,1)] [InlineData(3,1)] [InlineData(4,2)] [InlineData(5,3)] [InlineData(6,1)]
    public void ShouldGetAllCarsForAMakeExplicitlyWithQueryFilters(int makeId, int carCount)
    {
    var make = Context.Makes.First(x => x.Id == makeId);
    IQueryable query = Context.Entry(make).Collection(c => c.Cars).Query(); var qs = query.ToQueryString();
    OutputHelper.WriteLine($"Query: {qs}"); query.Load(); Assert.Equal(carCount,make.Cars.Count());
    }

    This test is similar to ShouldGetTheCarsByMake() from the “Filter Records” section. However, instead of just getting the Car records that have a certain MakeId, the test first gets a Make record and then explicitly loads the Car records for the already retrieved Make record. The generated query is shown here:

    DECLARE @ p_0 int = 1;

    SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],
    [i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp] FROM [dbo].[Inventory] AS [i]
    WHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND ([i].[MakeId] = @ p_0)

    Notice that the query filter is still being used, even though the principal entity in the query is the Make record. To turn off query filters when explicitly loading records, call IgnoreQueryFilters() in conjunction with the Query() method. Here is the test that turns off query filters (again, in the MakeTests.cs class):

    [Theory] [InlineData(1, 2)]
    [InlineData(2, 1)]
    [InlineData(3, 1)]
    [InlineData(4, 2)]
    [InlineData(5, 3)]
    [InlineData(6, 1)]
    public void ShouldGetAllCarsForAMakeExplicitly(int makeId, int carCount)
    {
    var make = Context.Makes.First(x => x.Id == makeId); IQueryable query =
    Context.Entry(make).Collection(c => c.Cars).Query().IgnoreQueryFilters(); var qs = query.IgnoreQueryFilters().ToQueryString(); OutputHelper.WriteLine($"Query: {qs}");
    query.Load();
    Assert.Equal(carCount, make.Cars.Count());
    }

    Temporal Queries
    This section exercises EF Core’s ability to retrieve historical data from temporal tables. As a reminder, the repos that derive from the TemporalTableBaseRepo contain the methods to query temporal tables using one of the five temporal query operators. To demonstrate this, open the MakeTests class, and add the following test at the bottom of the file:

    [Fact]
    public void ShouldGetAllHistoryRows()
    {
    var make = new Make { Name = "TestMake" };
    _repo.Add(make); Thread.Sleep(2000); make.Name = "Updated Name";
    _repo.Update(make);
    Thread.Sleep(2000);
    _repo.Delete(make);

    var list = _repo.GetAllHistory().Where(x => x.Entity.Id == make.Id).ToList(); Assert.Equal(2, list.Count);
    Assert.Equal("TestMake", list[0].Entity.Name); Assert.Equal("Updated Name", list[1].Entity.Name); Assert.Equal(list[0].ValidTo, list[1].ValidFrom);
    }

    The test creates a new Make record and adds it to the database. After pausing operation for two seconds, the Name is updated, and the change saved. After another two-second pause, the record is deleted. The test then uses the MakeRepo to get all the history for the Make record, confirms that there are two records in the history, makes sure the records are retrieved in ValidFrom order, and makes sure that the ValidTo of the first record exactly matches the ValidFrom of the second record.

    SQL Queries with LINQ
    This section exercises EF Core’s ability to retrieve data using raw SQL queries using the DbSet’s FromSqlRaw() or FromSqlInterpolated() methods. The first test (in the CarTests.cs class) uses a raw SQL query to get all the records from the Inventory table. Notice that the query must use the database names and not the entity names as well as add in the timestamp columns for the temporal functionality:

    [Fact]
    public void ShouldNotGetTheLemonsUsingFromSql()
    {
    var entity = Context.Model.FindEntityType($"{typeof(Car).FullName}"); var tableName = entity.GetTableName();
    var schemaName = entity.GetSchema(); var query = Context.Cars
    .FromSqlRaw($"Select *,ValidFrom,ValidTo from {schemaName}.{tableName}"); var qs = query.ToQueryString();
    OutputHelper.WriteLine($"Query: {qs}"); var cars = query.ToList(); Assert.Equal(9, cars.Count);
    }

    When using raw SQL queries, the query gets wrapped into a larger query by EF Core to support the query filter. If the statement was terminated with a semicolon, the query would not be executable on SQL Server.

    SELECT [a].[Id], [a].[Color], [a].[DateBuilt], [a].[Display], [a].[IsDrivable], [a].
    [MakeId], [a].[PetName], [a].[Price], [a].[TimeStamp], [a].[ValidFrom], [a].[ValidTo] FROM (
    Select *,ValidFrom,ValidTo from dbo.Inventory
    ) AS [a]
    WHERE [a].[IsDrivable] = CAST(1 AS bit)

    When the query filter is removed, the generated SQL becomes the same SQL as the string passed into the FromSqlRaw() method:

    Select *,ValidFrom,ValidTo from dbo.Inventory

    The following test demonstrates using FromSqlInterpolated() with additional LINQ statements (including the MakeNavigation):

    [Fact]
    public void ShouldGetOneCarUsingInterpolation()
    {
    var carId = 1;
    var query = Context.Cars
    .FromSqlInterpolated($"Select *,ValidFrom,ValidTo from dbo.Inventory where Id =
    {carId}")
    .Include(x => x.MakeNavigation);
    var qs = query.ToQueryString(); OutputHelper.WriteLine($"Query: {qs}"); var car = query.First(); Assert.Equal("Black", car.Color);
    Assert.Equal("VW", car.MakeNavigation.Name);
    }

    Here is the generated SQL:

    DECLARE p0 int = 1;

    SELECT [a].[Id], [a].[Color], [a].[DateBuilt], [a].[Display], [a].[IsDrivable], [a].[MakeId],
    [a].[PetName], [a].[Price], [a].[TimeStamp], [a].[ValidFrom], [a].[ValidTo],
    [m].[Id], [m].[Name], [m].[TimeStamp], [m].[ValidFrom], [m].[ValidTo] FROM (
    Select *,ValidFrom,ValidTo from dbo.Inventory where Id = @p0
    ) AS [a]
    INNER JOIN [dbo].[Makes] AS [m] ON [a].[MakeId] = [m].[Id] WHERE [a].[IsDrivable] = CAST(1 AS bit)

    Aggregate Methods
    The next set of tests demonstrate the server-side aggregate methods (Max(), Min(), Count(), Average(), etc.). Aggregate methods can be added to the end of a LINQ query with Where() methods, or the filter expression can be contained in the aggregate method itself (just like First() and Single()). The aggregation executes on the server side, and the single value is returned from the query. Global query filters affect aggregate methods as well and can be disabled with IgnoreQueryFilters().
    All the SQL statements shown in this section were collected using SQL Server Profiler.
    This first test (in CarTests.cs) simply counts all the Car records in the database. Since the query filter is still active, the count returns nine cars.

    [Fact]
    public void ShouldGetTheCountOfCars()
    {
    var count = Context.Cars.Count(); Assert.Equal(9, count);
    }

    The executed SQL is shown here:

    SELECT COUNT(*)
    FROM [dbo].[Inventory] AS [i]
    WHERE [i].[IsDrivable] = CAST(1 AS bit)

    By adding IgnoreQueryFilters(), the Count() method returns 10, and the where clause is removed from the SQL query.

    [Fact]
    public void ShouldGetTheCountOfCarsIgnoreQueryFilters()
    {
    var count = Context.Cars.IgnoreQueryFilters().Count(); Assert.Equal(10, count);
    }

    --Generated SQL
    SELECT COUNT(*) FROM [dbo].[Inventory] AS [i]

    The following tests (also in CarTests.cs) demonstrate the Count() method with a where condition. The first test adds the expression directly into the Count() method, and the second adds the Count() method to the end of the LINQ statement.

    [Theory] [InlineData(1, 1)]
    [InlineData(2, 1)]
    [InlineData(3, 1)]
    [InlineData(4, 2)]
    [InlineData(5, 3)]
    [InlineData(6, 1)]
    public void ShouldGetTheCountOfCarsByMakeP1(int makeId, int expectedCount)
    {
    var count = Context.Cars.Count(x=>x.MakeId == makeId); Assert.Equal(expectedCount, count);
    }

    [Theory] [InlineData(1, 1)]
    [InlineData(2, 1)]
    [InlineData(3, 1)]
    [InlineData(4, 2)]
    [InlineData(5, 3)]
    [InlineData(6, 1)]
    public void ShouldGetTheCountOfCarsByMakeP2(int makeId, int expectedCount)
    {
    var count = Context.Cars.Where(x => x.MakeId == makeId).Count(); Assert.Equal(expectedCount, count);
    }

    Both tests create the same SQL calls to the server, as shown here (the MakeId changes with each test based on the InlineData):

    exec sp_executesql N'SELECT COUNT(*) FROM [dbo].[Inventory] AS [i]
    WHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND ([i].[MakeId] = @ makeId_0)'
    ,N'@ makeId_0 int',@ makeId_0=6

    Any() and All()
    The Any() and All() methods check a set of records to see whether any records match the criteria (Any()) or whether all records match the criteria (All()). Global query filters affect Any() and All() methods functions as well and can be disabled with IgnoreQueryFilters(). All the SQL statements shown in this section were collected using SQL Server Profiler.
    This first test (in CarTests.cs) checks if any car records have a specific MakeId:

    [Theory] [InlineData(1, true)]
    [InlineData(11, false)]
    public void ShouldCheckForAnyCarsWithMake(int makeId, bool expectedResult)
    {
    var result = Context.Cars.Any(x => x.MakeId == makeId); Assert.Equal(expectedResult, result);
    }

    The executed SQL for the first theory test is shown here:

    exec sp_executesql N'SELECT CASE WHEN EXISTS (
    SELECT 1
    FROM [dbo].[Inventory] AS [i]
    WHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND ([i].[MakeId] = @ makeId_0)) THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
    END',N'@ makeId_0 int',@ makeId_0=1
    This second test checks if all car records have a specific MakeId: [Theory]
    [InlineData(1, false)] [InlineData(11, false)]
    public void ShouldCheckForAllCarsWithMake(int makeId, bool expectedResult)
    {
    var result = Context.Cars.All(x => x.MakeId == makeId); Assert.Equal(expectedResult, result);
    }

    The executed SQL for the first theory test is shown here:
    exec sp_executesql N'SELECT CASE WHEN NOT EXISTS (
    SELECT 1
    FROM [dbo].[Inventory] AS [i]
    WHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND ([i].[MakeId] <> @ makeId_0)) THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
    END',N'@ makeId_0 int',@ makeId_0=1

    Getting Data from Stored Procedures
    The final test is to make sure that the CarRepo can get the PetName from the stored procedure. With this code in place, the test becomes trivial. Add the following test to the CarTests.cs class:

    [Theory]
    [InlineData(1, "Zippy")]
    [InlineData(2, "Rusty")]
    [InlineData(3, "Mel")] [InlineData(4, "Clunker")] [InlineData(5, "Bimmer")] [InlineData(6, "Hank")]
    [InlineData(7, "Pinky")]
    [InlineData(8, "Pete")] [InlineData(9, "Brownie")]
    public void ShouldGetValueFromStoredProc(int id, string expectedName)
    {
    Assert.Equal(expectedName, _carRepo.GetPetName(id));
    }

    Creating Records
    Records are added to the database by creating them in code, adding them to their DbSet, and calling S aveChanges()/SaveChangesAsync() on the context. When SaveChanges() is executed, the ChangeTracker reports all the added entities, and EF Core (along with the database provider) creates the appropriate SQL statement(s) to insert the record(s).
    As a reminder, SaveChanges() executes in an implicit transaction, unless an explicit transaction is used.
    If the save was successful, the server-generated values are then queried to set the values on the entities. These tests will all use an explicit transaction so the changes can be rolled back, leaving the database in the same state as when the test execution began.
    All the SQL statements shown in this section were collected using SQL Server Profiler.

    ■Note records can also be added using the derived DbContext as well. these examples will all use the DbSet collection properties to add the records. Both DbSet and DbContext have async versions of Add()/AddRange(). only the synchronous versions are shown.

    Add a Single Record
    The following test demonstrates how to add a single record to the Inventory table:

    [Fact]
    public void ShouldAddACar()
    {
    ExecuteInATransaction(RunTheTest);

    void RunTheTest()
    {
    var car = new Car
    {
    Color = "Yellow", MakeId = 1, PetName = "Herbie"
    };
    var carCount = Context.Cars.Count(); Context.Cars.Add(car); Context.SaveChanges();
    var newCarCount = Context.Cars.Count(); Assert.Equal(carCount+1,newCarCount);
    }
    }

    The executed SQL statement is shown here. Notice that the recently added entity is queried for the database-generated properties (Id and TimeStamp). When the results of the query come to EF Core, the entity is updated with the server-side values.

    exec sp_executesql N'SET NOCOUNT ON;
    INSERT INTO [dbo].[Inventory] ([Color], [MakeId], [PetName], [Price]) VALUES (@p0, @p1, @p2, @p3);
    SELECT [Id], [DateBuilt], [Display], [IsDrivable], [TimeStamp] FROM [dbo].[Inventory]
    WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity(); ',
    N'@p0 nvarchar(50),@p1 int,@p2 nvarchar(50),@p3 nvarchar(50)',@p0=N'Yellow',@p1=1, @p2=N'Herbie',@p3=NULL

    Add a Single Record Using Attach
    The following test creates a new Car entity with the Id left at the default value of zero. When the entity is attached to the ChangeTracker, the state is set to Added, and calling SaveChanges() adds the entity to the database.

    [Fact]
    public void ShouldAddACarWithAttach()
    {
    ExecuteInATransaction(RunTheTest);

    void RunTheTest()
    {

    var car = new Car
    {
    Color = "Yellow", MakeId = 1, PetName = "Herbie"
    };
    var carCount = Context.Cars.Count(); Context.Cars.Attach(car);
    Assert.Equal(EntityState.Added, Context.Entry(car).State); Context.SaveChanges();
    var newCarCount = Context.Cars.Count(); Assert.Equal(carCount + 1, newCarCount);
    }
    }

    Add Multiple Records at Once
    To insert multiple records in a single transaction, use the AddRange() method of DbSet, as shown in this test (note that with SQL Server, for batching to be used when persisting data, there must be at least four actions to execute):

    [Fact]
    public void ShouldAddMultipleCars()
    {
    ExecuteInATransaction(RunTheTest);

    void RunTheTest()
    {
    //Have to add 4 to activate batching var cars = new List
    {
    new() { Color = "Yellow", MakeId = 1, PetName = "Herbie" }, new() { Color = "White", MakeId = 2, PetName = "Mach 5" }, new() { Color = "Pink", MakeId = 3, PetName = "Avon" }, new() { Color = "Blue", MakeId = 4, PetName = "Blueberry" },
    };
    var carCount = Context.Cars.Count(); Context.Cars.AddRange(cars); Context.SaveChanges();
    var newCarCount = Context.Cars.Count(); Assert.Equal(carCount + 4, newCarCount);
    }
    }

    The add statements are batched into a single call to the database, and all the generated columns are queried. When the results of the query come to EF Core, the entities are updated with the server-side values. The executed SQL statement is shown here:

    exec sp_executesql N'SET NOCOUNT ON;
    DECLARE @inserted0 TABLE ([Id] int, [_Position] [int]); MERGE [dbo].[Inventory] USING (

    VALUES (@p0, @p1, @p2, @p3, 0), (@p4, @p5, @p6, @p7, 1),
    (@p8, @p9, @p10, @p11, 2), (@p12, @p13, @p14, @p15, 3))
    AS i ([Color], [MakeId], [PetName], [Price], _Position) ON 1=0 WHEN NOT MATCHED THEN
    INSERT ([Color], [MakeId], [PetName], [Price])
    VALUES (i.[Color], i.[MakeId], i.[PetName], i.[Price]) OUTPUT INSERTED.[Id], i._Position
    INTO @inserted0;

    SELECT [t].[Id], [t].[DateBuilt], [t].[Display], [t].[IsDrivable], [t].[TimeStamp] FROM [dbo].[Inventory] t
    INNER JOIN @inserted0 i ON ([t].[Id] = [i].[Id]) ORDER BY [i].[_Position];
    ',
    N'@p0 nvarchar(50),@p1 int,@p2 nvarchar(50),@p3 nvarchar(50), @p4 nvarchar(50),@p5 int,@p6 nvarchar(50),@p7 nvarchar(50), @p8 nvarchar(50),@p9 int,@p10 nvarchar(50),@p11 nvarchar(50),
    @p12 nvarchar(50),@p13 int,@p14 nvarchar(50),@p15 nvarchar(50)', @p0=N'Yellow',@p1=1,@p2=N'Herbie',@p3=NULL,@p4=N'White',@p5=2,
    @p6=N'Mach 5',@p7=NULL,@p8=N'Pink',@p9=3,@p10=N'Avon',@p11=NULL,@p12=N'Blue', @p13=4,@p14=N'Blueberry',@p15=NULL

    Adding an Object Graph
    The following test demonstrates adding an object graph (related Make, Car, and Radio records):

    [Fact]
    public void ShouldAddAnObjectGraph()
    {
    ExecuteInATransaction(RunTheTest);

    void RunTheTest()
    {
    var make = new Make {Name = "Honda"}; var car = new Car
    {
    Color = "Yellow", MakeId = 1,
    PetName = "Herbie", RadioNavigation = new Radio
    {
    HasSubWoofers = true, HasTweeters = true, RadioId = "Bose 1234"
    }
    };
    //Cast the Cars property to List from IEnumerable ((List)make.Cars).Add(car);
    Context.Makes.Add(make);

    var carCount = Context.Cars.Count(); var makeCount = Context.Makes.Count(); Context.SaveChanges();
    var newCarCount = Context.Cars. Count(); var newMakeCount = Context.Makes. Count(); Assert.Equal(carCount+1,newCarCount); Assert.Equal(makeCount+1,newMakeCount);
    }
    }

    The executed SQL statements (one for each table) are shown here:

    exec sp_executesql N'SET NOCOUNT ON; INSERT INTO [dbo].[Makes] ([Name]) VALUES (@p0);
    SELECT [Id], [TimeStamp] FROM [dbo].[Makes]
    WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity(); ',
    N'@p0 nvarchar(50)',@p0=N'Honda'

    exec sp_executesql N'SET NOCOUNT ON;
    INSERT INTO [dbo].[Inventory] ([Color], [MakeId], [PetName], [Price]) VALUES (@p1, @p2, @p3, @p4);
    SELECT [Id], [DateBuilt], [Display], [IsDrivable], [TimeStamp] FROM [dbo].[Inventory]
    WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

    ',N'@p1 nvarchar(50),@p2 int,@p3 nvarchar(50),@p4 nvarchar(50)',@p1=N'Yellow',@p2=7, @p3=N'Herbie',@p4=NULL

    exec sp_executesql N'SET NOCOUNT ON;
    INSERT INTO [dbo].[Radios] ([InventoryId], [HasSubWoofers], [HasTweeters], [RadioId])
    VALUES (@p5, @p6, @p7, @p8); SELECT [Id], [TimeStamp] FROM [dbo].[Radios]
    WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity(); ',
    N'@p5 int,@p6 bit,@p7 bit,@p8 nvarchar(50)',@p5=11, @p6=1,@p7=1,@p8=N'Bose 1234'

    Updating Records
    Records are updated by loading them into DbSet as a tracked entity, changing them through code, and then calling SaveChanges() on the context. When SaveChanges() is executed, the ChangeTracker reports all the modified entities, and EF Core (along with the database provider) creates the appropriate SQL statement(s) to update the record(s).

    Update Tracked Entities
    The following test updates a single record, but the process is the same if multiple tracked entities are updated and saved.

    [Fact]
    public void ShouldUpdateACar()
    {
    ExecuteInASharedTransaction(RunTheTest);

    void RunTheTest(IDbContextTransaction trans)
    {
    var car = Context.Cars.First(c => c.Id == 1); Assert.Equal("Black",car.Color);
    car.Color = "White";
    //Calling update is not needed because the entity is tracked
    //Context.Cars.Update(car); Context.SaveChanges(); Assert.Equal("White", car.Color);
    var context2 = TestHelpers.GetSecondContext(Context, trans); var car2 = context2.Cars.First(c => c.Id == 1); Assert.Equal("White", car2.Color);
    }
    }

    The prior code uses a shared transaction across two instances of ApplicationDbContext. This is to provide isolation between the context executing the test and the context checking the result of the test.

    The executed SQL statement is listed here:

    exec sp_executesql N'SET NOCOUNT ON; UPDATE [dbo].[Inventory] SET [Color] = @p0 WHERE [Id] = @p1 AND [TimeStamp] = @p2; SELECT [TimeStamp]
    FROM [dbo].[Inventory]
    WHERE @@ROWCOUNT = 1 AND [Id] = @p1;

    ',N'@p1 int,@p0 nvarchar(50),@p2 varbinary(8)',@p1=1,@p0=N'White',@p2=0x000000000000862D

    Update Nontracked Entities
    Untracked entities can also be used to update database records. The process is similar to updating tracked entities except that the entity is created in code (and not queried), and EF Core must be notified that the entity should already exist in the database and needs to be updated.
    The following example reads a record in as nontracked, creates a new instance of the Car class from this record, and changes one property (Color). Then it either sets the state or uses the Update() method on
    DbSet, depending on which line of code you uncomment. The Update() method also changes the state to Modified. The test then calls SaveChanges(). All of the extra contexts are there to ensure the test is accurate, and there isn’t any crossover between contexts.

    [Fact]
    public void ShouldUpdateACarUsingState()
    {
    ExecuteInASharedTransaction(RunTheTest);

    void RunTheTest(IDbContextTransaction trans)
    {
    var car = Context.Cars.AsNoTracking().First(c => c.Id == 1); Assert.Equal("Black", car.Color);
    var updatedCar = new Car
    {
    Color = "White", //Original is Black Id = car.Id,
    MakeId = car.MakeId, PetName = car.PetName, TimeStamp = car.TimeStamp, IsDrivable = car.IsDrivable
    };
    var context2 = TestHelpers.GetSecondContext(Context, trans);
    //Either call Update or modify the state context2.Entry(updatedCar).State = EntityState.Modified;
    //context2.Cars.Update(updatedCar); context2.SaveChanges();
    var context3 = TestHelpers.GetSecondContext(Context, trans);
    var car2 = context3.Cars.First(c => c.Id == 1); Assert.Equal("White", car2.Color);
    }
    }

    Concurrency Checking When Updating Records
    The previous chapters covered concurrency checking in great detail. As a reminder, when an entity has a Timestamp property defined, the value of that property is used in the where clause when changes (updates or deletes) are being persisted to the database. Instead of just searching for the primary key, the TimeStamp value is added to the query, like this example:

    UPDATE [dbo].[Inventory] SET [PetName] = @p0 WHERE [Id] = @p1 AND [TimeStamp] = @p2;

    The following test shows an example of creating a concurrency exception, catching it, and using the
    Entries to get the original values, current values, and values that are currently stored in the database. Getting the current values requires another database call.

    [Fact]
    public void ShouldThrowConcurrencyException()
    {
    ExecuteInATransaction(RunTheTest);

    void RunTheTest()
    {

    var car = Context.Cars.First();
    //Update the database outside of the context Context.Database.ExecuteSqlInterpolated(
    $"Update dbo.Inventory set Color='Pink' where Id = {car.Id}"); car.Color = "Yellow";
    var ex = Assert.Throws( () => Context.SaveChanges());
    var entry = ((DbUpdateConcurrencyException) ex.InnerException)?.Entries[0]; PropertyValues originalProps = entry.OriginalValues;
    PropertyValues currentProps = entry.CurrentValues;
    //This needs another database call
    PropertyValues databaseProps = entry.GetDatabaseValues();
    }
    }

    Deleting Records
    A single entity is marked for deletion by calling Remove() on DbSet or by setting its state to Deleted. A list of records are marked for deletion by calling RemoveRange() on the DbSet. The removal process will
    cause cascade effects on navigation properties based on the rules configured in the Fluent API (or by EF Core conventions). If deletion is prevented due to cascade policy, an exception is thrown.

    Delete Tracked Records
    The delete process mirrors the update process. Once an entity is tracked, call Remove() on that instance, and then call SaveChanges() to remove the record from the database.

    [Fact]
    public void ShouldRemoveACar()
    {
    ExecuteInATransaction(RunTheTest);

    void RunTheTest()
    {
    var carCount = Context.Cars. Count();
    var car = Context.Cars.First(c => c.Id == 9); Context.Cars.Remove(car); Context.SaveChanges();
    var newCarCount = Context.Cars.Count(); Assert.Equal(carCount - 1, newCarCount); Assert.Equal(
    EntityState.Detached, Context.Entry(car).State);
    }
    }

    After SaveChanges() is called, the entity instance still exists, but is no longer in the ChangeTracker.
    When checking the EntityState, the state will be Detached.

    The executed SQL call for the delete is listed here:

    exec sp_executesql N'SET NOCOUNT ON; DELETE FROM [dbo].[Inventory]
    WHERE [Id] = @p0 AND [TimeStamp] = @p1; SELECT @@ROWCOUNT;'
    ,N'@p0 int,@p1 varbinary(8)',@p0=2,@p1=0x0000000000008680

    Delete Nontracked Entities
    Untracked entities can delete records the same way untracked entities can update records. The difference is that the entity is tracked by calling Remove()/RemoveRange() or setting the state to Deleted and then calling SaveChanges().
    The following example reads a record in as nontracked, creates a new instance of the Car class from this record, and changes one property (Color). Then it either sets the state or uses the Remove() method on DbSet (depending on which line you uncomment). The test then calls SaveChanges(). All the extra contexts are there to ensure there isn’t any crossover between contexts.

    [Fact]
    public void ShouldRemoveACarUsingState()
    {
    ExecuteInASharedTransaction(RunTheTest);

    void RunTheTest(IDbContextTransaction trans)
    {
    var carCount = Context.Cars.Count();
    var car = Context.Cars.AsNoTracking().First(c => c.Id == 1); var context2 = TestHelpers.GetSecondContext(Context, trans);
    //Either call Remove or modify the state context2.Entry(car).State = EntityState.Deleted;
    //context2.Cars.Remove(car); context2.SaveChanges();
    var newCarCount = Context.Cars.Count(); Assert.Equal(carCount - 1, newCarCount); Assert.Equal(
    EntityState.Detached, Context.Entry(car).State);
    }
    }

    Catch Cascade Delete Failures
    EF Core will throw a DbUpdateException when an attempt to delete a record fails due to the cascade rules. The following test shows this in action:

    [Fact]
    public void ShouldFailToRemoveACar()
    {
    ExecuteInATransaction(RunTheTest);

    void RunTheTest()
    {
    var car = Context.Cars.First(c => c.Id == 1); Context.Cars.Remove(car); Assert.Throws(
    ()=>Context.SaveChanges());
    }
    }

    Concurrency Checking When Deleting Records
    Delete also uses concurrency checking if the entity has a TimeStamp property. See the section “Concurrency Checking” in the “Updating Records” section for more information.

    Summary
    This chapter used the knowledge gained in the previous chapter to complete the data access layer for the AutoLot database. You used the EF Core command-line tools to scaffold an existing database, updated the model to its final version, and then created migrations and applied them. Repositories were added for the encapsulation of the data access, and database initialization code with sample data can drop and create the database in a repeatable, reliable manner. The rest of the chapter focused on test-driving the data access layer. This completes our journey through data access and Entity Framework Core.