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

Note

此版本不是本文的最新版本。 有关当前版本,请参阅 本文的 .NET 10 版本

Warning

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 有关当前版本,请参阅 本文的 .NET 10 版本

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

最小 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);
}

前面的代码:

  • 创建具有预配置默认值的 WebApplicationBuilderWebApplication
  • 使用 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 模板会向 Program.cs 添加一个示例 GET /weatherforecast 端点,并在文件底部添加一条 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 会显示以下对话框:

此项目已配置为使用 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 (无内容)

Note

PATCH操作允许对资源进行部分更新。 有关使用 JSON Patch 文档执行更高级的部分更新操作,请参阅 ASP.NET Core Web API 中的 JsonPatch

测试 PATCH 终结点

  • Ctrl+F5 重新生成并使用新的 PATCH 终结点运行应用。

此示例使用每次启动应用时必须初始化的内存中数据库。 在进行 PATCH 调用之前,数据库必须包含一个项。 在调用 PATCH 之前,先调用 POST 以确保数据库中已有一个项。

仅更新具有 Id = 1 的 Todo 项的 name 属性,并将其名称设置为 "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 字段外的所有字段。

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

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

后续步骤

Learn more

请参阅《最小 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

创建 API 项目

  • 启动 Visual Studio 2022 并选择“创建新项目”

  • 在“创建新项目”对话框中:

    • 在“搜索模板”搜索框中输入 Empty
    • 选择“ASP.NET Core Empty”模板,然后选择“下一步”。

    Visual Studio 创建新项目

  • 将项目命名为 TodoApi,然后选择“下一步”。

  • 在“其他信息”对话框中:

    • 选择 .NET 7.0
    • 取消选中“不使用顶级语句”
    • 选择 创建

    其他信息

检查代码

Program.cs 文件包含以下代码:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

前面的代码:

运行应用

按 Ctrl+F5 以在不使用调试程序的情况下运行。

Visual Studio 会显示以下对话框:

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

如果信任 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”,然后选择
  • 在右侧窗格中选中“项目”复选框。
  • 在“版本”下拉列表中选择可用的最新版本 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 中间件

  • app 行中定义 var app = builder.Build(); 之前,添加以下突出显示的代码

    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 入门

  • app 行中定义 var app = builder.Build(); 后,将以下突出显示的代码添加到下一行

    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 测试页

  • 在 Swagger API 测试页上,选择Post /todoitems>试一试

  • 请注意,“请求正文”字段包含了反映 API 参数的生成的示例格式。

  • 在请求正文中,输入待办项的 JSON,无需指定可选的 id

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • 选择“ 执行”。

    使用 Post 的 Swagger

Swagger在“执行”按钮下方提供“响应”窗格。

Swagger 与 Post 响应

请注意一些有用的详细信息:

  • 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 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 终结点

通过在浏览器中或使用 Swagger 工具调用端点来测试应用程序。

  • 在 Swagger 中,选择“GET /todoitems”>“试用”>“执行”。

  • 或者,通过输入 URI 从浏览器调用 GET /todoitems。http://localhost:<port>/todoitems 例如: http://localhost:5001/todoitems

GET /todoitems 的调用生成如下响应:

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
  • 在 Swagger 中调用 GET /todoitems/{id} 以从特定 ID 返回数据:

    • 选择“GET /todoitems”>“试用”。
    • id 字段设置为1并选择执行
  • 或者,通过输入 URI 从浏览器调用 GET /todoitems。https://localhost:<port>/todoitems/1 例如: 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} 可以返回两个不同的状态值:

  • 如果没有任何项与请求的 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 调用之前,数据库中必须有一个项。 调用 GET,以确保在调用 PUT 之前数据库中存在项。

更新具有 Id = 1 的 to-do 项,并将其名称设置为 "feed fish"

使用 Swagger 发送 PUT 请求:

  • 选择“Put /todoitems/{id}”>“试用”。

  • 将“ID”字段设置为 1

  • 将请求正文设置为以下 JSON:

    {
      "name": "feed fish",
      "isComplete": false
    }
    
  • 选择“ 执行”。

