实现航班信息查询端点及HTTP请求路由
1. 实现 GET /Flight/{FlightNumber} 端点
1.1 端点功能概述
GET /Flight/{FlightNumber} 端点的主要功能是根据传入的航班号返回单个航班的信息。为了实现这一功能,我们需要完成以下四个步骤:
1. 从路径参数中获取提供的航班号。
2. 调用服务层以请求该航班的信息。
3. 处理服务层可能抛出的任何异常。
4. 将正确的信息返回给调用者。
1.2 代码实现
首先,我们在
FlightController
类中创建一个新方法
GetFlightByFlightNumber
,并将航班号作为参数:
public async Task<IActionResult> GetFlightByFlightNumber(int flightNumber)
{
return StatusCode((int)HttpStatusCode.OK, "Hello from GetFlightByFlightNumber");
}
1.3 单元测试
为了遵循测试驱动开发的原则,我们添加一个单元测试
GetFlightByFlightNumber_Success
:
[TestMethod]
public async Task GetFlightByFlightNumber_Success()
{
Mock<FlightService> service = new Mock<FlightService>();
FlightController controller = new FlightController(service.Object);
await controller.GetFlightByFlightNumber(0);
}
当前状态下,该单元测试可以通过,因为它只是检查是否可以使用整数类型的输入参数调用
FlightController
类中的
GetFlightByFlightNumber
方法。
为了进一步实现该方法,我们需要在单元测试中添加预期行为:
public async Task GetFlightByFlightNumber_Success()
{
Mock<FlightService> service = new Mock<FlightService>();
FlightView returnedFlightView = new FlightView("0", ("Lagos", "LOS"), ("Marrakesh", "RAK"));
service.Setup(s => s.GetFlightByFlightNumber(0)).Returns(Task.FromResult(returnedFlightView));
FlightController controller = new FlightController(service.Object);
ObjectResult response = await controller.GetFlightByFlightNumber(0) as ObjectResult;
Assert.IsNotNull(response);
Assert.AreEqual((int)HttpStatusCode.OK, response.StatusCode);
FlightView content = response.Value as FlightView;
Assert.IsNotNull(content);
Assert.AreEqual(returnedFlightView, content);
}
此时,
GetFlightByFlightNumber_Success
单元测试无法通过,因为
FlightController.GetFlightByFlightNumber
方法调用返回的数据不正确。我们可以使用与
GetFlights
方法相同的
try-catch
模式来修复这个问题:
public async Task<IActionResult> GetFlightByFlightNumber(int flightNumber)
{
try
{
FlightView flight = await _service.GetFlightByFlightNumber(flightNumber);
return StatusCode((int)HttpStatusCode.OK, flight);
}
catch (FlightNotFoundException)
{
return StatusCode((int)HttpStatusCode.NotFound, "The flight was not found in the database");
}
catch (Exception)
{
return StatusCode((int)HttpStatusCode.InternalServerError, "An error occurred");
}
}
再次运行
GetFlightByFlightNumber_Success
单元测试,它应该可以通过。
1.4 添加失败情况的单元测试
我们还需要添加两个失败情况的单元测试,与
GetFlights
单元测试类似:
[TestMethod]
public async Task GetFlightByFlightNumber_Failure_FlightNotFoundException_404()
{
Mock<FlightService> service = new Mock<FlightService>();
service.Setup(s => s.GetFlightByFlightNumber(1)).Throws(new FlightNotFoundException());
FlightController controller = new FlightController(service.Object);
ObjectResult response = await controller.GetFlightByFlightNumber(1) as ObjectResult;
Assert.IsNotNull(response);
Assert.AreEqual((int)HttpStatusCode.NotFound, response.StatusCode);
Assert.AreEqual("The flight was not found in the database", response.Value);
}
[TestMethod]
public async Task GetFlightByFlightNumber_Failure_ArgumentException_500()
{
Mock<FlightService> service = new Mock<FlightService>();
service.Setup(s => s.GetFlightByFlightNumber(1)).Throws(new ArgumentException());
FlightController controller = new FlightController(service.Object);
ObjectResult response = await controller.GetFlightByFlightNumber(1) as ObjectResult;
Assert.IsNotNull(response);
Assert.AreEqual((int)HttpStatusCode.InternalServerError, response.StatusCode);
Assert.AreEqual("An error occurred", response.Value);
}
1.5 验证 OpenAPI 规范
通过查看 OpenAPI 规范,我们发现需要接受一个名为
flightNumber
的参数,并且有三种返回情况:200 状态码返回
FlightView
对象,404 状态码表示航班未找到,400 状态码表示提供的航班号无效。我们目前已经实现了其中两种情况,只需要将 500 内部错误改为 400 状态码(错误请求),并验证传入的航班号是否为有效数字(对于我们的目的,有效航班号是任何正整数)。
首先,修改单元测试:
[TestMethod]
[DataRow(-1)]
[DataRow(1)]
public async Task GetFlightByFlightNumber_Failure_ArgumentException_400(int flightNumber)
{
Mock<FlightService> service = new Mock<FlightService>();
service.Setup(s => s.GetFlightByFlightNumber(1)).Throws(new ArgumentException());
FlightController controller = new FlightController(service.Object);
ObjectResult response = await controller.GetFlightByFlightNumber(flightNumber) as ObjectResult;
Assert.IsNotNull(response);
Assert.AreEqual((int)HttpStatusCode.BadRequest, response.StatusCode);
Assert.AreEqual("Bad request", response.Value);
}
当然,这个单元测试现在无法通过,我们还需要修改
FlightController.GetFlightByFlightNumber
方法:
public async Task<IActionResult> GetFlightByFlightNumber(int flightNumber)
{
try
{
FlightView flight = await _service.GetFlightByFlightNumber(flightNumber);
return StatusCode((int)HttpStatusCode.OK, flight);
}
catch (FlightNotFoundException)
{
return StatusCode((int)HttpStatusCode.NotFound, "The flight was not found in the database");
}
catch (Exception)
{
return StatusCode((int)HttpStatusCode.BadRequest, "Bad request");
}
}
2. 路由 HTTP 请求到控制器和方法
2.1 路由的重要性
我们已经有了一系列的存储库、服务和控制器,它们都包含了许多强大的方法。但是,如何使用这些代码呢?与桌面应用程序不同,我们处理的是一个部署在某个环境中的 Web 服务(甚至可能是微服务)。我们通过 HTTP 请求来与服务器进行交互。目前,服务就像一堵砖墙,如果发送 HTTP 请求,它不知道如何处理。但是,如果引入“路由”的概念,情况就会改变。
2.2 路由的实现步骤
路由允许我们将 URL 映射到特定的控制器和端点。为了添加路由支持,我们需要完成以下两个步骤:
2.2.1 为控制器类添加 [Route] 属性
我们在
FlightController
类上添加
[Route]
属性:
[Route("{controller}")]
public class FlightController : Controller
{
…
}
[Route]
属性可以接受硬编码的路由或模板。这里我们使用了
"{controller}"
模板,当使用这个模板时,路由将解析为控制器类的名称,减去“controller”这个词。因此,在我们的例子中,类名为
FlightController
,路由为 “/Flight”。
2.2.2 定义方法特定的路由
我们使用一组映射到 HTTP 动作的属性来定义方法特定的路由:
-
[HttpGet]
-
[HttpPost]
-
[HttpPut]
-
[HttpDelete]
-
[HttpHead]
-
[HttpPatch]
这些属性可以直接使用,也可以提供额外的路由。例如,我们在
FlightController.GetFlights
方法上使用
[HttpGet]
属性:
[HttpGet]
public async Task<IActionResult> GetFlights()
{
…
}
这样,该方法的路由为 GET /Flight,符合 OpenAPI 规范。
2.3 测试端点
我们可以使用 cURL 命令行工具(包含在 Windows、macOS 和 Linux 中)或专门的 HTTP 工具(如 Postman)来测试端点。
2.3.1 启动服务
通常,我们在本地端口 8080 上启动服务。如果该端口有冲突,可以在
Startup.cs
中更改端口。例如,我们使用端口 8081:
> dotnet run
2.3.2 测试 GET /Flight 端点
使用 cURL 测试 GET /Flight 端点:
> curl -v http://localhost:8081/flight
如果服务正在运行,你将收到一个包含数据库中所有航班信息的响应。
2.3.3 测试 GET /Flight/{FlightNumber} 端点
对于 GET /Flight/{FlightNumber} 端点,我们可以在
[HttpGet]
属性中提供额外的路由指令:
[HttpGet("{flightNumber}")]
public async Task<IActionResult> GetFlightByFlightNumber(int flightNumber)
{
…
}
使用 cURL 测试航班号为 23 的航班信息:
> curl -v http://localhost:8081/flight/23
该端点将返回航班 23 的
FlightView
类的序列化版本。
2.4 路由流程图
graph LR
A[HTTP 请求] --> B{是否有路由设置}
B -- 否 --> C(请求未解决)
B -- 是 --> D(路由到端点)
D --> E(执行相应逻辑)
E --> F[返回响应]
2.5 总结
通过以上步骤,我们实现了 GET /Flight 和 GET /Flight/{FlightNumber} 端点,并添加了相应的单元测试。同时,我们还学习了如何将 HTTP 请求路由到控制器和方法,使得我们的服务可以接受外部系统的请求。
2.6 练习
以下是一些相关的练习题:
1. 判断题:在服务 - 存储库模式架构中,控制器是唯一应该接受来自外部系统的 HTTP 请求的层。( )
2. 一个典型的 HTTP 响应包含以下三个属性:
a) 发送者信息、路由信息、IP 目的地
b) 发送者名称、服务中使用的编程语言、原产国
c) 状态码、头部信息、主体
3. 对于路由 GET /Books/Fantasy,应该实现哪个控制器?
a) BookController
b) FantasyController
c) BookShopController
4. 判断题:中间件在任何端点方法逻辑之前执行。( )
5. 哪种类型的注入依赖允许我们在每次请求时都获得一个新的依赖实例,无论是否处理的是同一个 HTTP 请求?
a) 单例
b) 作用域
c) 瞬态
6. 哪种类型的注入依赖允许我们在每次请求时都使用同一个依赖实例,无论是否处理的是同一个 HTTP 请求?
a) 单例
b) 作用域
c) 瞬态
7. 哪种类型的注入依赖允许我们仅在 HTTP 请求的生命周期内使用同一个依赖实例?
a) 单例
b) 作用域
c) 瞬态
2.7 知识点总结
- 控制器层是服务 - 存储库模式架构中的外层,因为它可以接受 HTTP 请求并与外部系统通信。
- HTTP 请求总是包含头部信息(如 CORS、身份验证等),有时还包含主体(JSON 或 XML)。
- HTTP 响应总是包含头部信息、HTTP 状态码(如 200 OK、404 Not Found 等),有时还包含主体。
- “You ain’t gonna need it”(YAGNI)是一个干净代码原则,教导我们不要编写未来可能需要的代码,以免浪费时间开发很少会实现的潜在功能。
-
ASP.NET 的
IActionResult接口允许我们轻松地从方法中返回 HTTP 响应,使代码清晰简洁。 - “Coding to interfaces” 是一个干净代码原则,提倡使用通用构造而不是受限的具体类,以便遵循开闭原则并轻松扩展代码。
- 中间件是在处理控制器端点方法中的 HTTP 请求之前执行的任何代码,可用于执行身份验证检查、依赖注入和路由等操作。
- 在中间件中注入依赖时,有三种类型的注入依赖可供选择:单例、作用域和瞬态。单例依赖模仿单例设计模式,保证所有请求都操作同一个注入依赖的实例;作用域依赖在同一个请求中共享,但在多个请求之间不共享;瞬态依赖在每次构造函数请求时都会实例化一个新的依赖实例。
- 要将 HTTP 请求路由到端点,我们需要在中间件中设置路由,并为控制器类和方法添加路由属性,以便对路由进行精细控制。
- HttpAttribute 路由方法属性包含了最常见 HTTP 动作的方法属性,可以直接使用或提供额外的路由并使用路径参数。
3. JSON 序列化与反序列化及自定义模型绑定
3.1 相关功能概述
在开发过程中,JSON 数据的序列化和反序列化是非常重要的操作,同时自定义模型绑定也能让我们更灵活地处理数据。以下将详细介绍这些内容:
-
JSON 数据的序列化和反序列化
:这是在不同系统或组件之间传递数据时常用的操作,能将对象转换为 JSON 字符串(序列化),也能将 JSON 字符串转换为对象(反序列化)。
-
使用 [FromBody] 特性
:可以神奇地将 JSON 数据反序列化为方法参数。
-
实现自定义模型绑定
:通过实现
IModelBinder
接口,我们可以根据自己的需求来处理模型绑定。
-
动态生成 OpenAPI 规范
:能在运行时生成 API 文档,方便开发和测试。
3.2 使用 [FromBody] 特性
在控制器方法中,我们可以使用
[FromBody]
特性来自动将请求体中的 JSON 数据反序列化为对象。例如:
[HttpPost]
public async Task<IActionResult> CreateFlight([FromBody] FlightCreateModel flightModel)
{
// 处理创建航班的逻辑
return StatusCode((int)HttpStatusCode.Created, flightModel);
}
在这个例子中,当客户端发送一个包含 JSON 数据的 POST 请求时,
[FromBody]
会自动将 JSON 数据反序列化为
FlightCreateModel
对象。
3.3 实现自定义模型绑定
如果默认的模型绑定无法满足需求,我们可以实现自定义模型绑定。首先,创建一个实现
IModelBinder
接口的类:
public class CustomFlightModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
// 自定义模型绑定逻辑
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
var value = valueProviderResult.FirstValue;
// 解析值并创建模型对象
var flight = new Flight { FlightNumber = int.Parse(value) };
bindingContext.Result = ModelBindingResult.Success(flight);
return Task.CompletedTask;
}
}
然后,在控制器方法中使用自定义模型绑定器:
[HttpGet("{flightNumber}")]
public async Task<IActionResult> GetFlight([ModelBinder(typeof(CustomFlightModelBinder))] int flightNumber)
{
// 处理获取航班的逻辑
return StatusCode((int)HttpStatusCode.OK, flightNumber);
}
3.4 动态生成 OpenAPI 规范
动态生成 OpenAPI 规范可以帮助我们在运行时自动生成 API 文档。以下是一个简单的示例,展示如何配置并生成 OpenAPI 规范:
// 配置服务
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Flight API", Version = "v1" });
});
// 启用中间件
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Flight API V1");
});
配置完成后,访问
/swagger
路径,就可以看到自动生成的 API 文档。
3.5 总结
通过 JSON 序列化和反序列化、使用
[FromBody]
特性、实现自定义模型绑定以及动态生成 OpenAPI 规范,我们可以更高效地处理数据传输、模型绑定和 API 文档生成。这些技术的应用可以提高代码的可维护性和开发效率。
3.6 相关概念对比表格
| 概念 | 描述 | 应用场景 |
|---|---|---|
| 单例依赖 | 所有请求操作同一个注入依赖的实例 | 当某个对象需要全局唯一时使用,如配置信息管理类 |
| 作用域依赖 | 在同一个请求中共享,但在多个请求之间不共享 | 处理与请求相关的资源,如数据库上下文 |
| 瞬态依赖 | 每次请求时都实例化一个新的依赖实例 | 当每次使用都需要全新状态的对象时使用,如随机数生成器 |
3.7 开发流程总结流程图
graph LR
A[定义控制器和方法] --> B[添加路由属性]
B --> C[编写业务逻辑]
C --> D[添加单元测试]
D --> E[验证 OpenAPI 规范]
E --> F[处理 JSON 序列化和反序列化]
F --> G[考虑自定义模型绑定]
G --> H[生成 OpenAPI 规范]
H --> I[测试和部署]
3.8 最终总结
在整个开发过程中,我们从实现具体的端点(如 GET /Flight 和 GET /Flight/{FlightNumber})开始,学习了如何使用路由将 HTTP 请求映射到相应的控制器和方法。同时,我们掌握了单元测试的编写,确保代码的正确性和稳定性。在数据处理方面,我们了解了 JSON 数据的序列化和反序列化,以及如何使用
[FromBody]
特性和自定义模型绑定来处理复杂的数据场景。最后,通过动态生成 OpenAPI 规范,我们能够方便地生成 API 文档,提高开发和测试的效率。
这些技术和方法的综合应用,让我们能够构建出功能强大、易于维护和扩展的 Web 服务。在实际开发中,我们要始终遵循相关的规范和原则,如“YAGNI”和“Coding to interfaces”,以确保代码的质量和可维护性。同时,不断通过练习和实践来巩固所学知识,提升自己的开发能力。
超级会员免费看
34

被折叠的 条评论
为什么被折叠?



