- 下载source from GitHub
对ASP进行深度重构和优化。NET Core WEB API应用程序代码 介绍 第1部分。创建一个测试的RESTful WEB API应用程序。 第2部分。增加了ASP。NET Core WEB API应用程序的生产力。 在第3部分中,我们将回顾以下内容: 为什么我们需要重构和改进代码?在try/catch/finally中的不重复(DRY)原则异常处理阻止了我们的异常处理中间件在。net Core统一异常消息格式中记录到文件的需求。net Core异常处理中间件实现业务逻辑层应该返回给控制器什么类型的结果?自定义异常处理中间件使用类型的客户提供httpclientfactory处理应用程序的设置缓存关注分离的通用异步DistributedCache库内存和数据库内分页在实体框架控制器vs controllerbase定义Id参数验证过滤器和属性自定义分页参数模型验证过滤器跨源资源共享(歌珥) 使歌珥ASP。NET core没有CORS报头发送的情况下的HTTP错误如何发送HTTP 4xx-5xx响应与CORS报头在一个ASPNET。核心web应用程序 API版本控制 控制内部HTTP调用中的API版本控制错误消息格式版本控制 解析DNS名称本地文档。net核心API应用程序 XML为RESTful api注释OpenApi文档,使用swagger swagger响应示例标记和属性来形成OpenApi文档 摆脱不使用或重复的NuGet包Microsoft.AspNetCore。所有和Microsoft.AspNetCore。从ASP迁移应用程序元打包。NET Core 2.2到3.0的有趣点 为什么我们需要重构和改进代码? 第1部分的目标是创建一个非常简单的基本应用程序,我们可以从中开始。主要关注的是如何使应用和检查不同的方法、修改代码和检查结果变得更容易。 第二部分是关于生产力的。实现了多种方法。与第一部分相比,代码变得更加复杂。 现在,在选择并实现这些方法之后,我们可以将应用程序作为一个整体来考虑。很明显,代码需要深度重构和细化,以满足良好编程风格的各种原则。 不要重复自己(干)原则 根据DRY原则,我们应该消除代码的重复。因此,让我们检查一下ProductsService代码,看看它是否有任何重复。我们可以立即看到,下面的片段在所有返回ProductViewModel或IEnumerable
…
new ProductViewModel()
{
Id = p.ProductId,
Sku = p.Sku,
Name = p.Name
}
…
我们总是从一个产品类型对象创建一个ProductViewModel类型对象。将ProductViewModels对象的字段初始化移动到它的构造函数中是合乎逻辑的。让我们在ProductViewModel类中创建一个构造函数方法。在构造函数中,我们用Product参数的适当值填充对象的字段值: 隐藏,复制Code
public ProductViewModel(Product product)
{
Id = product.ProductId;
Sku = product.Sku;
Name = product.Name;
}
现在我们可以重写复制的代码在FindProductsAsync和GetAllProductsAsync方法的ProductsService: 隐藏,复制Code
… 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))); …
修改ProductsService类的GetProductAsync和DeleteProductAsync方法: 隐藏,复制Code
… return new OkObjectResult(new ProductViewModel() { Id = product.ProductId, Sku = product.Sku, Name = product.Name }); return new OkObjectResult(new ProductViewModel(product)); …
对PriceViewModel类重复同样的操作。 隐藏,复制Code
…
new PriceViewModel()
{
Price = p.Value,
Supplier = p.Supplier
}
…
尽管我们在PricesService中只使用了一次片段,但最好还是将PriceViewModel的字段初始化封装在其构造函数中的类中。 让我们创建一个PriceViewModel类构造函数 隐藏,复制Code
…
public PriceViewModel(Price price)
{
Price = price.Value;
Supplier = price.Supplier;
}
…
然后改变片段: 隐藏,复制Code
… 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块中的异常处理 下一个需要解决的问题是异常处理。在整个应用程序中,所有可能导致异常的操作都在try-catch构造中被调用。这种方法在调试过程中非常方便,因为它允许我们在异常发生的特定位置检查异常。但是这种方法也有代码重复的缺点。ASP中更好的异常处理方法。NET Core是在中间件或异常过滤器中全局地处理它们。 我们将创建异常处理中间件,通过日志记录和生成用户友好的错误消息来集中异常处理。 我们的异常处理中间件的需求 将详细信息记录到日志文件中;调试时详细的错误信息和生产时友好的信息;统一错误信息格式 在。net Core中登录到一个文件 在。net Core应用的主要方法中,我们创建并运行了web服务器。 隐藏,复制Code
… BuildWebHost(args).Run(); …
此时,将自动创建ILoggerFactory的一个实例。现在可以通过depend访问它注入并在代码中的任何位置执行日志记录。但是,使用标准的ILoggerFactory,我们不能将日志记录到文件中。为了克服这个限制,我们将使用Serilog库,它扩展了ILoggerFactory并允许将日志记录到文件中。” 让我们安装serilog . extension . logging。文件NuGet包第一: 我们应该添加使用微软。extension。logging;语句模块,我们将在其中应用日志记录。 Serilog库可以以不同的方式配置。在我们的简单示例中,要为Serilog设置日志记录规则,我们应该在Configure方法的Startup类中添加下一段代码 隐藏,复制Code
… public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddFile("Logs/log.txt"); …
这意味着,日志记录器将写入相对的\日志目录,日志文件的名称格式将为:log- yyyymd .txt 统一异常消息格式 在工作期间,我们的应用程序可以生成不同类型的异常消息。我们的目标是统一这些消息的格式,以便它们可以由客户机应用程序的某种通用方法进行处理。 让所有消息具有以下格式: 隐藏,复制Code
{ "message": "Product not found" }
格式非常简单。对于像我们这样的简单应用程序来说,这是可以接受的。但是我们应该预见到扩大它的机会并且集中在一个地方。为此,我们将创建一个ExceptionMessage类,它将封装消息格式化过程。我们将在任何需要生成异常消息的地方使用这个类。 让我们在我们的项目中创建一个文件夹异常,并添加一个类ExceptionMessage:> 隐藏,复制Code
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) }); } } }
现在我们可以创建ExceptionsHandlingMiddleware了 .NET核心异常处理中间件实现 在异常文件夹中创建一个类ExceptionsHandlingMiddleware: 隐藏,收缩,复制Code
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_logger; public ExceptionsHandlingMiddleware(RequestDelegate next, ILogger 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; // 500 string message = string.Empty; #if DEBUG message = exception.Message; #else message = "An unhandled exception has occurred"; #endif context.Response.Clear(); context.Response.ContentType = "application/json"; context.Response.StatusCode = statusCode; var result = new ExceptionMessage(message).ToString(); await context.Response.WriteAsync(result); } } } }
这个中间件在调试(#if调试)或不进行调试的用户友好的消息时拦截未处理的异常,记录异常的详细信息并发出详细消息。 注意,我们是如何使用ExceptionMessage类来格式化结果的。 现在,我们应该在启动时将这个中间件添加到应用程序HTTP请求管道中。在app.UseMvc()之前配置方法;声明。 隐藏,复制Code
app.UseMiddleware(); ; … app.UseMvc();
让我们来看看它是如何工作的。为此,我们将更改ProductsRepository中的存储过程名称。对于不存在的GetProductsBySKUError方法的FindProductsAsync方法。 隐藏,复制Code
public async Task> FindProductsAsync(string sku) { return await _context.Products.AsNoTracking().FromSql("[dbo].GetProductsBySKUError @sku = {0}", sku).ToListAsync(); }
并从ProductsService中删除Try-Catch块。FindProductsAsync方法 隐藏,复制Code
public async TaskFindProductsAsync(string sku) { try { IEnumerabler products = await _productsRepository.FindProductsAsync(sku); … } catch { return new ConflictResult(); } … }
让我们运行应用程序并检查结果 打电话到http://localhost:49858/api/products/find/aa 我们将有500 Http响应代码和一条消息: 让我们检查一下日志文件 现在我们有了带有文件的日志文件夹 在文件中我们有详细的异常描述: 隐藏,复制Code
… ""[dbo].GetProductsBySKUError @sku = @p0" (627a98df) System.Data.SqlClient.SqlException (0x80131904): Could not find stored procedure 'dbo.GetProductsBySKUError'. …
我们声称,我们的异常处理中间件应该在调试模式下生成详细的错误消息,在生产模式下生成友好的消息。让我们检查一下。为此,我们将在工具栏中更改发布的活动解决方案配置: 或在配置管理器: 然后再次调用不正确的API。如我们所料,结果将是: 因此,我们的异常处理程序如我们预期的那样工作。 注意!如果我们没有删除Try-Catch块,我们将永远不会让这个处理程序工作,因为未处理的豁免将由Catch语句中的代码处理。 不要忘记恢复正确的存储过程名称GetProductsBySKU! 现在我们可以移除product service和PricesService clacces中的所有Try-Catch块。 注意!为了简洁起见,我们省略了删除Try-Catch块实现的代码。 我们唯一还需要尝试的地方是产品和服务。PreparePricesAsync PricesService。PreparePricesAsync方法。正如我们在第2部分中所讨论的那样,我们不希望在这些地方中断应用程序工作流 删除了Try-Catch块之后,代码变得更加简单和直接。但是当我们返回时,在大多数服务的方法中仍然有一些重复 隐藏,复制Code
return new NotFoundResult();
让我们也改进这一点。 在所有方法中,查找值的集合,如ProductsService。GetAllProductsAsync ProductsService。FindProductsAsync PricesService。我们有两个问题。 第一个是检查从存储库接收的集合是否为空。为此我们使用а声明 隐藏,复制Code
… if (products != null) …
但是在我们的例子中,集合永远不会是空的(除非存储库中发生了处理过的异常)。由于所有异常现在都在服务和存储库之外的专用中间件中处理,所以我们总是会收到一个值集合(如果没有找到任何东西,则为空)。所以,检查结果的正确方法是 隐藏,复制Code
if (products.Any())
或 隐藏,复制Code
(products.Count() > 0)
GetPricesAsync方法中的PricesService类也是如此:change 隐藏,复制Code
… if (pricess != null) if (pricess.Any()) …
第二个问题lem是空集合应该返回的结果。到目前为止,我们已经返回了NotFoundResult(),但它也不是真正正确的。例如,如果我们创建另一个API,它应该返回一个由产品及其价格组成的值,那么一个空的价格集合将在JSON结构中表示为一个空的massive,而StatusCode将为200——好的。所以,为了保持一致,我们应该重写上述方法的代码,删除空集合的NotFoundResult: 隐藏,复制Code
public async TaskFindProductsAsync(string sku) { IEnumerable products = await _productsRepository.FindProductsAsync(sku); if (products.Count() == 1) { //only one record found - prepare prices beforehand ThreadPool.QueueUserWorkItem(delegate { PreparePricesAsync(products.FirstOrDefault().ProductId); }); }; return new OkObjectResult(products.Select(p => new ProductViewModel(p))); } public async Task GetAllProductsAsync() { IEnumerable products = await _productsRepository.GetAllProductsAsync(); return new OkObjectResult(products.Select(p => new ProductViewModel(p))); }
而在PricesService 隐藏,复制Code
public async TaskGetPricesAsync(int productId) { IEnumerable pricess = await _pricesRepository.GetPricesAsync(productId); return new OkObjectResult(pricess.Select(p => new PriceViewModel(p)) .OrderBy(p => p.Price) .ThenBy(p => p.Supplier)); }
代码变得非常简单,但另一个问题仍然存在:这是从服务返回IActionResult的正确解决方案吗? 业务逻辑层应该向控制器返回什么类型的结果? 通常,业务层的方法向控制器返回一个POCO(普通的旧CLR对象)类型的值,然后控制器使用适当的StatusCode形成适当的响应。例如,产品服务。GetProductAsync方法应该返回ProductViewModel对象或null(如果没有找到产品)。控制器应该分别生成OkObjectResult(ProductViewModel)或NotFound()响应。 但这种方法并不总是可行的。实际上,我们可以有不同的理由从服务返回null。例如,让我们设想一个用户可以访问某些内容的应用程序。这些内容可以是公开的、私有的或预付的。当用户请求一些内容时,ISomeContentService可以返回ISomeContent或null。有一些可能的原因,这个空: 隐藏,复制Code
401 Unauthorized 402 Payment Required 403 Forbidden 404 Not Found …
原因在服务内部变得很清楚。如果一个方法只返回null值,服务如何通知控制器这个原因?对于控制器来说,这还不足以创建适当的响应。为了解决这个问题,我们使用IActionResult类型作为服务-业务层的返回类型。这种方法非常灵活,与IActionResult result一样,我们可以将所有内容传递给控制器。但是业务层应该形成API的响应,执行控制器的工作吗?它会不会打破关注点分离的设计原则? 在业务层摆脱IActionResult的一种可能方法是使用自定义异常来控制应用程序的工作流和生成正确的响应。为了提供这一点,我们将增强异常处理中间件,使其能够处理定制异常。 自定义异常处理中间件 让我们创建一个简单的HttpException类,它继承自Exception。并增强了out异常处理程序中间件来处理HttpException类型的异常。 在HttpException文件夹中添加类HttpException 隐藏,复制Code
using System; using System.Net; namespace SpeedUpCoreAPIExample.Exceptions { // Custom Http Exception public 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; } } }
并更改ExceptionsHandlingMiddleware类代码 隐藏,收缩,复制Code
… 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); } }
在中间件中,我们在处理一般异常类型之前处理HttpException类型的异常,调用HandleHttpExceptionAsync方法。如果提供,我们会记录详细的异常消息。 现在,我们可以重写产品和服务。GetProductAsync和ProductsService.DeleteProductAsync 隐藏,收缩,复制Code
… public async TaskGetProductAsync(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 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)); } …
在这个版本中,我们不是用IActionResult返回404 Not Found from the services,而是抛出一个定制的HttpException,异常处理中间件会返回一个正确的响应给用户。让我们通过使用productid调用API来检查它是如何工作的,它显然不在Products表中: http://localhost:49858/api/products/100 我们的通用异常处理中间件工作得很好。 由于我们已经创建了一种替代方法来传递任何StatucCode和来自业务层的消息,所以我们可以轻松地将返回值类型从IActionResult更改为合适的POCO类型。为此,我们必须重写以下接口: 隐藏,复制Code
public interface IProductsService { TaskGetAllProductsAsync(); TaskGetProductAsync(int productId); TaskFindProductsAsync(string sku); TaskDeleteProductAsync(int productId); Task> GetAllProductsAsync(); Task }GetProductAsync(int productId); Task > FindProductsAsync(string sku); Task DeleteProductAsync(int productId);
和改变 隐藏,复制Code
public interface IPricesService { Task> GetPricesAsync(int productId); Task> GetPricesAsync(int productId); … }
我们还应该在ProductsService和PricesService类中重新声明适当的方法,方法是将IActionResult类型从接口更改为类型。通过删除OkObjectResult语句,也改变了它们的返回语句。例如,在产品服务中。GetAllProductsAsync方法: 新版本将是: 隐藏,复制Code
public async Task> GetAllProductsAsync() { IEnumerable products = await _productsRepository.GetAllProductsAsync(); return products.Select(p => new ProductViewModel(p)); }
最后一个任务是更改控制器的操作,以便它们创建OK响应。它总是200 OK,因为NotFound将被ExceptionsHandlingMiddleware返回 例如,对于product service。返回语句应该更改为: 隐藏,复制Code
// GET /api/products [HttpGet] public async TaskGetAllProductsAsync() { return await _productsService.GetAllProductsAsync(); }
: 隐藏,复制Code
// GET /api/products [HttpGet] public async TaskGetAllProductsAsync() { return new OkObjectResult(await _productsService.GetAllProductsAsync()); }
您可以在所有ProductsController的操作和PricesService中执行此操作。GetPricesAsync行动。 使用HttpClientFactory的类型化客户端 我们以前的HttpClient实现有一些问题,我们可以改进。首先,我们必须注入IHttpContextAccessor以在GetFullyQualifiedApiUrl方法中使用它。IHttpContextAccessor和GetFullyQualifiedApiUrl方法只专用于HttpClient,从未在产品服务的其他地方使用。如果我们想在其他服务中应用相同的功能,我们将不得不编写几乎相同的代码。因此,最好是在HttpClient周围创建一个单独的helper类包装器,并将所有必要的HttpClient调用业务逻辑封装在这个类中。 我们将使用另一种处理HttpClientFactory类型的客户机类的方法。 在接口文件夹中创建一个ISelfHttpClient intetface: 隐藏,复制Code
using System.Threading.Tasks; namespace SpeedUpCoreAPIExample.Interfaces { public interface ISelfHttpClient { Task PostIdAsync(string apiRoute, string id); } }
我们只声明了一个方法,它使用HttpPost方法和Id参数调用任何控制器的动作 让我们创建一个助手文件夹,并在那里添加一个新的类SelfHttpClient继承自ISelfHttpClient接口: 隐藏,收缩,复制Code
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 } } } }
在这个类中,我们获得了要在类构造函数中调用的API的一个基地址。在PostIdAsync方法中,我们通过HttpPost方法的相对apiRoute路由调用API,并将Id作为响应参数传递。注意,我们只发送null,而不是创建一个空的HttpContent 我们应该在启动时声明这个类。ConfigureServices方法: 隐藏,复制Code
… services.AddHttpClient(); services.AddHttpClient(); …
现在我们可以在应用程序的任何地方使用。在ProductsService service中,我们应该在类构造函数中注入它。我们可以删除IHttpContextAccessor和IHttpClientFactory,因为我们不再使用它们了,我们可以删除GetFullyQualifiedApiUrl方法。 新版本的ProductsService构造函数将是: 隐藏,复制Code
public class ProductsService : IProductsService { private readonly IProductsRepository _productsRepository; private readonly ISelfHttpClient _selfHttpClient; public ProductsService(IProductsRepository productsRepository, ISelfHttpClient selfHttpClient) { _productsRepository = productsRepository; _selfHttpClient = selfHttpClient; } }
让我们更改PreparePricesAsync方法。首先,我们将它重命名为CallPreparePricesApiAsync,因为这个名称更有信息,并且方法: 隐藏,复制Code
private async void CallPreparePricesApiAsync(string productId) { await _selfHttpClient.PostIdAsync("prices/prepare", productId); }
当我们在ProductsService中调用这个方法时,不要忘记将PreparePricesAsync更改为CallPreparePricesApiAsync。还要考虑到,在CallPreparePricesApiAsync中,我们使用了字符串类型的productId参数 可以看到,我们将API URL的尾部部分作为PostIdAsync参数传递。新的SelfHttpClient是真正可重用的。例如,如果我们有一个API /products/prepare,我们可以这样调用API: 隐藏,复制Code
private async void CallPrepareProductAPIAsync(string productId) { await _selfHttpClient.PostIdAsync("products/prepare", productId); }
处理应用程序的设置 在前面的部分中,我们通过注入IConfiguration来访问应用程序的设置。然后,在类构造器中,我们创建了设置类,在其中解析适当的设置变量并应用默认值。这种方法很适合调试,但是在调试之后,使用简单的POCO类访问应用程序的设置似乎更可取。让我们稍微改变一下appsets .json。我们将为产品和价格服务设置两个部分: 隐藏,复制Code
"Caching": { "PricesExpirationPeriod": 15 } "Products": { "CachingExpirationPeriod": 15, "DefaultPageSize": 20 }, "Prices": { "CachingExpirationPeriod": 15, "DefaultPageSize": 20 }, …
注意!在本文中,我们将使用DefaultPageSize值。 让我们创建设置POCO类。创建一个设置文件夹与以下文件: 隐藏,复制Code
namespace SpeedUpCoreAPIExample.Settings { public class ProductsSettings { public int CachingExpirationPeriod { get; set; } public int DefaultPageSize { get; set; } } }
和 隐藏,复制Code
namespace SpeedUpCoreAPIExample.Settings { public class PricesSettings { public int CachingExpirationPeriod { get; set; } public int DefaultPageSize { get; set; } } }
尽管这些类仍然相似,但在实际应用程序中,不同服务的设置可能会有很大差异。因此,我们将使用这两个类,以便以后不分割它们。 现在,我们使用这些类所需要的就是在start . configureservices中声明它们: 隐藏,复制Code
… //Settings services.Configure(Configuration.GetSection("Products")); services.Configure (Configuration.GetSection("Prices")); //Repositories …
在此之后,我们可以在应用程序的任何地方注入设置类,我们将在下面几节中演示 缓存担心分离 在PricesRepository中,我们使用IDistributedCache缓存实现了缓存。缓存在存储库中的想法是完全关闭数据存储源的业务层细节。在这种情况下,不知道服务是否通过了缓存阶段的数据。这个解决方案真的好吗? 存储库负责使用DbContext,即从数据库中获取数据或将数据保存到数据库中。但是缓存肯定是出于这个考虑。此外,在更复杂的系统中,在从数据库接收到原始数据之后,可能需要在将数据传递给用户之前对其进行修改。将数据缓存到最终状态是合理的。根据这一点,最好将缓存应用于服务中的业务逻辑层。 注意!PricesRepository。GetPricesAsync PricesRepository。用于缓存的代码几乎是相同的。从逻辑上讲,我们应该将这些代码移到一个单独的类中,以避免重复。 通用异步分布式缓存存储库 其思想是创建一个存储库来封装IDistributedCache业务逻辑。存储库将是通用的,并能够缓存任何类型的对象。这是它的界面 隐藏,复制Code
using Microsoft.Extensions.Caching.Distributed; using System; using System.Threading.Tasks; namespace SpeedUpCoreAPIExample.Interfaces { public interface IDistributedCacheRepository{ Task GetOrSetValueAsync(string key, Func > valueDelegate, DistributedCacheEntryOptions options); Task IsValueCachedAsync(string key); Task GetValueAsync(string key); Task SetValueAsync(string key, T value, DistributedCacheEntryOptions options); Task RemoveValueAsync(string key); } }
这里唯一有趣的地方是作为GetOrSetValueAsync方法的第二个参数的异步委托。它将在实现部分进行讨论。在Repositories文件夹中创建一个新的类DistributedCache存储库: 隐藏,收缩,复制Code
using Microsoft.Extensions.Caching.Distributed; using Newtonsoft.Json; using SpeedUpCoreAPIExample.Interfaces; using System; using System.Threading.Tasks; namespace SpeedUpCoreAPIExample.Repositories { public abstract class DistributedCacheRepository: IDistributedCacheRepository 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 GetOrSetValueAsync(string key, Func > 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 IsValueCachedAsync(string key) { var value = await _distributedCache.GetStringAsync(_keyPrefix + key); return value != null; } public async Task GetValueAsync(string key) { var value = await _distributedCache.GetStringAsync(_keyPrefix + key); return value != null ? JsonConvert.DeserializeObject (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(); } }
这个类是抽象的,因为我们不打算直接创建它的实例。相反,它将是PricesCacheRepository和ProductsCacheRepository类的基类。注意,GetOrSetValueAsync有一个虚拟修饰符—我们将在继承的类中重写这个方法。GetDefaultOptions方法也是如此,在这种情况下,它被声明为抽象,因此它将在派生类中实现。当它在父DistributedCacheRepository类中调用时,将调用从派生类继承的方法。 GetOrSetValueAsync方法的第二个参数声明为异步委托:valueDelegate。在GetOrSetValueAsync方法中,我们首先尝试从缓存中获取一个值。如果它还没有缓存,我们通过调用valueDelegate函数获得它,然后缓存值。 让我们从DistributedCacheRepository创建具有明确类型的继承类。 隐藏,复制Code
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> GetOrSetValueAsync(string key, Func >> valueDelegate, DistributedCacheEntryOptions options = null); Task IsValueCachedAsync(string key); Task RemoveValueAsync(string key); } }
隐藏,复制Code
using Microsoft.Extensions.Caching.Distributed; using SpeedUpCoreAPIExample.Models; using System; using System.Threading.Tasks; namespace SpeedUpCoreAPIExample.Interfaces { public interface IProductCacheRepository { TaskGetOrSetValueAsync(string key, Func > valueDelegate, DistributedCacheEntryOptions options = null); Task IsValueCachedAsync(string key); Task RemoveValueAsync(string key); Task SetValueAsync(string key, Product value, DistributedCacheEntryOptions options = null); } }
然后我们将在Repositories文件夹中创建两个类 隐藏,收缩,复制Code
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>, IPricesCacheRepository { private const string KeyPrefix = "Prices: "; private readonly PricesSettings _settings; public PricesCacheRepository(IDistributedCache distributedCache, IOptions settings) : base(distributedCache, KeyPrefix) { _settings = settings.Value; } public override async Task > GetOrSetValueAsync(string key, Func >> 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 parameter return new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_settings.CachingExpirationPeriod) }; } } }
和 隐藏,收缩,复制Code
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, IProductCacheRepository { private const string KeyPrefix = "Product: "; private readonly ProductsSettings _settings; public ProductCacheRepository(IDistributedCache distributedCache, IOptions settings) : base(distributedCache, KeyPrefix) { _settings = settings.Value; } public override async Task GetOrSetValueAsync(string key, Func > 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 parameter return new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_settings.CachingExpirationPeriod) }; } } }
注意!GetDefaultOptions的实现在ProductCacheRepository和PricesCacheRepository类中是相等的,似乎可以移到基类中。但在真实的应用程序中,缓存策略可能因对象的不同而不同,如果我们将GetDefaultOptions的一些通用实现移动到基类中,当派生类的缓存逻辑发生变化时,我们将不得不更改基类。这将违反“启闭”设计原则。这就是我们在派生类中实现GetDefaultOptions方法的原因。 在Startup类中声明存储库 隐藏,复制Code
… services.AddScoped(); services.AddScoped (); …
现在,我们可以从PricesRepository删除缓存,使其尽可能简单: 隐藏,复制Code
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> GetPricesAsync(int productId) { return await _context.Prices.AsNoTracking().FromSql("[dbo].GetPricesByProductId @productId = {0}", productId).ToListAsync(); } } }
我们还可以重写PricesService类。我们注入了IPricesCacheRepository,而不是IDistributedCache。 隐藏,收缩,复制Code
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> GetPricesAsync(int productId) { IEnumerable 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 TaskIsPriceCachedAsync(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 { } } } }
在GetPricesAsync和PreparePricesAsync方法中,我们使用了PricesCacheRepository的GetOrSetValueAsync方法。如果期望的值不在缓存中,则调用异步方法GetPricesAsync。 我们还创建了IsPriceCachedAsync和RemovePriceAsync方法,它们将在后面使用。不要忘记在IPricesService接口中声明它们: 隐藏,复制Code
using SpeedUpCoreAPIExample.ViewModels; using System.Collections.Generic; using System.Threading.Tasks; namespace SpeedUpCoreAPIExample.Interfaces { public interface IPricesService { Task> GetPricesAsync(int productId); Task IsPriceCachedAsync(int productId); Task RemovePriceAsync(int productId); Task PreparePricesAsync(int productId); } }
让我们来看看新的缓存方法是如何工作的。为此,在GetPricesAsync方法中设置一个断点: 使用Swagger Inspector扩展调用http://localhost:49858/api/prices/1 api两次: 在第一次调用期间,调试器到达断点。这意味着,GetOrSetValueAsync方法无法在缓存中找到结果,因此必须调用_pricesRepository.GetPricesAsync(productId)方法,该方法作为委托传递给GetOrSetValueAsync。但是在第二次调用时,应用程序工作流不会在断点处停止,因为它从缓存中获取一个值。 现在我们可以在ProductService中使用通用缓存机制 隐藏,收缩,复制Code
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, IOptionssettings , ISelfHttpClient selfHttpClient) { _productsRepository = productsRepository; _selfHttpClient = selfHttpClient; _pricesCacheRepository = pricesCacheRepository; _productCacheRepository = productCacheRepository; _settings = settings.Value; } public async TaskFindProductsAsync(string sku) { IEnumerable products = await _productsRepository.FindProductsAsync(sku); if (products.Count() == 1) { //only one record found Product product = products.FirstOrDefault(); string productId = product.ProductId.ToString(); //cache a product if not in cache yet if (!await _productCacheRepository.IsValueCachedAsync(productId)) { await _productCacheRepository.SetValueAsync(productId, product); } //prepare prices if (!await _pricesCacheRepository.IsValueCachedAsync(productId)) { //prepare prices beforehand ThreadPool.QueueUserWorkItem(delegate { CallPreparePricesApiAsync(productId); }); } }; return new OkObjectResult(products.Select(p => new ProductViewModel(p))); } … public async Task 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 prices if (!await _pricesCacheRepository.IsValueCachedAsync(productId.ToString())) { //prepare prices beforehand ThreadPool.QueueUserWorkItem(delegate { CallPreparePricesApiAsync(productId.ToString()); }); } return new ProductViewModel(product); } … public async Task 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 cache await _productCacheRepository.RemoveValueAsync(productId.ToString()); await _pricesCacheRepository.RemoveValueAsync(productId.ToString()); return new OkObjectResult(new ProductViewModel(product)); } …
实体框架中的内存分页和数据库分页 您可能已经注意到,ProductsController的方法GetAllProductsAsync和FindProductsAsync以及PricesController的GetPricesAsync方法返回产品和价格的集合,根据集合的大小,这些集合没有限制。这意味着,在真实的数据库庞大的应用程序中,某些API的响应可能会返回大量数据,以至于客户端应用程序无法在合理的时间内处理甚至接收这些数据。为了避免这个问题,一个好的实践是建立API结果的分页。 有两种组织分页的方法:在内存中和在数据库中。例如,当我们收到一些产品的价格时,我们会将结果缓存到Redis缓存中。所以,我们已经有了整套的价格,可以建立内存分页,这是更快的方法。 另一方面,在GetAllProductsAsync方法中使用内存分页不是一个好主意,因为要在内存中进行分页,我们应该将整个产品集合从数据库读入内存。这是一个非常缓慢的操作,会消耗很多资源。因此,在这种情况下,最好根据页面大小和索引在数据库中过滤必要的数据集。 对于分页,我们将创建一个通用的PaginatedList类,它将能够处理任何数据类型的集合,并支持内存和数据库中的分页方法。 让我们创建一个通用的PaginatedList
using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace SpeedUpCoreAPIExample.Helpers { public class PaginatedList: List { 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 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 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 > FromIQueryable(IQueryable 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 list return new PaginatedList (new List (), pageSize, pageIndex, totalCount); } if (pageIndex == 1 && pageSize == totalCount) { //no paging needed } else { source = source.Skip((pageIndex - 1) * pageSize).Take(pageSize); }; List sourceList = await source.ToListAsync(); return new PaginatedList (sourceList, pageSize, pageIndex, totalCount); } } }
我们需要第一个constructor,用于任何类型的内存数据收集。第二个构造函数也用于内存中的集合,但前提是已经知道页面大小和页面数量。我们将它标记为私有,因为它只在FromIQueryable类本身中使用。 FromIQueryable用于建立数据库内分页。源参数具有IQueryable类型。使用IQueryable在执行对数据库的实际请求之前,我们不会处理物理数据,如source.CountAsync()或source.ToListAsync()。因此,我们能够格式化一个适当的分页查询,并且在一个请求中只接收一小组过滤后的数据。 让我们也调整一下ProductsRepository。GetAllProductsAsync ProductsRepository。FindProductsAsync方法,以便它们能够处理数据库内分页。现在它们应该返回IQueryable,而不是以前的IEnumerable。 隐藏,复制Code
namespace SpeedUpCoreAPIExample.Interfaces { public interface IProductsRepository { … Task> GetAllProductsAsync(); Task> FindProductsAsync(string sku); IQueryableGetAllProductsAsync(); IQueryable … } }FindProductsAsync(string sku);
在ProductsRepository类中正确的方法代码 隐藏,复制Code
… public async Task> GetAllProductsAsync() { return await _context.Products.AsNoTracking().ToListAsync(); } public IQueryable GetAllProductsAsync() { return _context.Products.AsNoTracking(); } public async Task > FindProductsAsync(string sku) { return await _context.Products.AsNoTracking().FromSql("[dbo].GetProductsBySKU @sku = {0}", sku).ToListAsync(); } public IQueryable FindProductsAsync(string sku) { return _context.Products.AsNoTracking().FromSql("[dbo].GetProductsBySKU @sku = {0}", sku); } …
让我们定义一些类,在这些类中我们将把分页结果返回给用户。在ViewModels文件夹中创建一个基类PageViewModel 隐藏,复制Code
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; } } }
ProductsPageViewModel和PricesPageViewModel类,继承自PageViewModel 隐藏,复制Code
using SpeedUpCoreAPIExample.Helpers; using SpeedUpCoreAPIExample.Models; using System.Collections.Generic; using System.Linq; namespace SpeedUpCoreAPIExample.ViewModels { public class ProductsPageViewModel : PageViewModel { public IListItems; public ProductsPageViewModel(PaginatedList paginatedList) : base(paginatedList.PageIndex, paginatedList.PageSize, paginatedList.TotalPages, paginatedList.TotalCount) { this.Items = paginatedList.Select(p => new ProductViewModel(p)).ToList(); } } }
隐藏,复制Code
using SpeedUpCoreAPIExample.Helpers; using SpeedUpCoreAPIExample.Models; using System.Collections.Generic; using System.Linq; namespace SpeedUpCoreAPIExample.ViewModels { public class PricesPageViewModel : PageViewModel { public IListItems; public PricesPageViewModel(PaginatedList 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(); } } }
在PricesPageViewModel中,我们对PriceViewModel的分页列表应用了额外的排序 现在我们应该改变产品和服务。GetAllProductsAsync ProductsService。FindProductsAsync,以便它们返回ProductsPageViewMode 隐藏,复制Code
public interface IProductsService … Task> GetAllProductsAsync(); Task> FindProductsAsync(string sku); TaskGetAllProductsAsync(int pageIndex, int pageSize); Task …FindProductsAsync(string sku, int pageIndex, int pageSize);
隐藏,收缩,复制Code
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, IOptionssettings, 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 products = await PaginatedList .FromIQueryable(_productsRepository.FindProductsAsync(sku), pageIndex, pageSize); if (products.Count() == 1) { //only one record found Product product = products.FirstOrDefault(); string productId = product.ProductId.ToString(); //cache a product if not in cache yet if (!await _productCacheRepository.IsValueCachedAsync(productId)) { await _productCacheRepository.SetValueAsync(productId, product); } //prepare prices if (!await _pricesCacheRepository.IsValueCachedAsync(productId)) { //prepare prices beforehand ThreadPool.QueueUserWorkItem(delegate { CallPreparePricesApiAsync(productId); }); } }; return new ProductsPageViewModel(products); } public async Task<ProductsPageViewModel> GetAllProductsAsync(int pageIndex, int pageSize) { pageSize = pageSize == 0 ? _settings.DefaultPageSize : pageSize; PaginatedList products = await PaginatedList .FromIQueryable(_productsRepository.GetAllProductsAsync(), pageIndex, pageSize); return new ProductsPageViewModel(products); } …
注意,如果没有将有效参数PageIndex和PageSize传递给PaginatedList构造器,则使用默认值——PageIndex = 1和PageSize =整个datatable大小。为了避免返回所有的产品和价格表的记录,我们将使用默认值DefaultPageSize从ProductsSettings和PricesSettings相应地。 更改PricesServicePricesAsync返回PricesPageViewModel 隐藏,复制Code
public interface IPricesService … TaskGetPricesAsync(int productId); Task<PricesPageViewModel> GetPricesAsync(int productId, int pageIndex, int pageSize); …
隐藏,复制Code
public class PricesService : IPricesService { private readonly IPricesRepository _pricesRepository; private readonly IPricesCacheRepository _pricesCacheRepository; private readonly PricesSettings _settings; public PricesService(IPricesRepository pricesRepository, IPricesCacheRepository pricesCacheRepository, IOptionssettings) { _pricesRepository = pricesRepository; _pricesCacheRepository = pricesCacheRepository; _settings = settings.Value; } public async Task GetPricesAsync(int productId, int pageIndex, int pageSize) { IEnumerableprices = await _pricesCacheRepository.GetOrSetValueAsync(productId.ToString(), async () => await _pricesRepository.GetPricesAsync(productId)); pageSize = pageSize == 0 ? _settings.DefaultPageSize : pageSize; return new PricesPageViewModel(new PaginatedList (prices, pageIndex, pageSize)); } …
现在我们可以重写ProductsController和PricesController,以便它们能够使用新的分页机制 让我们更改ProductsController。GetAllProductsAsync ProductsController。FindProductsAsync方法。新版本将是: 隐藏,复制Code
[HttpGet] public async TaskGetAllProductsAsync(int pageIndex, int pageSize) { ProductsPageViewModel productsPageViewModel = await _productsService.GetAllProductsAsync(pageIndex, pageSize); return new OkObjectResult(productsPageViewModel); } [HttpGet("find/{sku}")] public async Task FindProductsAsync(string sku, int pageIndex, int pageSize) { ProductsPageViewModel productsPageViewModel = await _productsService.FindProductsAsync(sku, pageIndex, pageSize); return new OkObjectResult(productsPageViewModel); }
和PricesController。GetPricesAsync方法: 隐藏,复制Code
[HttpGet("{Id:int}")] public async TaskGetPricesAsync(int id, int pageIndex, int pageSize) { PricesPageViewModel pricesPageViewModel = await _pricesService.GetPricesAsync(id, pageIndex, pageSize); return new OkObjectResult(pricesPageViewModel); }
如果我们有一些客户端使用旧版本的api,它仍然可以使用新版本,因为如果我们错过pageIndex或pageSize参数,它们的值将为0,我们的分页机制可以正确处理pageIndex=0和/或pageSize=0的情况。 既然我们已经在代码重构中达到了控制器,就让我们留在这里,把所有最初的混乱整理出来吧。 控制器vs ControllerBase 您可能已经注意到,在我们的解决方案中,ProductsController继承自Controller类,而PricesController继承自ControllerBase类。两个控制器都工作得很好,那么我们应该使用哪个版本呢?控制器类支持视图,因此它应该用于创建使用视图的web站点。对于WEB API服务,ControllerBase更可取,因为它更轻量级,因为它没有我们在WEB API中不需要的特性。 因此,我们将从ControllerBase继承我们的控制器,并使用属性[ApiController],它支持诸如自动模型验证、属性路由等有用特性 因此,更改ProductsController的声明为: 隐藏,复制Code
… [Route("api/[controller]")] [ApiController] public class ProductsController : ControllerBase { …
让我们看看模型验证是如何使用ApiController属性的。为此,我们将调用一些带有无效参数的api。例如,下面的操作期望整数Id,但我们发送一个字符串代替: http://localhost:49858/api/products/aa 结果将是: 状态:400错误请求 隐藏,复制Code
{ "id": [ "The value 'aa' is not valid." ] }
在这种情况下,当我们有意声明参数的类型[HttpGet("{Id:int}")]情况更糟: http://localhost:49858/api/prices/aa 状态:404未找到,没有任何关于Id参数类型不正确的消息。 因此,首先,我们将从PricesController中的HttpGet属性中删除Id类型声明。GetPricesAsync方法: 隐藏,复制Code
[HttpGet("{Id:int}")] [HttpGet("{id}")]
这将给我们一个标准的400错误请求和一个类型不匹配的消息。 另一个直接关系到应用程序生产力的问题是消除无意义的工作。例如,http://localhost:49858/api/prices/-1 api显然将返回404 Not Found,因为我们的数据库永远不会有任何负Id值。 我们在应用程序中多次使用正整数Id参数。我们的想法是创建一个Id验证过滤器,并在有Id参数时使用它。 自定义Id参数验证过滤器和属性 在解决方案中,创建一个过滤器文件夹和一个新类ValidateIdAsyncActi在它onFilter: 隐藏,收缩,复制Code
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 integer public 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); } } }
在筛选器中,我们检查请求是否只有一个Id参数。如果Id参数丢失或没有正整数值,筛选器将生成BadRequest HttpException。抛出HttpException涉及到我们的exceptionshandling中间件,以及它的所有好处,比如日志记录、统一的消息格式等等。 为了能够在控制器的任何位置应用这个过滤器,我们将在相同的过滤器文件夹中创建一个ValidateIdAttribute: 隐藏,复制Code
using Microsoft.AspNetCore.Mvc; namespace SpeedUpCoreAPIExample.Filters { public class ValidateIdAttribute : ServiceFilterAttribute { public ValidateIdAttribute() : base(typeof(ValidateIdAsyncActionFilter)) { } } }
在ProductsController中添加引用过滤器类名称空间 隐藏,复制Code
…
using SpeedUpCoreAPIExample.Filters;
…
并将[ValidateId]属性添加到所有需要Id参数的GetProductAsync和DeleteProductAsync动作: 隐藏,复制Code
… [HttpGet("{id}")] [ValidateId] public async TaskGetProductAsync(int id) { … [HttpDelete("{id}")] [ValidateId] public async Task DeleteProductAsync(int id) { …
我们可以将ValidateId属性应用到整个PricesController控制器,因为它的所有动作都需要一个Id参数。此外,我们需要纠正PricesController类名称空间中的错误——它显然应该是namespace SpeedUpCoreAPIExample。控制器,而不是命名空间SpeedUpCoreAPIExample.Contexts 隐藏,复制Code
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 { …
最后一步是在Startup.cs中声明过滤器 隐藏,复制Code
using SpeedUpCoreAPIExample.Filters; … public void ConfigureServices(IServiceCollection services) … services.AddSingleton(); …
让我们检查一下新的过滤器是如何工作的。为此,我们将再次错误地调用API http://localhost:49858/api/prices/-1。结果将完全符合我们的期望: 状态:400错误请求 隐藏,复制Code
{ "message": "'Id' must be a positive integer." }
注意!我们使用了ExceptionMessage类,现在消息通常满足我们的格式约定,但并不总是这样!如果我们再次尝试http://localhost:49858/api/prices/aa,仍然会得到标准的400错误请求消息。这是因为[ApiController]属性。当它被应用时,框架自动注册一个ModelStateInvalidFilter,它将在ValidateIdAsyncActionFilter过滤器之前工作,并生成自己格式的消息。 我们可以在启动类的ConfigureServices方法中抑制这种行为: 隐藏,复制Code
… services.AddMvc(); services.AddApiVersioning(); … services.Configure(options => { options.SuppressModelStateInvalidFilter = true; }); …
在此之后,只有我们的过滤器工作,我们可以控制模型验证消息的格式。但是现在我们有义务组织控制器动作的所有参数的显式验证。 分页参数自定义模型验证过滤器 我们在简单的应用程序中使用了分页树时间。让我们看看如果参数不正确会发生什么。为此,我们将调用http://localhost:49858/api/products?pageindex=-1 结果将是: 状态:500内部服务器错误 隐藏,复制Code
{ "message": "The offset specified in a OFFSET clause may not be negative." }
这条消息确实令人困惑,因为没有服务器错误,它是一个纯粹的坏请求。如果你不知道它是关于分页的,那么文本本身就是神秘的。 我们希望得到一个答复: 状态:400错误请求 隐藏,复制Code
{ "message": "'pageindex' must be 0 or a positive integer." }
另一个问题是在哪里应用参数检查。注意,如果省略任何一个或两个参数,分页机制工作得很好——它使用默认值。我们应该只控制负面参数。在PaginatedList级别的id上抛出HttpException不是一个好主意,因为代码应该在不改变它的情况下可重用,并且下一次PaginatedList将不一定在ASP中使用。网络应用程序。在服务级别检查参数更好,但是需要重复验证代码或创建其他具有验证方法的公共助手类。 如果分页参数来自外部,那么在传递给分页过程之前,最好在控制器中组织它们的检查。 因此,我们必须创建另一个模型验证过滤器,它将验证PageIndex和PageSize参数。验证的思想略有不同——可以省略任何或两个参数,可以等于零或大于零的整数。 在相同的过滤器文件夹中创建一个新的类ValidatePagingAsyncActionFilter: 隐藏,收缩,复制Code
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 integer public 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); } } } } }
然后创建ValidatePagingAttribute类: 隐藏,复制Code
using Microsoft.AspNetCore.Mvc; namespace SpeedUpCoreAPIExample.Filters { public class ValidatePagingAttribute : ServiceFilterAttribute { public ValidatePagingAttribute() : base(typeof(ValidatePagingAsyncActionFilter)) { } } }
然后在start .cs中声明过滤器 隐藏,复制Code
… public void ConfigureServices(IServiceCollection services) … services.AddSingleton(); …
最后,添加[ValidatePaging]属性到ProductsController。GetAllProductsAsync ProductsController。FindProductsAsync方法: 隐藏,复制Code
… [HttpGet] [ValidatePaging] public async TaskGetAllProductsAsync(int pageIndex, int pageSize) { … [HttpGet("find/{sku}")] [ValidatePaging] public async Task FindProductsAsync(string sku, int pageIndex, int pageSize) { …
和PricesController。GetPricesAsync方法: 隐藏,复制Code
… [HttpGet("{id}")] [ValidatePaging] public async TaskGetPricesAsync(int id, int pageIndex, int pageSize) { …
现在我们有了针对所有敏感参数的自动验证机制,并且我们的应用程序能够正常工作(至少在本地) 跨源资源共享 在实际的应用程序中,我们将把一些域名绑定到web服务,其URL类似于http://mydomainname.com/api/ 同时,使用我们服务的api的客户机应用程序可以驻留在不同的域上。如果客户端(例如web站点)对API请求使用AJAX,并且响应不包含value = *(所有域都允许)的Access-Control-Allow-Origin头文件,或者不包含与orig相同的主机在(客户端主机)中,支持CORS的浏览器会出于安全原因阻止响应。 让我们确保。构建并发布我们的应用程序到IIS,绑定它与测试URL(在我们的例子中是mydomainname.com),并调用任何API https://resttesttest.com/ -在线工具API检查: 使歌珥ASP。网络核心 要强制应用程序发送正确的标头,我们应该启用CORS。为此,请安装Microsoft.AspNetCore。Cors NuGet包(如果你仍然没有安装它与其他包像微软。aspnetcore。MVC或Microsoft.AspNetCore.All) 启用CORS的最简单方法是在Startup.cs中添加以下代码: 隐藏,复制Code
… public void Configure( … app.UseCors(builder => builder .AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader()); … app.UseMvc(); …
这样我们就允许从任何主机访问我们的API。我们还可以添加. allowcredentials()选项,但是在AllowAnyOrigin中使用它是不安全的。 之后,重新构建,将应用程序重新发布到IIS,并使用resttest.com或其他工具测试它。乍一看,一切工作良好- CORS错误消息消失。但是这只在我们的ExceptionsHandlingMiddleware进入游戏之前有效。 没有CORS头发送的情况下,HTTP错误 这是因为实际上,当HttpException或任何其他异常发生并且中间件处理它时,response headers集合是空的。这意味着没有向客户机应用程序传递任何访问控制允许原点头,从而出现CORS问题。 如何发送HTTP 4xx-5xx响应与CORS头在一个ASPNET。核心web应用程序 为了克服这个问题,我们应该稍微不同地启用CORS。在启动。ConfigureServices输入以下代码: 隐藏,复制Code
… public void ConfigureServices(IServiceCollection services) { services.AddCors(options => { options.AddPolicy("Default", builder => { builder.AllowAnyOrigin(); builder.AllowAnyMethod(); builder.AllowAnyHeader(); }); }); …
而在Startup.Configure: 隐藏,复制Code
… public void Configure( … app.UseCors("Default"); … app.UseMvc(); …
通过这种方式启用CORS,我们可以通过依赖注入在应用程序的任何位置访问CorsOptions。其思想是用取自CorsOptions的CORS策略在ExceptionsHandlingMiddleware中重新填充响应头。 ExceptionsHandlingMiddleware类的正确代码: 隐藏,收缩,复制Code
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_logger; private readonly ICorsService _corsService; private readonly CorsOptions _corsOptions; public ExceptionsHandlingMiddleware(RequestDelegate next, ILogger logger, ICorsService corsService, IOptions 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; // 500 string message = string.Empty; #if DEBUG message = exception.Message; #else message = "An unhandled exception has occurred"; #endif 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); } } …
如果我们重新构建并重新发布我们的应用程序,当它的api被从任何主机调用时,它将工作得很好,没有任何CORS问题。 API版本控制 在公开我们的应用程序之前,我们必须考虑如何使用它的api。一段时间后,需求可能会发生变化,我们将不得不重写应用程序,以便其API将返回不同的数据集。如果我们发布有新变化的web服务,但不更新使用api的客户机应用程序,那么客户机-服务器兼容性将会出现大问题。 为了避免这些问题,我们应该建立API版本控制。例如,旧版本的产品API会有一个路径: http://mydomainname.com/api/v1.0/products/ 新版本会有一条路线 http://mydomainname.com/api/v2.0/products/ 在这种情况下,即使是旧的客户机应用程序也将继续正常工作,直到它们被更新为可以在版本2.0中正常工作的版本为止 在我们的应用程序中,我们将实现基于URL路径的版本控制,其中版本号是api URL的一部分,就像上面的示例一样。 在。net Core微软。aspnetcore。mvc中。版本控制包负责版本控制。所以,我们应该先安装包: 然后将services.AddApiVersioning()添加到启动的类ConfigureServices方法中: 隐藏,复制Code
… services.AddMvc(); services.AddApiVersioning(); …
最后,为两个控制器添加ApiVersion和正确路由属性: 隐藏,复制Code
… [ApiVersion("1.0")] [Route("/api/v{version:apiVersion}/[controller]/")] …
现在我们有了版本控制。这样做之后,如果我们想在2.0版本中增强应用程序,例如,我们可以在控制器中添加[ApiVersion("2.0")]属性: 隐藏,复制Code
… [ApiVersion("1.0")] [ApiVersion("2.0")] …
然后创建一个操作,我们希望只使用2.0版本,并添加add [MapToApiVersion("2.0")]属性到操作。 版本控制机制完美的几乎没有任何编码,但像往常一样,美中不足之处:如果我们不小心使用了错误的版本的API URL (http://localhost: 49858 / API / v10.0 /价格/ 1),我们将有一个错误消息在以下格式: 状态:400错误请求 隐藏,复制Code
{ "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 } }
这是标准的错误响应格式。它的信息量大得多,但与我们想要的格式相去甚远。因此,如果我们想对所有类型的消息使用统一格式,就必须在详细的标准错误响应格式和我们为应用程序设计的简单格式之间做出选择。 要应用标准错误响应格式,我们只需扩展ExceptionMessage类。幸运的是,我们已经预见到这个机会,这并不困难。但是这种格式的消息比我们想传递给用户的还要详细。在一个简单的应用程序中,这样的去talization可能并不真正相关。所以,为了不让事情复杂化,我们将使用简单的格式。 控制API版本控制错误消息format 让我们在异常文件夹中创建一个VersioningErrorResponseProvider类: 隐藏,收缩,复制Code
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); } } }
这个类继承了DefaultErrorResponseProvider。它只是根据ErrorCode(代码列表)格式化一个友好的消息,并抛出HttpException BadRequest异常。然后异常由我们的ExceptionHandlerMiddleware通过日志记录、统一的错误消息格式等处理。 最后一步是注册VersioningErrorResponseProvider类作为版本管理HTTP错误响应生成器。在Startup类中,在ConfigureServices方法中添加API版本化服务注册选项: 隐藏,复制Code
… services.AddMvc(); services.AddApiVersioning(options => { options.ErrorResponses = new VersioningErrorResponseProvider(); }); …
因此,我们已经将标准错误响应行为更改为我们想要的。 内部HTTP调用的版本控制 我们还必须在SelfHttpClient类中应用版本控制。在类中,我们设置HttpClient的BaseAddress属性来调用API。在构建基址时,我们应该考虑版本控制。 为了避免对将要调用的API版本进行硬编码,我们创建了一个用于API版本控制的settings类。appsettings。json文件创建一个API节: 隐藏,复制Code
… , "Api": { "Version": "1.0" } …
然后在设置文件夹中创建apiset .cs文件: 隐藏,复制Code
namespace SpeedUpCoreAPIExample.Settings { public class ApiSettings { public string Version { get; set; } } }
在启动的ConfigureServices方法中声明类: 隐藏,复制Code
… public void ConfigureServices(IServiceCollection services) … //Settings services.Configure(Configuration.GetSection("Api")); …
最后,更改SelfHttpClient的构造函数: 隐藏,复制Code
public SelfHttpClient(HttpClient httpClient, IHttpContextAccessor httpContextAccessor, IOptionssettings ) { 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名称 让我们以SelfHttpClient类结束。我们使用它来调用我们自己的API来提前进行数据准备。在类包中,我们使用HttpContextAccessor构建API的基地址。当我们开始在internet上发布我们的应用程序时,基本地址将是http://mydomainname.com/api/v1.0/。当我们调用API时,HttpClient在后台请求DNS服务器将这个mydomainname.com主机名解析到应用程序运行的web服务器的IP中,然后转到这个IP。但我们知道IP——它是我们自己服务器的IP。因此,为了避免这种无意义的访问DNS服务器,我们应该在本地解析主机名,将其添加到我们服务器上的主机文件中。 “驱动模块”“etc\” 您应该添加以下条目: 隐藏,复制Code
192.168.1.1 mydomainname.com 192.168.1.1 www.mydomainname.com
192.168.1.1 -我们的网络服务器的IP是否在本地网络 在此改进之后,HTTP响应甚至不会离开服务器边界,因此执行速度会快得多。 编写。net核心API应用程序文档 我们可以考虑记录应用程序的两个方面: 代码的XML文档——实际上,代码应该是自文档化的。但是,有时我们仍然需要对一些方法及其参数的细节进行额外的解释。我们将用XML注释来记录代码;OpenAPI文档——为API编制文档,以便客户端应用程序的开发人员能够以OpenAPI规范格式应用到该文档,并接收反映所有API细节的全面信息。 XML注释 要启用XML注释,请打开项目属性并选择Build选项卡: 在这里,我们应该选中XML文档文件复选框并保留默认值。我们还应该在“禁止警告”文本框中添加1591个警告号,以防止在忽略某些公共类、属性、方法等的XML注释时出现编译器警告。 现在我们可以这样注释我们的代码: 隐藏,复制Code
… /// <summary> /// Call any controller's action with HttpPost method and Id parameter. /// </summary> /// <paramname="apiRoute">Relative API route.</param> /// <paramname="id">The parameter.</param> public async Task PostIdAsync(string apiRoute, string id) …
在这里,您可以找到关于用XML注释记录代码的详细信息。 将创建一个名称在XML文档文件文本框中指定的XML文件。稍后我们将需要这个文件。 用于RESTful api的OpenAPI文档 API文档机制要求: 文件应自动生成; 应该支持API版本控制和自动发现; 也应该使用XML注释文件中的文档; 该机制应该为UI提供文档,这样用户无需编写真正的客户端应用程序就可以测试api; 文档应该包括使用示例。 我们将大摇大摆地完成所有这些要求。让我们安装必要的NuGet包。在NuGet包管理器安装: 隐藏,复制Code
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)
注意!我们需要ApiExplorer包来自动发现所有API版本,并为每个发现的版本生成描述和端点。 安装后我们的依赖- NuGet列表还将包括: 注意!尽管在写这篇文章的时候有些虚张声势。AspNetCore Swashbuckle.AspNetCore。过滤器版本5.0.0-rc8可用,我们使用较低的版本。原因是版本2.9.0和5.0.0-rc8之间存在一些兼容性问题。因此,我们选择了经过验证的稳定的NuGet包组合。希望在新版本中,大摇大摆的开发者会重新来过解决所有的兼容性问题。 让我们在应用程序中创建一个Swagger文件夹,然后在其中创建一个SwaggerServiceExtensions类。这个静态Swagger extensions类将封装所有关于服务设置的逻辑。我们将从启动的ConfigureServices和Configure方法中调用这个类的方法,从而使启动类更短且可读。 下面是整个类的解释: 隐藏,收缩,复制Code
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 =. options.GroupNameFormat = "'v'VV"; //Order API explorer to change /api/v{version}/ to /api/v1/ options.SubstituteApiVersionInUrl = true; }); // Get IApiVersionDescriptionProvider service IApiVersionDescriptionProvider provider = services.BuildServiceProvider().GetRequiredService) (); services.AddSwaggerGen(options => { //Create description for each discovered API version foreach (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 examples options.OperationFilter (); //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; } } }
在AddSwaggerDocumentation方法中,我们添加了带有选项的VersionedApiExplorer,这允许ApiExplorer理解我们在API路径中的版本化格式,并自动将OpenApi文档中的/v{version:apiVersion}/更改为/v1.1/。 注意!“'v' vv '”模式符合我们的版本控制考虑:少校。小少校。即v1.0。但是Swagger将把v1.0变成v1,而v1.1将保持原样。然而,api在v1.0和v1符号下都能很好地工作。在这里您可以找到关于自定义API版本格式字符串的详细信息 然后我们实例化ApiVersionDescriptionProvider。我们需要此服务来获取版本列表,并为每个发现的版本生成描述。在服务。AddSwaggerGen命令我们生成这些描述。 在这里您可以找到关于OpenAPI规范的详细信息。 在下一行中,我们将扩展Swagger Generator,以便它能够在OpenApi文档中添加响应示例(和请求示例,尽管不是在我们的例子中): 隐藏,复制Code
… options.OperationFilter(); …
AddSwaggerDocumentation方法的最后一个阶段是让Swagger知道XML注释文件的路径。因此,Swagger将在其json OpenApi文件和UI中包含XML注释。 在UseSwaggerDocumentation方法中,我们启用了Swagger并为所有API版本构建了Swagger UA端点。我们再次使用IApiVersionDescriptionProvider来发现所有api,但这一次我们将provider作为方法的参数传递,因为我们从启动时就调用了UseSwaggerDocumentation方法。方法,在这里我们已经能够通过依赖项注入获得提供程序引用。 RoutePrefix =字符串。空选项意味着Swagger UI将在我们的应用程序的根URL处可用,即http://mydomainname.com或http://mydomainname.com/index.html DocExpansion(DocExpansion. none)意味着招展UI中的请求主体在打开时都将崩溃。 昂首阔步的反应的例子 我们已经在AddSwaggerDocumentation方法中使用示例扩展了Swagger。让我们创建示例数据类。在Swagger文件夹中创建一个swaggerexample .cs文件,它将包含所有的示例类: 隐藏,收缩,复制Code
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() { 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 () { 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"); } } }
这些类非常简单,它们只返回viewmodel中带有示例数据或错误消息示例的统一消息格式。然后,我们将把API的响应代码与适当的示例链接起来。 现在我们在创业中加入了招摇服务。ConfigureServices方法: 隐藏,复制Code
… public void ConfigureServices(IServiceCollection services) … services.AddSwaggerDocumentation(); …
并在启动时添加Swagger中间件。配置方法: 隐藏,复制Code
… public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApiVersionDescriptionProvider provider) … app.UseSwaggerDocumentation(provider); app.UseCors("Default"); app.UseMvc(); …
注意!我们通过依赖注入获得IApiVersionDescriptionProvider,并将其作为参数传递给UseSwaggerDocumentation。 形成OpenApi文档的标记和属性 Swagger能够理解大多数XML注释标记,并且拥有各种自己的属性。我们只选择了其中的一小部分,但对于生成简短而清晰的文档已经足够了。 我们应该在actions声明控制器中应用这些标记和属性。下面是一些ProductsController的例子和解释: 隐藏,复制Code
… /// <summary> /// Gets all Products with pagination. /// </summary> /// <remarks>GET /api/v1/products/?pageIndex=1&pageSize=20</remarks> /// <paramname="pageIndex">Index of page to display (if not set, defauld value = 1 - first page is used).</param> /// <paramname="pageSize">Size of page (if not set, defauld value is used).</param> /// <returns>List of product swith pagination state</returns> /// <responsecode="200">Products found and returned successfully.</response> [ProducesResponseType(typeof(ProductsPageViewModel), StatusCodes.Status200OK)] [SwaggerResponseExample(StatusCodes.Status200OK, typeof(ProductsExample))] [HttpGet] [ValidatePaging] public async TaskGetAllProductsAsync(int pageIndex, int pageSize) …
隐藏,复制Code
… /// <summary> /// Gets a Product by Id. /// </summary> /// <remarks>GET /api/v1/products/1</remarks> /// <paramname="id">Product's Id.</param> /// <returns>A Product information</returns> /// <responsecode="200">Product found and returned successfully.</response> /// <responsecode="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 TaskGetProductAsync(int id) …
标签显然是不言自明的。让我们回顾一下属性: 隐藏,复制Code
[ProducesResponseType(typeof(ProductViewModel), StatusCodes.Status200OK)]
我们在这里声明,如果操作成功,返回值的类型将是ProductViewModel: Response code = 200 OK) 隐藏,复制Code
[SwaggerResponseExample(StatusCodes.Status200OK, typeof(ProductExample))]
在这里,我们链接StatusCodes。Status200OK和ProductExample类,我们已经创建并填充了演示数据。 注意!Swagger从[HttpGet("{id}")]属性自动识别id参数。 out api的响应代码列表并不是真正的完整。异常处理中间件还可以为任何API返回Status500InternalServerError(内部服务器错误)。我们可以为整个控制器声明一次,而不是为每个动作添加响应代码= 500代码的描述: 隐藏,复制Code
… [ApiVersion("1.0")] [Route("/api/v{version:apiVersion}/[controller]/")] [ApiController] [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] [SwaggerResponseExample(StatusCodes.Status500InternalServerError, typeof(InternalServerErrorExample))] public class ProductsController : ControllerBase { …
注意!我们不希望公开我们的内部API API /v1/prices/prepare of PricesController,以便它对客户端的应用程序开发人员可见。这就是为什么我们用IgnoreApi = true来赋值这个动作: 隐藏,复制Code
… [ApiExplorerSettings(IgnoreApi = true)] [HttpPost("prepare/{id}")] public async TaskPreparePricesAsync(int id) { …
如果我们启动我们的应用程序并进入它的根URL,我们会发现根据提供的选项、XML注释和属性形成的Swagger UI: 在右上角,我们可以看到“选择spec”会话是版本选择器。如果我们在某些控制器中添加[ApiVersion("2.0")]属性,2.0版本将自动被发现,并出现在下面的下拉列表中: Swagger UI真的很简单。我们可以扩展/折叠每个API,并观察其描述、参数、示例等。如果我们想测试API,我们应该点击“TryItOut”按钮: 然后输入一个你想要检查的值,在相应参数的输入框中点击检查: 本案例的结果将如预期的那样: 对于客户端应用的开发者,可以下载一个OpenApi json文件: 例如,可以使用NSwagStudio自动生成客户端应用程序的代码,也可以导入一些测试框架,如Postman来建立api的自动测试。 摆脱不使用或重复的NuGet包 代码重构和优化似乎是一个永无止境的过程。我们就讲到这里。但是,您可以继续使用一个有用的工具,比如ReSharper,来获得关于如何提高代码质量的新想法。 因为代码不会再被更改,至少在本文的范围内,我们可以修改我们现在拥有的NuGet包。现在很明显,我们有一些包是复制的,而且它们的版本控制非常混乱。 目前我们的依赖结构是这样的: 实际上,Microsoft.AspNetCore。所有的包都包括这四个选择的包,所以我们可以很容易地从应用程序中删除它们。 但是在删除这些包时,我们应该考虑版本兼容性。例如,微软。aspnetcore。所有(2.0.5)包包括Microsoft.AspNetCore。Mvc(2.0.2)。这意味着,我们在控制器中使用的ApiController属性将会出现问题,该属性自MVC版本2.1以来就可用了。 所以,在删除额外的包之后,我们还应该升级微软。aspnetcore。所有到最新的稳定版本。首先,我们应该在开发机器上安装新版本的SDK(如果还没有的话)。因为我们已经安装了版本2.2,所以我们只需要将应用程序的目标框架更改为。net Core 2.2。为此,右键单击项目,转到Properties菜单,并将目标框架更改为2.2。 然后Microsoft.AspNetCore升级。所有的包。在NuGet包管理器中选择Microsoft.AspNetCore。所有从安装包和安装新版本: 如果我们尝试重新构建我们的解决方案与新的依赖,它将成功构建,但有以下警告: 隐藏,复制Code
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
简单地说,我们应该删除Microsoft.AspNetCore的明确版本规范。所有都在CSPROJ文件中。为此,右键单击项目并选择Upload project菜单。卸载完成后,再次右键单击项目,选择: 只要从Microsoft.AspNetCore.All的PackageReference中删除Version="2.2.6"即可。结果应该是: 隐藏,复制Code
"Microsoft.NET.Sdk.Web"> … "Microsoft.AspNetCore.All" /> "Serilog.Extensions.Logging.File" Version="1.1.0" /> ItemGroup> …
再次重新加载项目 注意,在删除了显式的版本规范之后,我们可以看到Microsoft.AspNetCore。所有这些都在NuGet和SDK部分(仍然在它的版本中)。 但是如果我们重新构建解决方案,它将在没有任何警告的情况下成功构建。我们可以使用Swagger或任何其他工具启动应用程序和测试api。它将正常工作。 Microsoft.AspNetCore。所有和Microsoft.AspNetCore。应用metapackages 即使在像我们这样的小而简单的应用程序中,我们也有NuGet和版本地狱的开始。通过使用微软。aspnetcore . all软件,我们轻松地解决了这些问题。 使用元包装的另一个好处是应用程序的大小。它变得更小,因为元封装遵循共享框架的概念。使用共享框架,构成metapackage的所有Dll文件都安装在一个共享文件夹中,其他应用程序也可以使用这些文件。在我们的应用程序中,这个文件夹中只有到Dll的链接。当我们构建应用程序时,所有这些Dll不会被复制到应用程序的文件夹中。这意味着,要正常工作,. net Core 2.0(或更高版本)运行时必须安装在目标机器上。 当我们包含我们的应用程序时,共享框架概念的好处就更大了。元包装将是ASP的一部分。NET Core运行时Docker镜像。应用程序映像将只包含不属于元包装的包,因此,应用程序映像将更小,可以更快地部署。 最后一个奇迹是隐式版本控制。由于我们已经在CSPROJ文件中删除了准确的metapackage版本,所以如果该运行时的版本与我们引用的metapackage相同或更高,那么我们的应用程序将适用于目标机器上安装的任何版本的。net Core runtime。这使得在另一个环境中部署我们的应用程序和更新。net核心运行时变得更加容易,而不需要重新构建the应用程序。 注意,隐式版本控制只有在我们的项目使用< project Sdk="Microsoft.NET.Sdk.Web"> 从ASP的迁移。NET Core 2.2到3.0 本文的代码是用ASP编写的。2.2网络核心。在准备本文时,发布了一个新的3.0版本。如果您想用ASP检查代码。NET Core 3.0,考虑从asp.net Core 3.0迁移。NET Core 2.2到3.0 的兴趣点 即使经过了如此重大的改进,我们的应用程序仍然没有准备好投入生产。它缺乏HTTPS支持、自动测试、保持连接字符串安全等等。这些可能是即将发表的文章的重点。 本文转载于:http://www.diyabc.com/frontweb/news18239.html