检查 PATCH 接口

使用以下代码创建名为 TodoPatchDto.cs 的文件:

public class TodoPatchDto
{
    public string? Name { get; set; }
    public bool? IsComplete { get; set; }
}

TodoPatchDto 类使用可以为 null 的属性 (string?bool?) 来区分请求中未提供的字段与显式设置为值的字段。

示例应用使用 MapPatch 实现一个 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 (无内容)。 根据 HTTP 规范,PATCH 请求启用部分更新,允许客户端仅发送需要更改的字段。

PATCH 终结点使用 TodoPatchDto 具有可为 null 属性的类来正确处理部分更新。 使用可为空属性可让终结点区分“未提供字段 (null)”和“显式设置为某个值(包括布尔字段的 false)”两种情况。 若不使用可为空属性,不可为空的布尔值会默认为 false,当请求中不包含该字段时,可能会覆盖原有的 true 值。

Note

PATCH操作允许对资源进行部分更新。 有关使用 JSON Patch 文档执行更高级的部分更新操作,请参阅 ASP.NET Core Web API 中的 JsonPatch

测试 PATCH 终结点

本示例使用内存内、数据库,每次启动应用时都必须对其进行初始化。 在进行 PATCH 调用之前,数据库中必须有一个项。 调用 GET 以确保在进行 PATCH 调用之前数据库中存在项。

仅更新 ID 为 name 的待办事项的 Id = 1 属性,并将其名称设置为 "run errands"

使用 Swagger 发送 PATCH 请求:

  • 选择 Patch /todoitems/{id}>试用

  • 将“ID”字段设置为 1

  • 将请求正文设置为以下 JSON:

    {
      "name": "run errands"
    }
    
  • 选择“ 执行”。

检查并测试 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 方法可用于帮助组织此类组。 它减少了重复代码,并允许通过对 RequireAuthorizationWithMetadata 等方法的单一调用来自定义整个终结点组。

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>
  • /todoitems 方法调用中移除 URL 前缀 Map<HttpVerb>

测试终结点以验证它们的运行方式是否相同。

使用 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>();
}

确保可以发布和获取除机密字段以外的所有字段。

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

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

后续步骤

Learn more

请参阅《最小 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
DELETE /todoitems/{id}     删除项 None None

Prerequisites

创建 API 项目

  • 启动 Visual Studio 2022 并选择“创建新项目”

  • 在“创建新项目”对话框中:

    • 在“搜索模板”搜索框中输入 Empty
    • 选择“ASP.NET Core Empty”模板,然后选择“下一步”。

    Visual Studio 创建新项目

  • 将项目命名为 TodoApi,然后选择“下一步”。

  • 在“其他信息”对话框中:

    • 选择 .NET 6.0
    • 取消选中“不使用顶级语句”
    • 选择 创建

检查代码

Program.cs 文件包含以下代码:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

前面的代码:

运行应用

按 Ctrl+F5 以在不使用调试程序的情况下运行。

Visual Studio 会显示以下对话框:

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

如果信任 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”,然后选择
  • 在右侧窗格中选中“项目”复选框。
  • 在“版本”下拉列表中选择可用的最新版本 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;
    
  • app 行中定义 var app = builder.Build(); 之前,添加以下突出显示的代码

    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 入门

  • app 行中定义 var app = builder.Build(); 后,将以下突出显示的代码添加到下一行

    
    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 测试页

  • 在 Swagger API 测试页上,选择Post /todoitems>试一试

  • 请注意,“请求正文”字段包含了反映 API 参数的生成的示例格式。

  • 在请求正文中,输入待办项的 JSON,无需指定可选的 id

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • 选择“ 执行”。

    Swagger 与 Post 数据

Swagger在“执行”按钮下方提供“响应”窗格。

Swagger 与 Post 响应窗格

请注意一些有用的详细信息:

  • 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 Description 请求主体 响应正文
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 从浏览器调用 GET /todoitems。http://localhost:<port>/todoitems 例如: http://localhost:5001/todoitems

