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

CHAPTER 32

RESTful Services with ASP.NET Core

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

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

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

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

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

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

1407

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

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

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

[
"value1", "value2"
]

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

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

global using Microsoft.AspNetCore.Mvc;

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

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

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

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

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

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

Figure 32-1. The Swagger documentation page

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

Figure 32-2. The Swagger server response information

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

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

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

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

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

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

global using System.Text.Json.Serialization;

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

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

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

[ApiController]
public abstract class BaseCrudController : ControllerBase
{
}

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

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

[assembly: ApiController]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Table 32-1. Binding Source Inference Conventions

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

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

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

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

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

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

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

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

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

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

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

This updates the return values to the following:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

builder.Services.AddApiVersioning();

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

Add the new namespace to the GlobalUsings.cs file:

global using AutoLot.Api.ApiVersionSupport;

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

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

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

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

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

Table 32-2. The APIVersioningOptions properties

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

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

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

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

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

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

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

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

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

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

Table 32-3. API Versioning Attributes

Attribute Meaning in Life

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

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

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

Now, update the call to this:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

[ "2.0", "2.0" ]

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

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

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

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

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

//omitted for brevity
}

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

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

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

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

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

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

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

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

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

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

Table 32-4. Some of the APIExplorerOptions properties

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

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

public static IServiceCollection AddAutoLotApiVersionConfiguration(

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

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

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

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

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

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

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

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

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


net6.0
disable
enable
True
AutoLot.Api.xml

1701;1702;1591


1701;1702;1591

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


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

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



Always

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

///

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

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

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

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

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

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



AutoLot.Api


This is an example Get method returning JSON

This is one of several examples for returning JSON:

 [
"value1", "value2"
]


List of strings


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

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

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

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

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

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

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

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

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

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

namespace AutoLot.Api.Swagger;

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

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

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

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

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

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

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

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

namespace AutoLot.Api.Swagger;

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

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

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

namespace AutoLot.Api.Swagger;

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

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

readonly IApiVersionDescriptionProvider _provider;
private readonly SwaggerApplicationSettings _settings;

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

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

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

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

internal static OpenApiInfo CreateInfoForApiVersion( ApiVersionDescription description, SwaggerApplicationSettings settings)

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

This API version has been deprecated.

";
}

return info;
}

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

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

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

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

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

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

{

}
});

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

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

Figure 32-5. Updates to the Swagger UI page

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

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

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

Figure 32-7. XML documentation integrated into Swagger UI

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

Figure 32-8. Two entries for each end point

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

Figure 32-9. Two entries for each end point

The authorize button will be covered later in this chapter.

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

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

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

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

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

Figure 32-10. Updated responses in Swagger UI

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

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

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

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

namespace AutoLot.Api.Controllers.Base [ApiController]

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

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

global using AutoLot.Api.Controllers.Base;

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

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

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

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

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

///

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

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

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

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

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

///

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

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

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

///

/// Gets all records
///

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

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

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

///

/// Gets a single record
///

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

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

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

///

/// Updates a single record
///

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

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

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

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

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

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

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

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

///

///

/// Adds a single record
///

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

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

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

{

}
try
{

}

return ValidationProblem(ModelState);

MainRepo.Add(entity);

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

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

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

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

///

/// Deletes a single record
///

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

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

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

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

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

global using AutoLot.Api.Controllers.Base;

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

namespace AutoLot.Api.Controllers

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

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

///

/// Gets all cars by make
///

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

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

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

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

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

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

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

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

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

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

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

: base(logger, repo)
{
}
}

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

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

{

"RebuildDataBase": true,

}

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

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

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

global using Microsoft.AspNetCore.Mvc.Filters;

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

namespace AutoLot.Api.Filters

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

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

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

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

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

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

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

context.ExceptionHandled = true;

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

global using AutoLot.Api.Filters;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

namespace AutoLot.Services.ViewModels; public class SecuritySettings
{

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

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

global using AutoLot.Services.ViewModels;

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

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

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

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

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

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

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

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

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

private readonly SecuritySettings _securitySettings;

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

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

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

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

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

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

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

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

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

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

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

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

Next, opt-in to authentication for the application:

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

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

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

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

app.MapControllers().RequireAuthorization();

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

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

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

using Microsoft.AspNetCore.Mvc.Authorization;

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

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

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

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

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

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

Figure 32-11. The Swagger Authorize dialog

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

发表评论