深度重构和完善ASP.NET Core WEB API应用程序代码 (Deep refactoring and refinement of ASP.NET Core WEB API application code)

介绍 (Introduction)

Part 1. Creating a test RESTful WEB API application.

第1部分。创建一个测试RESTful WEB API应用程序。

Part 2. Increasing ASP.NET Core WEB API application's productivity.

第2部分。提高ASP.NET Core WEB API应用程序的生产率。

In Part 3 we will review the following:

在第3部分中,我们将回顾以下内容:

  • Why do we need to refactoring and refinement the code?

    为什么我们需要重构和完善代码?

  • The Don't Repeat Yourself (DRY) principle

    不要重复自己(DRY)原则

  • Exceptions handling in try/catch/finally blocks

    在try / catch / finally块中处理异常

  • Requirements for our Exception handling middleware

    我们的异常处理中间件的要求

  • Logging to a file in .Net Core

    登录到.Net Core中的文件

  • Unified exception message format

    统一异常消息格式

  • .NET Core exception handling middleware implementation

    .NET Core异常处理中间件实现

  • What type of result should business logic layer return to a controller?

    业务逻辑层应将什么类型的结果返回给控制器?

  • Custom exception handling middleware

    自定义异常处理中间件

  • Using Typed Clients with HttpClientFactory

    在HttpClientFactory中使用类型化的客户端

  • Handling the application's settings

    处理应用程序的设置

  • Caching concern separation

    缓存关注点分离

  • Generic Asynchronous DistributedCache repository

    通用异步DistributedCache存储库

  • In-Memory and In-Database pagination in Entity Framework

    实体框架中的内存中和数据库中分页

  • Controller vs ControllerBase

    控制器与ControllerBase

  • Custom Id parameter validation filter and Attribute

    自定义ID参数验证过滤器和属性

  • Pagination parameters custom model validation filter

    分页参数自定义模型验证过滤器

    • Enable CORS ASP.NET Core

      启用CORS ASP.NET Core

    • No CORS headers sent in case of HTTP error

      如果发生HTTP错误,则不会发送任何CORS标头

    • How to send HTTP 4xx-5xx response with CORS headers in an ASPNET.Core web app

      如何在ASPNET.Core Web应用程序中发送带有CORS标头的HTTP 4xx-5xx响应

    Cross-origin resource sharing (CORS)

    跨域资源共享(CORS)

    • Controlling API versioning error message format

      控制API版本控制错误消息格式

    • Versioning in inner HTTP invocations

      内部HTTP调用中的版本控制

    API versioning

    API版本控制

  • Resolving DNS name locally

    在本地解析DNS名称

    • XML comments

      XML注释

    • OpenAPI documentation for RESTful APIs with Swagger

      带有Swagger的RESTful API的OpenAPI文档

    • Swagger response examples

      昂首阔步的回应范例

    • Tags and attributes to form OpenApi documentation

      标记和属性以形成OpenApi文档

    Documenting .NET Core API application

    记录.NET Core API应用程序

  • Getting rid of unused or duplicated NuGet packages

    摆脱未使用或重复的NuGet软件包

  • The Microsoft.AspNetCore.All and Microsoft.AspNetCore.App metapackages

    Microsoft.AspNetCore.All和Microsoft.AspNetCore.App元包

  • Migration from ASP.NET Core 2.2 to 3.0

    从ASP.NET Core 2.2迁移到3.0

  • Points of Interest

    兴趣点

为什么我们需要重构和完善代码? (Why do we need to refactoring and refinement the code?)

The goal of Part1 was to create a really simple basic application we can start from. The main focus was on how to make it easier to apply and examine different approaches, to modify code and check results.

第一部分的目标是创建一个非常简单的基本应用程序,我们可以从此开始。 主要重点是如何使更容易应用和检查不同的方法,修改代码和检查结果变得容易。

Part2 was dedicated to productivity. A variety approaches were realized. And the code became more complicated compared to Part1.

第2部分致力于生产力。 实现了多种方法。 与Part1相比,代码变得更加复杂。

Now, after making a choice of approaches and implementing them, we can consider our application as a whole. It becomes evident that the code requires deep refactoring and refinement so that it satisfies various principles of good programming style.

现在,在选择了方法并实施之后,我们可以将我们的应用程序视为一个整体。 很明显,代码需要深度重构和完善,以便满足良好编程风格的各种原理。