GET /todoitems 的调用生成如下响应:

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": true
  }
]
  • 在 Swagger 中调用 GET /todoitems/{id} 以从特定 ID 返回数据:

    • 选择“GET /todoitems”>“试用”。
    • id 字段设置为1并选择执行
  • 或者,通过输入 URI 从浏览器调用 GET /todoitems。https://localhost:<port>/todoitems/1 例如,例如 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} 可以返回两个不同的状态值:

  • 如果没有任何项与请求的 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 调用之前,数据库中必须有一个项。 调用 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

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

创建 API 项目

  • 启动 Visual Studio 2022 并选择“创建新项目”

  • 在“创建新项目”对话框中:

    • 在“搜索模板”搜索框中输入 Empty
    • 选择“ASP.NET Core Empty”模板,然后选择“下一步”。

    Visual Studio 创建新项目

  • 将项目命名为 TodoApi,然后选择“下一步”。

  • 在“其他信息”对话框中:

    • 选择“.NET 8.0 (长期支持)”
    • 取消选中“不使用顶级语句”
    • 选择 创建

    其他信息

检查代码

Program.cs 文件包含以下代码:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

前面的代码:

运行应用

按 Ctrl+F5 以在不使用调试程序的情况下运行。

Visual Studio 会显示以下对话框:

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

如果信任 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.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 终结点将用于向应用添加数据。

  • 选择查看>其他窗口>端点浏览器

  • 右键单击“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 接口端点

示例应用通过调用 MapGet 实现多个 GET 终结点:

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
      }
    ]
    
  • 终结点资源管理器中,右键单击/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} 可以返回两个不同的状态值:

  • 如果没有任何项与请求的 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 调用之前,数据库中必须有一个项。 调用 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。

检查 PATCH 接口

使用以下代码创建名为 TodoPatchDto.cs 的文件:

public class TodoPatchDto
{
    public string? Name { get; set; }
    public bool? IsComplete { get; set; }
}

TodoPatchDto 类使用可以为 null 的属性 (string?bool?) 来区分请求中未提供的字段与显式设置为值的字段。

示例应用使用 MapPatch 实现一个 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 (无内容)。 根据 HTTP 规范,PATCH 请求启用部分更新,允许客户端仅发送需要更改的字段。

PATCH 终结点使用 TodoPatchDto 具有可为 null 属性的类来正确处理部分更新。 使用可为空属性可让终结点区分“未提供字段 (null)”和“显式设置为某个值(包括布尔字段的 false)”两种情况。 若不使用可为空属性,不可为空的布尔值会默认为 false,当请求中不包含该字段时,可能会覆盖原有的 true 值。

Note

PATCH操作允许对资源进行部分更新。 有关使用 JSON Patch 文档执行更高级的部分更新操作,请参阅 ASP.NET Core Web API 中的 JsonPatch

测试 PATCH 终结点

本示例使用内存内、数据库,每次启动应用时都必须对其进行初始化。 在进行 PATCH 调用之前,数据库中必须有一个项。 调用 GET 以确保在进行 PATCH 调用之前数据库中存在项。

仅更新 ID 为 name 的待办事项的 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 终结点

示例应用使用 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 请求添加到 TodoApi.http

  • 将 DELETE 请求行中的 {id} 替换为 1。 DELETE 请求应如以下示例所示:

    DELETE {{TodoApi_HostAddress}}/todoitems/1
    
    ###
    
  • 选择 DELETE 请求的“发送请求”链接。

    DELETE 请求将发送到应用,响应将显示在“响应”窗格中。 响应正文为空,状态代码为 204。

使用 MapGroup API

示例应用代码每次设置终结点时都会重复 todoitems URL 前缀。 API 通常具有带常见 URL 前缀的终结点组,并且 MapGroup 方法可用于帮助组织此类组。 它减少了重复代码,并允许通过对 RequireAuthorizationWithMetadata 等方法的单一调用来自定义整个终结点组。

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>
  • /todoitems 方法调用中移除 URL 前缀 Map<HttpVerb>

