在 .NET 8 中轻松构建 REST API!
本文的目的是向您介绍一种使用 ASP.NET 8 而不是更常用的 MVC 控制器来构建 Web API 的替代方法,这种方法对开发人员更加友好。
我们将探索基于 .NET 6 引入的 Minimal API 构建的开源库FastEndpoints ,它能够让我们获得所有性能优势,同时避免 Minimal API 的痛点。与垂直切片架构 (Vertical Slice Architecture)结合使用,无论项目规模或复杂程度如何,代码库的导航更加清晰,项目维护也变得轻而易举。因为框架不会干扰您的工作,您可以专注于系统的工程和功能方面。
让我们动手为 Dev.to 的简化版本构建一个 REST API,让作者/发布者可以注册账户并发布文章。新文章将进入审核队列,网站管理员必须审核通过后才能公开发布。
我们的系统中的主要实体如下:
- 行政
- 作者
- 文章
该系统的功能/用户故事可分类如下:
- 行政
- 登录网站
- 获取需要审核的文章列表
- 批准文章并发布
- 拒绝文章并说明理由
- 作者
- 现场报名
- 登录网站
- 获取自己的文章列表
- 查看状态[待定/批准/拒绝]
- 创建新文章
- 编辑现有文章
- 公共区域
- 获取最新 50 篇文章列表
- 通过 id 获取文章
- 获取文章的最新 50 条评论
- 对文章发表评论
使用的技术栈如下:
- 基础框架: ASP.NET 8
- 端点框架: FastEndpoints
- 身份验证方案: JWT Bearer
- 输入验证: FluentValidations
- 数据存储: MongoDB
- API可视化: SwaggerUI
我们走吧...
创建一个新的 Web 项目并使用 Visual Studio 或在终端窗口中运行以下命令来安装依赖项:
dotnet new web -n MiniDevTo
dotnet add package FastEndpoints
dotnet add package FastEndpoints.Swagger
dotnet add package MongoDB.Entities
为我们的功能创建文件夹结构,使其如下所示:
树的最后一层将是一个端点,它可以是我们应用程序的 UI/前端可以调用的命令或查询。查询以 为前缀,Get
按照惯例表示它是数据检索,而命令以Save
、Approve
、Reject
等动词为前缀,表示提交某些状态更改。如果您以前遇到过这种情况,这可能听起来很熟悉CQRS
,但我们在这里不像 CQRS 那样区分读取和写入。相反,我们根据 来组织我们的功能/端点Vertical Slice Architecture
。
FastEndpoints 是REPR 模式的一个实现。我保证,这将是我在这篇文章中讨论的最后一个模式!
REPR 设计模式将 Web API 端点定义为包含三个组件:请求、端点和响应。它简化了常用的 MVC 模式,并更加专注于 API 开发。
因此,为了减轻创建端点所需的多个类文件的繁琐重复工作,请安装FastEndpoints 提供的Visual Studio或VS Code扩展。或者,您也可以手动创建这些文件。
程序.cs
首先...让我们更新Program.cs
文件使其如下所示:
global using FastEndpoints;
global using FluentValidation;
var builder = WebApplication.CreateBuilder();
builder.Services.AddFastEndpoints();
var app = builder.Build();
app.UseFastEndpoints();
app.Run();
这就是成为 Web API 项目所需要的全部内容。但是...如果您现在尝试运行该程序,您将遇到如下运行时异常:
InvalidOperationException: “FastEndpoints 无法找到任何端点声明!”
让我们通过使用 VS 扩展创建我们的第一个端点来解决这个问题。
作者注册端点
右键单击Author/Signup
Visual Studio 中的文件夹 > 添加 > 新建项。然后选择FastEndpoints Feature FileSet
位于Installed > Visual C#
节点下的新项目模板。然后对于文件名,输入Author.Signup.cs
如下所示:
它会在你选择的文件夹下创建一组新文件,其中包含特定于此端点的命名空间。打开Endpoint.cs
文件并查看顶部的命名空间。它就是我们之前输入的文件名。
当我们打开端点类时,继续用下面的代码替换它的内容:
namespace Author.Signup;
public class Endpoint : Endpoint<Request, Response, Mapper>
{
public override void Configure()
{
Post("/author/signup");
AllowAnonymous();
}
public override async Task HandleAsync(Request r, CancellationToken c)
{
await SendAsync(new Response()
{
//blank for now
});
}
}
这里我们得到了什么?我们有一个从通用基类继承的端点类定义Endpoint<TRequest, TResponse, TMapper>
。它有 2 个重写的方法Configure()
和HandleAsync()
。
在 configure 方法中,我们指定希望端点监听POST
路由上的http 动词/方法/author/signup
。我们还指定未经身份验证的用户应该被允许使用该AllowAnonymous()
方法访问此端点。
该HandleAsync()
方法用于编写处理传入请求的逻辑。目前,它只是发送一个空白响应,因为我们尚未向请求和响应 DTO 类添加任何字段/属性。
模型.cs
打开Models.cs
文件并用以下内容替换请求和响应类:
public class Request
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
}
public class Response
{
public string Message { get; set; }
}
Swagger 用户界面
现在,让我们设置 Swagger,这样我们就可以使用 Web 浏览器与端点进行交互,而不是使用 Postman 之类的工具。Program.cs
再次打开它,让它看起来像这样:
global using FastEndpoints;
using FastEndpoints.Swagger; //add this
var builder = WebApplication.CreateBuilder();
builder.Services.AddFastEndpoints();
builder.Services.SwaggerDocument() //add this
var app = builder.Build();
app.UseFastEndpoints();
app.UseSwaggerGen(); //add this
app.Run();
然后打开Properties/launchSettings.json
文件并将内容替换为:
{
"profiles": {
"MiniDevTo": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:8080",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
更新启动设置不是强制性的,但为了8080
本文的目的,让我们修复 API 服务器的监听端口。
接下来,在调试模式下构建并运行你的项目(在 Visual Studio 中按 CTRL+F5)。打开你的 Web 浏览器,访问 URLhttp://localhost:8080/swagger
即可查看 Swagger UI。
您现在应该看到类似这样的内容:
展开/author/signup
端点,并修改请求主体/json 使其如下所示(单击Try It Out
按钮即可执行此操作):
{
"FirstName": "Johnny",
"LastName": "Lawrence",
"Email": "what@is.uber",
"UserName": "EagleFang",
"Password": "death2kobra"
}
在执行请求之前,请前往Endpoint.cs
文件并在第 14 行设置断点。然后继续并点击 Swagger 中的“执行”按钮。断点触发后,检查该HandleAsync()
方法的请求 DTO 参数,您将看到类似以下内容:
这基本上就是你从客户端(在本例中是 Swagger UI)接收请求的方式。处理程序方法会从传入的 http 请求中获取一个完整填充的 POCO。有关此模型绑定工作原理的详细说明,请参阅此处的文档页面。
现在让我们从端点返回响应HandleAsync()
。停止调试并更新方法,如下所示:
public override async Task HandleAsync(Request r, CancellationToken c)
{
await SendAsync(new Response()
{
Message = $"hello {r.FirstName} {r.LastName}! your request has been received!"
});
}
再次启动应用程序并在 Swagger UI 中执行相同的请求。服务器的响应应该显示如下:
有多种方法可以将响应从处理程序发送回客户端。这里我们发送一个填充了自定义消息的响应 DTO 的新实例。
输入验证
打开Models.cs
文件并使验证器类如下所示:
public class Validator : Validator<Request>
{
public Validator()
{
RuleFor(x => x.FirstName)
.NotEmpty().WithMessage("your name is required!")
.MinimumLength(3).WithMessage("name is too short!")
.MaximumLength(25).WithMessage("name is too long!");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("email address is required!")
.EmailAddress().WithMessage("the format of your email address is wrong!");
RuleFor(x => x.UserName)
.NotEmpty().WithMessage("a username is required!")
.MinimumLength(3).WithMessage("username is too short!")
.MaximumLength(15).WithMessage("username is too long!");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("a password is required!")
.MinimumLength(10).WithMessage("password is too short!")
.MaximumLength(25).WithMessage("password is too long!");
}
}
这里我们使用Fluent Validation规则定义输入验证要求。让我们看看当用户输入不符合上述条件时会发生什么。在 Swagger 中执行相同的请求,并使用以下错误的 JSON 内容:
{
"LastName": "Lawrence",
"Email": "what is email?",
"UserName": "EagleFang",
"Password": "123"
}
服务器将会做出如下响应:
{
"StatusCode": 400,
"Message": "One or more errors occured!",
"Errors": {
"FirstName": [ "your name is required!" ],
"Email": [ "the format of your email address is wrong!" ],
"Password": ["password is too short!" ]
}
}
如您所见,如果传入的请求数据不符合验证标准,http 400
则会返回错误响应,其中包含错误的详细信息。如果传入请求中存在验证错误,处理程序逻辑将不会执行。如果需要,可以像这样更改此默认行为。
处理程序逻辑
让我们继续Author
通过修改处理程序逻辑将新实体持久保存到数据库中。
public override async Task HandleAsync(Request r, CancellationToken c)
{
var author = Map.ToEntity(r);
var emailIsTaken = await Data.EmailAddressIsTaken(author.Email);
if (emailIsTaken)
AddError(r => r.Email, "Sorry! Email address is already in use...");
var userNameIsTaken = await Data.UserNameIsTaken(author.UserName);
if (userNameIsTaken)
AddError(r => r.UserName, "Sorry! Ehat username is not available...");
ThrowIfAnyErrors();
await Data.CreateNewAuthor(author);
await SendAsync(new()
{
Message = "Thank you for signing up as an author!"
});
}
首先,我们使用端点类属性ToEntity()
上的方法Map
将请求 dto 转换为Author
域实体。映射的逻辑位于此处的Mapper.cs
文件中。您可以在此处阅读有关映射器类的更多信息。
然后,我们询问数据库此邮箱地址是否已被他人占用(代码见此处)。如果已被占用,我们将使用该方法将验证错误添加到端点的错误集合中AddError()
。
接下来,我们询问数据库该用户名是否已被他人使用,如果已被使用则添加错误。
所有业务规则检查完成后,如果之前的任何业务规则检查失败,我们希望向客户端发送错误响应。这就是 的作用ThrowIfAnyErrors()
。当用户名或电子邮件地址被获取时,将向客户端发送类似以下的响应。执行会在此停止,并且不会执行后续代码行。
{
"StatusCode": 400,
"Message": "One or more errors occured!",
"Errors": {
"Email": [ "sorry! email address is already in use." ],
"UserName": [ "sorry! that username is not available." ]
}
}
如果没有添加验证错误,并且作者创建成功,客户端将收到以下 json 响应。
{
"Message": "Thank you for signing up as an author!"
}
恭喜!
到目前为止,您已经坚持不懈,并拥有了第一个可以正常工作的端点。如果您有兴趣完成此练习,请前往 GitHub 查看完整源代码。现在应该已经不言自明了。如果您有任何不清楚的地方,请在此处评论或创建GitHub 问题。我会尽力在 24 小时内回复。此外,您还可以查看以下资源,它们将解释大部分代码。