不要重复自己(DRY)原则 (The Don't Repeat Yourself (DRY) principle)

According to the DRY principle, we should eliminate duplication of code. Therefore, let us examine the ProductsService code to see whether it has any repetition. We can see at once, that the following fragment is repeated several times in all the methods which return the ProductViewModel or IEnumerable<productviewmodel> typed value:

根据DRY原理,我们应该消除代码重复。 因此,让我们检查ProductsService代码以查看其是否重复。 我们可以立即看到,在所有返回ProductViewModelIEnumerable<productviewmodel>类型值的方法中,以下片段重复了几次:

…
new ProductViewModel(){Id = p.ProductId,Sku = p.Sku,Name = p.Name}
…

We have always created a ProductViewModel type object from a Product type object. It is logical, to move field initialization of the ProductViewModels object into its constructor. Let us create a constructor method in the ProductViewModel class. In the constructor we fill the object’s field values with appropriate values of the Product parameter:

我们始终从Product类型对象创建ProductViewModel类型对象。 将ProductViewModels对象的字段初始化移到其构造函数中是合乎逻辑的。 让我们在ProductViewModel类中创建一个构造函数方法。 在构造函数中,我们使用Product参数的适当值填充对象的字段值:

public ProductViewModel(Product product)
{Id = product.ProductId;Sku = product.Sku;Name = product.Name;
}

Now we can rewrite the duplicated code in the FindProductsAsync and GetAllProductsAsync methods of the ProductsService:

现在,我们可以重写的复制代码FindProductsAsyncGetAllProductsAsync的方法ProductsService

return new OkObjectResult(products.Select(p => new ProductViewModel(){Id = p.ProductId,Sku = p.Sku,Name = p.Name}));return new OkObjectResult(products.Select(p => new ProductViewModel(p)));
…                    

And change GetProductAsync and DeleteProductAsync methods of the ProductsService class:

并更改ProductsService类的GetProductAsyncDeleteProductAsync方法:

return new OkObjectResult(new ProductViewModel(){Id = product.ProductId,Sku = product.Sku,Name = product.Name});return new OkObjectResult(new ProductViewModel(product));
…                    

And repeat the same for the PriceViewModel class.

并对PriceViewModel类重复相同的PriceViewModel

…    new PriceViewModel(){Price = p.Value,Supplier = p.Supplier}
…                    

Although we use the fragment only once in the PricesService, it is better to encapsulate the PriceViewModel’s fields initialization inside the class in its constructor.

尽管我们在PricesService仅使用一次该片段,但最好将PriceViewModel的字段初始化封装在其构造函数的类中。

Let us create a PriceViewModel class constructor

让我们创建一个PriceViewModel类构造函数

…
public PriceViewModel(Price price)
{Price = price.Value;Supplier = price.Supplier;
}
…                    

And change the fragment:

并更改片段:

return new OkObjectResult(pricess.Select(p => new PriceViewModel(){Price = p.Value,Supplier = p.Supplier}).OrderBy(p => p.Price).ThenBy(p => p.Supplier));return new OkObjectResult(pricess.Select(p => new PriceViewModel(p)).OrderBy(p => p.Price).ThenBy(p => p.Supplier));… 

在try / catch / finally块中处理异常 (Exceptions handling in try/catch/finally blocks)

The next problem that should be solved is the exception handling. Throughout the application all operations that can cause exception have been called inside a try-catch construction. This approach is very convenient during the debugging process, because it allows us to examine an exception at the particular place it occurs. But this approach also has a disadvantage of code repetition. A better way of exception handling in ASP.NET Core is to handle them globally in middleware or in Exception filters.

下一个应该解决的问题是异常处理。 在整个应用程序中,所有可能导致异常的操作都在try-catch结构中被调用。 这种方法在调试过程中非常方便,因为它使我们可以在异常发生的特定位置检查异常。 但是这种方法也有代码重复的缺点。 ASP.NET Core中异常处理的一种更好的方法是在中间件或异常过滤器中全局处理它们。

We will create Exception handling middleware to centralize exceptions handling with logging and generating user friendly error messages.

我们将创建异常处理中间件,以通过记录日志并生成用户友好的错误消息来集中处理异常。

我们的异常处理中间件的要求 (Requirements for our Exception handling middleware)

  • Logging detailed information to a log file;将详细信息记录到日志文件中;
  • Detailed error message in debug mode and friendly message in production;调试模式下的详细错误消息和生产环境中的友好消息;
  • Unified error message format统一错误消息格式

登录到.Net Core中的文件 (Logging to a file in .Net Core)

At start of .NET Core application in Main method we have created and run the web server.

在Main方法中的.NET Core应用程序启动时,我们已经创建并运行了Web服务器。

…
BuildWebHost(args).Run();
… 

At this moment, an instance of ILoggerFactory is created automatically. Now it can be accessed via dependency injection and perform logging anywhere in the code. However, with the standard ILoggerFactory we cannot log to a file. To overcome this limitation, we will use the Serilog library, that extends the ILoggerFactory and allows logging to a file."

此时,将自动创建ILoggerFactory的实例。 现在可以通过依赖注入来访问它,并在代码中的任何位置执行日志记录。 但是,使用标准的ILoggerFactory,我们无法登录到文件。 为了克服此限制,我们将使用Serilog库,该库扩展了ILoggerFactory并允许记录到文件。”

Let us install the Serilog.Extensions.Logging.File NuGet package first:

让我们首先安装Serilog.Extensions.Logging.File NuGet包:

We should add using Microsoft.Extensions.Logging; statement modules in which we are going to apply logging.

我们应该using Microsoft.Extensions.Logging;添加using Microsoft.Extensions.Logging; 语句模块,我们将在其中应用日志记录。

The Serilog library can be configured in different ways. In our simple example, to setup logging rules for Serilog, we should add the next code in the Startup class in the Configure method

可以用不同的方式配置Serilog库。 在我们的简单示例中,要设置Serilog的日志记录规则,我们应该在Configure方法的Startup类中添加下一个代码

… public void Configure(IApplicationBuilder app, IHostingEnvironment env,ILoggerFactory loggerFactory){loggerFactory.AddFile("Logs/log.txt");
… 

This means, that the logger will write to relative \Logs directory and log files’ name format will be: log-yyyymmdd.txt

这意味着记录器将写入相对的\ Logs目录,并且日志文件的名称格式为:log-yyyymmdd.txt

统一异常消息格式 (Unified exception message format)

During its work our application can generate different types of exception messages. Our aim is to unify the format of these messages so that they could be processed by some universal method of a client application.

在其工作期间,我们的应用程序可以生成不同类型的异常消息。 我们的目标是统一这些消息的格式,以便可以通过客户端应用程序的某些通用方法对其进行处理。

Let all messages have the following format:

让所有消息具有以下格式:

{"message": "Product not found"
}    

The format is really very simple. It is acceptable for a simple application, like ours. But we should foresee the opportunity to expand it and to do this centralized in one place. For this, we will create an ExceptionMessage class, which will encapsulate message formatting procedures. And we will use this class wherever we need to generate exception messages.

格式真的非常简单。 对于像我们这样的简单应用程序,它是可以接受的。 但是,我们应该预见到有机会进行扩展并将其集中在一个地方的机会。 为此,我们将创建一个ExceptionMessage类,该类将封装消息格式设置过程。 我们将在需要生成异常消息的任何地方使用此类。

Let us create a folder Exceptions in our project and add there a class ExceptionMessage:>

让我们在项目中创建一个文件夹Exceptions,并在其中添加一个ExceptionMessage类:>

using Newtonsoft.Json;namespace SpeedUpCoreAPIExample.Exceptions
{public class ExceptionMessage{public string Message { get; set; }public ExceptionMessage() {}public ExceptionMessage(string message){Message = message;}public override string ToString(){return JsonConvert.SerializeObject(new { message = new string(Message) });}}
}

Now we can create our ExceptionsHandlingMiddleware

现在我们可以创建ExceptionsHandlingMiddleware

.NET Core异常处理中间件实现 (.NET Core exception handling middleware implementation)

In the Exceptions folder create a class ExceptionsHandlingMiddleware:

Exceptions文件夹中,创建一个ExceptionsHandlingMiddleware类:

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Net;
using System.Threading.Tasks;namespace SpeedUpCoreAPIExample.Exceptions
{public class ExceptionsHandlingMiddleware{private readonly RequestDelegate _next;private readonly ILogger<ExceptionsHandlingMiddleware> _logger;public ExceptionsHandlingMiddleware(RequestDelegate next, ILogger<ExceptionsHandlingMiddleware> logger){_next = next;_logger = logger;}public async Task InvokeAsync(HttpContext httpContext){try{await _next(httpContext);}catch (Exception ex){await HandleUnhandledExceptionAsync(httpContext, ex);}}private async Task HandleUnhandledExceptionAsync(HttpContext context,Exception exception){_logger.LogError(exception, exception.Message);if (!context.Response.HasStarted){int statusCode = (int)HttpStatusCode.InternalServerError; // 500string message = string.Empty;
#if DEBUGmessage = exception.Message;
#elsemessage = "An unhandled exception has occurred";
#endifcontext.Response.Clear();context.Response.ContentType = "application/json";context.Response.StatusCode = statusCode;var result = new ExceptionMessage(message).ToString();await context.Response.WriteAsync(result);}}}
}

This middleware intercepts unhandled exceptions, logs exceptions’ details and emits detailed messages while debugging (#if DEBUG) or user-friendly messages without debugging.

该中间件拦截未处理的异常,记录异常的详细信息,并在调试(#if DEBUG)时发出详细消息,或在不进行调试的情况下向用户友好的消息。

Note, how we use ExceptionMessage class, to format the result.

注意,我们如何使用ExceptionMessage类来格式化结果。

Now, we should add this middleware in the application HTTP request pipeline in the Startup.Configure method before app.UseMvc(); statement.

现在,我们应该在app.UseMvc();之前的Startup.Configure方法中的应用程序HTTP请求管道中添加此中间件app.UseMvc(); 声明。

app.UseMiddleware<ExceptionsHandlingMiddleware>();;
…
app.UseMvc();

Let us check, how it works. For this, we will change a stored procedure name in the ProductsRepository.FindProductsAsync method for a nonexistent method GetProductsBySKUError.

让我们检查一下它是如何工作的。 为此,我们将为不存在的方法GetProductsBySKU Error更改ProductsRepository.FindProductsAsync方法中的存储过程名称

public async Task<IEnumerable<product>> FindProductsAsync(string sku)
{return await _context.Products.AsNoTracking().FromSql("[dbo].GetProductsBySKUError @sku = {0}", sku).ToListAsync();
}

And remove Try-Catch block from the ProductsService.FindProductsAsync method

并从ProductsService.FindProductsAsync方法中删除Try-Catch块

public async Task<IActionResult> FindProductsAsync(string sku)
{try{IEnumerabler<Product> products = await _productsRepository.FindProductsAsync(sku);}catch{return new ConflictResult();}…
}

Let us run our application and check the results

让我们运行我们的应用程序并检查结果

Call http://localhost:49858/api/products/find/aa with Swagger

使用Swagger呼叫http:// localhost:49858 / api / products / find / aa

We will have 500 Http Response code and a message:

我们将有500个Http响应代码和一条消息:

And let us check log files

让我们检查日志文件

Now we have Logs folder with a file

现在我们有一个带有文件的Logs文件夹

Inside the file we have detailed exception description:

在文件中,我们有详细的异常描述:

…
""[dbo].GetProductsBySKUError @sku = @p0" (627a98df)
System.Data.SqlClient.SqlException (0x80131904): Could not find stored procedure 'dbo.GetProductsBySKUError'.
…

We have claimed, that our Exception Handling Middleware should generate detailed error message in debug mode and friendly message in production. Let us check it. For this, we will change Active solution configuration for Release in the toolbar:

我们声称,我们的异常处理中间件应在调试模式下生成详细的错误消息,并在生产中生成友好的消息。 让我们检查一下。 为此,我们将在工具栏中更改“发布”的活动解决方案配置:

or in the Configuration manager:

或在配置管理器中:

Then call incorrect API ones again. The result, as we expected, will be:

然后再次调用不正确的API。 正如我们预期的那样,结果将是:

So, our exception handler works as we expected.

因此,我们的异常处理程序将按预期工作。

Note! If we did not remove Try-Catch block we would never let this handler work, because the unhandled exemption would be processed by the code inside Catch statement.

注意! 如果不删除Try-Catch块,我们将永远不会让此处理程序工作,因为未处理的豁免将由Catch语句中的代码处理。

Do not forget to restore the correct stored procedure name GetProductsBySKU!

不要忘记恢复正确的存储过程名称GetProductsBySKU!

Now we can remove all Try-Catch block in the ProductsService and PricesService clacces.

现在,我们可以删除ProductsService和pricesService类别中的所有Try-Catch块。

Note! We omit code of removing Try-Catch blocks implementation for brevity.

注意! 为了简洁起见,我们省略了删除Try-Catch块实现的代码。

The only places we still need Try-Catch blocks are ProductsService.PreparePricesAsync and PricesService.PreparePricesAsync methods. We do not want to breake the application workflow in those places, as we discussed in Part 2

我们仍然需要Try-Catch块的唯一地方是ProductsService.PreparePricesAsyncPricesService.PreparePricesAsync方法。 正如我们在第2部分中讨论的那样,我们不想破坏这些地方的应用程序工作流程。

After removing Try-Catch block the code became much simpler and straightforward. But we still have some repetition in most services’ method, when we return

删除Try-Catch块后,代码变得更加简单明了。 但是,当我们返回时,大多数服务的方法中仍有一些重复

return new NotFoundResult();

Let us improve this too.

让我们也对此进行改进。

In all methods, that find a collection of values, such as ProductsService.GetAllProductsAsync, ProductsService.FindProductsAsync and PricesService.GetPricesAsync we have two problems.

在所有找到值集合的方法中,例如ProductsService.GetAllProductsAsyncProductsService.FindProductsAsyncPricesService.GetPricesAsync我们都有两个问题。

The first one is in checking, whether a collection, received from a repository, is not empty. For this we have used а statement

第一个是检查从存储库收到的集合是否不为空。 为此,我们使用了а语句

…if (products != null)
…

But a collection will never be null in our case (except if a handled exception happens in a repository). Since all exceptions are handled now in a dedicated middleware outside services and repositories, we will always receive a collection of values (empty, if nothing was found). So, the proper way to check the results will be

但是在我们的情况下,集合永远不会为空(除非在存储库中发生了处理的异常)。 由于现在所有异常都在服务和存储库之外的专用中间件中处理,因此我们将始终收到值的集合(如果未找到,则为空)。 因此,检查结果的正确方法是

if (products.Any())

or

要么

(products.Count() > 0)

and the same for PricesService class in GetPricesAsync method: change

GetPricesAsync方法中的PricesService类相同:change

if (pricess != null)if (pricess.Any())

The second problem is what result we should return for empty collections. So far, we have returned NotFoundResult(), but it is also not really correct. For example, if we create another API that should return a value composed of a Product and its Prices, an empty prices collection will be represented in a JSON structure as an empty massive and StatusCode will be 200 - OK. So, to be consistent, we should rewrite the code of the abovementioned methods to remove NotFoundResult for empty collections:

第二个问题是我们应该为空集合返回什么结果。 到目前为止,我们已经返回NotFoundResult() ,但它也不是正确的。 例如,如果我们创建另一个API,该API应该返回由Product及其Prices组成的值,则空价格集合将以JSON结构表示为空质量,并且StatusCode为200-确定。 因此,为了保持一致,我们应该重写上述方法的代码,以删除空集合的NotFoundResult

public async Task<IActionResult> FindProductsAsync(string sku)
{IEnumerable<Product> products = await _productsRepository.FindProductsAsync(sku);if (products.Count() == 1){//only one record found - prepare prices beforehandThreadPool.QueueUserWorkItem(delegate{PreparePricesAsync(products.FirstOrDefault().ProductId);});};return new OkObjectResult(products.Select(p => new ProductViewModel(p)));
}    public async Task<IActionResult> GetAllProductsAsync()
{IEnumerable<Product> products = await _productsRepository.GetAllProductsAsync();return new OkObjectResult(products.Select(p => new ProductViewModel(p)));
}

And in PricesService

并在PricesService

public async Task<IActionResult> GetPricesAsync(int productId)
{IEnumerable<Price> pricess = await _pricesRepository.GetPricesAsync(productId);return new OkObjectResult(pricess.Select(p => new PriceViewModel(p)).OrderBy(p => p.Price).ThenBy(p => p.Supplier));
}

The code becomes really straightforward, but another problem still remains: is this a correct solution to return IActionResult from Services.

代码变得非常简单,但是仍然存在另一个问题:这是从Services返回IActionResult的正确解决方案。

业务逻辑层应将什么类型的结果返回给控制器? (What type of result should the business logic layer return to a controller?)

Classically, the business layer’s methods return a POCO (Plain old CLR object) typed value to a controller and then the controller forms a proper response with an appropriate StatusCode. For example, the ProductsService.GetProductAsync method should return either a ProductViewModel object or null (if a product is not found). And the Controller should generate OkObjectResult(ProductViewModel) or NotFound() response respectively.

传统上,业务层的方法将POCO(普通旧CLR对象)类型的值返回给控制器,然后控制器使用适当的StatusCode形成适当的响应。 例如, ProductsService.GetProductAsync方法应返回ProductViewModel对象或null(如果未找到产品)。 控制器应分别生成OkObjectResult(ProductViewModel)NotFound()响应。

But this approach is not always possible. Actually, we can have different reasons to return null from a Service. For example, let us imagine an application in which a user can access some content. This content can be either public, private or prepaid. When a user requests some content, an ISomeContentService can return either an ISomeContent or null. There are some possible reasons for this null:

但是这种方法并不总是可行的。 实际上,我们可能有不同的原因从Service返回null。 例如,让我们想象一个用户可以访问某些内容的应用程序。 此内容可以是公共的,私人的或预付费的。 当用户请求某些内容时, ISomeContentService可以返回ISomeContent或null。 此空值可能有一些原因:

401 Unauthorized
402 Payment Required
403 Forbidden
404 Not Found
…

The reason becomes clear inside the Service. How can the Service notify a Controller about this reason, if a method returns just the null? This is not enough for a controller to create a proper response. To solve this issue, we have used IActionResult type as a return type from Services – business layer. This approach is really flexible, as with IActionResult result we can pass everything to a controller. But should a business layer form an API’s response, performing a controller’s job? Will it not break the separation of concerns design principal?

服务中的原因变得很清楚。 如果方法仅返回null,那么服务如何将这个原因通知控制器? 这对于控制器创建适当的响应是不够的。 为了解决此问题,我们使用IActionResult类型作为Services –业务层的返回类型。 这种方法非常灵活,因为有了IActionResult结果,我们可以将所有内容传递给控制器​​。 但是业务层是否应该形成API的响应,执行控制器的工作? 会不会打破关注点设计主体的分离?

One possible way to get rid of IActionResult in a business layer is using custom exceptions to control the application’s workflow and generate proper Responses. To provide this we will enhance our Exception handling middleware to make it able to process custom exceptions.

在业务层中摆脱IActionResult一种可能方法是使用自定义异常来控制应用程序的工作流程并生成适当的响应。 为此,我们将增强我们的异常处理中间件,使其能够处理自定义异常。

自定义异常处理中间件 (Custom exceptions handling middleware)

Let us create a simple HttpException class, inherited from Exception. And enhance out exception handler middleware to process exceptions of HttpException type.

让我们创建一个从Exception继承的简单HttpException类。 并增强异常处理程序中间件来处理HttpException类型的异常。

In the HttpException folder add class HttpException

HttpException文件夹中添加类HttpException

using System;
using System.Net;namespace SpeedUpCoreAPIExample.Exceptions
{// Custom Http Exceptionpublic class HttpException : Exception{// Holds Http status code: 404 NotFound, 400 BadRequest, ...public int StatusCode { get; }public string MessageDetail { get; set; }public HttpException(HttpStatusCode statusCode, string message = null, string messageDetail = null) : base(message){StatusCode = (int)statusCode;MessageDetail = messageDetail;}}
}

And change the ExceptionsHandlingMiddleware class code

并更改ExceptionsHandlingMiddleware类代码

…public async Task InvokeAsync(HttpContext httpContext){try{await _next(httpContext);}catch (HttpException ex){await HandleHttpExceptionAsync(httpContext, ex);}catch (Exception ex){await HandleUnhandledExceptionAsync(httpContext, ex);}}
…
…
private async Task HandleHttpExceptionAsync(HttpContext context, HttpException exception)
{_logger.LogError(exception, exception.MessageDetail);if (!context.Response.HasStarted){int statusCode = exception.StatusCode;string message = exception.Message;context.Response.Clear();context.Response.ContentType = "application/json";context.Response.StatusCode = statusCode;var result = new ExceptionMessage(message).ToString();await context.Response.WriteAsync(result);}
}    

In the middleware, we process HttpException type exception before general Exception type, invoking the HandleHttpExceptionAsync method. And we log detailed exception message, if provided.

在中间件中,我们在处理常规Exception类型之前先处理HttpException类型的异常,并调用HandleHttpExceptionAsync方法。 并且,如果有提供,我们会记录详细的异常消息。

Now, we can rewrite ProductsService.GetProductAsync and ProductsService.DeleteProductAsync

现在,我们可以重写ProductsService.GetProductAsyncProductsService.DeleteProductAsync

…
public async Task<IActionResult> GetProductAsync(int productId)
{Product product = await _productsRepository.GetProductAsync(productId);if (product == null)throw new HttpException(HttpStatusCode.NotFound, "Product not found",  $"Product Id: {productId}");ThreadPool.QueueUserWorkItem(delegate{PreparePricesAsync(productId);});return new OkObjectResult(new ProductViewModel(product));
}public async Task<IActionResult> DeleteProductAsync(int productId)
{Product product = await _productsRepository.DeleteProductAsync(productId);if (product == null)throw new HttpException(HttpStatusCode.NotFound, "Product not found",  $"Product Id: {productId}");return new OkObjectResult(new ProductViewModel(product));
}
…

In this version, instead of returning 404 Not Found from the services with IActionResult, we are throwing a custom HttpException and the exceptions handle middleware returns a proper response to a user. Let us check how it works by calling an API with a productid, that is evidently not in Products table:

在此版本中,我们没有使用IActionResult从服务中返回404 Not Found,而是抛出了自定义HttpException,并且异常处理中间件向用户返回了正确的响应。 让我们通过调用带有productid的API来检查其工作原理,该产品显然不在Products表中:

http://localhost:49858/api/products/100

http:// localhost:49858 / api / products / 100

Our universal Exceptions Handling Middleware works fine.

我们通用的异常处理中间件工作正常。

Since we have created an alternative way to pass any StatucCode and message from the business layer, we can easily change a return value type from IActionResult to a proper POCO type. For this we have to rewrite the following interfaces:

由于我们创建了另一种方式来从业务层传递任何StatucCode和消息,因此我们可以轻松地将返回值类型从IActionResult更改为适当的POCO类型。 为此,我们必须重写以下接口:

public interface IProductsService
{Task<IActionResult> GetAllProductsAsync();Task<IActionResult> GetProductAsync(int productId);Task<IActionResult> FindProductsAsync(string sku);Task<IActionResult> DeleteProductAsync(int productId);Task<IEnumerable<ProductViewModel>> GetAllProductsAsync();Task<ProductViewModel> GetProductAsync(int productId);Task<IEnumerable<ProductViewModel>> FindProductsAsync(string sku);Task<ProductViewModel> DeleteProductAsync(int productId);
}    

And change

并改变

public interface IPricesService
{Task<IEnumerable<Price>> GetPricesAsync(int productId);Task<IEnumerable<PriceViewModel>> GetPricesAsync(int productId);
…
}

We should also redeclare appropriate methods in the ProductsService and PricesService classes, by changing IActionResult type to a type from the interfaces. And also change their return statements, by removing OkObjectResult statement. For example, in the ProductsService.GetAllProductsAsync method:

通过将IActionResult类型更改为接口中的类型,我们还应该在ProductsServicePricesService类中重新声明适当的方法。 并通过删除OkObjectResult语句来更改其return语句。 例如,在ProductsService.GetAllProductsAsync方法中:

the new version will be:

新版本将是:

public async Task<IEnumerable<ProductViewModel>> GetAllProductsAsync()
{IEnumerable<Product> products = await _productsRepository.GetAllProductsAsync();return products.Select(p => new ProductViewModel(p));
}

The final task is to change the controllers’ actions so that they create an OK response. It will always be 200 OK, because NotFound will be returned by the ExceptionsHandlingMiddleware

最后的任务是更改控制器的动作,以便它们创建OK响应。 它将始终为200,因为NotFound将由ExceptionsHandlingMiddleware返回

For example, for the ProductsService.GetAllProductsAsync the return statement should be changed from:

例如,对于ProductsService.GetAllProductsAsync ,return语句应更改为:

// GET /api/products
[HttpGet]
public async Task<IActionResult> GetAllProductsAsync()
{return await _productsService.GetAllProductsAsync();
}

to:

至:

// GET /api/products
[HttpGet]
public async Task<IActionResult> GetAllProductsAsync()
{ return new OkObjectResult(await _productsService.GetAllProductsAsync());
} 

You do this in all the ProductsController’s actions and in the PricesService.GetPricesAsync action.

您可以在所有ProductsController的操作和PricesService.GetPricesAsync操作中执行PricesService.GetPricesAsync操作。

在HttpClientFactory中使用类型化的客户端 (Using Typed Clients with HttpClientFactory)

Our previous implementation of HttpClient has some issues, we can improve. First of all, we have to inject IHttpContextAccessor to use it in the GetFullyQualifiedApiUrl method. Both IHttpContextAccessor and GetFullyQualifiedApiUrl method are dedicated only to HttpClient and never used in other places of ProductsService. If we want to apply the same functionality in another services, we will have to write almost the same code. So, it is better, to create a separate helper class – wrapper around HttpClient and encapsulate all the necessary HttpClient calling business logic inside this class.

我们先前对HttpClient的实现存在一些问题,我们可以改进。 首先,我们必须注入IHttpContextAccessor才能在GetFullyQualifiedApiUrl方法中使用它。 IHttpContextAccessorGetFullyQualifiedApiUrl方法仅专用于HttpClient,而从未在ProductsService的其他位置使用。 如果要在其他服务中应用相同的功能,则必须编写几乎相同的代码。 因此,最好创建一个单独的帮助程序类-围绕HttpClient的包装,并将所有必需的HttpClient调用业务逻辑封装在此类内。

We will use another way of working with the HttpClientFactory - Typed Clients class.

我们将使用另一种使用HttpClientFactory-Typed Clients类的方法。

In the Interfaces folder create an ISelfHttpClient intetface:

在“接口”文件夹中,创建一个ISelfHttpClient接口:

using System.Threading.Tasks;namespace SpeedUpCoreAPIExample.Interfaces
{public interface ISelfHttpClient{Task PostIdAsync(string apiRoute, string id);}
}

We have declared only one method, that calls any controller's action with HttpPost method and Id parameter

我们仅声明了一个方法,该方法使用HttpPost方法和Id参数调用任何控制器的操作

Let us create a Helpers folder and add there a new class SelfHttpClient inherited from the ISelfHttpClient interface:

让我们创建一个Helpers文件夹,并在其中添加一个从ISelfHttpClient接口继承的新类SelfHttpClient

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;namespace SpeedUpCoreAPIExample.Helpers
{// HttpClient for application's own controllers access public class SelfHttpClient : ISelfHttpClient{private readonly HttpClient _client;public SelfHttpClient(HttpClient httpClient, IHttpContextAccessor httpContextAccessor){string baseAddress = string.Format("{0}://{1}/api/",httpContextAccessor.HttpContext.Request.Scheme,httpContextAccessor.HttpContext.Request.Host);_client = httpClient;_client.BaseAddress = new Uri(baseAddress);}// Call any controller's action with HttpPost method and Id parameter.// apiRoute - Relative API route.// id - The parameter.public async Task PostIdAsync(string apiRoute, string id){try{var result = await _client.PostAsync(string.Format("{0}/{1}", apiRoute, Id), null).ConfigureAwait(false);}catch (Exception ex){//ignore errors}}}
}

In this class we obtain a baseAddress of API to be called in the class constructor. In PostIdAsync method we call the API with the HttpPost method by its relative apiRoute route and passing Id as a response parameter. Note, that instead of creating an empty HttpContent we just send null

在此类中,我们获得要在类构造函数中调用的API的baseAddress。 在PostIdAsync方法中,我们通过HttpPost方法的相对apiRoute路由并传递Id作为响应参数来调用API。 请注意,我们没有发送空的HttpContent,而是发送了null

We should declare this class in the Startup.ConfigureServices method:

我们应该在Startup.ConfigureServices方法中声明此类:

services.AddHttpClient();services.AddHttpClient<ISelfHttpClient, SelfHttpClient>();

Now we can use in any place of the application. In ProductsService service we should inject it at the class constructor. And we can remove both IHttpContextAccessor and IHttpClientFactory as we do not use them anymore and we can remove the GetFullyQualifiedApiUrl method.

现在我们可以在应用程序的任何地方使用。 在ProductsService服务中,我们应该在类构造函数中注入它。 而且我们可以删除IHttpContextAccessorIHttpClientFactory因为我们不再使用它们,并且可以删除GetFullyQualifiedApiUrl方法。

New version of ProductsService constructor will be:

ProductsService构造函数的新版本将是:

public class ProductsService : IProductsService
{private readonly IProductsRepository _productsRepository;private readonly ISelfHttpClient _selfHttpClient;public ProductsService(IProductsRepository productsRepository, ISelfHttpClient selfHttpClient){_productsRepository = productsRepository;_selfHttpClient = selfHttpClient;}
}    

Let us change the PreparePricesAsync method. First of all, we rename it CallPreparePricesApiAsync as this name is more informative and the method:

让我们更改PreparePricesAsync方法。 首先,我们将其重命名为CallPreparePricesApiAsync因为该名称更具信息性,并且该方法如下:

private async void CallPreparePricesApiAsync(string productId)
{await _selfHttpClient.PostIdAsync("prices/prepare", productId);
}

Do not forget to change PreparePricesAsync for CallPreparePricesApiAsync everywhere when we call this method in the ProductsService. Also take into account, that in CallPreparePricesApiAsync we use type of string productId parameter

当我们在ProductsService调用此方法时,请不要忘记在各处更改PreparePricesAsyncCallPreparePricesApiAsync 。 还应考虑到,在CallPreparePricesApiAsync我们使用字符串productId参数的类型

You can see, that we pass a tailing part of the API URL as a PostIdAsync parameter. The new SelfHttpClient is really reusable. For example, if we had an API /products/prepare, we could call the API like this:

您可以看到,我们将API URL的PostIdAsync作为PostIdAsync参数PostIdAsync 。 新的SelfHttpClient确实可重用。 例如,如果我们有一个API / products / prepare,则可以这样调用该API:

private async void CallPrepareProductAPIAsync(string productId)
{await _selfHttpClient.PostIdAsync("products/prepare", productId);
}

处理应用程序的设置 (Handling the application's settings)

In previous parts, we accessed the application’s settings, by injecting IConfiguration. Then, in class constructers we created a Settings class, in which we parsed appropriate settings variables and applied default values. This approach is good for debugging, but after debugging, using simple POCO classes to access the application’s settings seems to be more preferable. So, let us slightly change our appsettings.json. We will form two sections with settings for the Products and Prices services:

在前面的部分中,我们通过注入IConfiguration访问应用程序的设置。 然后,在类构造器中,我们创建了一个Settings类,在其中解析了适当的设置变量并应​​用了默认值。 这种方法非常适合调试,但是在调试之后,使用简单的POCO类访问应用程序的设置似乎更为可取。 因此,让我们稍微更改一下appsettings.json。 我们将形成两个部分,分别设置“产品和价格”服务:

  "Caching": {"PricesExpirationPeriod": 15}"Products": {"CachingExpirationPeriod": 15,"DefaultPageSize": 20},"Prices": {"CachingExpirationPeriod": 15,"DefaultPageSize": 20},
…    

Note! We will use DefaultPageSize values letter in this article.

注意! 我们将在本文中使用DefaultPageSize值字母。

Let us create settings POCO classes. Create a Settings folder with the following files:

让我们创建设置POCO类。 使用以下文件创建一个Settings文件夹:

namespace SpeedUpCoreAPIExample.Settings
{public class ProductsSettings{public int CachingExpirationPeriod { get; set; }public int DefaultPageSize { get; set; }}
}

and

namespace SpeedUpCoreAPIExample.Settings
{public class PricesSettings{public int CachingExpirationPeriod { get; set; }public int DefaultPageSize { get; set; }}
}

Although the classes are still similar, in a real application the setting of different services can vary significantly. So, we will use both classes in order to not divide them later.

尽管类仍然相似,但是在实际应用中,不同服务的设置可能会有很大差异。 因此,我们将使用这两个类,以便以后不对其进行划分。

Now, all we need for using these classes is to declare them in Startup.ConfigureServices:

现在,我们需要使用这些类的就是在Startup.ConfigureServices声明它们:

…      //Settingsservices.Configure<ProductsSettings>(Configuration.GetSection("Products"));services.Configure<PricesSettings>(Configuration.GetSection("Prices"));//Repositories
…      

After that, we can inject settings classes anywhere in our application, as we will demonstrate in following sections

之后,我们可以在应用程序中的任何位置注入设置类,如以下各节所示

缓存关注点分离 (Caching concern separation)

In the PricesRepository we have implemented caching with an IDistributedCache cache. Caching in a repository based on the idea to entirely close from the business layer details of data storage sources. In this case, it is not known for a Service whether the data passes the caching stage. Is this solution really good?

PricesRepository我们使用IDistributedCache缓存实现了缓存。 根据这种想法在存储库中进行缓存,以完全关闭数据存储源的业务层详细信息。 在这种情况下,服务不知道数据是否通过了缓存阶段。 这个解决方案真的好吗?

Repositories are responsible for working with the DbContext, i.e. getting the data from or saving to a database. But caching is definitely out of this concern. In addition, in more complex systems, after receiving the raw data from the database, the data may need to be modified before it is delivered to the user. And it is reasonable to cache the data in the final state. According to this it is better to apply caching at the business logic layer – in services.

存储库负责使用DbContext,即从数据库获取数据或将数据保存到数据库。 但是缓存绝对不是出于这种考虑。 另外,在更复杂的系统中,从数据库接收原始数据后,可能需要先修改数据,然后再将其交付给用户。 并且以最终状态缓存数据是合理的。 因此,最好在服务的业务逻辑层应用缓存。

Note! In the PricesRepository.GetPricesAsync and PricesRepository.PreparePricesAsync methods the code for caching is almost the same. Logically we should move this code to a separate class to avoid duplication.

注意! 在PriceRepository.GetPricesAsync和PriceRepository.PreparePricesAsync方法中,用于缓存的代码几乎相同。 从逻辑上讲,我们应该将此代码移到一个单独的类中,以避免重复。

通用异步DistributedCache存储库 (Generic Asynchronous DistributedCache repository)

The idea is to create a repository that will encapsulate IDistributedCache business logic. The repository will be generic and be able to cache any type of objects. Here is its Interface

这个想法是创建一个存储库,该存储库将封装IDistributedCache业务逻辑。 该存储库将是通用的,并且能够缓存任何类型的对象。 这是它的界面

using Microsoft.Extensions.Caching.Distributed;
using System;
using System.Threading.Tasks;namespace SpeedUpCoreAPIExample.Interfaces
{public interface IDistributedCacheRepository<T>{Task<T> GetOrSetValueAsync(string key, Func<Task<T>> valueDelegate, DistributedCacheEntryOptions options);Task<bool> IsValueCachedAsync(string key);Task<T> GetValueAsync(string key);Task SetValueAsync(string key, T value, DistributedCacheEntryOptions options);Task RemoveValueAsync(string key);}
}

The only interesting place here is an asynchronous delegate as a second parameter of the GetOrSetValueAsync method. It will be discussed in the implementation section. In the Repositories folder create a new class DistributedCacheRepository:

这里唯一有趣的地方是作为GetOrSetValueAsync方法的第二个参数的异步委托。 将在实现部分中讨论。 在Repositories文件夹中,创建一个新类DistributedCacheRepository

using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
using SpeedUpCoreAPIExample.Interfaces;
using System;
using System.Threading.Tasks;namespace SpeedUpCoreAPIExample.Repositories
{public abstract class DistributedCacheRepository<T> : IDistributedCacheRepository<T> where T : class{private readonly IDistributedCache _distributedCache;private readonly string _keyPrefix;protected DistributedCacheRepository(IDistributedCache distributedCache, string keyPrefix){_distributedCache = distributedCache;_keyPrefix = keyPrefix;}public virtual async Task<T> GetOrSetValueAsync(string key, Func<Task<T>> valueDelegate, DistributedCacheEntryOptions options){var value = await GetValueAsync(key);if (value == null){value = await valueDelegate();if (value != null)await SetValueAsync(key, value, options ?? GetDefaultOptions());}return null;}public async Task<bool> IsValueCachedAsync(string key){var value = await _distributedCache.GetStringAsync(_keyPrefix + key);return value != null;}public async Task<T> GetValueAsync(string key){var value = await _distributedCache.GetStringAsync(_keyPrefix + key);return value != null ? JsonConvert.DeserializeObject<T>(value) : null;}public async Task SetValueAsync(string key, T value, DistributedCacheEntryOptions options){await _distributedCache.SetStringAsync(_keyPrefix + key, JsonConvert.SerializeObject(value), options ?? GetDefaultOptions());}public async Task RemoveValueAsync(string key){await _distributedCache.RemoveAsync(_keyPrefix + key);}protected abstract DistributedCacheEntryOptions GetDefaultOptions();        }
}

The class is abstract as we are not going to create its instances directly. Instead, it will be a base class for the PricesCacheRepository and ProductsCacheRepository classes. Note, that the GetOrSetValueAsync has a virtual modifier – we will override this method in inherited classes. The same is true with the GetDefaultOptions method, in which case it is declared as abstract, so it will have its implementation in derived classes. And when it is called within the parent DistributedCacheRepository class, inherited methods from derived classes will be called.

该类是抽象的,因为我们不会直接创建其实例。 相反,它将是PricesCacheRepositoryProductsCacheRepository类的基类。 请注意, GetOrSetValueAsync具有一个虚拟修饰符–我们将在继承的类中重写此方法。 GetDefaultOptions方法也是如此,在这种情况下,它被声明为抽象,因此它将在派生类中实现。 当在父DistributedCacheRepository类中调用它时,将从派生类继承的方法被调用。

The second parameter of the GetOrSetValueAsync method is declared as an asynchronous delegate: Func<Task<T>> valueDelegate. In the GetOrSetValueAsync method we are first trying to get a value from the Cache. If it is not already cached, we get it by calling the valueDelegate function and then cache the value.

GetOrSetValueAsync方法的第二个参数声明为异步委托: Func<Task<T>> valueDelegate 。 在GetOrSetValueAsync方法中,我们首先尝试从Cache获取值。 如果尚未缓存,则可以通过调用valueDelegate函数来获取它,然后缓存该值。

Let us create inherited classes of definite types from the DistributedCacheRepository.

让我们从DistributedCacheRepository创建确定类型的继承类。

using Microsoft.Extensions.Caching.Distributed;
using SpeedUpCoreAPIExample.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;namespace SpeedUpCoreAPIExample.Interfaces
{public interface IPricesCacheRepository{Task<IEnumerable<Price>> GetOrSetValueAsync(string key, Func<Task<IEnumerable<Price>>> valueDelegate, DistributedCacheEntryOptions options = null);Task<bool> IsValueCachedAsync(string key);Task RemoveValueAsync(string key);}
}
using Microsoft.Extensions.Caching.Distributed;
using SpeedUpCoreAPIExample.Models;
using System;
using System.Threading.Tasks;namespace SpeedUpCoreAPIExample.Interfaces
{public interface IProductCacheRepository{Task<Product> GetOrSetValueAsync(string key, Func<Task<Product>> valueDelegate, DistributedCacheEntryOptions options = null);Task<bool> IsValueCachedAsync(string key);Task RemoveValueAsync(string key);Task SetValueAsync(string key, Product value, DistributedCacheEntryOptions options = null);}
}

Then we will create two classes in the Repositories folder

然后,我们将在Repositories文件夹中创建两个类

using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.Models;
using SpeedUpCoreAPIExample.Settings;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;namespace SpeedUpCoreAPIExample.Repositories
{public class PricesCacheRepository : DistributedCacheRepository<IEnumerable<Price>>, IPricesCacheRepository{private const string KeyPrefix = "Prices: ";private readonly PricesSettings _settings;public PricesCacheRepository(IDistributedCache distributedCache, IOptions<PricesSettings> settings): base(distributedCache, KeyPrefix){_settings = settings.Value;}public override async Task<IEnumerable<Price>> GetOrSetValueAsync(string key, Func<Task<IEnumerable<Price>>> valueDelegate, DistributedCacheEntryOptions options = null){return base.GetOrSetValueAsync(key, valueDelegate, options);}protected override DistributedCacheEntryOptions GetDefaultOptions(){//use default caching options for the class if they are not defined in options parameterreturn new DistributedCacheEntryOptions(){AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_settings.CachingExpirationPeriod)};}}
}

And

using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.Models;
using SpeedUpCoreAPIExample.Settings;
using System;
using System.Threading.Tasks;namespace SpeedUpCoreAPIExample.Repositories
{public class ProductCacheRepository : DistributedCacheRepository<Product>, IProductCacheRepository{private const string KeyPrefix = "Product: ";private readonly ProductsSettings _settings;public ProductCacheRepository(IDistributedCache distributedCache, IOptions<ProductsSettings> settings) : base(distributedCache, KeyPrefix){_settings = settings.Value;}public override async Task<Product> GetOrSetValueAsync(string key, Func<Task<Product>> valueDelegate, DistributedCacheEntryOptions options = null){return await base.GetOrSetValueAsync(key, valueDelegate, options);}protected override DistributedCacheEntryOptions GetDefaultOptions(){//use default caching options for the class if they are not defined in options parameterreturn new DistributedCacheEntryOptions(){AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_settings.CachingExpirationPeriod)};}        }
}

Note! Implementation of GetDefaultOptions is equal in both the ProductCacheRepository and the PricesCacheRepository classes and, it seems, could be moved to the base class. But in a real application, the caching policy can vary for different objects and if we move some universal implementation of GetDefaultOptions to the base class, we will have to change the base class when the caching logic of a derived class changes. This will violate the "Open-Close" design principle. That is why, we have implemented GetDefaultOptions method in derived classes.

注意! 在ProductCacheRepository和PriceCacheRepository类中,GetDefaultOptions的实现是相同的,并且看起来可以将其移至基类。 但是在实际的应用程序中,缓存策略可能会因对象的不同而不同,如果我们将GetDefaultOptions的某些通用实现移至基类,则当派生类的缓存逻辑发生更改时,我们将不得不更改基类。 这将违反“开-关”设计原则。 这就是为什么我们在派生类中实现了GetDefaultOptions方法。

Declare the repositories in the Startup class

Startup类中声明存储库

…    services.AddScoped<IPricesCacheRepository, PricesCacheRepository>();services.AddScoped<IProductCacheRepository, ProductCacheRepository>();
…    

Now, we can remove caching from PricesRepository and make it as simple as possible:

现在,我们可以从PricesRepository删除缓存,并使其尽可能简单:

using Microsoft.EntityFrameworkCore;
using SpeedUpCoreAPIExample.Contexts;
using SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.Models;
using System.Collections.Generic;
using System.Threading.Tasks;namespace SpeedUpCoreAPIExample.Repositories
{public class PricesRepository : IPricesRepository{private readonly DefaultContext _context;public PricesRepository(DefaultContext context){_context = context;}public async Task<IEnumerable<Price>> GetPricesAsync(int productId){return await _context.Prices.AsNoTracking().FromSql("[dbo].GetPricesByProductId @productId = {0}", productId).ToListAsync();}}
}

We can also rewrite the PricesService class. Instead of IDistributedCache we have injected IPricesCacheRepository.

我们还可以重写PricesService类。 代替IDistributedCache我们注入了IPricesCacheRepository

using SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.Models;
using SpeedUpCoreAPIExample.ViewModels;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;namespace SpeedUpCoreAPIExample.Services
{public class PricesService : IPricesService{private readonly IPricesRepository _pricesRepository;private readonly IPricesCacheRepository _pricesCacheRepository;public PricesService(IPricesRepository pricesRepository, IPricesCacheRepository pricesCacheRepository){_pricesRepository = pricesRepository;_pricesCacheRepository = pricesCacheRepository;}public async Task<IEnumerable<PriceViewModel>> GetPricesAsync(int productId){IEnumerable<Price> pricess = await _pricesCacheRepository.GetOrSetValueAsync(productId.ToString(), async () =>await _pricesRepository.GetPricesAsync(productId));return pricess.Select(p => new PriceViewModel(p)).OrderBy(p => p.Price).ThenBy(p => p.Supplier);}public async Task<bool> IsPriceCachedAsync(int productId){return await _pricesCacheRepository.IsValueCachedAsync(productId.ToString());}public async Task RemovePriceAsync(int productId){await _pricesCacheRepository.RemoveValueAsync(productId.ToString());
        }public async Task PreparePricesAsync(int productId){try{await _pricesCacheRepository.GetOrSetValueAsync(productId.ToString(), async () => await _pricesRepository.GetPricesAsync(productId));}catch{}}}
}

In the GetPricesAsync and PreparePricesAsync methods we have used the GetOrSetValueAsync method of the PricesCacheRepository. If a desired value is not in the cache, the asynchronous method GetPricesAsync is called.

GetPricesAsyncPreparePricesAsync方法,我们已经使用了GetOrSetValueAsync的方法PricesCacheRepository 。 如果所需的值不在缓存中,则调用异步方法GetPricesAsync

We have also created IsPriceCachedAsync and RemovePriceAsync methods which will be used later. Do not forget to declare them in the IPricesService interface:

我们还创建了IsPriceCachedAsyncRemovePriceAsync方法,这些方法将在以后使用。 不要忘记在IPricesService接口中声明它们:

using SpeedUpCoreAPIExample.ViewModels;
using System.Collections.Generic;
using System.Threading.Tasks;namespace SpeedUpCoreAPIExample.Interfaces
{public interface IPricesService{Task<IEnumerable<PriceViewModel>> GetPricesAsync(int productId);Task<bool> IsPriceCachedAsync(int productId);Task RemovePriceAsync(int productId);Task PreparePricesAsync(int productId);}
}

Let us check how the new caching approach works. For this, set a breakpoint inside the GetPricesAsync method:

让我们检查一下新的缓存方法如何工作。 为此,请在GetPricesAsync方法内设置一个断点:

And call http://localhost:49858/api/prices/1 API with the Swagger Inspector Extension two times:

并使用Swagger Inspector Extension两次调用http:// localhost:49858 / api / prices / 1 API:

During the first call, the debugger reaches the breakpoint. This means, that the GetOrSetValueAsync method cannot find a result in the cache and has to call the _pricesRepository.GetPricesAsync(productId) method, passed to the GetOrSetValueAsync as a delegate. But at the second call, the application workflow does not stop at the breakpoint, because it takes a value from the cache.

在第一个调用期间,调试器到达断点。 这意味着, GetOrSetValueAsync方法无法在缓存中找到结果,而必须调用_pricesRepository.GetPricesAsync(productId)方法,该GetOrSetValueAsync作为委托传递给GetOrSetValueAsync 。 但是在第二次调用时,应用程序工作流不会在断点处停止,因为它从缓存中获取一个值。

Now we can use our universal caching mechanism in the ProductService

现在我们可以在ProductService使用通用缓存机制

namespace SpeedUpCoreAPIExample.Services
{public class ProductsService : IProductsService{private readonly IProductsRepository _productsRepository;private readonly ISelfHttpClient _selfHttpClient;private readonly IPricesCacheRepository _pricesCacheRepository;private readonly IProductCacheRepository _productCacheRepository;private readonly ProductsSettings _settings;public ProductsService(IProductsRepository productsRepository, IPricesCacheRepository pricesCacheRepository,IProductCacheRepository productCacheRepository, IOptions<ProductsSettings> settings, ISelfHttpClient selfHttpClient){_productsRepository = productsRepository;_selfHttpClient = selfHttpClient;_pricesCacheRepository = pricesCacheRepository;_productCacheRepository = productCacheRepository;_settings = settings.Value;}public async Task<ProductsPageViewModel> FindProductsAsync(string sku){IEnumerable<product> products = await _productsRepository.FindProductsAsync(sku);if (products.Count() == 1){//only one record foundProduct product = products.FirstOrDefault();string productId = product.ProductId.ToString();//cache a product if not in cache yetif (!await _productCacheRepository.IsValueCachedAsync(productId)){await _productCacheRepository.SetValueAsync(productId, product);}//prepare pricesif (!await _pricesCacheRepository.IsValueCachedAsync(productId)){//prepare prices beforehandThreadPool.QueueUserWorkItem(delegate{CallPreparePricesApiAsync(productId);});}};return new OkObjectResult(products.Select(p => new ProductViewModel(p)));}…public async Task<ProductViewModel> GetProductAsync(int productId){ Product product = await _productCacheRepository.GetOrSetValueAsync(productId.ToString(), async () => await _productsRepository.GetProductAsync(productId));if (product == null){throw new HttpException(HttpStatusCode.NotFound, "Product not found",  $"Product Id: {productId}");}//prepare pricesif (!await _pricesCacheRepository.IsValueCachedAsync(productId.ToString())){//prepare prices beforehandThreadPool.QueueUserWorkItem(delegate{CallPreparePricesApiAsync(productId.ToString());});}return new ProductViewModel(product);}…public async Task<ProductViewModel> DeleteProductAsync(int productId){Product product = await _productsRepository.DeleteProductAsync(productId);if (product == null){throw new HttpException(HttpStatusCode.NotFound, "Product not found",  $"Product Id: {productId}");}//remove product and its prices from cacheawait _productCacheRepository.RemoveValueAsync(productId.ToString());await _pricesCacheRepository.RemoveValueAsync(productId.ToString());return new OkObjectResult(new ProductViewModel(product));}        …

实体框架中的内存中和数据库中分页 (In-Memory and In-Database pagination in Entity Framework)

You may have noticed, that the ProductsController’s methods GetAllProductsAsync and FindProductsAsync and the PricesController’s GetPricesAsync method, return collections of products and prices, which have no limitation according to the size of the collections. This mean, that in real application with huge database, responses of some API can return such a large amount of data, that a client application will not be able to process this data or even receive it in a reasonable period of time. To avoid this issue, a good practice is to establish pagination of the API’s results.

您可能已经将注意到, ProductsController的方法GetAllProductsAsyncFindProductsAsyncPricesControllerGetPricesAsync方法,产品和价格的回报集合,它们没有什么限制,根据集合的大小。 这意味着,在具有巨大数据库的实际应用程序中,某些API的响应可能返回如此大量的数据,以至于客户端应用程序将无法在合理的时间内处理该数据,甚至无法接收该数据。 为避免此问题,一个好的做法是建立API结果的分页。

There two ways of organizing pagination: in the memory and in the database. For example, when we receive prices for some product, we cache the result in Redis cache. So, we already have available the whole set of the prices and can establish in-memory pagination, which is the fasters approach.

有两种组织分页的方法:在内存中和在数据库中。 例如,当我们收到某些产品的价格时,会将结果缓存在Redis缓存中。 因此,我们已经掌握了全部价格,并可以建立内存分页,这是一种更快的方法。

On the other hand, using in-memory pagination in the GetAllProductsAsync method is not a good idea, because to do pagination in memory, we should read the entire Products collection to memory from a database. It is a really slow operation, which consumes a lot of resources. So, in this case, it is better to filter necessary set of data in the database, according to page size and index.

另一方面,在GetAllProductsAsync方法中使用内存中的分页并不是一个好主意,因为要在内存中进行分页,我们应该将整个Products集合从数据库中读取到内存中。 这是一个非常缓慢的操作,消耗大量资源。 因此,在这种情况下,最好根据页面大小和索引过滤数据库中必要的数据集。

For pagination, we will create a universal PaginatedList class, that will be able to work with collections of any data type and support both in-memory and in-database pagination approaches.

对于分页,我们将创建一个通用的PaginatedList类,该类将能够与任何数据类型的集合一起使用,并支持内存中和数据库中的分页方法。

Let us create a generic PaginatedList <T>, inherited from List <T> in the Helpers folder

让我们创建一个通用的PaginatedList <T> ,该继承自Helpers文件夹中的List <T>

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;namespace SpeedUpCoreAPIExample.Helpers
{public class PaginatedList<T> : List<T>{public int PageIndex { get; private set; }public int PageSize { get; private set; }public int TotalCount { get; private set; }public int TotalPages { get; private set; }public PaginatedList(IEnumerable<T> source, int pageSize, int pageIndex = 1){TotalCount = source.Count();PageIndex = pageIndex;PageSize = pageSize == 0 ? TotalCount : pageSize;TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);this.AddRange(source.Skip((PageIndex - 1) * PageSize).Take(PageSize));}private PaginatedList(IEnumerable<T> source, int pageSize, int pageIndex, int totalCount) : base(source){PageIndex = pageIndex;PageSize = pageSize;TotalCount = totalCount;TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);}public static async Task<PaginatedList<T>> FromIQueryable(IQueryable<T> source, int pageSize, int pageIndex = 1){int totalCount = await source.CountAsync();pageSize = pageSize == 0 ? totalCount : pageSize;int totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);if (pageIndex > totalPages){//return empty listreturn new PaginatedList<T>(new List<T>(), pageSize, pageIndex, totalCount);}if (pageIndex == 1 && pageSize == totalCount){//no paging needed}else{source = source.Skip((pageIndex - 1) * pageSize).Take(pageSize);};List<T> sourceList = await source.ToListAsync();return new PaginatedList<T>(sourceList, pageSize, pageIndex, totalCount);}}
}

We need the first constructor, to work with in-memory data collection of any type. The second constructor is also being used with collections in memory, but when the page size and the number of pages are already known. We mark it private, as it's being used only in the PaginatedList class itself in the FromIQueryable method.

我们需要第一个构造函数,以处理任何类型的内存数据收集。 第二个构造函数也与内存中的集合一起使用,但是当页面大小和页数已知时。 我们纪念它私有的,因为它是被只用于在PaginatedList类本身在FromIQueryable方法。

FromIQueryable is used to establish in-database pagination. The source parameter has IQueryable type. With IQueryable we do not work with physical data until we execute a real request to the database, like source.CountAsync() or source.ToListAsync(). So, we are able to format a proper pagination query and receive only a small set of filtered data in one request.

FromIQueryable用于建立数据库内分页。 源参数具有IQueryable类型。 使用IQueryable ,直到执行对数据库的实际请求source.CountAsync()source.CountAsync()source.ToListAsync() ,我们才使用物理数据。 因此,我们能够格式化适当的分页查询,并且在一个请求中仅接收一小部分已过滤的数据。

Let us also adjust the ProductsRepository.GetAllProductsAsync and ProductsRepository.FindProductsAsync methods so that they can work with in-database pagination. Now they should return IQueryable, but not IEnumerable as before.

让我们还调整ProductsRepository.GetAllProductsAsyncProductsRepository.FindProductsAsync方法,以便它们可以与数据库内分页一起使用。 现在,他们应该返回IQueryable ,但不像以前那样返回IEnumerable。

namespace SpeedUpCoreAPIExample.Interfaces
{public interface IProductsRepository{
…  Task<IEnumerable<Product>> GetAllProductsAsync();Task<IEnumerable<Product>> FindProductsAsync(string sku); IQueryable<Product> GetAllProductsAsync();IQueryable<Product> FindProductsAsync(string sku);
 …  }
}

Correct methods’ code in ProductsRepository class

ProductsRepository类中更正方法的代码

…      public async Task<IEnumerable<Product>> GetAllProductsAsync(){return await _context.Products.AsNoTracking().ToListAsync();}public IQueryable<Product> GetAllProductsAsync(){return  _context.Products.AsNoTracking();}public async Task<IEnumerable<Product>> FindProductsAsync(string sku){return await _context.Products.AsNoTracking().FromSql("[dbo].GetProductsBySKU @sku = {0}", sku).ToListAsync();}public IQueryable<Product> FindProductsAsync(string sku){return _context.Products.AsNoTracking().FromSql("[dbo].GetProductsBySKU @sku = {0}", sku);}
…  

Let us define the classes, in which we will return pagination results to users. In the ViewModels folder create PageViewModel – a base class

让我们定义类,将分页结果返回给用户。 在ViewModels文件夹中,创建PageViewModel –一个基类

namespace SpeedUpCoreAPIExample.ViewModels
{public class PageViewModel{public int PageIndex { get; private set; }public int PageSize { get; private set; }public int TotalPages { get; private set; }public int TotalCount { get; private set; }public bool HasPreviousPage => PageIndex > 1;public bool HasNextPage => PageIndex < TotalPages;public PageViewModel(int pageIndex, int pageSize, int totalPages, int totalCount){PageIndex = pageIndex;PageSize = pageSize;TotalPages = totalPages;TotalCount = totalCount;}}
}

And ProductsPageViewModel and PricesPageViewModel classes, inherited from PageViewModel

以及从PageViewModel继承的ProductsPageViewModelPricesPageViewModel

using SpeedUpCoreAPIExample.Helpers;
using SpeedUpCoreAPIExample.Models;
using System.Collections.Generic;
using System.Linq;namespace SpeedUpCoreAPIExample.ViewModels
{public class ProductsPageViewModel : PageViewModel{public IList<ProductViewModel> Items;public ProductsPageViewModel(PaginatedList<Product> paginatedList) :base(paginatedList.PageIndex, paginatedList.PageSize, paginatedList.TotalPages, paginatedList.TotalCount){this.Items = paginatedList.Select(p => new ProductViewModel(p)).ToList();}}
}
using SpeedUpCoreAPIExample.Helpers;
using SpeedUpCoreAPIExample.Models;
using System.Collections.Generic;
using System.Linq;namespace SpeedUpCoreAPIExample.ViewModels
{public class PricesPageViewModel : PageViewModel{public IList<PriceViewModel> Items;public PricesPageViewModel(PaginatedList<Price> paginatedList) :base(paginatedList.PageIndex, paginatedList.PageSize, paginatedList.TotalPages, paginatedList.TotalCount){this.Items = paginatedList.Select(p => new PriceViewModel(p)).OrderBy(p => p.Price).ThenBy(p => p.Supplier).ToList();}}
}

In PricesPageViewModel we applied additional sorting to the paginated list of PriceViewModel

PricesPageViewModel我们应用额外的排序来的分页列表PriceViewModel

Now we should change ProductsService.GetAllProductsAsync and ProductsService.FindProductsAsync so that they return ProductsPageViewMode

现在,我们应该更改ProductsService.GetAllProductsAsyncProductsService.FindProductsAsync以便它们返回ProductsPageViewMode

public interface IProductsService
…Task<IEnumerable<ProductViewModel>> GetAllProductsAsync();Task<IEnumerable<ProductViewModel>> FindProductsAsync(string sku);Task<ProductsPageViewModel> GetAllProductsAsync(int pageIndex, int pageSize);Task<ProductsPageViewModel> FindProductsAsync(string sku, int pageIndex, int pageSize);
    public class ProductsService : IProductsService{private readonly IProductsRepository _productsRepository;private readonly ISelfHttpClient _selfHttpClient;private readonly IPricesCacheRepository _pricesCacheRepository;private readonly IProductCacheRepository _productCacheRepository;private readonly ProductsSettings _settings;public ProductsService(IProductsRepository productsRepository, IPricesCacheRepository pricesCacheRepository,IProductCacheRepository productCacheRepository, IOptions<ProductsSettings> settings, ISelfHttpClient selfHttpClient){_productsRepository = productsRepository;_selfHttpClient = selfHttpClient;_pricesCacheRepository = pricesCacheRepository;_productCacheRepository = productCacheRepository;_settings = settings.Value;}public async Task<ProductsPageViewModel> FindProductsAsync(string sku, int pageIndex, int pageSize){pageSize = pageSize == 0 ? _settings.DefaultPageSize : pageSize;PaginatedList<Product> products = await PaginatedList<Product>.FromIQueryable(_productsRepository.FindProductsAsync(sku), pageIndex, pageSize);if (products.Count() == 1){//only one record foundProduct product = products.FirstOrDefault();string productId = product.ProductId.ToString();//cache a product if not in cache yetif (!await _productCacheRepository.IsValueCachedAsync(productId)){await _productCacheRepository.SetValueAsync(productId, product);}//prepare pricesif (!await _pricesCacheRepository.IsValueCachedAsync(productId)){//prepare prices beforehandThreadPool.QueueUserWorkItem(delegate{CallPreparePricesApiAsync(productId);});}};return new ProductsPageViewModel(products);}public async Task<ProductsPageViewModel> GetAllProductsAsync(int pageIndex, int pageSize){pageSize = pageSize == 0 ? _settings.DefaultPageSize : pageSize;PaginatedList<Product> products = await PaginatedList<Product>.FromIQueryable(_productsRepository.GetAllProductsAsync(), pageIndex, pageSize);return new ProductsPageViewModel(products);}
…        

Note, that if no valid parameters PageIndex and PageSize were passed to a PaginatedList constractor, default values – PageIndex = 1 and PageSize = whole datatable size are used. To avoid returning all records of Products and Prices tables, we will use default values DefaultPageSize taken from ProductsSettings and PricesSettings accordingly.

请注意,如果没有将有效参数PageIndex和PageSize传递给PaginatedList承包商,则使用默认值– PageIndex = 1和PageSize =整个数据表大小。 为了避免返回Products和Price表的所有记录,我们将相应地使用从ProductsSettings和PriceSettings中获取的默认值DefaultPageSize。

And change PricesServicePricesAsync to return PricesPageViewModel

并更改PricesServicePricesAsync以返回PricesPageViewModel

public interface IPricesService
…Task<IEnumerable<PriceViewModel> GetPricesAsync(int productId);Task<PricesPageViewModel> GetPricesAsync(int productId, int pageIndex, int pageSize);
…    
    public class PricesService : IPricesService{private readonly IPricesRepository _pricesRepository;private readonly IPricesCacheRepository _pricesCacheRepository;private readonly PricesSettings _settings;public PricesService(IPricesRepository pricesRepository, IPricesCacheRepository pricesCacheRepository, IOptions<PricesSettings> settings){_pricesRepository = pricesRepository;_pricesCacheRepository = pricesCacheRepository;_settings = settings.Value;} public async Task<PricesPageViewModel> GetPricesAsync(int productId, int pageIndex, int pageSize){IEnumerable<Price> prices = await _pricesCacheRepository.GetOrSetValueAsync(productId.ToString(), async () =>await _pricesRepository.GetPricesAsync(productId));            pageSize = pageSize == 0 ? _settings.DefaultPageSize : pageSize;return new PricesPageViewModel(new PaginatedList<Price>(prices, pageIndex, pageSize));}
…    

Now we can rewrite ProductsController and PricesController so that they can work with the new pagination mechanism

现在我们可以重写ProductsControllerPricesController以便它们可以使用新的分页机制

Let us change the ProductsController.GetAllProductsAsync and ProductsController.FindProductsAsync methods. The new versions will be:

让我们更改ProductsController.GetAllProductsAsyncProductsController.FindProductsAsync方法。 新版本将是:

[HttpGet]
public async Task<IActionResult> GetAllProductsAsync(int pageIndex, int pageSize)
{ProductsPageViewModel productsPageViewModel = await _productsService.GetAllProductsAsync(pageIndex, pageSize);return new OkObjectResult(productsPageViewModel);
}[HttpGet("find/{sku}")]
public async Task<IActionResult> FindProductsAsync(string sku, int pageIndex, int pageSize)
{ProductsPageViewModel productsPageViewModel = await _productsService.FindProductsAsync(sku, pageIndex, pageSize);return new OkObjectResult(productsPageViewModel);
}

And PricesController.GetPricesAsync method:

PricesController.GetPricesAsync方法:

[HttpGet("{Id:int}")]
public async Task<IActionResult> GetPricesAsync(int id, int pageIndex, int pageSize)
{PricesPageViewModel pricesPageViewModel = await _pricesService.GetPricesAsync(id, pageIndex, pageSize);return new OkObjectResult(pricesPageViewModel);
}

If we had some client that worked with an old version of our APIs, it could still work with the new version because, if we miss the pageIndex or pageSize parameter or both, their value will be 0, and our pagination mechanism can correctly process cases with pageIndex=0 and/or pageSize=0.

如果我们有一些使用旧版本API的客户端,它仍然可以使用新版本的API,因为如果错过了pageIndexpageSize参数或两者都pageSize ,它们的值将为0,并且我们的分页机制可以正确处理案例pageIndex = 0和/或pageSize = 0。

Since we have reached controllers in our code refactoring, let us stay here and sort out all the initial mess.

由于我们在代码重构中已到达控制器,因此让我们呆在这里并整理所有初始混乱情况。

控制器与ControllerBase (Controller vs ControllerBase)

You might have noticed, that in our solution ProductsController inherited from the Controller class, and PricesController inherited from the ControllerBase class. Both controllers work well, so which version should we use? Controller class supports Views and so it should be used for creating web-sites that use views. For a WEB API service, ControllerBase is preferable, because it is more lightweight as it does not have features that we do not need in WEB API.

您可能已经注意到,在我们的解决方案中, ProductsController继承自Controller类,而PricesController继承自ControllerBase类。 两个控制器都可以正常工作,那么我们应该使用哪个版本? Controller类支持Views,因此应将其用于创建使用View的网站。 对于WEB API服务,首选ControllerBase,因为它更轻量,因为它不具有WEB API不需要的功能。

So, we will inherit both our controllers from ControllerBase and use the attribute [ApiController] that enables such useful features as automatic model validation, attribute Routing and others

因此,我们将从ControllerBase继承我们的两个控制器,并使用属性[ApiController]启用诸如自动模型验证,属性路由和其他功能的有用功能。

So, change declaration of ProductsController for:

因此,将ProductsController声明更改为:

…
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
…

Let us examine, how model validation works with the ApiController attribute. For this, we will call some APIs with invalid parameters. For instance, the following action expects integer Id, but we send a string instead:

让我们研究一下模型验证如何与ApiController属性一起工作。 为此,我们将调用一些带有无效参数的API。 例如,以下操作需要整数Id,但我们改为发送字符串:

http://localhost:49858/api/products/aa

http:// localhost:49858 / api / products / aa

The result will be:

结果将是:

Status: 400 Bad Request

状态:400错误请求

{"id": ["The value 'aa' is not valid."]
}

In case, when we have intentionally declared type of parameter [HttpGet("{Id:int}")] things are even worse:

如果我们故意声明了参数[HttpGet("{Id:int}")]情况会更糟:

http://localhost:49858/api/prices/aa

http:// localhost:49858 / api / prices / aa

Status: 404 Not Found without any message about incorrect type of Id parameter.

状态:404未找到,没有任何有关Id参数类型错误的消息。

So, firstly, we will remove Id type declaration from the HttpGet attribute in the PricesController.GetPricesAsync method:

因此,首先,我们将从PricesController.GetPricesAsync方法的HttpGet属性中删除Id类型声明:

[HttpGet("{Id:int}")]
[HttpGet("{id}")]

This will give us a standard 400 Bad Request and a type mismatch message.

这将为我们提供标准的400错误请求和类型不匹配消息。

Another problem that directly concerns application productivity is eliminating senseless job. For instance, http://localhost:49858/api/prices/-1 API will evidently return 404 Not Found, as our database will never have any negative Id value.

直接关系到应用程序生产力的另一个问题是消除毫无意义的工作。 例如,http:// localhost:49858 / api / prices / -1 API显然会返回404 Not Found,因为我们的数据库永远不会有任何负Id值。

And we use positive integer Id parameter several times in our application. So, the idea is to create an Id validation filter and use it whenever we have an Id parameter.

并且我们在应用程序中多次使用了正整数Id参数。 因此,我们的想法是创建一个ID验证过滤器,并在有Id参数时使用它。

自定义ID参数验证过滤器和属性 (Custom Id parameter validation filter and Attribute)

In your solution create a Filters folder and a new class ValidateIdAsyncActionFilter in it:

在您的解决方案中,创建一个Filters文件夹并在其中创建一个新类ValidateIdAsyncActionFilter

using Microsoft.AspNetCore.Mvc.Filters;
using SpeedUpCoreAPIExample.Exceptions;
using System.Linq;
using System.Threading.Tasks;namespace SpeedUpCoreAPIExample.Filters
{// Validating Id request parameter ActionFilter. Id is required and must be a positive integerpublic class ValidateIdAsyncActionFilter : IAsyncActionFilter{public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next){ValidateParameter(context, "id");await next();}private void ValidateParameter(ActionExecutingContext context, string paramName){string message = $"'{paramName.ToLower()}' must be a positive integer.";var param = context.ActionArguments.SingleOrDefault(p => p.Key == paramName);if (param.Value == null){throw new HttpException(System.Net.HttpStatusCode.BadRequest, message, $"'{paramName.ToLower()}' is empty.");}var id = param.Value as int?;if (!id.HasValue || id < 1){throw new HttpException(System.Net.HttpStatusCode.BadRequest, message,param.Value != null ? $"{paramName}: {param.Value}" : null);}}
}    

In the filter we check whether a request has only one Id parameter. If the Id parameter is missed or does not have positive integer value, the filter generates BadRequest HttpException. Throwing an HttpException involves our ExceptionsHandlingMiddleware in the process, with all its benefits like logging, unified message format and so on.

在过滤器中,我们检查一个请求是否只有一个Id参数。 如果Id参数丢失或不具有正整数值,则过滤器将生成BadRequest HttpException。 抛出HttpException涉及流程中的ExceptionsHandlingMiddleware ,它具有日志记录,统一消息格式等所有优点。

To be able to apply this filter in any place of our controllers, we will create a ValidateIdAttribute in the same Filters folder:

为了能够在我们的控制器的任何位置应用此过滤器,我们将在同一Filters文件夹中创建ValidateIdAttribute

using Microsoft.AspNetCore.Mvc;namespace SpeedUpCoreAPIExample.Filters
{public class ValidateIdAttribute : ServiceFilterAttribute{public ValidateIdAttribute() : base(typeof(ValidateIdAsyncActionFilter)){}}
}    

In ProductsController add reference filters classes namespace

ProductsController添加引用过滤器类的命名空间

…
using SpeedUpCoreAPIExample.Filters;
…

and add the [ValidateId] attribute to all the GetProductAsync and DeleteProductAsync actions that need an Id parameter:

并将[ValidateId]属性添加到所有需要Id参数的GetProductAsyncDeleteProductAsync操作中:

…[HttpGet("{id}")][ValidateId]public async Task<IActionResult> GetProductAsync(int id){
…[HttpDelete("{id}")][ValidateId]public async Task<IActionResult> DeleteProductAsync(int id){
…

And we can apply the ValidateId attribute to the whole PricesController controller as all its actions need an Id parameter. In addition, we need to correct inaccuracies in the PricesController class namespace – it should obviously be namespace SpeedUpCoreAPIExample.Controllers, but not namespace SpeedUpCoreAPIExample.Contexts

并且我们可以将ValidateId属性应用于整个PricesController控制器,因为其所有操作都需要一个Id参数。 另外,我们需要纠正pricesController类命名空间中的不正确之处–它显然应该是namespace SpeedUpCoreAPIExample.Controllers ,而不是命名空间SpeedUpCoreAPIExample.Contexts

using Microsoft.AspNetCore.Mvc;
using SpeedUpCoreAPIExample.Filters;
using SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.ViewModels;
using System.Threading.Tasks;namespace SpeedUpCoreAPIExample.Contexts
namespace SpeedUpCoreAPIExample.Controllers
{[Route("api/[controller]")][ApiController]public class PricesController : ControllerBase{
…

The last step is to declare the filter in Startup.cs

最后一步是在Startup.cs中声明过滤器

using SpeedUpCoreAPIExample.Filters;
…
public void ConfigureServices(IServiceCollection services)
…
services.AddSingleton<ValidateIdAsyncActionFilter>();
…

Let us check how the new filter works. For this we will again call incorrectly API http://localhost:49858/api/prices/-1. The result will be exactly as we desired:

让我们检查一下新过滤器的工作原理。 为此,我们将再次错误地调用API http:// localhost:49858 / api / prices / -1 。 结果将完全符合我们的期望:

Status: 400 Bad Request

状态:400错误请求

{"message": "'Id' must be a positive integer."
}

Note! We have used the ExceptionMessage class and now Messages usually satisfy our format conventions, but not always! If we try the http://localhost:49858/api/prices/aa ones again, we will still have a standard 400 Bad Request message. This happens, because of the [ApiController] attribute. When it is applied, the framework automatically registers a ModelStateInvalidFilter, which will work before our ValidateIdAsyncActionFilter filter and will generate a message of its own format.

注意! 我们使用了ExceptionMessage类,现在消息通常满足我们的格式约定,但并非总是如此! 如果我们再次尝试http:// localhost:49858 / api / prices / aa ,我们仍然会收到标准的400错误请求消息。 发生这种情况,因为[ApiController]属性。 当应用它时,框架会自动注册一个ModelStateInvalidFilter,它将在我们的ValidateIdAsyncActionFilter过滤器之前工作,并将生成自己格式的消息。

We can suppress this behavior in the ConfigureServices method of the Startup class:

我们可以在Startup类的ConfigureServices方法中抑制此行为:

…
services.AddMvc();
services.AddApiVersioning();
…
services.Configure<ApiBehaviorOptions>(options =>
{options.SuppressModelStateInvalidFilter = true;
});

After that, only our filter is working and we can control the model validation messages format. But now we are obligated to organize explicit validation for all parameters of controller actions.

之后,只有我们的过滤器起作用,并且我们可以控制模型验证消息的格式。 但是现在我们必须对控制器动作的所有参数进行显式验证。

分页参数自定义模型验证过滤器 (Pagination parameters custom model validation filter)

We have used pagination tree times in our simple application. Let us examine what will happen with incorrect parameters. For this we will call http://localhost:49858/api/products?pageindex=-1

我们在简单的应用程序中使用了分页树时间。 让我们检查不正确的参数会发生什么。 为此,我们将调用http:// localhost:49858 / api / products?pageindex = -1

The result will be:

结果将是:

Status: 500 Internal Server Error

状态:500内部服务器错误

{"message": "The offset specified in a OFFSET clause may not be negative."
}

This message is really confusing, because there was not a Server Error, it was a pure BadRequest. And the text itself is mysterious if you do not know that it is about pagination.

该消息确实令人困惑,因为没有服务器错误,这是一个纯粹的BadRequest 。 如果您不知道文字与分页有关,则文字本身就是神秘的。

We would prefer to have a response:

我们希望有一个回应:

Status: 400 Bad Request

状态:400错误请求

{"message": "'pageindex' must be 0 or a positive integer."
}

Another question is where to apply parameter checking. Note, that our pagination mechanism works well if any or both parameters are omitted – it uses default values. We should control only negative parameters. Throwing HttpException at PaginatedList level id not a good idea, as code should be reusable without changing it, and next time a PaginatedList will not necessarily be used in ASP.NET applications. Checking parameters at the Services level is better, but will demand duplication of the validation code or creating other public helper classes with validation methods.

另一个问题是在哪里应用参数检查。 请注意,如果省略任何一个参数或两个参数,我们的分页机制都可以很好地工作-它使用默认值。 我们应该只控制负参数。 在PaginatedList级别上将HttpException抛出id不是一个好主意,因为代码应该可以重用而不更改它,并且下次PaginatedList不一定会在ASP.NET应用程序中使用。 在“服务”级别检查参数更好,但是将要求重复验证代码或使用验证方法创建其他公共帮助程序类。

As far as pagination parameters come from outside,better places to organize their checking are in controllers before passing to a pagination procedure.

至于分页参数来自外部,在进行分页程序之前,最好在控制器中进行组织检查的位置。

So, we have to create another model validation filter, that will validate the PageIndex and PageSize parameters. The idea of validation is slightly different – any or both parameters can be omitted, can be equal zero or an integer greater than zero.

因此,我们必须创建另一个模型验证过滤器,该过滤器将验证PageIndexPageSize参数。 验证的概念略有不同-可以省略任何一个或两个参数,可以等于零或大于零的整数。

In the same Filters folder create a new class ValidatePagingAsyncActionFilter:

在同一Filters文件夹中,创建一个新类ValidatePagingAsyncActionFilter

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Newtonsoft.Json.Linq;
using System.Linq;
using System.Threading.Tasks;namespace SpeedUpCoreAPIExample.Filters
{// Validating PageIndex and PageSize request parameters ActionFilter. If exist, must be 0 or a positive integerpublic class ValidatePagingAsyncActionFilter : IAsyncActionFilter{public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next){ValidateParameter(context, "pageIndex");ValidateParameter(context, "pageSize");await next();}private void ValidateParameter(ActionExecutingContext context, string paramName){var param = context.ActionArguments.SingleOrDefault(p => p.Key == paramName);if (param.Value != null){var id = param.Value as int?;if (!id.HasValue || id < 0){string message = $"'{paramName.ToLower()}' must be 0 or a positive integer.";throw new HttpException(System.Net.HttpStatusCode.BadRequest, message,param.Value != null ? $"{paramName}: {param.Value}" : null);}}}}
}

Then create ValidatePagingAttribute class:

然后创建ValidatePagingAttribute类:

using Microsoft.AspNetCore.Mvc;namespace SpeedUpCoreAPIExample.Filters
{public class ValidatePagingAttribute : ServiceFilterAttribute{public ValidatePagingAttribute() : base(typeof(ValidatePagingAsyncActionFilter)){}}
}

Then declare the filter in Startup.cs

然后在Startup.cs中声明过滤器

…
public void ConfigureServices(IServiceCollection services)
…
services.AddSingleton<ValidatePagingAsyncActionFilter>();
…

And finally, add [ValidatePaging] attribute to ProductsController.GetAllProductsAsync, ProductsController.FindProductsAsync methods:

最后,将[ValidatePaging]属性添加到ProductsController.GetAllProductsAsyncProductsController.FindProductsAsync方法:

…[HttpGet][ValidatePaging]public async Task<IActionResult> GetAllProductsAsync(int pageIndex, int pageSize){
…    [HttpGet("find/{sku}")][ValidatePaging]public async Task<IActionResult> FindProductsAsync(string sku, int pageIndex, int pageSize){
…    

and PricesController.GetPricesAsync method:

PricesController.GetPricesAsync方法:

…[HttpGet("{id}")][ValidatePaging]public async Task<IActionResult> GetPricesAsync(int id, int pageIndex, int pageSize){
…    

Now we have an auto validation mechanism for all the sensitive parameters, and our application works correctly (at least locally)

现在,我们为所有敏感参数提供了一种自动验证机制,并且我们的应用程序正常运行(至少在本地)

跨域资源共享(CORS) (Cross-origin resource sharing (CORS))

In a real application we will bind some domain name to our web-service and its URL will look like http://mydomainname.com/api/

在实际的应用程序中,我们将某些域名绑定到我们的Web服务,其URL看起来像http://mydomainname.com/api/

At the same time, a client application that consumes APIs of our service can host on a different domain. If a client, a web-site for example, uses AJAX for API requests, and the response does not contain Access-Control-Allow-Origin header with value = * (all domains allowed), or with the same host as origin (client's host), browsers that support CORS, will block the response for safety reasons.

同时,使用我们服务的API的客户端应用程序可以托管在其他域上。 如果客户端(例如网站)使用AJAX进行API请求,并且响应中不包含值= *(允许所有域)或与源主机相同的主机的Access-Control-Allow-Origin标头(客户端的主机),则出于安全原因,支持CORS的浏览器将阻止响应。

Let us make sure. Build and publish our application to IIS, bind it with a test URL (mydomainname.com in our example), and call any API with https://resttesttest.com/ - on-line tool for API checking:

让我们确定一下。 构建我们的应用程序并将其发布到IIS,将其与测试URL绑定(在本示例中为mydomainname.com),然后使用https://resttesttest.com/调用任何API-在线工具进行API检查:

启用CORS ASP.NET Core (Enable CORS ASP.NET Core)

To enforce our application sending the right header, we should enable CORS. For this install the Microsoft.AspNetCore.Cors NuGet package (if you still do not have it installed with another package like Microsoft.AspNetCore.MVC or Microsoft.AspNetCore.All)

要强制我们的应用发送正确的报头,我们应该启用CORS。 为此,请安装Microsoft.AspNetCore.Cors NuGet程序包(如果仍未与其他程序包(如Microsoft.AspNetCore.MVC或Microsoft.AspNetCore.All)一起安装)

The simplest way to enable CORS is to add to the Startup.cs the following code:

启用CORS的最简单方法是在Startup.cs中添加以下代码:

…
public void Configure(
…
app.UseCors(builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
…
app.UseMvc();
…

This way we have allowed access to our API from any host. We could also add the .AllowCredentials() option, but it is not secure to use it with AllowAnyOrigin.

这样,我们就可以从任何主机访问我们的API。 我们还可以添加.AllowCredentials()选项,但是将其与AllowAnyOrigin一起使用并不安全。

After that, rebuild, republish the application to IIS and test it with resttesttest.com or another tool. At first glance, everything works fine - the CORS error message disappeared. But this works only until our ExceptionsHandlingMiddleware enters the game.

之后,重新生成,将应用程序重新发布到IIS,并使用resttesttest.com或其他工具对其进行测试。 乍一看,一切正常-CORS错误消息消失了。 但这仅在我们的ExceptionsHandlingMiddleware进入游戏之前有效。

如果发生HTTP错误,则不会发送任何CORS标头 (No CORS headers sent in case of HTTP error)

This happens because in fact, the response headers collection is empty, when an HttpException or any other Exception occurs and the middleware processes it. This means, that no Access-Control-Allow-Origin header is passed to a client application and CORS issue arises.

发生这种情况的原因是,实际上,当HttpException或任何其他Exception发生并且中间件对其进行处理时,响应标头集合为空。 这意味着没有Access-Control-Allow-Origin标头传递给客户端应用程序,并且会出现CORS问题。

如何在ASPNET.Core Web应用程序中发送带有CORS标头的HTTP 4xx-5xx响应 (How to send HTTP 4xx-5xx response with CORS headers in an ASPNET.Core web app)

To overcome this problem, we should enable CORS slightly differently. In Startup.ConfigureServices enter the following code:

为了克服这个问题,我们应该稍微启用CORS。 在Startup.ConfigureServices输入以下代码:

…
public void ConfigureServices(IServiceCollection services)
{services.AddCors(options =>{options.AddPolicy("Default", builder =>{builder.AllowAnyOrigin();builder.AllowAnyMethod();builder.AllowAnyHeader();});});
…

And in Startup.Configure:

并在Startup.Configure

…
public void Configure(
…app.UseCors("Default");
…app.UseMvc();
…

Enabling CORS this way, gives us access to CorsOptions in any place of our application via dependency injection. And the idea is to repopulate the response header in ExceptionsHandlingMiddleware with the CORS policy, taken from CorsOptions.

以这种方式启用CORS,使我们可以通过依赖注入在应用程序的任何位置访问CorsOptions。 想法是用CorsOptions中的CORS策略重新填充ExceptionsHandlingMiddleware的响应头。

Correct code of the ExceptionsHandlingMiddleware class:

ExceptionsHandlingMiddleware类的正确代码:

using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System;
using System.Net;
using System.Threading.Tasks;namespace SCARWebService.Exceptions
{public class ExceptionsHandlingMiddleware{private readonly RequestDelegate _next;private readonly ILogger<ExceptionsHandlingMiddleware> _logger;private readonly ICorsService _corsService;private readonly CorsOptions _corsOptions;public ExceptionsHandlingMiddleware(RequestDelegate next, ILogger<ExceptionsHandlingMiddleware> logger,ICorsService corsService, IOptions<CorsOptions> corsOptions){_next = next;_logger = logger;_corsService = corsService;_corsOptions = corsOptions.Value;}…private async Task HandleHttpExceptionAsync(HttpContext context, HttpException exception){_logger.LogError(exception, exception.MessageDetail);if (!context.Response.HasStarted){int statusCode = exception.StatusCode;string message = exception.Message;context.Response.Clear();//repopulate Response header with CORS policy_corsService.ApplyResult(_corsService.EvaluatePolicy(context, _corsOptions.GetPolicy("Default")), context.Response);context.Response.ContentType = "application/json";context.Response.StatusCode = statusCode;var result = new ExceptionMessage(message).ToString();await context.Response.WriteAsync(result);}}private async Task HandleUnhandledExceptionAsync(HttpContext context, Exception exception){_logger.LogError(exception, exception.Message);if (!context.Response.HasStarted){int statusCode = (int)HttpStatusCode.InternalServerError; // 500string message = string.Empty;
#if DEBUGmessage = exception.Message;
#elsemessage = "An unhandled exception has occurred";
#endifcontext.Response.Clear();//repopulate Response header with CORS policy_corsService.ApplyResult(_corsService.EvaluatePolicy(context, _corsOptions.GetPolicy("Default")), context.Response);context.Response.ContentType = "application/json";context.Response.StatusCode = statusCode;var result = new ExceptionMessage(message).ToString();await context.Response.WriteAsync(result);}}
…

If we rebuild and republish our application, it will work fine without any CORS issue, when its APIs are being called from any host.

如果我们重建并重新发布我们的应用程序,则从任何主机调用其API时,它将正常运行而不会出现任何CORS问题。

API版本控制 (API versioning)

Before making our application public, we must consider how its APIs will be consumed. After a certain period of time the requirements might be changed and we will have to rewrite the application so that its API will return different sets of data. If we publish web-service with new changes, but do not update the client’s applications that consume the APIs we will have big problems with client-server compatibility.

在将我们的应用程序公开之前,我们必须考虑如何使用其API。 一段时间后,需求可能会更改,我们将不得不重写应用程序,以便其API返回不同的数据集。 如果我们发布具有新更改的Web服务,但不更新使用API​​的客户端应用程序,则客户端与服务器的兼容性将面临很大的问题。

To avoid these problems, we should establish API versioning. For instance, an old version of Products API will have a route:

为了避免这些问题,我们应该建立API版本控制。 例如,旧版本的Products API将具有以下路线:

http://mydomainname.com/api/v1.0/products/

http://mydomainname.com/api/v1.0/products/

and a new version will have a route

新版本将有一条路线

http://mydomainname.com/api/v2.0/products/

http://mydomainname.com/api/v2.0/products/

In this case, even old client applications will continue working fine, until they are updated for a release that can work correctly with version v2.0

在这种情况下,即使是旧的客户端应用程序也可以继续正常运行,直到为适用于v2.0版本的发行版更新它们为止

In our application we will realize URL Path Based Versioning, where a version number is a part of the APIs URL, like in the above-mentioned example.

在我们的应用程序中,我们将实现基于URL路径的版本控制,其中版本号是API URL的一部分,如上述示例所示。

In .NET Core Microsoft.AspNetCore.Mvc.Versioning package is responsible for Versioning. So, we should install the package first:

在.NET Core中,Microsoft.AspNetCore.Mvc.Versioning包负责版本控制。 因此,我们应该先安装该软件包:

Then add services.AddApiVersioning() to the Startup's class ConfigureServices method:

然后将services.AddApiVersioning()添加到启动类的ConfigureServices方法中:

…services.AddMvc();services.AddApiVersioning();

And finally, add ApiVersion and correct Route attributes to both controllers:

最后,添加ApiVersion并将正确的Route属性添加到两个控制器:

[ApiVersion("1.0")][Route("/api/v{version:apiVersion}/[controller]/")]

Now we have versioning. Having done that, if we want to enhance the application for a version 2.0, for example, we can add the [ApiVersion("2.0")] attribute to a controller:

现在我们有了版本控制。 完成此操作后,例如,如果要增强版本2.0的应用程序,可以将[ApiVersion(“ 2.0”)]属性添加到控制器:

[ApiVersion("1.0")][ApiVersion("2.0")]

then create an action, we want to be working only with v2.0 and add add [MapToApiVersion("2.0")] attribute to the action.

然后创建一个动作,我们只想使用v2.0,并向该动作添加添加[MapToApiVersion("2.0")]属性。

The versioning mechanism works perfectly out of the box almost without any coding but, as usual, with a fly in the ointment: if we have accidentally used a wrong version in the API URL (http://localhost:49858/api/v10.0/prices/1), we will have an error message in the following format:

版本控制机制几乎不需要任何编码就可以很好地开箱即用,但是像往常一样,美中不足的是:如果我们在API URL中意外使用了错误的版本( http:// localhost:49858 / api / v10。 0 / prices / 1 ),我们将收到以下格式的错误消息:

Status: 400 Bad Request

状态:400错误请求

{"error": {"code": "UnsupportedApiVersion","message": "The HTTP resource that matches the request URI 'http://localhost:49858/api/v10.0/prices/1' does not support the API version '10.0'.","innerError": null}
}

This is the standard error response format. It is much more informative, but absolutely far from our desired format. So, if we want to use unified format for all type of messages, we have to make a choice between the detailed standard error response format and the simple one, we have designed for our application.

这是标准的错误响应格式 。 它提供的信息更多,但与我们期望的格式绝对相去甚远。 因此,如果我们想对所有类型的消息使用统一的格式,则必须在详细的标准错误响应格式和简单的格式之间进行选择,这是我们为应用程序设计的。

To apply the standard error response format, we could just extend our ExceptionMessage class. Fortunately, we have foreseen this opportunity and it would not be difficult. But in this format messages are even more detailed, than we want to pass to users. Such detalization is probably not really relevant in a simple application. So, as far as we are not going to complicate things, we will use our simple format.

要应用标准错误响应格式,我们可以扩展ExceptionMessage类。 幸运的是,我们已经预见到了这个机会,这并不困难。 但是,这种格式的消息比我们想要传递给用户的消息还要详细。 在一个简单的应用程序中,这种销毁可能并不真正相关。 因此,就我们不会使事情复杂化而言,我们将使用简单的格式。

控制API版本控制错误消息格式 (Controlling API versioning error message format)

Let us create a VersioningErrorResponseProvider class in the Exceptions folder:

让我们在Exceptions文件夹中创建VersioningErrorResponseProvider类:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Versioning;namespace SpeedUpCoreAPIExample.Exceptions
{public class VersioningErrorResponseProvider : DefaultErrorResponseProvider{public override IActionResult CreateResponse(ErrorResponseContext context){string message = string.Empty;switch (context.ErrorCode){case "ApiVersionUnspecified":message = "An API version is required, but was not specified.";break;case "UnsupportedApiVersion":message = "The specified API version is not supported.";break;case "InvalidApiVersion":message = "An API version was specified, but it is invalid.";break;case "AmbiguousApiVersion":message = "An API version was specified multiple times with different values.";break;default:message = context.ErrorCode;break;}throw new HttpException(System.Net.HttpStatusCode.BadRequest, message, context.MessageDetail);}}
}

The class inherits from DefaultErrorResponseProvider. It just formats a friendly message, according to an ErrorCode (list of codes) and throws out HttpException BadRequest exception. Then the exception is processed by our ExceptionHandlerMiddleware with logging, unified error message formatting, etc.

该类继承自DefaultErrorResponseProvider 。 它只是根据ErrorCode( 代码列表 )格式化友好消息,并抛出HttpException BadRequest异常。 然后,异常由我们的ExceptionHandlerMiddleware处理,具有日志记录,统一错误消息格式等。

The last step is to register the VersioningErrorResponseProvider class as versioning HTTP error response generator. In the Startup class, in the ConfigureServices method add options at API versioning service registration:

最后一步是将VersioningErrorResponseProvider类注册为版本控制HTTP错误响应生成器。 在Startup类的ConfigureServices方法中,在API版本控制服务注册中添加选项:

…    services.AddMvc();services.AddApiVersioning(options =>{options.ErrorResponses = new VersioningErrorResponseProvider();});
…    

Thus, we have changed the standard error response behavior to our desired one.

因此,我们已将标准错误响应行为更改为所需的行为。

内部HTTP调用中的版本控制 (Versioning in inner HTTP invocations)

We also have to apply versioning in the SelfHttpClient class. In the class we set the BaseAddress property of HttpClient to call API. We should consider versioning when building the base address.

我们还必须在SelfHttpClient类中应用版本控制。 在该类中,我们将HttpClient的BaseAddress属性设置为调用API。 建立基址时,我们应该考虑版本控制。

To avoid hard coding of the APIs version we are going to invoke, we create a settings class for API versioning. In the appsettings.json file create an API section:"

为避免要对API版本进行硬编码,我们为API版本创建了一个设置类。 在appsettings.json文件中,创建一个API部分:“

…,"Api": {"Version": "1.0"}
…

Then in the Settings folder create ApiSettings.cs file:

然后在“ 设置”文件夹中创建ApiSettings.cs文件:

namespace SpeedUpCoreAPIExample.Settings
{public class ApiSettings{public string Version { get; set; }}
}

Declare the class in the Startup's ConfigureServices method:

在启动的ConfigureServices方法中声明该类:

…
public void ConfigureServices(IServiceCollection services)
…  //Settingsservices.Configure<ApiSettings>(Configuration.GetSection("Api"));
…

And, finally, change the SelfHttpClient's constructor:

最后,更改SelfHttpClient的构造函数:

public SelfHttpClient(HttpClient httpClient, IHttpContextAccessor httpContextAccessor, IOptions<ApiSettings> settings)
{string baseAddress = string.Format("{0}://{1}/api/v{2}/",httpContextAccessor.HttpContext.Request.Scheme,httpContextAccessor.HttpContext.Request.Host,settings.Value.Version);_client = httpClient;_client.BaseAddress = new Uri(baseAddress);
}

DNS名称本地解析 (DNS name resolving locally)

Let us finish with the SelfHttpClient class. We use it to call our own API for data preparation in advance. In the class contractor we build the base address of our API, using HttpContextAccessor. As far as we have started publishing our application on the internet, the base address will be like http://mydomainname.com/api/v1.0/. When we invoke an API, the HttpClient in background appeals to a DNS server to resolve this mydomainname.com host name into the IP of the web server where the application runs and then goes to this IP. But we know the IP - it is the IP of our own server. So, to avoid this senseless trip to a DNS server, we should resolve the host name locally, by adding it in the hosts file on our server.

让我们结束SelfHttpClient类。 我们使用它来提前调用我们自己的API进行数据准备。 在类承包商中,我们使用HttpContextAccessor构建API的基址。 就我们开始在互联网上发布应用程序而言,基址将类似于http://mydomainname.com/api/v1.0/。 当我们调用API时,后台的HttpClient会吸引DNS服务器将此mydomainname.com主机名解析为应用程序运行所在的Web服务器的IP,然后转到该IP。 但是我们知道IP-它是我们自己服务器的IP。 因此,为了避免无意间访问DNS服务器,我们应在本地解析主机名,方法是将其添加到服务器的hosts文件中。

Path to the hosts file is C:\Windows\System32\drivers\etc\

主机文件的路径是C:\ Windows \ System32 \ drivers \ etc \

You should add the next entries:

您应该添加以下条目:

192.168.1.1 mydomainname.com
192.168.1.1 www.mydomainname.com

where 192.168.1.1 - is the IP of our web-server in a local network

192.168.1.1-是本地网络中Web服务器的IP

After this improvement HTTP response will not even leave the boundaries of our server and, thus, will be executed much faster.

改进之后,HTTP响应甚至不会超出我们服务器的边界,因此执行速度将大大提高。

记录.NET Core API应用程序 (Documenting .NET Core API application)

We can consider two aspects of documenting the application:

我们可以考虑记录应用程序的两个方面:

  • XML documentation of code - actually, the code should be self-documented. However, sometimes we still need to give an extra explanation about the details of some methods and their parameters. We will document our code with XML comments;

    代码的XML文档 -实际上,代码应该是自文档的。 但是,有时我们仍然需要对某些方法及其参数的细节进行额外说明。 我们将用XML注释记录代码。

  • OpenAPI documentation - documenting APIs so that developers of the client’s application could be able to apply to this document in an OpenAPI Specification format and receive the comprehensive information that reflects all the API's details.

    OpenAPI文档 -记录API,以便客户端应用程序的开发人员能够以OpenAPI规范格式将此文档应用于此文档,并接收反映所有API详细信息的全面信息。

XML注释 (XML comments)

To enable XML comments, open project properties and select Build tab:

要启用XML注释,请打开项目属性,然后选择“构建”选项卡:

Here we should check the XML documentation file checkbox and leave the default value. We should also add 1591 warning numbers into the Suppress warnings textbox to prevent compiler warnings if we omit XML comments for some public classes, properties, methods etc.

在这里,我们应该选中XML文档文件复选框,并保留默认值。 如果我们省略一些公共类,属性,方法等的XML注释,我们还应该在禁止警告文本框中添加1591警告编号以防止编译器警告。

Now we can comment our code like this:

现在我们可以这样注释我们的代码:

…
/// <summary>
/// Call any controller's action with HttpPost method and Id parameter.
/// </summary>
/// <param name="apiRoute">Relative API route.</param>
/// <param name="id">The parameter.</param>
public async Task PostIdAsync(string apiRoute, string id)
…  

Here you can find detailed information about Documenting code with XML comments.

在这里,您可以找到有关带有XML注释的文档代码的详细信息。

An XML file with a name, specified in the XML documentation file textbox, will be created. We will need this file later.

将创建一个在XML文档文件文本框中指定名称的XML文件。 我们稍后将需要此文件。

带有Swagger的RESTful API的OpenAPI文档 (OpenAPI documentation for RESTful APIs with Swagger)

Requirements to API documentation mechanism:

API文档机制要求:

The documentation should be generated automatically;

文档应自动生成;

API versioning should be supported and autodiscovered;

应支持并自动发现API版本控制;

Documentation from the XML comments file should also be used;

还应使用XML注释文件中的文档。

The mechanism should provide the UI with the documentation where users are able to test the APIs without writing a real client application;

该机制应向UI提供文档,使用户能够在不编写真实客户端应用程序的情况下测试API。

The documentation should include examples of using.

该文档应包括使用示例。

We will use Swagger to fulfill all these requirements. Let us install the necessary NuGet packages. In the NuGet package manager install:

我们将使用Swagger来满足所有这些要求。 让我们安装必要的NuGet软件包。 在NuGet软件包管理器中安装:

Swashbuckle.AspNetCore (4.0.1),
Swashbuckle.AspNetCore.Examples (2.9.0),
Swashbuckle.AspNetCore.Filters (4.5.5),
Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer (3.2.1)

Note! We need the ApiExplorer package to discover all API versions automatically and generate descriptions and endpoint for each discovered version.

注意! 我们需要ApiExplorer软件包来自动发现所有API版本,并为每个发现的版本生成描述和端点。

After installation our Dependencies - NuGet list will also include:

安装后,我们的“依赖关系-NuGet”列表还将包括:

Note! Although at the time of writing this article Swashbuckle.AspNetCore and Swashbuckle.AspNetCore.Filters version 5.0.0-rc8 were available, we used lower versions. The reason for this was some compatibility issues between versions 2.9.0 and 5.0.0-rc8. So, the proven stable combination of NuGet packages was selected. Hopefully, in new releases, Swagger developers will resolve all the compatibility issues.

注意! 尽管在撰写本文时,Swashbuckle.AspNetCore和Swashbuckle.AspNetCore.Filters版本5.0.0-rc8可用,但我们使用的版本较低。 原因是版本2.9.0和5.0.0-rc8之间存在一些兼容性问题。 因此,选择了经过验证的NuGet软件包的稳定组合。 希望在新版本中,Swagger开发人员将解决所有兼容性问题。

Let us create a Swagger folder in our application and then a SwaggerServiceExtensions class in it. This static Swagger extensions class will encapsulate all the logic concerning service setup. We will call methods of this class from the Startup's ConfigureServices and Configure methods, and thus making the Startup class shorter and readable.

让我们在应用程序中创建一个Swagger文件夹,然后在其中SwaggerServiceExtensions类。 静态Swagger扩展类将封装与服务设置有关的所有逻辑。 我们将从Startup的ConfigureServicesConfigure方法中调用此类的方法,从而使Startup类更短且可读。

Here is the entire class with the following explanations:

这是整个类,具有以下说明:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.DependencyInjection;
using Swashbuckle.AspNetCore.Examples;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerUI;
using System;
using System.IO;
using System.Reflection;namespace SpeedUpCoreAPIExample.Swagger
{public static class SwaggerServiceExtensions{public static IServiceCollection AddSwaggerDocumentation(this IServiceCollection services){services.AddVersionedApiExplorer(options =>{//The format of the version added to the route URL (VV = <major>.<minor>) options.GroupNameFormat = "'v'VV";//Order API explorer to change /api/v{version}/ to /api/v1/  options.SubstituteApiVersionInUrl = true;});// Get IApiVersionDescriptionProvider serviceIApiVersionDescriptionProvider provider = services.BuildServiceProvider().GetRequiredService<IApiVersionDescriptionProvider>();services.AddSwaggerGen(options =>{//Create description for each discovered API versionforeach (ApiVersionDescription description in provider.ApiVersionDescriptions){options.SwaggerDoc(description.GroupName, new  Info(){Title = $"Speed Up ASP.NET Core WEB API Application {description.ApiVersion}",Version = description.ApiVersion.ToString(),Description = "Using various approaches to increase .Net Core RESTful WEB API productivity.",TermsOfService = "None",Contact = new Contact{Name = "Silantiev Eduard",Email = "",Url = "https://www.codeproject.com/Members/EduardSilantiev"},License = new License{Name = "The Code Project Open License (CPOL)",Url = "https://www.codeproject.com/info/cpol10.aspx"}});}//Extend Swagger for using examplesoptions.OperationFilter<ExamplesOperationFilter>();//Get XML comments file path and include it to Swagger for the JSON documentation and UI.string xmlCommentsPath = Assembly.GetExecutingAssembly().Location.Replace("dll", "xml");options.IncludeXmlComments(xmlCommentsPath);});return services;}public static IApplicationBuilder UseSwaggerDocumentation(this IApplicationBuilder app,IApiVersionDescriptionProvider provider){app.UseSwagger();app.UseSwaggerUI(options =>{//Build a swagger endpoint for each discovered API version  foreach (ApiVersionDescription description in provider.ApiVersionDescriptions){options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());options.RoutePrefix = string.Empty;options.DocumentTitle = "SCAR store API documentation";options.DocExpansion(DocExpansion.None);}});return app;}}
}

In the AddSwaggerDocumentation method we add VersionedApiExplorer with options, that allows ApiExplorer to understand the format of our versioning in API's routes and automatically change /v{version:apiVersion}/ to /v1.1/ in OpenApi documentation.

AddSwaggerDocumentation方法中,我们添加了带有选项的VersionedApiExplorer ,该选项使ApiExplorer能够理解API路由中的版本控制格式,并自动将OpenApi文档中的/ v {version:apiVersion} /更改为/v1.1/。

Note! The "'v'VV" pattern fits our versioning considerations: <major>.<minor> i.e. v1.0. But Swagger will turn v1.0 to v1 and v1.1 will stay as it is. Nevertheless, APIs will work fine with both v1.0 and v1 notations. Here you can find detailed information about Custom API Version Format Strings

注意! “'v'VV”模式符合我们的版本控制注意事项:<major>。<minor>即v1.0。 但是Swagger会将v1.0转换为v1,而v1.1将保持原样。 不过,API可以在v1.0和v1表示法中正常工作。 在这里您可以找到有关 自定义API版本格式字符串的 详细信息

Then we instantiate ApiVersionDescriptionProvider. We need this service to obtain a list of versions and generate description for each discovered version. In services.AddSwaggerGen command we generate these descriptions.

然后,我们实例化ApiVersionDescriptionProvider 。 我们需要此服务来获取版本列表并为每个发现的版本生成描述。 在services.AddSwaggerGen命令中,我们生成这些描述。

Here you can find details about OpenAPI Specification.

在这里,您可以找到有关OpenAPI规范的详细信息。

In the next line we extend Swagger Generator so that it will be able to add response example (and request example, although not in our case) to OpenApi documentation:

在下一行中,我们扩展了Swagger Generator,以便它能够将响应示例(和请求示例,尽管不是我们的情况)添加到OpenApi文档中:

…      options.OperationFilter<ExamplesOperationFilter>();
…

The final stage of the AddSwaggerDocumentation method is to let Swagger know the path to the XML comments file. Thus, Swagger will include XML comments in its json OpenApi file and UI.

AddSwaggerDocumentation方法的最后阶段是让Swagger知道XML注释文件的路径。 因此,Swagger将在其json OpenApi文件和UI中包含XML注释。

In the UseSwaggerDocumentation method we enable Swagger and build Swagger UA endpoints for all API versions. We use IApiVersionDescriptionProvider again to discover all APIs, but this time we pass the provider as a parameter of the method, because we call the UseSwaggerDocumentation method from the Startup.Configure method, where we are already able to get the provider reference via dependency injection.

UseSwaggerDocumentation方法中,我们启用Swagger并为所有API版本构建Swagger UA端点。 我们再次使用IApiVersionDescriptionProvider来发现所有API,但这一次我们将提供程序作为方法的参数传递,因为我们从Startup.Configure方法调用UseSwaggerDocumentation方法,在该方法中我们已经能够通过依赖注入获得提供程序引用。

RoutePrefix = string.Empty option means that the Swagger UI will be available at the root URL of our application, i.e. http://mydomainname.com or http://mydomainname.com/index.html

RoutePrefix = string.Empty选项意味着可以在我们应用程序的根URL上使用Swagger UI,即http://mydomainname.comhttp://mydomainname.com/index.html

DocExpansion(DocExpansion.None) means that request bodies in the Swagger UI will all be collapsed at opening.

DocExpansion(DocExpansion.None)意味着Swagger UI中的请求主体将在打开时全部折叠。

昂首阔步的回应范例 (Swagger response examples)

We have already extended Swagger for using examples in the AddSwaggerDocumentation method. Let us create example data classes. In the Swagger folder create a file SwaggerExamples.cs that will consist of all example classes:

我们已经扩展了Swagger,以便在AddSwaggerDocumentation方法中使用示例。 让我们创建示例数据类。 在Swagger文件夹中,创建一个SwaggerExamples.cs文件,其中将包含所有示例类:

using SpeedUpCoreAPIExample.Exceptions;
using SpeedUpCoreAPIExample.ViewModels;
using Swashbuckle.AspNetCore.Examples;
using System.Collections.Generic;namespace SpeedUpCoreAPIExample.Swagger
{public class ProductExample : IExamplesProvider{public object GetExamples(){return new ProductViewModel(1, "aaa", "Product1");}}public class ProductsExample : IExamplesProvider{public object GetExamples(){return new ProductsPageViewModel(){PageIndex = 1,PageSize = 20,TotalPages = 1,TotalCount = 3,Items = new List<ProductViewModel>(){new ProductViewModel(1, "aaa", "Product1"),new ProductViewModel(2, "aab", "Product2"),new ProductViewModel(3, "abc", "Product3")}};}}public class PricesExamples : IExamplesProvider{public object GetExamples(){return new PricesPageViewModel(){PageIndex = 1,PageSize = 20,TotalPages = 1,TotalCount = 3,Items = new List<PriceViewModel>(){ new PriceViewModel(100, "Bosch"),new PriceViewModel(125, "LG"),new PriceViewModel(130, "Garmin")}};}}public class ProductNotFoundExample : IExamplesProvider{public object GetExamples(){return new ExceptionMessage("Product not found");}}public class InternalServerErrorExample : IExamplesProvider{public object GetExamples(){return new ExceptionMessage("An unhandled exception has occurred");}}
}

The classes are really simple, they just return ViewModels with example data or error message examples in our unified messages format. Then we will link the API's response code with an appropriate example.

这些类非常简单,它们只是以统一消息格式返回带有示例数据或错误消息示例的ViewModels。 然后,我们将使用适当的示例链接API的响应代码。

Now we add the Swagger service in the Startup.ConfigureServices method:

现在,我们在Startup.ConfigureServices方法中添加Swagger服务:

…      public void ConfigureServices(IServiceCollection services)
…          services.AddSwaggerDocumentation();
…

and add Swagger middleware in the Startup.Configure method:

并在Startup.Configure方法中添加Swagger中间件:

…      public void Configure(IApplicationBuilder app, IHostingEnvironment env,ILoggerFactory loggerFactory, IApiVersionDescriptionProvider provider)
…          app.UseSwaggerDocumentation(provider);app.UseCors("Default");app.UseMvc();
…

Note! We get IApiVersionDescriptionProvider via dependency injection and pass it to UseSwaggerDocumentation as a parameter.

注意! 我们通过依赖注入获得IApiVersionDescriptionProvider,并将其作为参数传递给UseSwaggerDocumentation。

标记和属性以形成OpenApi文档 (Tags and attributes to form OpenApi documentation)

Swagger understands most XML comments tags and has a variety of its own attributes. We have chosen only a small part of them, but quite enough for generating brief and clear documentation.

Swagger可以理解大多数XML注释标签,并具有各种自己的属性。 我们只选择了其中的一小部分,但足以生成简短明了的文档。

We should apply these tags and attributes in controllers at actions declaration. Here are some examples for ProductsController with explanations:

我们应该在动作声明时将这些标签和属性应用于控制器。 这是ProductsController一些示例,并附有说明:

…      /// <summary>/// Gets all Products with pagination./// </summary>/// <remarks>GET /api/v1/products/?pageIndex=1&pageSize=20</remarks>/// <param name="pageIndex">Index of page to display (if not set, defauld value = 1 - first page is used).</param>/// <param name="pageSize">Size of page (if not set, defauld value is used).</param>/// <returns>List of product swith pagination state</returns>/// <response code="200">Products found and returned successfully.</response>[ProducesResponseType(typeof(ProductsPageViewModel), StatusCodes.Status200OK)][SwaggerResponseExample(StatusCodes.Status200OK, typeof(ProductsExample))][HttpGet][ValidatePaging]public async Task<IActionResult> GetAllProductsAsync(int pageIndex, int pageSize)
…
…      /// <summary>/// Gets a Product by Id./// </summary>/// <remarks>GET /api/v1/products/1</remarks>/// <param name="id">Product's Id.</param>/// <returns>A Product information</returns>/// <response code="200">Product found and returned successfully.</response>/// <response code="404">Product was not found.</response>[ProducesResponseType(typeof(ProductViewModel), StatusCodes.Status200OK)][ProducesResponseType(typeof(string), StatusCodes.Status404NotFound)][SwaggerResponseExample(StatusCodes.Status200OK, typeof(ProductExample))][SwaggerResponseExample(StatusCodes.Status404NotFound, typeof(ProductNotFoundExample))][HttpGet("{id}")][ValidateId]public async Task<IActionResult> GetProductAsync(int id)
…

The tags are clearly self-explanatory. Let us review the attributes:

标签显然是不言自明的。 让我们回顾一下属性:

[ProducesResponseType(typeof(ProductViewModel), StatusCodes.Status200OK)]

We state here, that the type of return value will be ProductViewModel if the operation is successful: Response code = 200 OK)

我们在这里声明,如果操作成功,则返回值的类型将为ProductViewModel :响应代码= 200 OK)

[SwaggerResponseExample(StatusCodes.Status200OK, typeof(ProductExample))]

Here we link the StatusCodes.Status200OK and ProductExample class, that we have created and filled with demo data.

在这里,我们链接了我们创建并填充了演示数据的StatusCodes.Status200OKProductExample类。

Note! Swagger has automatically recognized the id parameter as required from the [HttpGet("{id}")] attribute.

注意! Swagger已根据[HttpGet(“ {id}”))属性的要求自动识别了id参数。

The response codes list of out APIs is not really full. The exception handling middleware can also return Status500InternalServerError (internal server error) for any API. Instead of adding a description for the Response code = 500 code for each action we can declare this once for the entire controller:

Out API的响应代码列表实际上并不完整。 异常处理中间件还可以为任何API返回Status500InternalServerError(内部服务器错误)。 无需为每个操作添加响应代码= 500代码的描述,我们可以为整个控制器声明一次:

…[ApiVersion("1.0")][Route("/api/v{version:apiVersion}/[controller]/")][ApiController][ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)][SwaggerResponseExample(StatusCodes.Status500InternalServerError, typeof(InternalServerErrorExample))]public class ProductsController : ControllerBase{
…

Note! We do not want to expose our inner API api/v1/prices/prepare of PricesController so that it is visible to the client’s app developers. That's why we attributed the action with IgnoreApi = true:

注意! 我们不想公开我们的内部API api / v1 / prices / priceController的准备 ,以使它对客户端的应用程序开发人员可见。 这就是为什么我们将操作指定为IgnoreApi = true的原因:

[ApiExplorerSettings(IgnoreApi = true)][HttpPost("prepare/{id}")]public async Task<IActionResult> PreparePricesAsync(int id){
…

If we start our application and go to its root URL, we will find the Swagger UI that was formed according to the provided options, XML comments and Attributes:

如果启动应用程序并转到其根URL,则会找到根据所提供的选项,XML注释和属性形成的Swagger UI:

In the right-top corner we can see the "Select a spec" session, which is a version selector. If we add an [ApiVersion("2.0")] attribute in some controller, the 2.0 version will be discovered automatically and will appear in this dropdownlist:

在右上角,我们可以看到“选择规格”会话,这是一个版本选择器。 如果我们在某些控制器中添加[ApiVersion(“ 2.0”)]属性,则将自动发现2.0版本并将其显示在此下拉列表中:

The Swagger UI is really simple. We can expand/collapse each API and observe its description, parameter, examples, etc. If we want to test the API, we should click the "TryItOut" button:

Swagger UI非常简单。 我们可以展开/折叠每个API并观察其描述,参数,示例等。如果要测试API,则应单击“ TryItOut”按钮:

Then enter a value, you want to examine in the appropriate parameter's input box and click Examine:

然后在适当的参数输入框中输入要检查的值,然后单击检查:

The result in this case will be as expected:

在这种情况下的结果将是预期的:

For developers of the client’s apps, an OpenApi json file is available for downloading:

对于客户端应用程序的开发人员,可以下载OpenApi json文件:

It can be used for autogenerating code of client application with NSwagStudio, for example, or imported into some testing frameworks, like Postman, to establish automatic testing of APIs.

例如,它可用于通过NSwagStudio自动生成客户端应用程序的代码,或导入某些测试框架(如Postman)中以建立API的自动测试。

摆脱未使用或重复的NuGet软件包 (Getting rid of unused or duplicated NuGet packages)

Code refactoring and refinement seems an endless process. So, we have to stop here. However, you can continue with a useful tool, such as ReSharper, to get new ideas about how to improve your code quality.

代码重构和完善似乎是一个无休止的过程。 因此,我们必须在这里停止。 但是,您可以继续使用诸如ReSharper之类的有用工具,以获取有关如何提高代码质量的新想法。

Since the code will not be changed any more, at least in the boundaries of this article, we can revise the NuGet packages that we have at this moment. It now becomes evident that we have some packages duplication and a real mess in their versioning.

由于将不再更改代码,至少在本文的范围内,所以我们可以修改目前拥有的NuGet软件包。 现在变得很明显,我们在其版本控制中有一些软件包重复和一团糟。

At the moment our dependencies structure looks like this:

目前,我们的依赖项结构如下所示:

Actually, the Microsoft.AspNetCore.All package includes all four of these selected packages, so we can easily remove them from the application.

实际上,Microsoft.AspNetCore.All软件包包括所有这四个选定的软件包,因此我们可以轻松地将它们从应用程序中删除。

But when removing these packages, we should take into account version compatibility. For example, the Microsoft.AspNetCore.All (2.0.5) package includes Microsoft.AspNetCore.Mvc (2.0.2). This means, that we will have problems with the ApiController attribute we are using in our controllers and which is available since MVC version 2.1.

但是,在删除这些软件包时,我们应考虑版本兼容性。 例如,Microsoft.AspNetCore.All(2.0.5)程序包包括Microsoft.AspNetCore.Mvc(2.0.2)。 这意味着,我们将在控制器中使用的ApiController属性出现问题,该属性从MVC 2.1版开始可用。

So, after removing extra packages, we should also upgrade Microsoft.AspNetCore.All to the latest stable version. First, we should install the new version of SDK on our development machine (if we still have not). As we have already installed version 2.2, we will just change the Target framework of our application to .NET Core 2.2. For this, right click the project, go to the Properties menu and change the Target framework to 2.2.

因此,在删除额外的程序包之后,我们还应该将Microsoft.AspNetCore.All升级到最新的稳定版本。 首先,我们应该在开发机器上安装新版本的SDK(如果还没有的话)。 由于我们已经安装了2.2版,因此我们只需将应用程序的目标框架更改为.NET Core 2.2。 为此,右键单击该项目,转到“属性”菜单,然后将“目标”框架更改为2.2。

Then upgrade Microsoft.AspNetCore.All package. In the NuGet package manager choose Microsoft.AspNetCore.All from among installed packages and install new version:

然后升级Microsoft.AspNetCore.All程序包。 在NuGet软件包管理器中,从已安装的软件包中选择Microsoft.AspNetCore.All并安装新版本:

If we try to rebuild our solution with new dependencies, it will be built successfully but with the following warning:

如果我们尝试使用新的依赖项重建解决方案,则将成功构建该解决方案,但带有以下警告:

warning NETSDK1071: A PackageReference to 'Microsoft.AspNetCore.All' specified a Version of `2.2.6`. Specifying the version of this package is not recommended. For more information, see https://aka.ms/sdkimplicitrefs    

To put it simply, we should remove the explicit version specification of Microsoft.AspNetCore.All in the CSPROJ file. For this, Right click the project and select the Upload Project menu. When unloading is completed, right click the project again and select:

简单地说,我们应该在CSPROJ文件中删除Microsoft.AspNetCore.All的显式版本规范。 为此,右键单击项目,然后选择上载项目菜单。 卸载完成后,再次右键单击该项目,然后选择:

Just remove Version="2.2.6" from the PackageReference for the Microsoft.AspNetCore.All. The result should be:

只需从Microsoft.AspNetCore.All的PackageReference中删除Version =“ 2.2.6”。 结果应为:

<Project Sdk="Microsoft.NET.Sdk.Web">
…<ItemGroup><PackageReference Include="Microsoft.AspNetCore.All" /><PackageReference Include="Serilog.Extensions.Logging.File" Version="1.1.0" /></ItemGroup>
…

Reload the project again

再次重新加载项目

Note, that after removing explicit version specification we can see Microsoft.AspNetCore.All both under NuGet and SDK sections (and still with its version).

请注意,删除明确的版本规范后,我们可以在NuGet和SDK部分(以及其版本)下看到Microsoft.AspNetCore.All。

But if we rebuild the solution again, it will be built successfully without any warnings. We can start the application and test APIs with the Swagger or any other tool. It works fine.

但是,如果我们再次重建该解决方案,它将成功构建,而不会发出任何警告。 我们可以启动应用程序并使用Swagger或任何其他工具测试API。 它工作正常。

Microsoft.AspNetCore.All和Microsoft.AspNetCore.App元包 (The Microsoft.AspNetCore.All and Microsoft.AspNetCore.App metapackages)

Even in such a small and simple application like ours, we had the beginning of NuGet and Version Hell. We have easily solved these issues by using Microsoft.AspNetCore.All.

即使在像我们这样的小型简单应用程序中,我们也开始了NuGet和Version Hell。 我们使用Microsoft.AspNetCore.All轻松解决了这些问题。

Another benefit of using a metapackage is the size of our application. It becomes smaller, because metapackages follow the Shared Framework concept. With the Shared Framework, all the Dll files that make up the metapackage are being installed in a shared folder and can also be used by another applications. In our application, we have just links to Dll in this folder. When we build the application, all these Dll are not being copied into the application’s folder. This means that, to work properly, .NET Core 2.0 (or a higher version) runtime must be installed on a target machine.

使用元包的另一个好处是应用程序的大小。 它变小了,因为元包遵循了共享框架的概念。 使用共享框架,构成元包的所有Dll文件都将安装在共享文件夹中,并且也可以由其他应用程序使用。 在我们的应用程序中,我们只有指向该文件夹中Dll的链接。 当我们构建应用程序时,所有这些Dll都不会被复制到应用程序的文件夹中。 这意味着,要正常运行,必须在目标计算机上安装.NET Core 2.0(或更高版本)运行时。

When we containerize our application, the benefits of Shared Framework concept are even greater. The metapackage will be a part of the ASP.NET Core Runtime Docker Image. The application image will include only packages that are not parts of the metapackage and, thus, the application image will be smaller and can be deployed faster.

当我们对应用程序进行容器化时,共享框架概念的好处更大。 该元数据包将成为ASP.NET Core Runtime Docker Image的一部分。 应用程序映像将仅包括不是metapackage的一部分的程序包,因此,应用程序映像将更小并且可以更快地部署。

The last wonder to be uncovered - is implicit versioning. Since we have removed the exact metapackage version in the CSPROJ file, our application will work with any version of .NET Core runtime, installed on the target machine, if the runtime has an equal or higher version than the metapackage we have referenced to. This makes it easier to deploy our application in another environment and update .NET Core runtime without needing to rebuild the application.

要发现的最后一个奇迹-隐式版本控制。 由于我们已经删除了CSPROJ文件中的确切元数据包版本,因此,如果运行时的版本等于或高于我们所引用的元数据包,我们的应用程序将可以与目标计算机上安装的任何版本的.NET Core运行时一起使用。 这使我们更容易在另一个环境中部署应用程序并更新.NET Core运行时,而无需重建应用程序。

Note, that implicit versioning works only if our project uses <Project Sdk="Microsoft.NET.Sdk.Web">

请注意,仅当我们的项目使用<Project Sdk =“ Microsoft.NET.Sdk.Web”>

从ASP.NET Core 2.2迁移到3.0 (Migration from ASP.NET Core 2.2 to 3.0)

The code for this article is written with ASP.NET Core 2.2. While preparing the article, a new version 3.0 was released. If you want to examine the code with the ASP.NET Core 3.0 , consider migrating from ASP.NET Core 2.2 to 3.0

本文的代码是使用ASP.NET Core 2.2编写的。 在准备本文时,发布了新版本3.0。 如果要使用ASP.NET Core 3.0检查代码,请考虑从ASP.NET Core 2.2迁移到3.0

兴趣点 (Points of Interest)

Even after such a significant improvement, our application is still not ready for production. It lacks HTTPS support, auto testing, keeping connection strings safe, etc., etc. These will probably be the focus of forthcoming articles.

即使经过如此重大的改进,我们的应用程序仍未准备好投入生产。 它缺少HTTPS支持,自动测试,保持连接字符串安全等,这些等等可能是即将发表的文章的重点。

翻译自: https://www.codeproject.com/Articles/4049519/Speed-up-ASP-NET-Core-WEB-API-application-Part-3

加速ASP.NET Core WEB API应用程序。 第三部分相关推荐

  1. 加速ASP.NET Core WEB API应用程序——第2部分

    目录 应用程序生产力 异步设计模式 数据规范化与SQL查询效率 NCHAR与NVARCHAR 使用MSSQL服务器的全文引擎 存储过程 优化存储过程 预编译和重用存储过程执行计划 使用Entity F ...

  2. 加速ASP.NET Core WEB API应用程序——第1部分

    目录 介绍 创建测试RESTful WEB API服务 应用程序架构 数据库 创建ASP.NET核心WEB API应用程序 使用实体框架核心进行数据库访问 异步设计模式 存储库 存储库实现 服务 服务 ...

  3. Docker容器环境下ASP.NET Core Web API应用程序的调试

    本文主要介绍通过Visual Studio 2015 Tools for Docker – Preview插件,在Docker容器环境下,对ASP.NET Core Web API应用程序进行调试.在 ...

  4. 在docker中运行ASP.NET Core Web API应用程序

    本文是一篇指导快速演练的文章,将介绍在docker中运行一个ASP.NET Core Web API应用程序的基本步骤,在介绍的过程中,也会对docker的使用进行一些简单的描述.对于.NET Cor ...

  5. 使用Entity Developer构建ASP.NET Core Web API应用程序

    目录 介绍 先决条件 在Visual Studio 2019中创建新的ASP.NET Core Web API项目 使用Entity Developer创建实体数据模型 创建API控制器 通过实体数据 ...

  6. 支持多个版本的ASP.NET Core Web API

    基本配置及说明 版本控制有助于及时推出功能,而不会破坏现有系统. 它还可以帮助为选定的客户提供额外的功能. API版本可以通过不同的方式完成,例如在URL中添加版本或通过自定义标头和通过Accept- ...

  7. [译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了

    园子里关于ASP.NET Core Web API的教程很多,但大多都是使用EF+Mysql或者EF+MSSQL的文章.甚至关于ASP.NET Core Web API中使用Dapper+Mysql组 ...

  8. core webapi缩略图_在ASP.NET Core Web API 项目里无法访问(wwwroot)下的文件

    新建 ASP.NET Core Web API 项目 -- RESTFul 风格 Hello World! 一.创建一个空项目 请查看 新建 .NET Core 项目 -- Hello World!  ...

  9. 加快ASP。NET Core WEB API应用程序。第3部分

    下载source from GitHub 对ASP进行深度重构和优化.NET Core WEB API应用程序代码 介绍 第1部分.创建一个测试的RESTful WEB API应用程序. 第2部分.增 ...

最新文章

  1. 电脑台式计算机描述不可用,win7系统计算机描述不可用的解决方法
  2. vsftp实现只能上传不能下载、删除权限配置
  3. ppt页面样式html,PPT排版:一页PPT做出8种样式
  4. 用yum源配合源码包安装openresty、mariadb、php7服务
  5. 一些技术社区关于博客内图片 markdown 源代码的字数统计
  6. 用MATLAB函数绘制系统的,用matlab绘制odes系统定义的函数
  7. Mysql导入excel数据,解决某些特殊字符乱码问题
  8. 智慧、智能工业园区管理平台+登记管理+人行闸管理+车辆闸管理+统计分析+系统管理+地理信息+Axure高保真web端后台管理系统
  9. 解决select2插件下拉搜索框,输入拼音能够匹配中文汉字的问题
  10. mysql2005安装asp_Sql server 2005安装时ASP.Net版本注册要求警告的解决方法
  11. 利用ViewPager实现app的启动画面
  12. Android app客户端性能测试工具Emmagee 浅析
  13. SAP ERP常用事务代码和表
  14. ABAP OLE下载到EXCEL
  15. 微信开放平台创建应用时应用官网的问题
  16. 《Effective Java》读书笔记五(枚举和注解)
  17. 全光谱护眼灯哪个牌子好?2022最新全光谱灯与led灯区别
  18. 视频翻译成中文工具哪个好?3招教你视频翻译字幕
  19. 31岁,熬夜学习到2点的背后,是一个丈夫和父亲的责任与担当
  20. PHP语言基础知识+函数调用

热门文章

  1. H5获取手机GPS定位
  2. 笨方法学习Python-习题36: 设计和调试
  3. Javaweb-请求报500问题
  4. Google笔试集锦
  5. Android手机获取imei
  6. Android手机64位APP兼容
  7. 再回首往事如梦,再回首。。。。。。
  8. jquery (js中window.onload与jquery中$(document.ready())的区别)
  9. UE4 第一人称射击制作流程:05(图文)制作从Idle向前跑的动画融合
  10. 成为Java顶尖程序员 ,看这12本书就够了