测试终结点以验证它们的运行方式是否相同。

使用 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();
}

确保可以发布和获取除机密字段以外的所有字段。

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

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

后续步骤

Learn more

请参阅《最小 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

创建 API 项目

  • 启动 Visual Studio 2022 并选择“创建新项目”

  • 在“创建新项目”对话框中:

    • 在“搜索模板”搜索框中输入 Empty
    • 选择“ASP.NET Core Empty”模板,然后选择“下一步”。

    Visual Studio 创建新项目

  • 将项目命名为 TodoApi,然后选择“下一步”。

  • 在“其他信息”对话框中:

    • 选择“.NET 9.0”
    • 取消选中“不使用顶级语句”
    • 选择 创建

    其他信息

检查代码

Program.cs 文件包含以下代码:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

前面的代码:

运行应用

按 Ctrl+F5 以在不使用调试程序的情况下运行。

Visual Studio 会显示以下对话框:

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

如果信任 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.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 终结点将用于向应用添加数据。

  • 选择查看>其他窗口>端点浏览器

  • 右键单击“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 接口端点

示例应用通过调用 MapGet 实现多个 GET 终结点:

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
      }
    ]
    
  • 终结点资源管理器中,右键单击/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} 可以返回两个不同的状态值:

  • 如果没有任何项与请求的 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 调用之前,数据库中必须有一个项。 调用 GET,以确保在调用 PUT 之前数据库中存在项。

更新具有 Id = 1 的 to-do 项,并将其名称设置为 "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 接口

使用以下代码创建名为 TodoPatchDto.cs 的文件:

public class TodoPatchDto
{
    public string? Name { get; set; }
    public bool? IsComplete { get; set; }
}

TodoPatchDto 类使用可以为 null 的属性 (string?bool?) 来区分请求中未提供的字段与显式设置为值的字段。

示例应用使用 MapPatch 实现一个 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 (无内容)。 根据 HTTP 规范,PATCH 请求启用部分更新,允许客户端仅发送需要更改的字段。

PATCH 终结点使用 TodoPatchDto 具有可为 null 属性的类来正确处理部分更新。 使用可为空属性可让终结点区分“未提供字段 (null)”和“显式设置为某个值(包括布尔字段的 false)”两种情况。 若不使用可为空属性,不可为空的布尔值会默认为 false,当请求中不包含该字段时,可能会覆盖原有的 true 值。

Note

PATCH操作允许对资源进行部分更新。 有关使用 JSON Patch 文档执行更高级的部分更新操作,请参阅 ASP.NET Core Web API 中的 JsonPatch

测试 PATCH 终结点

本示例使用内存内、数据库,每次启动应用时都必须对其进行初始化。 在进行 PATCH 调用之前,数据库中必须有一个项。 调用 GET 以确保在进行 PATCH 调用之前数据库中存在项。

仅更新 ID 为 name 的待办事项的 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 终结点

示例应用使用 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 请求添加到 TodoApi.http

  • 将 DELETE 请求行中的 {id} 替换为 1。 DELETE 请求应如以下示例所示:

    DELETE {{TodoApi_HostAddress}}/todoitems/1
    
    ###
    
  • 选择 DELETE 请求的“发送请求”链接。

    DELETE 请求将发送到应用,响应将显示在“响应”窗格中。 响应正文为空,状态代码为 204。

使用 MapGroup API

示例应用代码每次设置终结点时都会重复 todoitems URL 前缀。 API 通常具有带常见 URL 前缀的终结点组,并且 MapGroup 方法可用于帮助组织此类组。 它减少了重复代码,并允许通过对 RequireAuthorizationWithMetadata 等方法的单一调用来自定义整个终结点组。

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>
  • /todoitems 方法调用中移除 URL 前缀 Map<HttpVerb>

测试终结点以验证它们的运行方式是否相同。

使用 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();
}

确保可以发布和获取除机密字段以外的所有字段。

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

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

后续步骤

Learn more

请参阅《最小 API 快速参考