教程:使用 ASP.NET Core 创建最小 API
注意
此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本。
作者:Rick Anderson 和 Tom Dykstra
构建最小 API,以创建具有最小依赖项的 HTTP API。 它们非常适合需要在 ASP.NET Core 中仅包括最少文件、功能和依赖项的微服务和应用。
本教程介绍使用 ASP.NET Core 生成最小 API 的基础知识。 在 ASP.NET Core 中创建 API 的另一种方法是使用控制器。 有关在最小 API 和基于控制器的 API 之间进行选择的帮助,请参阅 API 概述。 有关基于包含更多功能的控制器创建 API 项目的教程,请参阅创建 Web API。
概述
本教程将创建以下 API:
API | 描述 | 请求正文 | 响应正文 |
---|---|---|---|
GET /todoitems |
获取所有待办事项 | None | 待办事项的数组 |
GET /todoitems/complete |
获取已完成的待办事项 | None | 待办事项的数组 |
GET /todoitems/{id} |
按 ID 获取项 | None | 待办事项 |
POST /todoitems |
添加新项 | 待办事项 | 待办事项 |
PUT /todoitems/{id} |
更新现有项 | 待办事项 | None |
DELETE /todoitems/{id} |
删除项 | None | 无 |
先决条件
Visual Studio 2022 预览版与“ASP.NET 和 Web 开发”工作负载。
创建 API 项目
启动 Visual Studio 2022 并选择“创建新项目”。
在“创建新项目”对话框中:
- 在“搜索模板”搜索框中输入
Empty
。 - 选择“ASP.NET Core 空”模板,然后选择“下一步”。
- 在“搜索模板”搜索框中输入
将项目命名为 TodoApi,然后选择“下一步”。
在“其他信息”对话框中:
- 选择“.NET 9.0 (预览版)”
- 取消选中“不使用顶级语句”
- 选择“创建”
检查代码
Program.cs
文件包含以下代码:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
前面的代码:
- 创建具有预配置默认值的 WebApplicationBuilder 和 WebApplication。
- 创建返回
Hello World!
的 HTTP GET 终结点/
:
运行应用
按 Ctrl+F5 以在不使用调试程序的情况下运行。
Visual Studio 会显示以下对话框:
如果信任 IIS Express SSL 证书,请选择“是”。
将显示以下对话框:
如果你同意信任开发证书,请选择“是”。
有关信任 Firefox 浏览器的信息,请参阅 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 证书错误。
Visual Studio 启动 Kestrel Web 服务器,然后打开浏览器窗口。
Hello World!
将显示在浏览器中。 Program.cs
文件包含一个最小但完整的应用。
关闭浏览器窗口。
添加 NuGet 包
必须添加 NuGet 包以支持本教程中使用的数据库和诊断。
- 在“工具”菜单中,选择“NuGet 包管理器”>“管理解决方案的 NuGet 包”。
- 选择“浏览”选项卡。
- 选择“包括预发行版”。
- 在搜索框中输入“Microsoft.EntityFrameworkCore.InMemory”,然后选择
Microsoft.EntityFrameworkCore.InMemory
。 - 选中右窗格中的“项目”复选框,然后选择“安装” 。
- 按照上述说明添加
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
包。
模型和数据库上下文类
- 在项目文件夹中,创建名为
Todo.cs
的文件,包含以下代码:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
前面的代码为此应用创建模型。 模型是一个表示应用管理的数据的类。
- 使用以下代码创建名为
TodoDb.cs
的文件:
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
前面的代码定义了数据库上下文,它是为数据模型协调实体框架功能的主类。 此类从 Microsoft.EntityFrameworkCore.DbContext 类派生。
添加 API 代码
- 将
Program.cs
文件的内容替换为以下代码:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
以下突出显示的代码将数据库上下文添加到依赖关系注入 (DI) 容器,并且允许显示与数据库相关的异常:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
DI 容器提供对数据库上下文和其他服务的访问权限。
本教程使用终结点资源管理器和 .http 文件来测试 API。
测试发布数据
Program.cs
中的以下代码创建 HTTP POST 终结点 /todoitems
以将数据添加到内存中数据库:
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
运行应用。 浏览器显示 404 错误,因为不再存在 /
终结点。
POST 终结点将用于向应用添加数据。
选择“查看”>“其他 Windows”>“终结点资源管理器”。
右键单击“POST”终结点,然后选择“生成请求”。
在名为
TodoApi.http
的项目文件夹中创建一个新文件,其内容类似于以下示例:@TodoApi_HostAddress = https://localhost:7031 Post {{TodoApi_HostAddress}}/todoitems ###
- 第一行创建了一个变量,该变量适用于所有终结点。
- 下一行定义了 POST 请求。
- 三重井号标签 (
###
) 行是请求分隔符:对于不同的请求,该标签之后的内容属于另一个请求。
POST 请求需要标头和正文。 若要定义请求的这些部分,请紧随 POST 请求行之后添加以下行:
Content-Type: application/json { "name":"walk dog", "isComplete":true }
前面的代码会添加 Content-Type 标头和 JSON 请求正文。 TodoApi.http 文件现在应如以下示例所示,但带有端口号:
@TodoApi_HostAddress = https://localhost:7057 Post {{TodoApi_HostAddress}}/todoitems Content-Type: application/json { "name":"walk dog", "isComplete":true } ###
运行应用。
选择
POST
请求行上方的“发送请求”链接。POST 请求将发送到应用,响应将显示在“响应”窗格中。
检查 GET 终结点
示例应用通过调用 MapGet
实现多个 GET 终结点:
API | 描述 | 请求正文 | 响应正文 |
---|---|---|---|
GET /todoitems |
获取所有待办事项 | None | 待办事项的数组 |
GET /todoitems/complete |
获取所有已完成的待办事项 | None | 待办事项的数组 |
GET /todoitems/{id} |
按 ID 获取项 | None | 待办事项 |
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
测试 GET 终结点
通过从浏览器或使用终结点资源管理器调用 GET
终结点来测试应用。 以下步骤适用于终结点资源管理器。
在“终结点资源管理器”中,右键单击第一个 GET 终结点,然后选择“生成请求”。
将以下内容添加到
TodoApi.http
文件中:Get {{TodoApi_HostAddress}}/todoitems ###
选择新的
GET
请求行上方的“发送请求”链接。GET 请求将发送到应用,响应将显示在“响应”窗格中。
响应正文与以下 JSON 类似:
[ { "id": 1, "name": "walk dog", "isComplete": true } ]
在“终结点资源管理器”中,右键单击“
/todoitems/{id}
GET”终结点,然后选择“生成请求”。 将以下内容添加到TodoApi.http
文件中:GET {{TodoApi_HostAddress}}/todoitems/{id} ###
将
{id}
替换为1
。选择新的 GET 请求行上方的“发送请求”链接。
GET 请求将发送到应用,响应将显示在“响应”窗格中。
响应正文与以下 JSON 类似:
{ "id": 1, "name": "walk dog", "isComplete": true }
此应用使用内存中数据库。 如果已重新启动应用,GET 请求不会返回任何数据。 如果未返回任何数据,请将数据 POST 到应用,然后重新尝试 GET 请求。
返回值
ASP.NET Core 自动将对象序列化为 JSON,并将 JSON 写入响应消息的正文中。 此返回类型的响应代码为 200 OK(假设没有未处理的异常)。 未经处理的异常将转换为 5xx 错误。
返回类型可以表示大范围的 HTTP 状态代码。 例如,GET /todoitems/{id}
可以返回两个不同的状态值:
检查 PUT 终结点
示例应用使用 MapPut
实现单个 PUT 终结点:
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
此方法类似于 MapPost
方法,但它使用 HTTP PUT。 成功响应返回 204 (无内容)。 根据 HTTP 规范,PUT 请求需要客户端发送整个更新的实体,而不仅仅是更改。 若要支持部分更新,请使用 HTTP PATCH。
测试 PUT 终结点
本示例使用内存内、数据库,每次启动应用时都必须对其进行初始化。 在进行 PUT 调用之前,数据库中必须有一个项。 调用 GET,以确保在调用 PUT 之前数据库中存在项。
更新具有 Id = 1
的 to-do 项,并将其名称设置为 "feed fish"
。
在“终结点资源管理器”中,右键单击 PUT 终结点,然后选择“生成请求”。
将以下内容添加到
TodoApi.http
文件中:Put {{TodoApi_HostAddress}}/todoitems/{id} ###
在 PUT 请求行中,将
{id}
替换为1
。紧随 PUT 请求行之后添加以下行:
Content-Type: application/json { "name": "feed fish", "isComplete": false }
前面的代码会添加 Content-Type 标头和 JSON 请求正文。
选择新的 PUT 请求行上方的“发送请求”链接。
PUT 请求将发送到应用,响应将显示在“响应”窗格中。 响应正文为空,状态代码为 204。
检查并测试 DELETE 终结点
示例应用使用 MapDelete
实现单个 DELETE 终结点:
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
在“终结点资源管理器”中,右键单击 DELETE 终结点,然后选择“生成请求”。
将 DELETE 请求添加到
TodoApi.http
。将 DELETE 请求行中的
{id}
替换为1
。 DELETE 请求应如以下示例所示:DELETE {{TodoApi_HostAddress}}/todoitems/1 ###
选择 DELETE 请求的“发送请求”链接。
DELETE 请求将发送到应用,响应将显示在“响应”窗格中。 响应正文为空,状态代码为 204。
使用 MapGroup API
示例应用代码每次设置终结点时都会重复 todoitems
URL 前缀。 API 通常具有带常见 URL 前缀的终结点组,并且 MapGroup 方法可用于帮助组织此类组。 它减少了重复代码,并允许通过对 RequireAuthorization 和 WithMetadata 等方法的单一调用来自定义整个终结点组。
将 Program.cs
的内容替换为以下代码:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", async (TodoDb db) =>
await db.Todos.ToListAsync());
todoItems.MapGet("/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
前面的代码执行以下更改:
- 添加
var todoItems = app.MapGroup("/todoitems");
以使用 URL 前缀/todoitems
设置组。 - 将所有
app.Map<HttpVerb>
方法更改为todoItems.Map<HttpVerb>
。 - 从
Map<HttpVerb>
方法调用中移除 URL 前缀/todoitems
。
测试终结点以验证它们的运行方式是否相同。
使用 TypedResults API
返回 TypedResults(而不是 Results)有几个优点,包括可测试性和自动返回 OpenAPI 的响应类型元数据来描述终结点。 有关详细信息,请参阅 TypedResults 与 Results。
Map<HttpVerb>
方法可以调用路由处理程序方法,而不是使用 lambda。 若要查看示例,请使用以下代码更新 Program.cs:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
Map<HttpVerb>
代码现在调用方法,而不是 lambda:
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
这些方法返回实现 IResult 并由 TypedResults 定义的对象:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
单元测试可以调用这些方法并测试它们是否返回正确的类型。 例如,如果方法是 GetAllTodos
:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
单元测试代码可以验证是否从处理程序方法返回了 Ok<Todo[]> 类型的对象。 例如:
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
// Arrange
var db = CreateDbContext();
// Act
var result = await TodosApi.GetAllTodos(db);
// Assert: Check for the correct returned type
Assert.IsType<Ok<Todo[]>>(result);
}
防止过度发布
目前,示例应用公开了整个 Todo
对象。 生产应用程序中的生产应用是模型的一个子集,通常用于限制可输入和返回的数据。 这背后有多种原因,但安全性是主要原因。 模型的子集通常称为数据传输对象 (DTO)、输入模型或视图模型。 本文使用的是 DTO。
DTO 可以用于:
- 防止过度发布。
- 隐藏客户端不应查看的属性。
- 省略某些属性以减少有效负载大小。
- 平展包含嵌套对象的对象图。 对客户端而言,平展的对象图可能更方便。
若要演示 DTO 方法,请更新 Todo
类,使其包含机密字段:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
此应用需要隐藏机密字段,但管理应用可以选择公开它。
确保可以发布和获取机密字段。
使用以下代码创建名为 TodoItemDTO.cs
的文件:
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
将 Program.cs
文件的内容替换为以下代码以使用此 DTO 模型:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
RouteGroupBuilder todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db) {
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(new TodoItemDTO(todo))
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
todoItemDTO = new TodoItemDTO(todoItem);
return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}
static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
确保可以发布和获取除机密字段以外的所有字段。
用完成的示例进行故障排除
如果遇到无法解决的问题,请将你的代码与完成的项目进行比较。 查看或下载已完成的项目(如何下载)。
后续步骤
- 配置 JSON 序列化选项。
- 处理错误和异常:默认情况下,在开发环境中为最少 API 应用启用开发人员异常页。 有关如何处理错误和异常的信息,请参阅处理 ASP.NET Core API 中的错误。
- 有关测试最小 API 应用的示例,请参阅此 GitHub 示例。
- 最小 API 中的 OpenAPI 支持。
- 快速入门:发布到 Azure。
- 组织 ASP.NET Core 最小 API。
了解详细信息
请参阅《最小 API 快速参考》
构建最小 API,以创建具有最小依赖项的 HTTP API。 它们非常适合于需要在 ASP.NET Core 中仅包括最少文件、功能和依赖项的微服务和应用。
本教程介绍使用 ASP.NET Core 生成最小 API 的基础知识。 在 ASP.NET Core 中创建 API 的另一种方法是使用控制器。 有关在最小 API 和基于控制器的 API 之间进行选择的帮助,请参阅 API 概述。 有关基于包含更多功能的控制器创建 API 项目的教程,请参阅创建 Web API。
概述
本教程将创建以下 API:
API | 描述 | 请求正文 | 响应正文 |
---|---|---|---|
GET /todoitems |
获取所有待办事项 | None | 待办事项的数组 |
GET /todoitems/complete |
获取已完成的待办事项 | None | 待办事项的数组 |
GET /todoitems/{id} |
按 ID 获取项 | None | 待办事项 |
POST /todoitems |
添加新项 | 待办事项 | 待办事项 |
PUT /todoitems/{id} |
更新现有项 | 待办事项 | None |
DELETE /todoitems/{id} |
删除项 | None | 无 |
先决条件
带有 ASP.NET 和 Web 开发工作负载的 Visual Studio 2022。
创建 API 项目
启动 Visual Studio 2022 并选择“创建新项目”。
在“创建新项目”对话框中:
- 在“搜索模板”搜索框中输入
Empty
。 - 选择“ASP.NET Core 空”模板,然后选择“下一步”。
- 在“搜索模板”搜索框中输入
将项目命名为 TodoApi,然后选择“下一步”。
在“其他信息”对话框中:
- 选择“.NET 7.0”
- 取消选中“不使用顶级语句”
- 选择“创建”
检查代码
Program.cs
文件包含以下代码:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
前面的代码:
- 创建具有预配置默认值的 WebApplicationBuilder 和 WebApplication。
- 创建返回
Hello World!
的 HTTP GET 终结点/
:
运行应用
按 Ctrl+F5 以在不使用调试程序的情况下运行。
Visual Studio 会显示以下对话框:
如果信任 IIS Express SSL 证书,请选择“是”。
将显示以下对话框:
如果你同意信任开发证书,请选择“是”。
有关信任 Firefox 浏览器的信息,请参阅 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 证书错误。
Visual Studio 启动 Kestrel Web 服务器,然后打开浏览器窗口。
Hello World!
将显示在浏览器中。 Program.cs
文件包含一个最小但完整的应用。
添加 NuGet 包
必须添加 NuGet 包以支持本教程中使用的数据库和诊断。
- 在“工具”菜单中,选择“NuGet 包管理器”>“管理解决方案的 NuGet 包”。
- 选择“浏览”选项卡。
- 在搜索框中输入“Microsoft.EntityFrameworkCore.InMemory”,然后选择
Microsoft.EntityFrameworkCore.InMemory
。 - 在右侧窗格中选中“项目”复选框。
- 在“版本”下拉列表中选择可用的最新版本 7,例如
7.0.17
,然后选择“安装”。 - 按照上述说明添加包含可用最新版本 7 的
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
包。
模型和数据库上下文类
在项目文件夹中,创建名为 Todo.cs
的文件,包含以下代码:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
前面的代码为此应用创建模型。 模型是一个表示应用管理的数据的类。
使用以下代码创建名为 TodoDb.cs
的文件:
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
前面的代码定义了数据库上下文,它是为数据模型协调实体框架功能的主类。 此类从 Microsoft.EntityFrameworkCore.DbContext 类派生。
添加 API 代码
将 Program.cs
文件的内容替换为以下代码:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
以下突出显示的代码将数据库上下文添加到依赖关系注入 (DI) 容器,并且允许显示与数据库相关的异常:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
DI 容器提供对数据库上下文和其他服务的访问权限。
使用 Swagger 创建 API 测试 UI
可以从许多可用的 Web API 测试工具进行选择,并且可以使用自己的首选工具按照本教程的 API 测试步骤简介操作。
本教程利用 .NET 包 NSwag.AspNetCore,它集成了 Swagger 工具,可用于生成遵循 OpenAPI 规范的测试 UI:
- NSwag:将 Swagger 直接集成到 ASP.NET Core 应用程序中的 .NET 库,提供了中间件和配置。
- Swagger:一组开放源代码工具(如 OpenAPIGenerator 和 SwaggerUI),用于生成遵循 OpenAPI 规范的 API 测试页。
- OpenAPI 规范:基于控制器和模型中的 XML 和属性注释,描述 API 功能的文档。
有关将 OpenAPI 和 NSwag 与 ASP.NET 配合使用的详细信息,请参阅使用 Swagger / OpenAPI 的 ASP.NET Core Web API 文档。
安装 Swagger 工具
运行以下命令:
dotnet add package NSwag.AspNetCore
上一个命令添加了 NSwag.AspNetCore 包,其中包含用于生成 Swagger 文档和 UI 的工具。
配置 Swagger 中间件
在
var app = builder.Build();
行中定义app
之前,添加以下突出显示的代码using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList")); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddOpenApiDocument(config => { config.DocumentName = "TodoAPI"; config.Title = "TodoAPI v1"; config.Version = "v1"; }); var app = builder.Build();
在前面的代码中:
builder.Services.AddEndpointsApiExplorer();
:启用 API 资源管理器,该服务提供有关 HTTP API 的元数据。 该 API 资源管理器由 Swagger 用于生成 Swagger 文档。builder.Services.AddOpenApiDocument(config => {...});
:将 Swagger OpenAPI 文档生成器添加到应用程序服务,并配置它以提供有关 API 的详细信息,例如其标题和版本。 有关提供更可靠的 API 详细信息的信息,请参阅 NSwag 和 ASP.NET Core 入门在
var app = builder.Build();
行中定义app
后,将以下突出显示的代码添加到下一行var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseOpenApi(); app.UseSwaggerUi(config => { config.DocumentTitle = "TodoAPI"; config.Path = "/swagger"; config.DocumentPath = "/swagger/{documentName}/swagger.json"; config.DocExpansion = "list"; }); }
通过前面的代码,Swagger 中间件可以为生成的 JSON 文档和 Swagger UI 提供服务。 Swagger 仅在开发环境中启用。 在生产环境中启用 Swagger 可能会公开有关 API 结构和实现的潜在敏感详细信息。
测试发布数据
Program.cs
中的以下代码创建 HTTP POST 终结点 /todoitems
以将数据添加到内存中数据库:
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
运行应用。 浏览器显示 404 错误,因为不再存在 /
终结点。
POST 终结点将用于向应用添加数据。
当应用仍在运行时,在浏览器中导航到
https://localhost:<port>/swagger
以显示 Swagger 生成的 API 测试页。在 Swagger API 测试页上,选择“Post /todoitems”>“试用”。
请注意,“请求正文”字段包含了反映 API 参数的生成的示例格式。
在请求正文中,输入待办项的 JSON,无需指定可选的
id
:{ "name":"walk dog", "isComplete":true }
选择“执行”。
Swagger 在“执行”按钮下方提供le “响应”窗格。
请注意一些有用的详细信息:
- cURL:Swagger 在 Unix/Linux 语法中提供了一个示例 cURL 命令,可以通过任何使用 Unix/Linux 语法的 bash shell(包括 Git for Windows 中的 Git Bash)在命令行上运行。
- 请求 URL:Swagger UI 针对 API 调用的 JavaScript 代码发出的 HTTP 请求的简化表示形式。 实际请求可以包括标头和查询参数以及请求正文等详细信息。
- 服务器响应:包括响应正文和标头。 响应正文显示
id
已设置为1
。 - 响应代码:返回了 201
HTTP
状态代码,指示请求已成功处理并导致创建新资源。
检查 GET 终结点
示例应用通过调用 MapGet
实现多个 GET 终结点:
API | 描述 | 请求正文 | 响应正文 |
---|---|---|---|
GET /todoitems |
获取所有待办事项 | None | 待办事项的数组 |
GET /todoitems/complete |
获取所有已完成的待办事项 | None | 待办事项的数组 |
GET /todoitems/{id} |
按 ID 获取项 | None | 待办事项 |
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
测试 GET 终结点
通过从浏览器或 Swagger 调用终结点来测试应用。
在 Swagger 中,选择“GET /todoitems”>“试用”>“执行”。
或者,通过输入 URI
http://localhost:<port>/todoitems
从浏览器调用 GET /todoitems。 例如:http://localhost:5001/todoitems
对 GET /todoitems
的调用生成如下响应:
[
{
"id": 1,
"name": "walk dog",
"isComplete": true
}
]
在 Swagger 中调用 GET /todoitems/{id} 以从特定 ID 返回数据:
- 选择“GET /todoitems”>“试用”。
- 将“ID”字段设置为
1
并选择“执行”。
或者,通过输入 URI
https://localhost:<port>/todoitems/1
从浏览器调用 GET /todoitems。 例如:https://localhost:5001/todoitems/1
响应类似于以下内容:
{ "id": 1, "name": "walk dog", "isComplete": true }
此应用使用内存中数据库。 如果已重新启动应用,GET 请求不会返回任何数据。 如果未返回任何数据,请将数据 POST 到应用,然后重新尝试 GET 请求。
返回值
ASP.NET Core 自动将对象序列化为 JSON,并将 JSON 写入响应消息的正文中。 此返回类型的响应代码为 200 OK(假设没有未处理的异常)。 未经处理的异常将转换为 5xx 错误。
返回类型可以表示大范围的 HTTP 状态代码。 例如,GET /todoitems/{id}
可以返回两个不同的状态值:
检查 PUT 终结点
示例应用使用 MapPut
实现单个 PUT 终结点:
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
此方法类似于 MapPost
方法,但它使用 HTTP PUT。 成功响应返回 204 (无内容)。 根据 HTTP 规范,PUT 请求需要客户端发送整个更新的实体,而不仅仅是更改。 若要支持部分更新,请使用 HTTP PATCH。
测试 PUT 终结点
本示例使用内存内、数据库,每次启动应用时都必须对其进行初始化。 在进行 PUT 调用之前,数据库中必须有一个项。 调用 GET,以确保在调用 PUT 之前数据库中存在项。
更新具有 Id = 1
的 to-do 项,并将其名称设置为 "feed fish"
。
使用 Swagger 发送 PUT 请求:
选择“Put /todoitems/{id}”>“试用”。
将“ID”字段设置为
1
。将请求正文设置为以下 JSON:
{ "name": "feed fish", "isComplete": false }
选择“执行”。
检查并测试 DELETE 终结点
示例应用使用 MapDelete
实现单个 DELETE 终结点:
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
使用 Swagger 发送 DELETE 请求:
选择“DELETE /todoitems/{id}”>“试用”。
将“ID”字段设置为
1
并选择“执行”。DELETE 请求将发送到应用,然后响应将会显示在“响应”窗格中。 响应正文为空,并且服务器响应状态代码为 204。
使用 MapGroup API
示例应用代码每次设置终结点时都会重复 todoitems
URL 前缀。 API 通常具有带常见 URL 前缀的终结点组,并且 MapGroup 方法可用于帮助组织此类组。 它减少了重复代码,并允许通过对 RequireAuthorization 和 WithMetadata 等方法的单一调用来自定义整个终结点组。
将 Program.cs
的内容替换为以下代码:
using NSwag.AspNetCore;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{
config.DocumentName = "TodoAPI";
config.Title = "TodoAPI v1";
config.Version = "v1";
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseOpenApi();
app.UseSwaggerUi(config =>
{
config.DocumentTitle = "TodoAPI";
config.Path = "/swagger";
config.DocumentPath = "/swagger/{documentName}/swagger.json";
config.DocExpansion = "list";
});
}
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", async (TodoDb db) =>
await db.Todos.ToListAsync());
todoItems.MapGet("/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
前面的代码执行以下更改:
- 添加
var todoItems = app.MapGroup("/todoitems");
以使用 URL 前缀/todoitems
设置组。 - 将所有
app.Map<HttpVerb>
方法更改为todoItems.Map<HttpVerb>
。 - 从
Map<HttpVerb>
方法调用中移除 URL 前缀/todoitems
。
测试终结点以验证它们的运行方式是否相同。
使用 TypedResults API
返回 TypedResults(而不是 Results)有几个优点,包括可测试性和自动返回 OpenAPI 的响应类型元数据来描述终结点。 有关详细信息,请参阅 TypedResults 与 Results。
Map<HttpVerb>
方法可以调用路由处理程序方法,而不是使用 lambda。 若要查看示例,请使用以下代码更新 Program.cs:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
Map<HttpVerb>
代码现在调用方法,而不是 lambda:
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
这些方法返回实现 IResult 并由 TypedResults 定义的对象:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
单元测试可以调用这些方法并测试它们是否返回正确的类型。 例如,如果方法是 GetAllTodos
:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
单元测试代码可以验证是否从处理程序方法返回了 Ok<Todo[]> 类型的对象。 例如:
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
// Arrange
var db = CreateDbContext();
// Act
var result = await TodosApi.GetAllTodos(db);
// Assert: Check for the correct returned type
Assert.IsType<Ok<Todo[]>>(result);
}
防止过度发布
目前,示例应用公开了整个 Todo
对象。 生产应用程序中的生产应用是模型的一个子集,通常用于限制可输入和返回的数据。 这背后有多种原因,但安全性是主要原因。 模型的子集通常称为数据传输对象 (DTO)、输入模型或视图模型。 本文使用的是 DTO。
DTO 可以用于:
- 防止过度发布。
- 隐藏客户端不应查看的属性。
- 省略某些属性以减少有效负载大小。
- 平展包含嵌套对象的对象图。 对客户端而言,平展的对象图可能更方便。
若要演示 DTO 方法,请更新 Todo
类,使其包含机密字段:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
此应用需要隐藏机密字段,但管理应用可以选择公开它。
确保可以发布和获取机密字段。
使用以下代码创建名为 TodoItemDTO.cs
的文件:
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
将 Program.cs
文件的内容替换为以下代码以使用此 DTO 模型:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
app.MapPost("/todoitems", async (TodoItemDTO todoItemDTO, TodoDb db) =>
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/todoitems/{id}", async (int id, TodoItemDTO todoItemDTO, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
确保可以发布和获取除机密字段以外的所有字段。
用完成的示例进行故障排除
如果遇到无法解决的问题,请将你的代码与完成的项目进行比较。 查看或下载已完成的项目(如何下载)。
后续步骤
- 配置 JSON 序列化选项。
- 处理错误和异常:默认情况下,在开发环境中为最少 API 应用启用开发人员异常页。 有关如何处理错误和异常的信息,请参阅处理 ASP.NET Core API 中的错误。
- 有关测试最小 API 应用的示例,请参阅此 GitHub 示例。
- 最小 API 中的 OpenAPI 支持。
- 快速入门:发布到 Azure。
- 组织 ASP.NET Core 最小 API。
了解详细信息
请参阅《最小 API 快速参考》
构建最小 API,以创建具有最小依赖项的 HTTP API。 它们非常适合于需要在 ASP.NET Core 中仅包括最少文件、功能和依赖项的微服务和应用。
本教程介绍使用 ASP.NET Core 生成最小 API 的基础知识。 在 ASP.NET Core 中创建 API 的另一种方法是使用控制器。 有关在最小 API 和基于控制器的 API 之间进行选择的帮助,请参阅 API 概述。 有关基于包含更多功能的控制器创建 API 项目的教程,请参阅创建 Web API。
概述
本教程将创建以下 API:
API | 描述 | 请求正文 | 响应正文 |
---|---|---|---|
GET /todoitems |
获取所有待办事项 | None | 待办事项的数组 |
GET /todoitems/complete |
获取已完成的待办事项 | None | 待办事项的数组 |
GET /todoitems/{id} |
按 ID 获取项 | None | 待办事项 |
POST /todoitems |
添加新项 | 待办事项 | 待办事项 |
PUT /todoitems/{id} |
更新现有项 | 待办事项 | None |
DELETE /todoitems/{id} |
删除项 | None | 无 |
先决条件
- 带有 ASP.NET 和 Web 开发工作负载的 Visual Studio 2022。
- .NET 6.0 SDK
创建 API 项目
启动 Visual Studio 2022 并选择“创建新项目”。
在“创建新项目”对话框中:
- 在“搜索模板”搜索框中输入
Empty
。 - 选择“ASP.NET Core 空”模板,然后选择“下一步”。
- 在“搜索模板”搜索框中输入
将项目命名为 TodoApi,然后选择“下一步”。
在“其他信息”对话框中:
- 选择“.NET 6.0”
- 取消选中“不使用顶级语句”
- 选择“创建”
检查代码
Program.cs
文件包含以下代码:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
前面的代码:
- 创建具有预配置默认值的 WebApplicationBuilder 和 WebApplication。
- 创建返回
Hello World!
的 HTTP GET 终结点/
:
运行应用
按 Ctrl+F5 以在不使用调试程序的情况下运行。
Visual Studio 会显示以下对话框:
如果信任 IIS Express SSL 证书,请选择“是”。
将显示以下对话框:
如果你同意信任开发证书,请选择“是”。
有关信任 Firefox 浏览器的信息,请参阅 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 证书错误。
Visual Studio 启动 Kestrel Web 服务器,然后打开浏览器窗口。
Hello World!
将显示在浏览器中。 Program.cs
文件包含一个最小但完整的应用。
添加 NuGet 包
必须添加 NuGet 包以支持本教程中使用的数据库和诊断。
- 在“工具”菜单中,选择“NuGet 包管理器”>“管理解决方案的 NuGet 包”。
- 选择“浏览”选项卡。
- 在搜索框中输入“Microsoft.EntityFrameworkCore.InMemory”,然后选择
Microsoft.EntityFrameworkCore.InMemory
。 - 在右侧窗格中选中“项目”复选框。
- 在“版本”下拉列表中选择可用的最新版本 7,例如
6.0.28
,然后选择“安装”。 - 按照上述说明添加包含可用最新版本 7 的
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
包。
模型和数据库上下文类
在项目文件夹中,创建名为 Todo.cs
的文件,包含以下代码:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
前面的代码为此应用创建模型。 模型是一个表示应用管理的数据的类。
使用以下代码创建名为 TodoDb.cs
的文件:
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
前面的代码定义了数据库上下文,它是为数据模型协调实体框架功能的主类。 此类从 Microsoft.EntityFrameworkCore.DbContext 类派生。
添加 API 代码
将 Program.cs
文件的内容替换为以下代码:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
以下突出显示的代码将数据库上下文添加到依赖关系注入 (DI) 容器,并且允许显示与数据库相关的异常:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
DI 容器提供对数据库上下文和其他服务的访问权限。
使用 Swagger 创建 API 测试 UI
可以从许多可用的 Web API 测试工具进行选择,并且可以使用自己的首选工具按照本教程的 API 测试步骤简介操作。
本教程利用 .NET 包 NSwag.AspNetCore,它集成了 Swagger 工具,可用于生成遵循 OpenAPI 规范的测试 UI:
- NSwag:将 Swagger 直接集成到 ASP.NET Core 应用程序中的 .NET 库,提供了中间件和配置。
- Swagger:一组开放源代码工具(如 OpenAPIGenerator 和 SwaggerUI),用于生成遵循 OpenAPI 规范的 API 测试页。
- OpenAPI 规范:基于控制器和模型中的 XML 和属性注释,描述 API 功能的文档。
有关将 OpenAPI 和 NSwag 与 ASP.NET 配合使用的详细信息,请参阅使用 Swagger / OpenAPI 的 ASP.NET Core Web API 文档。
安装 Swagger 工具
运行以下命令:
dotnet add package NSwag.AspNetCore
上一个命令添加了 NSwag.AspNetCore 包,其中包含用于生成 Swagger 文档和 UI 的工具。
配置 Swagger 中间件
在 Program.cs 顶部添加以下
using
语句:using NSwag.AspNetCore;
在
var app = builder.Build();
行中定义app
之前,添加以下突出显示的代码using NSwag.AspNetCore; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList")); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddOpenApiDocument(config => { config.DocumentName = "TodoAPI"; config.Title = "TodoAPI v1"; config.Version = "v1"; }); var app = builder.Build();
在前面的代码中:
builder.Services.AddEndpointsApiExplorer();
:启用 API 资源管理器,该服务提供有关 HTTP API 的元数据。 该 API 资源管理器由 Swagger 用于生成 Swagger 文档。builder.Services.AddOpenApiDocument(config => {...});
:将 Swagger OpenAPI 文档生成器添加到应用程序服务,并配置它以提供有关 API 的详细信息,例如其标题和版本。 有关提供更可靠的 API 详细信息的信息,请参阅 NSwag 和 ASP.NET Core 入门在
var app = builder.Build();
行中定义app
后,将以下突出显示的代码添加到下一行var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseOpenApi(); app.UseSwaggerUi(config => { config.DocumentTitle = "TodoAPI"; config.Path = "/swagger"; config.DocumentPath = "/swagger/{documentName}/swagger.json"; config.DocExpansion = "list"; }); }
通过前面的代码,Swagger 中间件可以为生成的 JSON 文档和 Swagger UI 提供服务。 Swagger 仅在开发环境中启用。 在生产环境中启用 Swagger 可能会公开有关 API 结构和实现的潜在敏感详细信息。
测试发布数据
Program.cs
中的以下代码创建 HTTP POST 终结点 /todoitems
以将数据添加到内存中数据库:
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
运行应用。 浏览器显示 404 错误,因为不再存在 /
终结点。
POST 终结点将用于向应用添加数据。
当应用仍在运行时,在浏览器中导航到
https://localhost:<port>/swagger
以显示 Swagger 生成的 API 测试页。在 Swagger API 测试页上,选择“Post /todoitems”>“试用”。
请注意,“请求正文”字段包含了反映 API 参数的生成的示例格式。
在请求正文中,输入待办项的 JSON,无需指定可选的
id
:{ "name":"walk dog", "isComplete":true }
选择“执行”。
Swagger 在“执行”按钮下方提供le “响应”窗格。
请注意一些有用的详细信息:
- cURL:Swagger 在 Unix/Linux 语法中提供了一个示例 cURL 命令,可以通过任何使用 Unix/Linux 语法的 bash shell(包括 Git for Windows 中的 Git Bash)在命令行上运行。
- 请求 URL:Swagger UI 针对 API 调用的 JavaScript 代码发出的 HTTP 请求的简化表示形式。 实际请求可以包括标头和查询参数以及请求正文等详细信息。
- 服务器响应:包括响应正文和标头。 响应正文显示
id
已设置为1
。 - 响应代码:返回了 201
HTTP
状态代码,指示请求已成功处理并导致创建新资源。
检查 GET 终结点
示例应用通过调用 MapGet
实现多个 GET 终结点:
API | 描述 | 请求正文 | 响应正文 |
---|---|---|---|
GET /todoitems |
获取所有待办事项 | None | 待办事项的数组 |
GET /todoitems/complete |
获取所有已完成的待办事项 | None | 待办事项的数组 |
GET /todoitems/{id} |
按 ID 获取项 | None | 待办事项 |
app.MapGet("/", () => "Hello World!");
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
测试 GET 终结点
通过从浏览器或 Swagger 调用终结点来测试应用。
在 Swagger 中,选择“GET /todoitems”>“试用”>“执行”。
或者,通过输入 URI
http://localhost:<port>/todoitems
从浏览器调用 GET /todoitems。 例如:http://localhost:5001/todoitems
对 GET /todoitems
的调用生成如下响应:
[
{
"id": 1,
"name": "walk dog",
"isComplete": true
}
]
在 Swagger 中调用 GET /todoitems/{id} 以从特定 ID 返回数据:
- 选择“GET /todoitems”>“试用”。
- 将“ID”字段设置为
1
并选择“执行”。
或者,通过输入 URI
https://localhost:<port>/todoitems/1
从浏览器调用 GET /todoitems。 例如,例如https://localhost:5001/todoitems/1
响应类似于以下内容:
{ "id": 1, "name": "walk dog", "isComplete": true }
此应用使用内存中数据库。 如果已重新启动应用,GET 请求不会返回任何数据。 如果未返回任何数据,请将数据 POST 到应用,然后重新尝试 GET 请求。
返回值
ASP.NET Core 自动将对象序列化为 JSON,并将 JSON 写入响应消息的正文中。 此返回类型的响应代码为 200 OK(假设没有未处理的异常)。 未经处理的异常将转换为 5xx 错误。
返回类型可以表示大范围的 HTTP 状态代码。 例如,GET /todoitems/{id}
可以返回两个不同的状态值:
检查 PUT 终结点
示例应用使用 MapPut
实现单个 PUT 终结点:
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
此方法类似于 MapPost
方法,但它使用 HTTP PUT。 成功响应返回 204 (无内容)。 根据 HTTP 规范,PUT 请求需要客户端发送整个更新的实体,而不仅仅是更改。 若要支持部分更新,请使用 HTTP PATCH。
测试 PUT 终结点
本示例使用内存内、数据库,每次启动应用时都必须对其进行初始化。 在进行 PUT 调用之前,数据库中必须有一个项。 调用 GET,以确保在调用 PUT 之前数据库中存在项。
更新具有 Id = 1
的 to-do 项,并将其名称设置为 "feed fish"
。
使用 Swagger 发送 PUT 请求:
选择“Put /todoitems/{id}”>“试用”。
将“ID”字段设置为
1
。将请求正文设置为以下 JSON:
{ "name": "feed fish", "isComplete": false }
选择“执行”。
检查并测试 DELETE 终结点
示例应用使用 MapDelete
实现单个 DELETE 终结点:
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
使用 Swagger 发送 DELETE 请求:
选择“DELETE /todoitems/{id}”>“试用”。
将“ID”字段设置为
1
并选择“执行”。DELETE 请求将发送到应用,然后响应将会显示在“响应”窗格中。 响应正文为空,并且服务器响应状态代码为 204。
防止过度发布
目前,示例应用公开了整个 Todo
对象。 生产应用程序中的生产应用是模型的一个子集,通常用于限制可输入和返回的数据。 这背后有多种原因,但安全性是主要原因。 模型的子集通常称为数据传输对象 (DTO)、输入模型或视图模型。 本文使用的是 DTO。
DTO 可以用于:
- 防止过度发布。
- 隐藏客户端不应查看的属性。
- 省略某些属性以减少有效负载大小。
- 平展包含嵌套对象的对象图。 对客户端而言,平展的对象图可能更方便。
若要演示 DTO 方法,请更新 Todo
类,使其包含机密字段:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
此应用需要隐藏机密字段,但管理应用可以选择公开它。
确保可以发布和获取机密字段。
使用以下代码创建名为 TodoItemDTO.cs
的文件:
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
将 Program.cs
文件的内容替换为以下代码以使用此 DTO 模型:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
app.MapPost("/todoitems", async (TodoItemDTO todoItemDTO, TodoDb db) =>
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/todoitems/{id}", async (int id, TodoItemDTO todoItemDTO, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
确保可以发布和获取除机密字段以外的所有字段。
测试最小 API
有关测试最小 API 应用的示例,请参阅此 GitHub 示例。
发布到 Azure
有关部署到 Azure 的信息,请参阅快速入门:部署 ASP.NET Web 应用。
其他资源
构建最小 API,以创建具有最小依赖项的 HTTP API。 它们非常适合需要在 ASP.NET Core 中仅包括最少文件、功能和依赖项的微服务和应用。
本教程介绍使用 ASP.NET Core 生成最小 API 的基础知识。 在 ASP.NET Core 中创建 API 的另一种方法是使用控制器。 有关在最小 API 和基于控制器的 API 之间进行选择的帮助,请参阅 API 概述。 有关基于包含更多功能的控制器创建 API 项目的教程,请参阅创建 Web API。
概述
本教程将创建以下 API:
API | 描述 | 请求正文 | 响应正文 |
---|---|---|---|
GET /todoitems |
获取所有待办事项 | None | 待办事项的数组 |
GET /todoitems/complete |
获取已完成的待办事项 | None | 待办事项的数组 |
GET /todoitems/{id} |
按 ID 获取项 | None | 待办事项 |
POST /todoitems |
添加新项 | 待办事项 | 待办事项 |
PUT /todoitems/{id} |
更新现有项 | 待办事项 | None |
DELETE /todoitems/{id} |
删除项 | None | 无 |
先决条件
带有 ASP.NET 和 Web 开发工作负载的 Visual Studio 2022。
创建 API 项目
启动 Visual Studio 2022 并选择“创建新项目”。
在“创建新项目”对话框中:
- 在“搜索模板”搜索框中输入
Empty
。 - 选择“ASP.NET Core 空”模板,然后选择“下一步”。
- 在“搜索模板”搜索框中输入
将项目命名为 TodoApi,然后选择“下一步”。
在“其他信息”对话框中:
- 选择“.NET 8.0 (长期支持)”
- 取消选中“不使用顶级语句”
- 选择“创建”
检查代码
Program.cs
文件包含以下代码:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
前面的代码:
- 创建具有预配置默认值的 WebApplicationBuilder 和 WebApplication。
- 创建返回
Hello World!
的 HTTP GET 终结点/
:
运行应用
按 Ctrl+F5 以在不使用调试程序的情况下运行。
Visual Studio 会显示以下对话框:
如果信任 IIS Express SSL 证书,请选择“是”。
将显示以下对话框:
如果你同意信任开发证书,请选择“是”。
有关信任 Firefox 浏览器的信息,请参阅 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 证书错误。
Visual Studio 启动 Kestrel Web 服务器,然后打开浏览器窗口。
Hello World!
将显示在浏览器中。 Program.cs
文件包含一个最小但完整的应用。
关闭浏览器窗口。
添加 NuGet 包
必须添加 NuGet 包以支持本教程中使用的数据库和诊断。
- 在“工具”菜单中,选择“NuGet 包管理器”>“管理解决方案的 NuGet 包”。
- 选择“浏览”选项卡。
- 在搜索框中输入“Microsoft.EntityFrameworkCore.InMemory”,然后选择
Microsoft.EntityFrameworkCore.InMemory
。 - 选中右窗格中的“项目”复选框,然后选择“安装” 。
- 按照上述说明添加
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
包。
模型和数据库上下文类
- 在项目文件夹中,创建名为
Todo.cs
的文件,包含以下代码:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
前面的代码为此应用创建模型。 模型是一个表示应用管理的数据的类。
- 使用以下代码创建名为
TodoDb.cs
的文件:
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
前面的代码定义了数据库上下文,它是为数据模型协调实体框架功能的主类。 此类从 Microsoft.EntityFrameworkCore.DbContext 类派生。
添加 API 代码
- 将
Program.cs
文件的内容替换为以下代码:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
以下突出显示的代码将数据库上下文添加到依赖关系注入 (DI) 容器,并且允许显示与数据库相关的异常:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
DI 容器提供对数据库上下文和其他服务的访问权限。
本教程使用终结点资源管理器和 .http 文件来测试 API。
测试发布数据
Program.cs
中的以下代码创建 HTTP POST 终结点 /todoitems
以将数据添加到内存中数据库:
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
运行应用。 浏览器显示 404 错误,因为不再存在 /
终结点。
POST 终结点将用于向应用添加数据。
选择“查看”>“其他 Windows”>“终结点资源管理器”。
右键单击“POST”终结点,然后选择“生成请求”。
在名为
TodoApi.http
的项目文件夹中创建一个新文件,其内容类似于以下示例:@TodoApi_HostAddress = https://localhost:7031 Post {{TodoApi_HostAddress}}/todoitems ###
- 第一行创建了一个变量,该变量适用于所有终结点。
- 下一行定义了 POST 请求。
- 三重井号标签 (
###
) 行是请求分隔符:对于不同的请求,该标签之后的内容属于另一个请求。
POST 请求需要标头和正文。 若要定义请求的这些部分,请紧随 POST 请求行之后添加以下行:
Content-Type: application/json { "name":"walk dog", "isComplete":true }
前面的代码会添加 Content-Type 标头和 JSON 请求正文。 TodoApi.http 文件现在应如以下示例所示,但带有端口号:
@TodoApi_HostAddress = https://localhost:7057 Post {{TodoApi_HostAddress}}/todoitems Content-Type: application/json { "name":"walk dog", "isComplete":true } ###
运行应用。
选择
POST
请求行上方的“发送请求”链接。POST 请求将发送到应用,响应将显示在“响应”窗格中。
检查 GET 终结点
示例应用通过调用 MapGet
实现多个 GET 终结点:
API | 描述 | 请求正文 | 响应正文 |
---|---|---|---|
GET /todoitems |
获取所有待办事项 | None | 待办事项的数组 |
GET /todoitems/complete |
获取所有已完成的待办事项 | None | 待办事项的数组 |
GET /todoitems/{id} |
按 ID 获取项 | None | 待办事项 |
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
测试 GET 终结点
通过从浏览器或使用终结点资源管理器调用 GET
终结点来测试应用。 以下步骤适用于终结点资源管理器。
在“终结点资源管理器”中,右键单击第一个 GET 终结点,然后选择“生成请求”。
将以下内容添加到
TodoApi.http
文件中:Get {{TodoApi_HostAddress}}/todoitems ###
选择新的
GET
请求行上方的“发送请求”链接。GET 请求将发送到应用,响应将显示在“响应”窗格中。
响应正文与以下 JSON 类似:
[ { "id": 1, "name": "walk dog", "isComplete": true } ]
在“终结点资源管理器”中,右键单击“
/todoitems/{id}
GET”终结点,然后选择“生成请求”。 将以下内容添加到TodoApi.http
文件中:GET {{TodoApi_HostAddress}}/todoitems/{id} ###
将
{id}
替换为1
。选择新的 GET 请求行上方的“发送请求”链接。
GET 请求将发送到应用,响应将显示在“响应”窗格中。
响应正文与以下 JSON 类似:
{ "id": 1, "name": "walk dog", "isComplete": true }
此应用使用内存中数据库。 如果已重新启动应用,GET 请求不会返回任何数据。 如果未返回任何数据,请将数据 POST 到应用,然后重新尝试 GET 请求。
返回值
ASP.NET Core 自动将对象序列化为 JSON,并将 JSON 写入响应消息的正文中。 此返回类型的响应代码为 200 OK(假设没有未处理的异常)。 未经处理的异常将转换为 5xx 错误。
返回类型可以表示大范围的 HTTP 状态代码。 例如,GET /todoitems/{id}
可以返回两个不同的状态值:
检查 PUT 终结点
示例应用使用 MapPut
实现单个 PUT 终结点:
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
此方法类似于 MapPost
方法,但它使用 HTTP PUT。 成功响应返回 204 (无内容)。 根据 HTTP 规范,PUT 请求需要客户端发送整个更新的实体,而不仅仅是更改。 若要支持部分更新,请使用 HTTP PATCH。
测试 PUT 终结点
本示例使用内存内、数据库,每次启动应用时都必须对其进行初始化。 在进行 PUT 调用之前,数据库中必须有一个项。 调用 GET,以确保在调用 PUT 之前数据库中存在项。
更新具有 Id = 1
的 to-do 项,并将其名称设置为 "feed fish"
。
在“终结点资源管理器”中,右键单击 PUT 终结点,然后选择“生成请求”。
将以下内容添加到
TodoApi.http
文件中:Put {{TodoApi_HostAddress}}/todoitems/{id} ###
在 PUT 请求行中,将
{id}
替换为1
。紧随 PUT 请求行之后添加以下行:
Content-Type: application/json { "name": "feed fish", "isComplete": false }
前面的代码会添加 Content-Type 标头和 JSON 请求正文。
选择新的 PUT 请求行上方的“发送请求”链接。
PUT 请求将发送到应用,响应将显示在“响应”窗格中。 响应正文为空,状态代码为 204。
检查并测试 DELETE 终结点
示例应用使用 MapDelete
实现单个 DELETE 终结点:
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
在“终结点资源管理器”中,右键单击 DELETE 终结点,然后选择“生成请求”。
将 DELETE 请求添加到
TodoApi.http
。将 DELETE 请求行中的
{id}
替换为1
。 DELETE 请求应如以下示例所示:DELETE {{TodoApi_HostAddress}}/todoitems/1 ###
选择 DELETE 请求的“发送请求”链接。
DELETE 请求将发送到应用,响应将显示在“响应”窗格中。 响应正文为空,状态代码为 204。
使用 MapGroup API
示例应用代码每次设置终结点时都会重复 todoitems
URL 前缀。 API 通常具有带常见 URL 前缀的终结点组,并且 MapGroup 方法可用于帮助组织此类组。 它减少了重复代码,并允许通过对 RequireAuthorization 和 WithMetadata 等方法的单一调用来自定义整个终结点组。
将 Program.cs
的内容替换为以下代码:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", async (TodoDb db) =>
await db.Todos.ToListAsync());
todoItems.MapGet("/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
前面的代码执行以下更改:
- 添加
var todoItems = app.MapGroup("/todoitems");
以使用 URL 前缀/todoitems
设置组。 - 将所有
app.Map<HttpVerb>
方法更改为todoItems.Map<HttpVerb>
。 - 从
Map<HttpVerb>
方法调用中移除 URL 前缀/todoitems
。
测试终结点以验证它们的运行方式是否相同。
使用 TypedResults API
返回 TypedResults(而不是 Results)有几个优点,包括可测试性和自动返回 OpenAPI 的响应类型元数据来描述终结点。 有关详细信息,请参阅 TypedResults 与 Results。
Map<HttpVerb>
方法可以调用路由处理程序方法,而不是使用 lambda。 若要查看示例,请使用以下代码更新 Program.cs:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
Map<HttpVerb>
代码现在调用方法,而不是 lambda:
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
这些方法返回实现 IResult 并由 TypedResults 定义的对象:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
单元测试可以调用这些方法并测试它们是否返回正确的类型。 例如,如果方法是 GetAllTodos
:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
单元测试代码可以验证是否从处理程序方法返回了 Ok<Todo[]> 类型的对象。 例如:
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
// Arrange
var db = CreateDbContext();
// Act
var result = await TodosApi.GetAllTodos(db);
// Assert: Check for the correct returned type
Assert.IsType<Ok<Todo[]>>(result);
}
防止过度发布
目前,示例应用公开了整个 Todo
对象。 生产应用程序中的生产应用是模型的一个子集,通常用于限制可输入和返回的数据。 这背后有多种原因,但安全性是主要原因。 模型的子集通常称为数据传输对象 (DTO)、输入模型或视图模型。 本文使用的是 DTO。
DTO 可以用于:
- 防止过度发布。
- 隐藏客户端不应查看的属性。
- 省略某些属性以减少有效负载大小。
- 平展包含嵌套对象的对象图。 对客户端而言,平展的对象图可能更方便。
若要演示 DTO 方法,请更新 Todo
类,使其包含机密字段:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
此应用需要隐藏机密字段,但管理应用可以选择公开它。
确保可以发布和获取机密字段。
使用以下代码创建名为 TodoItemDTO.cs
的文件:
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
将 Program.cs
文件的内容替换为以下代码以使用此 DTO 模型:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
RouteGroupBuilder todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db) {
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(new TodoItemDTO(todo))
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
todoItemDTO = new TodoItemDTO(todoItem);
return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}
static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
确保可以发布和获取除机密字段以外的所有字段。
用完成的示例进行故障排除
如果遇到无法解决的问题,请将你的代码与完成的项目进行比较。 查看或下载已完成的项目(如何下载)。
后续步骤
- 配置 JSON 序列化选项。
- 处理错误和异常:默认情况下,在开发环境中为最少 API 应用启用开发人员异常页。 有关如何处理错误和异常的信息,请参阅处理 ASP.NET Core API 中的错误。
- 有关测试最小 API 应用的示例,请参阅此 GitHub 示例。
- 最小 API 中的 OpenAPI 支持。
- 快速入门:发布到 Azure。
- 组织 ASP.NET Core 最小 API。
了解详细信息
请参阅《最小 API 快速参考》