了解如何使用最小 API 和 .NET 6 生成 Web API。
简介
生成 Web API 是一种常见任务。 希望能够提交一些数据,了解应用程序或服务如何使用它。 生成 API 的方式在不同的技术堆栈之间可能会有很大的差别。 了解在生成 API 的过程中,有许多需要处理的事项,例如数据存储、安全性、版本控制和文档。 处理所有这些事项可能需要进行复杂的操作。
什么是最小API?
生成 API 的过程可能很复杂,因为它需要支持多种功能,例如路由、在数据存储中进行读取和写入,以及身份验证。 为了节省时间,请从 .NET 框架着手,这些框架提供许多需要的功能。 但在启动并运行基本 API 之前,这些框架可能需要进行大量设置。 对于适用于 .NET 6 的最小 API,情况并非如此。 通过几行代码即可快速入门。
什么是最小API?
如果已开发 .NET Core Web API,则已使用了一种使用控制器的方法。 其思路是让控制器类方法(表示各种 HTTP 谓词)执行操作来完成特定任务。 例如,GetProducts()
将使用 GET 作为 HTTP 谓词来返回产品。
这个基于控制器的方法和最小 API 有何区别呢?
-
无 Startup.cs:生成最小 API 时,不需要 Startup.cs 文件。 而习惯执行的所有任务都发生在 Program.cs 中。 习惯执行的任务包括设置路由和配置依赖项注入、安全和 CORS。
-
顶级语句:由于最小 API 使用 .NET 6,因此可以使用顶级语句。
请注意 program 类、Main()
方法和 using
语句的使用。 使用顶级语句时,所有这些都将消失。
最小 API 使用此方法来减少需要键入的行数。 若要创建 API,只需使用以下几行代码:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
不存在 using
语句、Main()
方法或类。 仅有四行代码。
路由看起来稍有不同:与基于控制器的 Web API 相比,路由看起来略有不同。 在 Web API 中,对于路由,编写如下所示的代码:
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
// add my own routes
});
对于最小 API,直接在 app
实例上添加路由:
app.MapGet("/todos", await (TodoDb db) => db.Todos.ToListAsync());
app.MapPost("/todos", await (Todo todo) => {});
app.MapPut("/todos", (Todo todo) => {});
app.MapDelete("/todos/{id}", (int id) => {}});
同样的功能仍然存在。 仍可以按照与过去几乎相同的方式来配置数据库、设置 CORS 和添加身份验证。
使用最小API来创建API
你需要做的第一件事就是安装 .NET 6。 安装它以后,就可以创建最小 API 项目了。 在命令行中运行以下代码来创建最小 API 项目:
dotnet new web -o PizzaStore -f net6.0
新创建的 PizzaStore 文件夹包含 API 项目。
检查文件
你获取的文件非常类似于你通过基于控制器的 API 获得的文件:
bin/
obj/
appsettings.Development.json
appsettings.json
PizzaStore.csproj
Program.cs
查看 PizzaStore.csproj 里的内容时,你会看到一个如下所示的条目:
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
此代码告知使用的是 .NET 6。
Program.cs 保存你的 API。 接下来查看此文件的内容。
了解代码
Program.cs 包含 API 代码。 让我们详细了解一个程序示例:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
在前两行代码中,将创建一个生成器。 在 builder
中,将构造一个应用程序实例 app
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
此生成器具有 Services
属性。 通过使用 Services
属性,可以添加 CORS、实体框架或 Swagger 等功能。 下面是一个示例:
builder.Services.AddCors(options => {});
在 Services
属性中,告知 API 这是一项可用的功能。 与之相反,app
实例用于实际使用它。 因此,可以使用 app
实例来设置路由:
app.MapGet("/", () => "Hello World!");
还可以使用 app
实例来添加中间件。 下面是如何使用 CORS 等功能的示例:
app.UseCors("some unique string");
中间件通常是用于截获请求并执行检查(例如检查身份验证,或确保客户端获允根据 CORS 执行此操作)的代码。 在中间件执行完以后,将执行实际请求。在存储中读取或写入数据,然后将响应发送回调用客户端。
最后,app.Run()
启动 API,让其侦听来自客户端的请求。
若要运行代码,请使用 dotnet run
来启动你的项目,就像任何 .NET Core 项目一样。 默认情况下,这意味着在 http://localhost:{PORT} 上有一个运行的项目,其中 PORT
是介于 5000 和 5300 之间的值。
通过Swagger添加文档
文档是对于 API 所需要的内容。它是使文档与 API 在后者发生更改的情况下保持同步的关键。 好的方法是以标准化方式描述 API,确保它是自文档化的。 自文档化是指在代码发生更改时文档会随之更改。
Swagger 实现 Open API 规范。 此格式描述路由,以及它们接受和生成的数据。 Swagger UI 是工具的集合,可将 Open API 规范呈现为网站,并使可以通过网站与 API 交互。
若要在 API 中使用 Swagger 和 Swagger UI,需要执行以下两项操作:
安装包。 若要安装 Swagger,请指定安装名为 Swashbuckle 的包:
dotnet add package Swashbuckle.AspNetCore --version 6.1.4
配置它。 安装包后,通过代码对其进行配置。 添加几个不同的条目:
添加命名空间。 稍后调用 SwaggerDoc()
并提供 API 的标头信息时,需要此命名空间。
using Microsoft.OpenApi.Models;
添加 AddSwaggerGen()
。 此方法在 API 上设置标头信息,例如它调用的内容和版本号。
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Todo API", Description = "Keep track of your tasks", Version = "v1" });
});
添加 UseSwagger()
和 UseSwaggerUI()
。 这两个代码行会告知 API 项目使用 Swagger,并告知在何处查找规范文件 swagger.json。
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Todo API V1");
});
可以启动项目并转到 http://localhost:5000/swagger。
创建最小API
本模块使用 .NET CLI(命令行接口)和 Visual Studio Code 进行本地开发。
此模块使用 .NET 6.0 SDK。 通过在首选终端中运行以下命令,确保你已安装 .NET 6.0:
dotnet --list-sdks
将显示类似于下面的输出:
3.1.100 [C:\program files\dotnet\sdk]
5.0.100 [C:\program files\dotnet\sdk]
6.0.100 [C:\program files\dotnet\sdk]
确保列出了以 6
开头的版本。
创建项目的基架
首先,需要搭建项目的基架。
通过运行 dotnet new
创建 Web API:
dotnet new web -o PizzaStore -f net6.0
应该会看到 PizzaStore 目录。
通过调用 dotnet run
运行应用。 它生成应用,并将其托管在 5000 到 5300 之间的端口上。 HTTPS 为其选择了范围为 7000 到 7300 的端口。
如果要替代随机端口选择行为,可以设置要在 launchSettings.json 中使用的端口。
dotnet run
下面是输出在终端中的情况:
Building...
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7200
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5100
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: /<path>/PizzaStore
在浏览器中,转到指示的端口。 根据终端 http://localhost:{PORT},应会看到文本“Hello World!”
添加Swagger
使用 Swagger 来确保你有一个自文档化 API,其中的文档会随着代码的更改而更改。
安装 Swashbuckle 包:
dotnet add package Swashbuckle.AspNetCore --version 6.2.3
打开 PizzaStore.csproj 验证安装。 你应该有一个如下所示的条目:
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
接下来,将项目配置为使用 Swagger。
打开 Program.cs,修改以下代码:
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "PizzaStore API", Description = "Making the Pizzas you love", Version = "v1" });
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "PizzaStore API V1");
});
app.MapGet("/", () => "Hello World!");
app.Run();
重新运行项目并转到应用的地址 http://localhost:{PORT}/swagger。
了解如何添加路由和使用其他高级命令
现在,已详细了解了什么是最小 API 以及为何使用它。 但如何生成应用并处理诸如路由器参数、已发布的正文以及返回比文本字符串更高级的数据之类的事项呢?
若要支持所谓的 RESTful API,需要支持使用 HTTP 谓词并将这些谓词附加到不同的路由。 不同的谓词具有不同的含义。 请遵守 HTTP 谓词的含义。
HTTP 谓词 | 说明 |
---|---|
GET | 返回数据 |
POST | 发送创建资源的数据 |
PUT | 发送更新资源的数据 |
DELETE | 删除资源 |
最小API中的HTTP谓词
当客户端发起请求时,它会向服务器终结点发起请求。 假设向 GET http://localhost:3000/products
发出请求。 服务器验证请求,以查看使用了什么 HTTP 谓词。 服务器还需要知道请求的去向,这由“/products”指示。然后,服务器会尝试通过生成响应来解析请求。
最小 API 通过提供便捷方法来处理路由和 HTTP 谓词。 可在对 localhost:3000/products
的请求中通过 HTTP 谓词和路由(其中路由为 "/products")来映射请求。 下面是此类便利方法的示例:
app.MapGet("/products", () => data);
应按以下方式解读此代码:如果客户端对路由 "/products"
使用 GET HTTP 谓词,则使用 data
进行响应。
GET:提取资源
使用 GET 请求进行路由时,需了解两个主要情况:
仅路由:已经见过此路由。 例如:
app.MapGet("/products", () => data);
使用路由参数:路由参数用于查找特定资源。 如果 "/products" 表示所有产品的列表,则 "/products/1" 表示特定记录。 唯一标识符的值为“1”。若要处理此类请求,请使用通配符来匹配它。 在前面的示例中,我们使用“{ID}”来捕获“1”。 还可以将捕获的值映射到参数:
app.MapGet("/products/{id}", (int id) => data.SingleOrDefault(product => product.Id == id));
在此代码中,id
参数已捕获客户端发送的路由参数,即“/products/1”中的“1”,或“11”,在此请求中为“/products/1”。 然后,使用 id
来查找特定记录。
POST:创建资源
通常还需要创建资源。 若要进行创建,请使用 POST HTTP 谓词。 要使用的方法称为 MapPost()
:
app.MapPost("/products", (Product product) => /**/);
请注意如何将 product
发送到处理请求的 Lambda 中。 假设下面的 JSON 是客户端发送并已由框架序列化的正文。 假设客户端已发送以下 JSON 作为其正文:
{
"Name" : "New product",
"Description": "a description"
}
那么,此 JSON 就可以将这些字段映射到相同形状的对象实例。 下面是与所述的已发布正文匹配的 Product
类:
public record Product(int Id, string Name);
PUT:更新资源
谓词 PUT 是指更新资源。 因此,框架使用方法 MapPut()
。 MapPut()
方法在语义上类似于 MapPost()
。 其思路是,作为客户端,发送的已发布正文应带有包含更改的资源。 需要将这些更改应用于服务器的现有资源。 下面介绍如何使用 MapPut()
:
app.MapPut("/products", (Product product) => /* Update the data store using the `product` instance */);
DELETE:删除资源
若要支持 HTTP 谓词 DELETE,请使用 MapDelete()
。 这里的思路是让客户端通过独一无二的标识符进行发送,该标识符有助于服务器识别要删除的记录。 下面是此方法的典型用法:
app.MapDelete("/products/{id}", (int id) => /* Remove the record whose unique identifier matches `id` */);
返回响应
默认情况下,当使用某些类型进行响应时,框架会认识到这些类型应序列化为 JSON。 下面是一些用例:
app.MapGet("/products", () => products);
app.MapGet("/products/{id}", (int id) => products.SingleOrDefault(product => product.Id == id));
app.MapGet("/product", () => new { id = 1 });
对于这些用例,会收到类似于以下示例的 JSON 响应:
[{
"id": 1,
"name": "a product"
}, {
"id": 2,
"name": "another product"
}]
[{
"id": 1,
"name": "a product"
}]
{
"id": 1,
}
添加路由
添加数据
首先需要一些数据。 你将使用内存中存储来存储和管理数据。
创建文件 Db.cs 并在其中提供以下内容:
namespace PizzaStore.DB;
public record Pizza
{
public int Id {get; set;}
public string ? Name { get; set; }
}
public class PizzaDB
{
private static List<Pizza> _pizzas = new List<Pizza>()
{
new Pizza{ Id=1, Name="Montemagno, Pizza shaped like a great mountain" },
new Pizza{ Id=2, Name="The Galloway, Pizza shaped like a submarine, silent but deadly"},
new Pizza{ Id=3, Name="The Noring, Pizza shaped like a Viking helmet, where's the mead"}
};
public static List<Pizza> GetPizzas()
{
return _pizzas;
}
public static Pizza ? GetPizza(int id)
{
return _pizzas.SingleOrDefault(pizza => pizza.Id == id);
}
public static Pizza CreatePizza(Pizza pizza)
{
_pizzas.Add(pizza);
return pizza;
}
public static Pizza UpdatePizza(Pizza update)
{
_pizzas = _pizzas.Select(pizza =>
{
if (pizza.Id == update.Id)
{
pizza.Name = update.Name;
}
return pizza;
}).ToList();
return update;
}
public static void RemovePizza(int id)
{
_pizzas = _pizzas.FindAll(pizza => pizza.Id == id).ToList();
}
}
有了数据存储后,接下来让 API 使用它。
将数据连续到路由
将内存中存储连接到 API:
- 添加命名空间。 这种添加很简单,只需添加适当的
using
语句即可。 - 设置路由。 请确保添加创建、读取、更新和删除所需的所有路由映射。
- 在路由中调用它。 最后,需要根据每个路由调用内存中存储,并传入请求中的任何参数或正文(如果适用)。
现在,在 API 中连接数据。
在 Program.cs 文件顶部添加以下代码行:
using PizzaStore.DB;
在 app.Run()
前面添加以下代码:
app.MapGet("/pizzas/{id}", (int id) => PizzaDB.GetPizza(id));
app.MapGet("/pizzas", () => PizzaDB.GetPizzas());
app.MapPost("/pizzas", (Pizza pizza) => PizzaDB.CreatePizza(pizza));
app.MapPut("/pizzas", (Pizza pizza) => PizzaDB.UpdatePizza(pizza));
app.MapDelete("/pizzas/{id}", (int id) => PizzaDB.RemovePizza(id));
使用 dotnet run
运行应用:
dotnet run
在浏览器中转到 http://localhost:{PORT}/swagger。