最小 API 旨在创建具有最小依赖项的 HTTP API。 它们非常适合需要在 ASP.NET Core 中仅包括最少文件、功能和依赖项的微服务和应用。
本教程介绍了使用 ASP.NET Core 生成最小 API 的基础知识。 在 ASP.NET Core 中创建 API 的另一种方法是使用控制器。 有关在最小 API 和基于控制器的 API 之间进行选择的帮助,请参阅 API 概述。 有关基于包含更多功能的控制器创建 API 项目的教程,请参阅创建 Web API。
Overview
本教程将创建以下 API:
| API | Description | 请求主体 | 响应正文 |
|---|---|---|---|
GET /todoitems |
获取所有待办事项 | None | 待办事项的数组 |
GET /todoitems/complete |
获取已完成的待办事项 | None | 待办事项的数组 |
GET /todoitems/{id} |
按 ID 获取项 | None | 待办事项 |
POST /todoitems |
添加新项 | 待办事项 | 待办事项 |
PUT /todoitems/{id} |
更新现有项 | 待办事项 | None |
PATCH /todoitems/{id} |
部分更新一个项目 | 部分待办事项 | None |
DELETE /todoitems/{id} |
删除项 | None | None |
Prerequisites
最新版本的 Visual Studio 具有 ASP.NET 和 Web 开发 工作负载。
创建 API 项目
启动 Visual Studio 2026,然后选择创建一个新项目。
在“创建新项目”对话框中:
- 选择“ASP.NET Core Web API”项目类型,然后选择“下一步”。
- 将项目命名 为 TodoApi,然后选择“ 下一步”。
在“其他信息”对话框中:
- 确认 Framework 是否为 .NET 10.0 (长期支持)。
- 确认已选中“启用 OpenAPI 支持”复选框。
- 确认“使用控制器”复选框未被选中。 取消选中此设置以创建本教程所需的最小 API 项目,而不是基于控制器的项目。
- 选择“创建”。
检查代码
Program.cs模板生成的文件包含以下代码:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
app.Run();
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
前面的代码:
- 创建具有预配置默认值的 WebApplicationBuilder 和 WebApplication。
- 使用
builder.Services.AddOpenApi()注册内置 OpenAPI 文档生成器。 - 仅在开发环境中,通过
app.MapOpenApi()将生成的 OpenAPI 文档映射到/openapi/v1.json。 - 定义返回五条随机生成的
GET /weatherforecast记录的示例WeatherForecast终结点。
在本教程中,你将用一个新的 Todo 示例替换 WeatherForecast;该示例包含用于创建、读取、更新和删除项目的终结点,并由模型和数据库提供支持。
添加 NuGet 包
添加 NuGet 包以支持本教程中使用的数据库。
- 在“工具”菜单中,选择“NuGet 包管理器”“管理解决方案的 NuGet 包”。
- 选择“浏览”选项卡。
- 在搜索框中输入“Microsoft.EntityFrameworkCore.InMemory”,然后选择 。
- 选中右窗格中的“项目”复选框,然后选择“安装” 。
模型和数据库上下文类
- 在项目文件夹中,创建名为
Todo.cs的文件,包含以下代码:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
前面的代码为此应用创建模型。 模型是一个表示应用管理的数据的类。
包含该 Secret 属性是为了说明一种现实中常见的需求:对于应用在内部存储和使用的数据(例如拥有该项的用户 ID),你不希望客户端查看或设置这些数据。 在本教程后面的“防止过度发布”步骤中,你将使用数据传输对象(DTO),使像 Secret 这样的字段不包含在 API 的输入和响应中。
- 使用以下代码创建名为
TodoDb.cs的文件:
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
前面的代码定义了数据库上下文,它是为数据模型协调实体框架功能的主类。 此类从 Microsoft.EntityFrameworkCore.DbContext 类派生。
该应用使用一个名为 TodoList 的内存数据库。 稍后将Program.csbuilder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));注册该数据库,其中字符串"TodoList"是数据库名称。 由于数据存储在内存中,因此每次应用重启时都会重置数据。
将 WeatherForecast 示例替换为 Todo API
该 webapi 模板会向 GET /weatherforecast 添加一个示例 Program.cs 端点,并在文件底部添加一条 WeatherForecast 记录。 此示例只是一个用于说明该模板可以正常工作的占位符——请将两者都替换为下一节中描述的 Todo 端点。
- 将所有代码
Program.cs替换为以下内容。 删除WeatherForecast示例终结点和记录,Todo 终结点将取代其位置:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
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();
模板添加的 OpenAPI 行(builder.Services.AddOpenApi() 和 app.MapOpenApi())保持不变。 现在描述的是 Todo 端点,而不是示例天气端点。
以下突出显示的代码在 依赖项注入(DI) 容器中注册应用的服务:数据库上下文(AddDbContext)和 OpenAPI 文档生成器(AddOpenApi):
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddOpenApi();
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);
});
- 按 Ctrl+F5 在不调试的情况下运行应用。 Visual Studio启动 Kestrel Web 服务器,并根据需要信任开发证书。
Visual Studio 会显示以下对话框:
如果信任 IIS Express SSL 证书,请选择“是”。
将显示以下对话框:
如果你同意信任开发证书,请选择“是”。
有关信任 Firefox 浏览器的信息,请参阅 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 证书错误。
使用 POST 终结点将数据添加到应用。
选择查看>其他窗口>端点浏览器。
右键单击“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 接口端点
您的 Program.cs 包含多个 GET 端点,这些端点由 MapGet 定义:
| API | Description | 请求主体 | 响应正文 |
|---|---|---|---|
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,
"secret": null
}
]
- 在终结点资源管理器中,右键单击
/todoitems/{id}GET终结点,然后选择生成请求。 将以下内容添加到TodoApi.http文件中:
GET {{TodoApi_HostAddress}}/todoitems/{id}
###
将
{id}替换为1。选择新的 GET 请求行上方的“发送请求”链接。
GET 请求将发送到应用,响应将显示在“响应”窗格中。
响应正文与以下 JSON 类似:
{
"id": 1,
"name": "walk dog",
"isComplete": true,
"secret": null
}
此应用使用内存中数据库。 如果重启应用,数据将丢失: GET /todoitems 返回一个空数组([]),并 GET /todoitems/{id} 返回一个 404 Not Found。 若要重新填充应用, 请将 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 调用之前,需要数据库中的项。 调用 POST 以确保在进行 PUT 调用之前数据库中存在项。
更新具有 Id = 1 的 Todo 项,并将其名称设置为 "feed fish"。
在“终结点资源管理器”中,右键单击 PUT 终结点,然后选择“生成请求”。
将以下内容添加到
TodoApi.http文件中:
PUT {{TodoApi_HostAddress}}/todoitems/{id}
###
在 PUT 请求行中,将
{id}替换为1。紧随 PUT 请求行之后添加以下行:
Content-Type: application/json
{
"id": 1,
"name": "feed fish",
"isComplete": false
}
前面的代码会添加 Content-Type 标头和 JSON 请求正文。
- 选择新的 PUT 请求行上方的“发送请求”链接。
PUT 请求将发送到应用,响应将显示在“响应”窗格中。 响应正文为空,状态代码为 204。
创建并查看 PATCH 端点
PATCH 端点允许客户端仅发送它们想要更新的字段,例如重命名某个待办事项条目,而无需重新发送其完成状态。 此方法不同于 PUT 请求,它替换了整个项,因此客户端必须发送每个字段,即使只有一个更改。
后续步骤将添加新文件并修改 Program.cs 该文件。 在进行这些更改之前,先停止 TodoApi 应用。 让浏览器中的 Scalar 页面保持打开状态。
- 若要停止应用,请选择Visual Studio工具栏中的“停止”按钮(红色正方形)。
此示例使用每次启动应用时必须初始化的内存中数据库。 在进行 PATCH 调用之前,数据库必须包含一个项。 先调用 POST,以确保数据库中已有该项,然后再发出 PATCH 请求。
PATCH 终结点使用 TodoPatchDto 具有可为 null 属性的类来正确处理部分更新。 通过使用可空属性,端点可以区分未提供的字段(null)和被显式设置了值的字段(包括布尔字段被设为 false 的情况)。 如果没有可空属性,非可空布尔属性默认值为 false;当请求中未包含该字段时,这可能会覆盖其原有的 true 值。
- 使用以下代码创建名为
TodoPatchDto.cs的文件:
public class TodoPatchDto
{
public string? Name { get; set; }
public bool? IsComplete { get; set; }
}
该 TodoPatchDto 类使用可以为 null 的属性 (string? 和 bool?) 来区分请求中未提供的字段与显式设置为值的字段。
- 在
Program.cs中,紧接在MapPut终结点之后添加以下 PATCH 终结点:
app.MapPatch("/todoitems/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;
await db.SaveChangesAsync();
return Results.NoContent();
});
此方法与 MapPut 该方法类似,但它使用 HTTP PATCH,并且仅更新请求中提供的字段。 成功响应返回 204 (无内容)。
说明
PATCH操作允许对资源进行部分更新。 有关使用 JSON Patch 文档执行更高级的部分更新操作,请参阅 ASP.NET Core Web API 中的 JsonPatch。
测试 PATCH 终结点
- 按 Ctrl+F5 重新生成并使用新的 PATCH 终结点运行应用。
此示例使用每次启动应用时必须初始化的内存中数据库。 在进行 PATCH 调用之前,数据库必须包含一个项。 先调用 POST,以确保数据库中已有该项,然后再发出 PATCH 请求。
仅更新具有 name 的 Todo 项的 Id = 1 属性,并将其名称设置为 "run errands"。
在 “终结点资源管理器”中,选择“刷新”按钮。 然后,右键单击 PATCH 终结点,然后选择“ 生成请求”。
将以下内容添加到
TodoApi.http文件中:
PATCH {{TodoApi_HostAddress}}/todoitems/{id}
###
在 PATCH 请求行中,将
{id}替换为1。紧接在 PATCH 请求行后面添加以下行:
Content-Type: application/json
{
"name": "run errands"
}
前面的代码添加一个 Content-Type 标头和一个仅包含要更新的字段的 JSON 请求正文。
- 选择新 PATCH 请求行上方的 “发送请求 ”链接。
PATCH 请求将发送到应用,响应将显示在 “响应 ”窗格中。 响应正文为空,状态代码为 204。
查看 DELETE 端点
你的 Program.cs 文件包含一个使用 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
- 后续步骤将修改
Program.cs文件,因此在进行这些更改之前停止应用。 让浏览器中的 Scalar 页面保持打开。
- 若要停止应用,请选择Visual Studio工具栏中的“停止”按钮(红色正方形)。
你编写的 Program.cs 文件在每次设置端点时,都会重复 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.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
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.MapPatch("/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;
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>。从
/todoitems方法调用中移除 URL 前缀Map<HttpVerb>。运行应用并测试终结点,以验证它们是否工作相同。
使用 TypedResults API
后续步骤将修改 Program.cs 文件,因此在进行这些更改之前停止应用。 让浏览器中的 Scalar 页面保持打开。
返回 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.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapPatch("/{id}", PatchTodo);
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> PatchTodo(int id, TodoPatchDto inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;
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.MapPatch("/{id}", PatchTodo);
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> PatchTodo(int id, TodoPatchDto inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;
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);
}
防止过度发布
目前,API 公开整个 Todo 对象,包括 Secret 模型 和数据库上下文类中添加的属性。 在生产应用程序中,使用模型的子集来限制客户端可以输入和接收的数据。 安全性是此限制的主要原因。 模型的此子集通常称为数据传输对象(DTO)、输入模型或视图模型。 本文使用 DTO。
使用 DTO 可以:
- 防止过度发布。
- 隐藏客户端不应查看的属性。
- 省略某些属性以减少有效负载大小。
- 平展包含嵌套对象的对象图。 对客户端而言,平展的对象图可能更方便。
此应用需要隐藏 Secret 字段,但管理应用可以选择公开它。
后续步骤添加文件,因此请在进行这些更改之前停止应用。 让浏览器中的 Scalar 页面保持打开。
使用以下代码创建名为
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.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapPatch("/{id}", PatchTodo);
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> PatchTodo(int id, TodoPatchDto inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;
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();
}
- 运行该应用并验证您能否对除
Secret字段外的所有字段进行 POST 和 GET 操作。
用完成的示例进行故障排除
如果遇到无法解决的问题,请将你的代码与完成的项目进行比较。 查看或下载已完成的项目(如何下载)。
后续步骤
- 配置 JSON 序列化选项。
- 处理错误和异常:开发人员 异常页 默认在
Development最小 API 应用的环境中启用。 有关如何处理错误和异常的信息,请参阅处理 ASP.NET Core API 中的错误。 - 有关测试最小 API 应用的示例,请参阅 此 GitHub 示例。
- 在最小 API 中提供 OpenAPI 支持。
- 快速入门:发布到 Azure。
- 组织 ASP.NET Core 最小 API。
Learn more
请参阅《最小 API 快速参考》
原文:教程:使用 ASP.NET Core 创建最小 API。作者/来源:Microsoft Learn 文档贡献者。本文依据所列原文整理为中文,代码、命令与配置示例保留原文。












暂无评论内容