教程:使用 ASP.NET Core 创建最小 API

作者: 韦德·皮克特 和 汤姆·迪克斯特拉

最小 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 开发 工作负载。

    VS26 安装程序工作负载

创建 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 容器提供对数据库上下文和其他服务的访问权限。

测试发布数据

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 会显示以下对话框:

此项目已配置为使用 SSL。为了避免浏览器中出现 SSL 警告,可以选择信任 IIS Express 已生成的自签名证书。是否要信任 IIS Express SSL 证书?

如果信任 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链接。

    突出显示运行链接的 .http 文件窗口。

    POST 请求将发送到应用,响应将显示在“响应”窗格中。

    .http 文件窗口,带有来自 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} 可以返回两个不同的状态值:

  • 如果没有任何项与请求的 ID 匹配,该方法将返回 404 状态NotFound 错误代码。
  • 否则,此方法将返回具有 JSON 响应正文的 200。 返回 item 则产生 HTTP 200 响应。

检查 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 操作。

用完成的示例进行故障排除

如果遇到无法解决的问题,请将你的代码与完成的项目进行比较。 查看或下载已完成的项目(如何下载)。

后续步骤

Learn more

请参阅《最小 API 快速参考》


原文:教程:使用 ASP.NET Core 创建最小 API。作者/来源:Microsoft Learn 文档贡献者。本文依据所列原文整理为中文,代码、命令与配置示例保留原文。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容