右击【解决方案】,然后点击【管理Nuget包】,搜索【Swashbuckle.AspNetCore】包,点击【安装】即可,博主这里下载的是【最新稳定版5.6.3】。
在【Startup.cs】文件中的【ConfigureService】类中引入命名空间,并注册Swagger服务并配置文档信息。
//引入命名空间
using Microsoft.OpenApi.Models;
//注册Swagger
services.AddSwaggerGen(u=> {
u.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Version="Ver:1.0.0",//版本
Title="xxx后台管理系统",//标题
Description="xxx后台管理系统:包括人员信息、团队管理等。",//描述
Contact=new Microsoft.OpenApi.Models.OpenApiContact {
Name="西瓜程序猿",
Email="xxx@qq.com"
}
});
});
如果安装的 Swashbuckle.AspNetCore Nuget包版本<=3.0.0,写法略有不同。将 【OpenApiInfo】 替换成 【Info】 即可。
services.AddSwaggerGen(x=>
{
x.SwaggerDoc("v1", new Info() { Title="Web Api", Version="v1" });
});
在【Startup.cs】文件中的【Configure】类中启用Swagger中间件,为生成的JSON文档和SwaggerUI提供服务。
//启用Swagger中间件
app.UseSwagger();
//配置SwaggerUI
app.UseSwaggerUI(u=>
{
u.SwaggerEndpoint("/swagger/v1/swagger.json", "WebAPI_v1");
});
2.5.1-如果我们直接运行项目,会出现这样的界面,说明我们的Web API程序没有问题。
2.5.2-然后我们在地址栏中输入【swagger/v1/swagger.json】。
可以看到浏览器出现这样的界面,如果出现这样的界面,说明Swagger也没有问题。
注意:按照2.5.1在地址栏中的【swagger/v1/swagger.json】需要与在【Startup.cs】文件中的【Configure】类中启用Swagger中间件添加的代码保持一致,因为这段代码就是自动生成JSON文件的,你配置的路径和JSON文件地址是什么,就在浏览器中输入对应的即可。
2.5.3-以上步骤都没问题的话,然后我们地址栏中输入【swagger/index.html】。
如果能出现以下界面,说明SwaggerUI也没有问题,都全部正常了。
2.6.1-打开【launchSettings.json】这个文件。
2.6.2-然后将【launchUrl】的值从【weatherforecast】修改成【swagger】。
2.6.3-然后运行项目就直接进入Swagger首页了。
3.1-双击解决方案
3-2-然后进入这个页面,加上这个代码
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
3-3.在【Startup.cs】文件中的【ConfigureService】类中注册读取XML信息的Swagger。
#region 读取xml信息
// 使用反射获取xml文件,并构造出文件的路径
var xmlFile=$"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath=Path.Combine(AppContext.BaseDirectory, xmlFile);
// 启用xml注释,该方法第二个参数启用控制器的注释,默认为false.
u.IncludeXmlComments(xmlPath, true);
#endregion
注意:
4.1-写一个实例:在【WeatherForecastController】控制器中写一个方法。
4.2-写上以下代码然后进行请求。
/// <summary>
/// 这是一个例子
/// </summary>
/// <remarks>
/// 描述:这是一个带参数的GET请求
/// Web API
/// </remarks>
/// <param name="id">主键ID</param>
/// <returns>测试字符串</returns>
[HttpGet("{id}")]
public ActionResult<string> Get(int id) {
return $"你请求的ID是:{id}";
}
4.3-可以看到【XML注释】起作用了,然后调用也成功了。
(1)在方法中加上以下特性:
[ProducesResponseType(typeof(xxx),200)]
(2)在Remarks节点下进行注释:
在需要进行隐藏的接口上加上以下特性即可:
[ApiExplorerSettings(IgnoreApi=true)]
如果不想加上"swagger",而输入5000即可访问,也可以自定义路由前缀,加上以下代码即可。
(1)新建一个【index.html】,右击该文件,点击【属性】,进行设置相关操作。
(2)在Startup.cs进行配置。
(3)静态页面下载地址:
https://github.com/swagger-api/swagger-ui/blob/master/dist/index.html
(1)在Startup.cs进行配置。
#region 开启JWT
u.OperationFilter<SecurityRequirementsOperationFilter>();
u.AddSecurityDefinition("oauth2", new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Description="JWT授权(数据将在请求头中进行传输)直接在下框中输入Bearer { token } (注意两者之间是一个空格) ",
Name="Authorization",
In=Microsoft.OpenApi.Models.ParameterLocation.Header,//jwt默认存放Authorazation信息的位置(请求头中)
Type=Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey
});
#endregion
(2)下载包,注意版本号问题,尽量保持一致,不然会报错。
(3)然后将接口加上权限,去请求该接口,可以看到请求头中会有以下信息了。
目录结构:
(1)在【index.html】文件导入abp.js/swagger.js文件。
(2)在【swagger.js】里面需要注意请求授权地址。
(3)后端授权逻辑。
小明最近又遇到麻烦了,小红希望对接接口传送的数据进行验证,既然是小红要求,那小明说什么都得满足呀,这还不简单嘛。
[HttpPost]
public async Task<ActionResult<Todo>> PostTodo(Todo todo)
{
if (string.IsNullOrEmpty(todo.Name))
{
return Ok("名称不能为空");
}
context.Todo.Add(todo);
await context.SaveChangesAsync();
return CreatedAtAction("GetTodo", new { id=todo.Id }, todo);
}
小明写着写着发现这样写,很多接口相同得地方都要写,使得代码比较臃肿。
在参数模型上打上注解
namespace App001.Models
{
/// <summary>
/// 待办事项
/// </summary>
public class Todo
{
/// <summary>
/// ID
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// 名称
/// </summary>
[Required(ErrorMessage="名称不能为空")]
public string Name { get; set; }
}
}
Postman测试Name传值未空时,则返回:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|df184e36-4e11844dfd38a626.",
"errors": {
"Name": [
"名称不能为空"
]
}
}
注意Web API 控制器具有 [ApiController] 特性,则它们不必检查ModelState.IsValid。在此情况下,如果模型状态无效,将返回包含错误详细信息的自动 HTTP 400 响应。
通过验证特性可以指定要为无效输入显示的错误消息。 例如:
[Required(ErrorMessage="名称不能为空")]
有两种方式:
首先,创建ModelValidateActionFilterAttribute过滤器。
public class ModelValidateActionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
//获取验证失败的模型字段
var errors=context.ModelState
.Where(e=> e.Value.Errors.Count > 0)
.Select(e=> e.Value.Errors.First().ErrorMessage)
.ToList();
var str=string.Join("|", errors);
//设置返回内容
var result=new
{
Code=10000,
Msg="未通过数据验证。",
FullMsg=str
};
context.Result=new BadRequestObjectResult(result);
}
}
}
然后,Startup.ConfigureServices将过滤器添加到控制器中并关闭默认模型验证,另外我们还添加了AddNewtonsoftJson。
//关闭默认模型验证
services.Configure<ApiBehaviorOptions>(opt=> opt.SuppressModelStateInvalidFilter=true);
services.AddControllers(opt=>
{
//添加过滤器
opt.Filters.Add(typeof(ModelValidateActionFilterAttribute));
}).AddNewtonsoftJson(opt=>
{
//json字符串大小写原样输出
opt.SerializerSettings.ContractResolver=new DefaultContractResolver();
});
最后,我们看一下返回效果:
{
"Code": 10000,
"Msg": "未通过数据验证。",
"FullMsg": "名称不能为空。"
}
services.Configure<ApiBehaviorOptions>(opt=>
{
opt.InvalidModelStateResponseFactory=actionContext=>
{
//获取验证失败的模型字段
var errors=actionContext.ModelState
.Where(e=> e.Value.Errors.Count > 0)
.Select(e=> e.Value.Errors.First().ErrorMessage)
.ToList();
var str=string.Join("|", errors);
//设置返回内容
var result=new
{
Code=10000,
Msg="未通过数据验证。",
FullMsg=str
};
return new BadRequestObjectResult(result);
};
});
目前为止,小明把数据验证也搞定了,是不是so easy!
章涵盖
几乎所有应用程序都需要数据,尤其是基于 Web 的应用程序,其中大量客户端与集中式实体(通常是基于 HTTP 的服务)交互,以访问信息并可能更新或操作它们。在本书中,我们将学习如何设计和开发一种特定类型的基于 HTTP 的服务,其唯一目的是向这些客户端提供数据,允许它们以统一、结构化和标准化的方式与所需的信息进行交互:Web API。
在本章的第一部分中,我们将了解 Web API 的独特特征,并了解如何将其应用于多个实际场景。在第二部分中,我们将熟悉 ASP.NET Core,我们将在本书中用于创建Web API的Web框架。
应用程序编程接口 (API) 是一种软件接口,它公开计算机程序用于相互交互和交换信息的工具和服务。执行此类交换所需的连接是通过通用通信标准(协议)、给定的可用操作集(规范)和数据交换格式(JSON、XML 等)建立的。
从这个定义中,我们可以很容易地看到,API 的主要目的是允许各方使用通用语法(或抽象)进行通信,该语法(或抽象)简化和标准化每个引擎盖下发生的事情。整体概念类似于现实世界的接口,后者也提供了一个通用的“语法”,允许不同的各方进行操作。一个完美的例子是插头插座,这是所有国家电气系统使用的抽象概念,允许家用电器和电子设备通过给定的电压、频率和插头类型标准与电源交互。
图1.1显示了形成电网的主要组成部分:一个互连的网络,用于从生产者向住宅消费者发电、传输和输送电力。正如我们所看到的,每个组件都以不同的方式处理电力,并使用各种“协议”和“适配器”(电缆线、变压器等)与其他组件进行通信,最终目标是将其带到人们的家中。当住宅单元连接到电网时,家用电器(电视机、烤箱、冰箱等)可以通过安全、受保护、易于使用的接口(交流电源插头)使用电力。如果我们考虑这些插座的工作原理,我们可以很容易地理解它们在多大程度上简化了底层电网的技术方面。家用电器不必知道这样的系统是如何工作的,只要它们能够处理接口。
图1.1 接口的常见示例:交流电源插头插座
Web API 与引入万维网的概念相同:可通过 Web 访问的接口,该接口公开一个或多个插头(端点),其他各方(客户端)可以使用这些接口通过通用通信协议 (HTTP) 和标准(JSON/XML 格式)与电源(数据)进行交互。
注意在本书中,术语Web API将互换使用,以表示接口和实际的Web应用程序。
图 1.2 说明了典型面向服务的体系结构 (SOA) 环境中 Web API 的用途。SOA 是一种围绕通过网络进行通信的各种独立服务的责任分离而构建的架构风格。
图 1.2 Web API 在典型的基于 SOA 的环境中的角色
如我们所见,Web API 起着关键作用,因为它负责从底层数据库管理系统 (DBMS) 中检索数据并使其可用于服务:
我们刚刚描述的是一个有趣的场景,可以帮助我们理解 Web API 在一个相当常见的基于服务的生态系统中的角色。但我们仍在讨论一种非个人的理论方法。让我们看看如何使相同的体系结构适应特定的现实场景。
在本节中,我们将图 1.1 中描述的抽象概念实例化为具体、可信和现实的场景。每个人都知道什么是棋盘游戏,对吧?我们谈论的是带有骰子、纸牌、棋子和类似东西的桌面游戏。假设我们在棋盘游戏俱乐部的 IT 部门工作。俱乐部有一个棋盘游戏数据库,其中包括游戏信息,例如姓名、出版年份、最小最大玩家、游戏时间、最低年龄、复杂性和机制,以及俱乐部成员、客人和其他玩家随时间给出的一些排名统计数据(评级数量和平均评级)。我们还假设俱乐部希望使用此数据库来提供一些基于 Web 的服务和应用程序,例如:
正如我们很容易猜到的那样,满足这些要求的一个好方法是实现一个专用的 Web API。这种方法允许我们向所有这些服务提供,而无需将数据库服务器及其底层数据模型暴露给其中任何一个。图 1.3 显示了体系结构。
图 1.3 由单个 MyBGList Web API 提供的多个与棋盘游戏相关的 Web 应用程序和服务
同样,Web API 是组织者,获取包含所有相关数据的数据源(MyBGList DBMS)并将其提供给其他服务:
此 Web API 是我们将在以下章节中使用的 API。
什么是 Jamstack?
Jamstack(JavaScript,API和Markup)是一种现代架构模式,基于将网站预呈现为静态HTML页面并使用JavaScript和Web API加载内容。有关 Jamstack 方法的更多信息,请参阅 https://jamstack.org。有关使用 Jamstack 方法开发基于标准的静态网站的综合指南,请查看 Raymond Camden 和 Brian Rinaldi (https://www.manning.com/books/the-jamstack-book) 的 The Jamstack Book: Beyond Static Sites with JavaScript, API and Markup。
现在我们已经了解了大致情况,我们可以花一些时间探索当今 Web API 开发人员可用的各种体系结构和消息传递协议。正如我们将能够看到的,每种类型都有特点、优缺点,使其适用于不同的应用程序和业务。
你在这本书中找不到的东西
出于篇幅的原因,我有意将本书的主题限制在HTTP Web API,跳过其他应用层协议,如高级消息队列协议(AMQP)。如果您有兴趣通过AMQP了解有关基于消息的应用程序的更多信息,我建议您阅读Gavin M. Roy(https://www.manning.com/books/rabbitmq-in-depth)的RabbitMQ in Depth。
在查看各种体系结构之前,让我们简要总结每个 Web API 通常属于的四个主要使用范围:
为了履行其在各方之间实现数据交换的角色,Web API 需要定义一组清晰、明确的规则、约束和格式。本书介绍了四种最常用的Web API架构风格和消息协议:REST、RPC、SOAP 和 GraphQL。每种方法都有独特的特征、权衡和支持的数据交换格式,使其可用于不同的目的。
休息
具象状态传输 (REST) 是一种体系结构样式,专为基于网络的应用程序设计,这些应用程序使用标准 HTTP GET、POST、PATCH、PUT 和 DELETE 请求方法(最初在现已取代的 RFC 2616 https://www.w3.org/Protocols/rfc2616/rfc2616.xhtml 中定义)来访问和操作数据。更具体地说,GET 用于读取数据,POST 用于创建新资源或执行状态更改,PATCH 用于更新现有资源,PUT 用于用新资源替换现有资源(如果不存在则创建它),DELETE 用于永久擦除资源。这种办法不需要额外的公约来允许当事人进行通信,这使得它易于采用和快速实施。
然而,REST远不止于此。它的体系结构范例依赖于六个指导约束,如果正确实现,可以极大地使多个 Web API 属性受益:
实现所有这些约束的 Web API 可以描述为 RESTful。
介绍 Roy Fielding,REST 无可争议的父亲
Roy Fielding描述了六个REST约束,他是HTTP规范的主要作者之一,被广泛认为是REST之父,在他的论文Architectural Styles and the Design of Network-based Software Architectures中,他在2000年获得了计算机科学博士学位。菲尔丁论文的原始文本可在 http://mng.bz/19ln 的加利福尼亚大学出版物档案中找到。
广泛接受的规则和指南,HTTP协议经过验证的可靠性以及实现和开发的整体简单性使REST成为当今最流行的Web API架构。出于这个原因,我们将在本书中开发的大多数 Web API 项目都使用 REST 架构风格,并遵循 RESTful 方法以及本节中介绍的六个约束。
SOAP
简单对象访问协议 (SOAP) 是一种消息传递协议规范,用于通过可扩展标记语言 (XML) 跨网络交换结构化信息。大多数 SOAP Web 服务都是通过使用 HTTP 进行消息协商和传输来实现的,但也可以使用其他应用层协议,例如简单邮件传输协议 (SMTP)。
SOAP规范由Microsoft于1999年24月发布,但直到2003年3月1日才成为Web标准,当时它最终获得了W2C推荐状态(SOAP v3.12,https://www.w<>.org/TR/soap<>)。接受的规范定义了 SOAP 消息传递框架,该框架由以下内容组成:
SOAP 的主要优点是其可扩展性模型,它允许开发人员使用其他官方消息级标准(WS-Policy、WS-Security、WS-Federation 等)扩展基本协议,以执行特定任务,以及创建自己的扩展。生成的 Web 服务可以用 Web 服务描述语言 (WSDL) 进行记录,这是一种描述各种端点支持的操作和消息的 XML 文件。
但是,此方法也是一个缺陷,因为用于发出请求和接收响应的 XML 可能会变得极其复杂,并且难以阅读、理解和维护。多年来,许多开发框架和 IDE 都缓解了此问题,包括 ASP.NET 和 Visual Studio,它们开始提供快捷方式和抽象来简化开发体验并自动生成所需的 XML 代码。即便如此,与REST相比,该协议也有几个缺点:
由于所有这些原因,今天在REST与SOAP的辩论中,普遍的共识是,除非有特定的原因使用SOAP,否则REST Web API是首选的方式。这些原因可能包括硬件、软件或基础结构的限制,以及现有实现的要求,这就是为什么我们不会在本书中使用 SOAP 的原因。
GraphQL
2015年,随着GraphQL的公开发布,REST的至高无上地位受到质疑,GraphQL是Facebook开发的API的开源数据查询和操作语言。这两种方法之间的主要区别不在于体系结构样式,而在于它们发送和检索数据的不同方式。
事实上,GraphQL 遵循大多数 RESTful 约束,依赖于相同的应用层协议 (HTTP),并采用相同的数据格式 (JSON)。但是,它不是使用不同的端点来获取不同的数据对象,而是允许客户端执行动态查询并询问单个端点的具体数据要求。
我将尝试使用从棋盘游戏 Web API 场景中获取的实际示例来解释此概念。假设我们要检索对 Citadels 棋盘游戏给予积极反馈的所有用户的名称和唯一 ID。当我们处理典型的 REST API 时,完成这样的任务需要以下操作:
完整的请求/响应周期如图1.4所示。
图 1.4 REST 中的 HTTP 请求-响应周期
该计划是可行的,但不可否认的是,它涉及大量工作。具体来说,我们必须执行多次往返(三个HTTP请求)和大量的过度获取 - Citadels游戏信息,所有反馈的数据以及所有给予正面评价的用户的数据 - 以获得一些名字。唯一的解决方法是实现额外的 API 端点以返回我们需要的内容或简化一些中间工作。我们可以添加一个端点来获取给定棋盘游戏 ID 甚至给定棋盘游戏名称的正面反馈,包括基础实现中的全文查询。如果我们愿意,我们甚至可以实现一个专用的端点,例如 api/positiveFeedbacksByName,来执行整个任务。
然而,不可否认的是,这种方法会影响后端开发时间,并增加我们 API 的复杂性,并且不够通用,无法在类似的、不相同的情况下提供帮助。如果我们想检索负面反馈或多个游戏或给定作者创建的所有游戏的正面反馈怎么办?正如我们很容易理解的那样,克服这些问题可能并不简单,特别是当我们在客户端数据获取要求方面需要高水平的多功能性时。
现在让我们看看 GraphQL 会发生什么。当我们采用这种方法时,我们不是从现有(或附加)端点的角度来考虑,而是专注于我们需要发送到服务器的查询以请求我们需要的内容,就像我们对 DBMS 所做的那样。完成后,我们将该查询发送到(单个)GraphQL 端点,并在单个 HTTP 调用中精确地返回我们请求的数据,而无需获取任何我们不需要的字段。图 1.5 显示了 GraphQL 客户端到服务器的往返行程。
图 1.5 GraphQL 中的 HTTP 请求-响应周期
正如我们所看到的,性能优化不仅限于 Web API。GraphQL 方法不需要客户端迭代,因为服务器已经返回了我们正在寻找的精确结果。
GraphQL 规范可在 https://spec.graphql.org 上找到。我将在第 10 章中广泛讨论 GraphQL,向您展示如何将其与 REST 一起使用以实现特定的面向查询的目标。
现在我们已经了解了什么是 Web API,以及如何使用它们在网络中的 Web 应用程序和服务中交换数据,现在是时候介绍我们将在本书中创建它们的框架了。该框架 ASP.NET Core,这是一个高性能,跨平台,开源Web开发框架,由Microsoft于2016年推出,作为 ASP.NET 的继任者。
在以下各节中,我将简要介绍其最独特的方面:整体体系结构、请求/响应管道管理、异步编程模式、路由系统等。
注意在本书中,我们将使用 .NET 6.0 和 ASP.NET Core Runtime 6.0.11,这是本文撰写时最新的正式发布版本,也是继 .NET Core 1.0、1.1、2.0、2.1、2.2、3.0、3.1 和 .NET 5.0 之后发布的第九部分。每个版本都引入了一些更新、改进和其他功能,但为了简单起见,我将回顾最新版本附带的结果特征。此外,从 .NET 5 开始,所有偶数 .NET 版本(包括 .NET 6)都授予长期支持 (LTS) 状态,这意味着它们将在未来许多年内得到支持,而不是具有较短时间范围的奇数版本。目前,偶数版本的支持期为三年,奇数版本的支持期为 18 个月 (http://mng.bz/JVpa)。
我选择将 ASP.NET Core 用于我们的 Web API 项目,因为新的 Microsoft 框架强制执行了几个现代架构原则和最佳实践,使我们能够构建轻量级、高度模块化的应用程序,这些应用程序具有高水平的可测试性和源代码可维护性。这种方法自然会指导开发人员构建(或采用)由离散和可重用组件组成的应用程序,这些组件执行特定任务并通过框架提供的一系列接口进行通信。这些组件称为服务和中间件,它们在 Web 应用程序的 Program.cs 文件中注册和配置,该文件是应用的入口点。
注意如果您来自较旧的 ASP.NET Core 版本(如 3.1 或 5.0),您可能想知道 Startup 类(及其相应的 Startup.cs 文件)发生了什么变化,该文件用于包含服务和中间件配置设置。此类已从 .NET 6 引入的最小宿主模型中删除,该模型将程序类和启动类合并到单个 Program.cs 文件中。
服务业
服务是应用程序提供其功能所需的组件。我们可以将它们视为应用依赖项,因为我们的应用依赖于它们的可用性才能按预期工作。我在这里使用术语依赖关系是有原因的:ASP.NET Core 支持依赖注入 (DI) 软件设计模式,这是一种架构技术,允许我们在类与其依赖关系之间实现控制反转。以下是整个服务实现、注册/配置和注入过程在 ASP.NET Core 中的工作方式:
框架提供的服务接口的典型示例包括可用于实现基于策略的权限的 IAuthorizationService,以及可用于发送电子邮件的 IEmailService。
中间件
中间件是一组在 HTTP 级别运行的组件,可用于处理整个 HTTP 请求处理管道。如果您还记得 ASP.NET 在 ASP.NET Core 出现之前使用的 HttpModules 和 HttpHandler,您可以很容易地看到中间件如何扮演类似的角色并执行相同的任务。ASP.NET 核心 Web 应用程序中使用的中间件的典型示例包括 HttpsRedirectionMiddleware,它将非 HTTPS 请求重定向到 HTTPS URL,以及 AuthorizationMiddleware,它在内部使用授权服务来处理 HTTP 级别的所有授权任务。
每种类型的中间件都可以将 HTTP 请求传递给管道中的下一个组件或提供 HTTP 响应,从而缩短管道本身并阻止其他中间件处理请求。这种类型的请求阻止中间件称为终端中间件,通常负责处理各种应用端点的主要业务逻辑任务。
警告值得注意的是,中间件是按注册顺序(先进先出)处理的。始终在程序.cs文件中的非终端中间件之后添加终端中间件;否则,该文件将不起作用。
终端中间件的一个很好的例子是 StaticFileMiddleware,它最终处理指向静态文件的端点 URL,只要它们是可访问的。发生这种情况时,它会使用适当的 HTTP 响应将请求的文件发送给调用方,从而终止请求管道。我之前简要提到的 HttpsRedirectionMiddleware 也是终端中间件,因为它最终会响应所有非 HTTPS 请求的 HTTP-to-HTTPS 重定向。
注意StaticFileMiddleware 和 HttpsRedirectionMiddleware 只有在满足某些情况时才会终止 HTTP 请求。如果请求的终结点与其激活规则不匹配(例如,对于前者,指向不存在的静态文件的 URL,或者对于后者,指向已在 HTTPS 中),则会将其传递给管道中存在的下一个中间件,而无需执行任何操作。出于这个原因,我们可以说它们可能是终端中间件,以区别于总是在 HTTP 请求到达请求管道时结束请求管道的中间件。
现在我已经介绍了服务和中间件,我们终于可以看一下在应用程序开始时执行的 Program.cs 文件:
var builder=WebApplication.CreateBuilder(args); ?
// Add services to the container.
builder.Services.AddControllers(); ?
// Learn more about configuring Swagger/OpenAPI
// at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer(); ?
builder.Services.AddSwaggerGen(); ?
var app=builder.Build(); ?
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger(); ?
app.UseSwaggerUI(); ?
}
app.UseHttpsRedirection(); ?
app.UseAuthorization(); ?
app.MapControllers(); ?
app.Run(); ?
? 创建 WebApplicationBuilder 工厂类
? 注册和配置服务
? 构建 Web 应用程序对象
? 注册和配置非终端和潜在的终端中间件
? 注册和配置终端中间件
通过分析这段代码,我们可以很容易地看到该文件负责以下初始化任务:
更准确地说,Web 应用程序由 WebApplicationBuilder 工厂类实例化,从而创建一个 WebApplication 对象。此实例存储在应用局部变量中,用于注册和配置所需的服务和中间件。
注意服务和中间件都通过专用的扩展方法注册和配置,这是一种简化设置过程并保持程序.cs文件尽可能简洁的便捷方法。在 ASP.NET 核心命名约定下,服务主要通过使用带有 Add 前缀的扩展方法进行配置;中间件前缀为“使用”、“映射”和“运行”。至少目前,我们关心的唯一区别是 Run 委托始终是 100% 终端和最后一个要处理的。稍后,我们将在试验最小 API 时详细讨论这些约定及其含义。
为了简单起见,我将中间件分为两类:潜在终端和终端。在这一点上,区别应该很明显。潜在的终端中间件只有在与某些规则匹配时才结束HTTP请求管道,而终端中间件总是这样做(所以难怪它是最后一个)。
在 ASP.NET Core 中,控制器是一个类,用于对一组处理类似 HTTP 请求的操作方法(也称为操作)进行分组。从这个角度来看,我们可以说控制器是可用于聚合具有共同点的操作方法的容器:路由规则和前缀、服务、实例、授权要求、缓存策略、HTTP 级过滤器等。这些常见要求可以直接在控制器类上定义,从而避免了为每个操作指定这些要求的需要。此方法由一些强大的内置命名和编码约定强制执行,有助于保持代码干燥并简化应用的体系结构。
注意 DRY 是 Don't Repeat Yourself 的首字母缩写词,这是一个众所周知的软件开发原则,可帮助我们记住避免冗余、模式重复以及可能导致代码异味的任何其他内容。它与 WET 相反(将所有内容写入两次或每次写入)。
从 ASP.NET Core 开始,控制器可以从两个内置基类继承:
正如我们很容易理解的那样,控制器基类旨在用于采用模型-视图-控制器 (MVC) 模式的 Web 应用程序,其中它们旨在返回来自业务逻辑(通常由依赖注入的服务处理)的数据。在典型的 ASP.NET Core Web 应用程序中,生成的数据通过视图返回到客户端,这些视图使用客户端标记和脚本语言(如 HTML、CSS、JavaScript 等)处理应用程序的数据表示层(或服务器端呈现的语法,如 Razor)。在处理 Web API 时,我们通常不使用视图,因为我们希望直接从控制器返回 JSON 或 XML 数据(无 HTML)。在这种情况下,从 ControllerBase 基类继承是在特定情况下的推荐方法。下面是处理两种类型的 HTTP 请求的示例 Web API 控制器:
[ApiController] ?
[Route("api/[controller]")] ?
public class SampleController : ControllerBase
{
public SampleController()
{
}
[HttpGet] ?
public string Get()
{
return "TODO: return all items";
}
[HttpGet("{id}")] ?
public string Get(int id)
{
return $"TODO: return the item with id #{id}";
}
[HttpDelete("{id}")] ?
public string Delete(int id)
{
return $"TODO: delete the item with id #{id}";
}
}
? 添加特定于 API 的行为
? 默认路由规则
? 处理 HTTP GET 到 /api/Sample/ 的操作
? 处理 HTTP GET 到 /api/Sample/{id} 的操作
? 处理 HTTP DELETE 到 /api/Sample/{id} 的操作
正如我们通过查看此代码所看到的,通过利用一些有用的内置约定,我们能够用几行代码完成给定的任务:
注意此外,由于我们使用了 [ApiController] 属性,因此我们还需要一些特定于 Web API 的其他约定,这些约定会自动返回某些 HTTP 状态代码,具体取决于操作类型和结果。我们将在第6章中详细讨论它们,届时我们将深入研究错误处理。
具有内置约定的控制器是使用几行代码实现 Web API 业务逻辑的好方法。然而,在 ASP.NET Core 6中,该框架引入了一个最小的范式,允许我们以更少的仪式来构建Web API。这个特性被称为最小API,尽管它很年轻,但它得到了新的和经验丰富的 ASP.NET 开发人员的大量关注,因为它通常允许更小,更易读的代码库。
解释最小 API 的最好方法是在基于控制器的方法和块上的新孩子之间执行快速代码比较。以下是我们使用 SampleController 处理的相同 HTTP 请求如何由最小 API 处理,并对程序.cs文件进行一些小的更新:
// app.MapControllers(); ?
app.MapGet("/api/Sample", ?
()=> "TODO: return all items"); ?
app.MapGet("/api/Sample/{id}", ?
(int id)=> $"TODO: return the item with id #{id}"); ?
app.MapDelete("/api/Sample/{id}", ?
(int id)=> $"TODO: delete the item with id #{id}"); ?
? 删除此行(和 SampleController 类)
? 改为添加这些行
如我们所见,所有实现都传输到 Program.cs 文件中,而无需单独的控制器类。此外,代码可以进一步简化(或DRYfied),例如通过添加前缀变量,无需多次重复“/api/Sample”。在代码可读性、简单性和减少开销方面,其优势显而易见。
提示任何最小 API 方法的业务逻辑都可以放在程序.cs文件之外;只需要 Map 方法才能到达那里。事实上,将实际实现移动到其他类几乎总是很好的做法,除非我们处理的是单行代码。
最小 API 范例不太可能取代控制器,但它肯定会简化大多数小型 Web API 项目(如微服务)的编码体验,并吸引大多数寻求时尚方法和浅学习曲线的开发人员。出于所有这些原因,我们将在本书中经常将其与控制器一起使用。
影响 Web 应用程序的大多数性能问题都是由于有限数量的线程需要处理可能无限量的并发 HTTP 请求。在响应这些请求之前,这些线程必须执行一些资源密集型(通常是不可缓存的)任务,在最终收到结果之前被阻止时,尤其如此。典型的示例包括通过 SQL 查询从 DBMS 读取或写入数据、访问第三方服务(如外部网站或 Web API)以及执行任何其他需要大量执行时间的任务。
当 Web 应用程序被迫同时处理大量这些请求时,可用线程的数量会迅速减少,从而导致响应时间变慢,并最终导致服务不可用(HTTP 503 等)。
注意这种情况是大多数基于 HTTP 的拒绝服务 (DoS) 攻击的主要目标,在这种攻击中,Web 应用程序被请求淹没,使整个服务器不可用。线程阻塞、非缓存的 HTTP 请求是 DoS 攻击者的金矿,因为他们很有可能迅速耗尽线程池。
处理资源密集型任务的一般经验法则是积极缓存生成的 HTTP 响应,如 REST 约束 3(可缓存性)所述。然而,在某些情况下,缓存不是一种选择,例如当我们需要写入DBMS(HTTP POST)或读取可变或高度可定制的数据(带有大量参数的HTTP GET)时。在这种情况下,我们该怎么办?
ASP.NET Core 框架允许开发人员通过实现 C# 语言级异步模型(通常称为基于任务的异步模式 (TAP))来有效地处理此问题。了解TAP如何工作的最好方法是看到它的实际效果。请看一下以下源代码:
[HttpGet]
public async Task<string> Get() ?
{
return await Task.Run(()=> { ?
return "TODO: return all items";
});
}
? 异步
? 等待
如我们所见,我们对之前用于实现 TAP 的 SampleController 的第一个操作方法应用了一些小的更新。新模式依赖于使用 async 和 await 关键字以非阻塞方式处理任务:
值得注意的是,因为我们在 Get() 操作方法中使用了 await 关键字,所以我们也必须将该方法标记为异步。这完全没问题;这意味着该方法将由调用线程等待而不是阻塞它,这是我们想要的。
我们的最小实现在性能方面没有任何好处。我们绝对不需要等待像字面 TODO 字符串这样微不足道的事情。但是它应该让我们了解当示例 Task.Run 被需要服务器执行一些实际工作(例如从 DBMS 检索实际数据)的内容所取代时,异步/等待模式将为 Web 应用程序带来的好处。我将在第 4 章中广泛讨论异步数据检索任务,其中介绍了 Microsoft 最流行的 .NET 数据访问技术:实体框架核心。
*请认真填写需求信息,我们会在24小时内与您取得联系。