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

作者:Rick AndersonTom Dykstra

注意

此版本不是本文的最新版本。 对于当前版本,请参阅本文的 .NET 7 版本

重要事项

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

对于当前版本,请参阅本文的 .NET 7 版本

构建最小 API,以创建具有最小依赖项的 HTTP API。 它们非常适合于需要在 ASP.NET Core 中仅包括最少文件、功能和依赖项的微服务和应用。

本教程介绍使用 ASP.NET Core 生成最小 API 的基础知识。 在 ASP.NET Core 中创建 API 的另一种方法是使用控制器。 有关在最小 API 和基于控制器的 API 之间进行选择的帮助,请参阅 API 概述。 有关基于包含更多功能的控制器创建 API 项目的教程,请参阅创建 Web API

概述

本教程将创建以下 API:

API 描述 请求正文 响应正文
GET /todoitems 获取所有待办事项 None 待办事项的数组
GET /todoitems/complete 获取已完成的待办事项 None 待办事项的数组
GET /todoitems/{id} 按 ID 获取项 None 待办事项
POST /todoitems 添加新项 待办事项 待办事项
PUT /todoitems/{id} 更新现有项 待办事项 None
DELETE /todoitems/{id}     删除项 None

先决条件

创建 API 项目

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

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

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

    Visual Studio Create a new project

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

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

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

    Additional information

检查代码

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

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

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

app.Run();

前面的代码:

运行应用

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

Visual Studio 会显示以下对话框:

This project is configured to use SSL. To avoid SSL warnings in the browser you can choose to trust the self-signed certificate that IIS Express has generated. Would you like to trust the IIS Express SSL certificate?

如果信任 IIS Express SSL 证书,请选择“是”

将显示以下对话框:

Security warning dialog

如果你同意信任开发证书,请选择“是”。

有关信任 Firefox 浏览器的信息,请参阅 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 证书错误

Visual Studio 启动 Kestrel Web 服务器,然后打开浏览器窗口。

Hello World! 将显示在浏览器中。 Program.cs 文件包含一个最小但完整的应用。

添加 NuGet 包

必须添加 NuGet 包以支持本教程中使用的数据库和诊断。

  • 在“工具”菜单中,选择“NuGet 包管理器”>“管理解决方案的 NuGet 包”。
  • 选择“浏览”选项卡。
  • 选择“包括预发行版”。
  • 在搜索框中输入“Microsoft.EntityFrameworkCore.InMemory”,然后选择 Microsoft.EntityFrameworkCore.InMemory
  • 选中右窗格中的“项目”复选框,然后选择“安装” 。
  • 按照上述说明添加 Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore 包。

模型和数据库上下文类

在项目文件夹中,创建名为 Todo.cs 的文件,包含以下代码:

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

前面的代码为此应用创建模型。 模型是一个表示应用管理的数据的类。

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

using Microsoft.EntityFrameworkCore;

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

前面的代码定义了数据库上下文,它是为数据模型协调实体框架功能的主类。 此类从 Microsoft.EntityFrameworkCore.DbContext 类派生。

添加 API 代码

Program.cs 文件的内容替换为以下代码:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();

以下突出显示的代码将数据库上下文添加到依赖关系注入 (DI) 容器,并且允许显示与数据库相关的异常:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

DI 容器提供对数据库上下文和其他服务的访问权限。

本教程使用终结点资源管理器和 .http 文件来测试 API。

测试发布数据

Program.cs 中的以下代码创建 HTTP POST 终结点 /todoitems 以将数据添加到内存中数据库:

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

运行应用。 浏览器显示 404 错误,因为不再存在 / 终结点。

使用 POST 终结点将数据添加到应用。

  • 选择“查看”>“其他 Windows”>“终结点资源管理器”。

  • 右键单击“POST”终结点,然后选择“生成请求”。

    Endpoints Explorer context menu highlighting Generate Request menu item.

    在名为 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 file window with run link highlighted.

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

    .http file window with response from the POST request.

检查 GET 终结点

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

API 描述 请求正文 响应正文
GET /todoitems 获取所有待办事项 None 待办事项的数组
GET /todoitems/complete 获取所有已完成的待办事项 None 待办事项的数组
GET /todoitems/{id} 按 ID 获取项 None 待办事项
app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

测试 GET 终结点

通过从浏览器或使用终结点资源管理器调用 GET 终结点来测试应用。 以下步骤适用于终结点资源管理器。

  • 在“终结点资源管理器”中,右键单击第一个 GET 终结点,然后选择“生成请求”。

    将以下内容添加到 TodoApi.http 文件中:

    Get {{TodoApi_HostAddress}}/todoitems
    
    ###
    
  • 选择新的 GET 请求行上方的“发送请求”链接。

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

  • 响应正文与以下 JSON 类似:

    [
      {
        "id": 1,
        "name": "walk dog",
        "isComplete": false
      }
    ]
    
  • 在“终结点资源管理器”中,右键单击第三个 GET 终结点,然后选择“生成请求”。 将以下内容添加到 TodoApi.http 文件中:

    GET {{TodoApi_HostAddress}}/todoitems/{id}
    
    ###
    
  • {id} 替换为 1

  • 选择新的 GET 请求行上方的“发送请求”链接。

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

  • 响应正文与以下 JSON 类似:

    {
      "id": 1,
      "name": "walk dog",
      "isComplete": false
    }
    

此应用使用内存中数据库。 如果已重新启动应用,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 的待办事项并将其名称设置为 "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 请求正文。

  • 选择新的 GET 请求行上方的“发送请求”链接。

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

检查并测试 DELETE 终结点

示例应用使用 MapDelete 实现单个 DELETE 终结点:

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});
  • 在“终结点资源管理器”中,右键单击 DELETE 终结点,然后选择“生成请求”。

    将 DELETE 请求添加到 TodoApi.http

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

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

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

使用 MapGroup API

示例应用代码每次设置终结点时都会重复 todoitems URL 前缀。 API 通常具有带常见 URL 前缀的终结点组,并且 MapGroup 方法可用于帮助组织此类组。 它减少了重复代码,并允许通过对 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>
  • Map<HttpVerb> 方法调用中移除 URL 前缀 /todoitems

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

使用 TypedResults API

返回 TypedResults(而不是 Results)有几个优点,包括可测试性和自动返回 OpenAPI 的响应类型元数据来描述终结点。 有关详细信息,请参阅 TypedResults 与 Results

Map<HttpVerb> 方法可以调用路由处理程序方法,而不是使用 lambda。 若要查看示例,请使用以下代码更新 Program.cs:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}

Map<HttpVerb> 代码现在调用方法,而不是 lambda:

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

这些方法返回实现 IResult 并由 TypedResults 定义的对象:

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}

单元测试可以调用这些方法并测试它们是否返回正确的类型。 例如,如果方法是 GetAllTodos

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

单元测试代码可以验证是否从处理程序方法返回了 Ok<Todo[]> 类型的对象。 例如:

public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
    // Arrange
    var db = CreateDbContext();

    // Act
    var result = await TodosApi.GetAllTodos(db);

    // Assert: Check for the correct returned type
    Assert.IsType<Ok<Todo[]>>(result);
}

防止过度发布

目前,示例应用公开了整个 Todo 对象。 生产应用通常使用模型的子集来限制输入和返回的数据。 这背后有多种原因,但安全性是主要原因。 模型的子集通常称为数据传输对象 (DTO)、输入模型或视图模型。 本文使用的是 DTO

DTO 可用于:

  • 防止过度发布。
  • 隐藏客户端不应查看的属性。
  • 省略一些属性以缩减有效负载大小。
  • 平展包含嵌套对象的对象图。 对客户端而言,平展的对象图可能更方便。

若要演示 DTO 方法,请更新 Todo 类,使其包含机密字段:

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}

此应用需要隐藏机密字段,但管理应用可以选择公开它。

确保可以发布和获取机密字段。

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

public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}

更新 Program.cs 中的代码以使用此 DTO 模型:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

RouteGroupBuilder todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db) {
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(new TodoItemDTO(todo))
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    db.Todos.Add(todoItem);
    await db.SaveChangesAsync();

    todoItemDTO = new TodoItemDTO(todoItem);

    return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}

static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}

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

后续步骤

配置 JSON 序列化选项

有关如何在最小 API 应用中配置 JSON 序列化的信息,请参阅配置 JSON 序列化选项

处理错误和异常

默认情况下,在开发环境中为最少 API 应用启用开发人员例外页。 有关如何处理错误和异常的信息,请参阅处理 ASP.NET Core API 中的错误

测试最小 API 应用

有关测试最小 API 应用的示例,请参阅此 GitHub 示例

使用 OpenAPI (Swagger)

有关如何将 OpenAPI 与最小 API 应用配合使用的信息,请参阅在最少 API 中使用 OpenAPI 支持

发布到 Azure

有关如何部署到 Azure 的信息,请参阅快速入门:部署 ASP.NET Web 应用

了解更多

有关最小 API 应用的详细信息,请参阅最小 API 快速参考

构建最小 API,以创建具有最小依赖项的 HTTP API。 它们非常适合于需要在 ASP.NET Core 中仅包括最少文件、功能和依赖项的微服务和应用。

本教程介绍使用 ASP.NET Core 生成最小 API 的基础知识。 在 ASP.NET Core 中创建 API 的另一种方法是使用控制器。 有关在最小 API 和基于控制器的 API 之间进行选择的帮助,请参阅 API 概述。 有关基于包含更多功能的控制器创建 API 项目的教程,请参阅创建 Web API

概述

本教程将创建以下 API:

API 描述 请求正文 响应正文
GET /todoitems 获取所有待办事项 None 待办事项的数组
GET /todoitems/complete 获取已完成的待办事项 None 待办事项的数组
GET /todoitems/{id} 按 ID 获取项 None 待办事项
POST /todoitems 添加新项 待办事项 待办事项
PUT /todoitems/{id} 更新现有项 待办事项 None
DELETE /todoitems/{id}     删除项 None

先决条件

创建 API 项目

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

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

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

    Visual Studio Create a new project

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

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

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

    Additional information

检查代码

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

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

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

app.Run();

前面的代码:

运行应用

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

Visual Studio 会显示以下对话框:

This project is configured to use SSL. To avoid SSL warnings in the browser you can choose to trust the self-signed certificate that IIS Express has generated. Would you like to trust the IIS Express SSL certificate?

如果信任 IIS Express SSL 证书,请选择“是”

将显示以下对话框:

Security warning dialog

如果你同意信任开发证书,请选择“是”。

有关信任 Firefox 浏览器的信息,请参阅 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 证书错误

Visual Studio 启动 Kestrel Web 服务器,然后打开浏览器窗口。

Hello World! 将显示在浏览器中。 Program.cs 文件包含一个最小但完整的应用。

添加 NuGet 包

必须添加 NuGet 包以支持本教程中使用的数据库和诊断。

  • 在“工具”菜单中,选择“NuGet 包管理器”>“管理解决方案的 NuGet 包”。
  • 选择“浏览”选项卡。
  • 在搜索框中输入“Microsoft.EntityFrameworkCore.InMemory”,然后选择 Microsoft.EntityFrameworkCore.InMemory
  • 选中右窗格中的“项目”复选框,然后选择“安装” 。
  • 按照上述说明添加 Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore 包。

模型和数据库上下文类

在项目文件夹中,创建名为 Todo.cs 的文件,包含以下代码:

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

前面的代码为此应用创建模型。 模型是一个表示应用管理的数据的类。

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

using Microsoft.EntityFrameworkCore;

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

前面的代码定义了数据库上下文,它是为数据模型协调实体框架功能的主类。 此类从 Microsoft.EntityFrameworkCore.DbContext 类派生。

添加 API 代码

Program.cs 文件的内容替换为以下代码:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
});

app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    return Results.NotFound();
});

app.Run();

以下突出显示的代码将数据库上下文添加到依赖关系注入 (DI) 容器,并且允许显示与数据库相关的异常:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

DI 容器提供对数据库上下文和其他服务的访问权限。

安装 Postman 以测试应用

本教程使用 Postman 测试 Web API。

  • 安装 Postman
  • 启动 Web 应用。
  • 启动 Postman。
  • 选择“工作区”>“创建工作区”,然后选择“下一步”。
  • 将工作区命名为 TodoApi,然后选择“创建”。
  • 选择设置齿轮图标>设置(“常规”选项卡),然后禁用 SSL 证书验证

    警告

    在测试示例应用之后重新启用 SSL 证书验证。

测试发布数据

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 终结点将数据添加到应用:

  • 在 Postman 中,通过选择“新建”>“HTTP”来创建新的 HTTP 请求。

  • 将 HTTP 方法设置为 POST

  • 将 URI 设置为 https://localhost:<port>/todoitems。 例如: https://localhost:5001/todoitems

  • 选择“正文”选项卡。

  • 选择“raw”。

  • 将类型设置为 JSON。

  • 在请求正文中,输入待办事项的 JSON:

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • 选择Send

    Postman with Post request details

检查 GET 终结点

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

API 描述 请求正文 响应正文
GET /todoitems 获取所有待办事项 None 待办事项的数组
GET /todoitems/complete 获取所有已完成的待办事项 None 待办事项的数组
GET /todoitems/{id} 按 ID 获取项 None 待办事项
app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync());

app.MapGet("/todoitems/complete", async (TodoDb db) =>
    await db.Todos.Where(t => t.IsComplete).ToListAsync());

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

测试 GET 终结点

通过从浏览器或 Postman 调用终结点来测试应用。 以下步骤适用于 Postman。

  • 创建新的 HTTP 请求。
  • 将 HTTP 方法设置为“GET”。
  • 将请求 URI 设置为 https://localhost:<port>/todoitems。 例如 https://localhost:5001/todoitems
  • 选择Send

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

[
  {
    "id": 1,
    "name": "walk dog",
    "isComplete": false
  }
]
  • 将请求 URI 设置为 https://localhost:<port>/todoitems/1。 例如 https://localhost:5001/todoitems/1
  • 选择Send
  • 响应类似于以下内容:
    {
      "id": 1,
      "name": "walk dog",
      "isComplete": false
    }
    

此应用使用内存中数据库。 如果已重新启动应用,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 的待办事项并将其名称设置为 "feed fish"

{
  "id": 1,
  "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();
});

使用 Postman 删除待办事项:

  • 将方法设置为 DELETE
  • 设置要删除的对象的 URI,例如 https://localhost:5001/todoitems/1
  • 选择Send

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

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

使用 TypedResults API

返回 TypedResults(而不是 Results)有几个优点,包括可测试性和自动返回 OpenAPI 的响应类型元数据来描述终结点。 有关详细信息,请参阅 TypedResults 与 Results

Map<HttpVerb> 方法可以调用路由处理程序方法,而不是使用 lambda。 若要查看示例,请使用以下代码更新 Program.cs:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}

Map<HttpVerb> 代码现在调用方法,而不是 lambda:

var todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

这些方法返回实现 IResult 并由 TypedResults 定义的对象:

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(todo)
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}

static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}

单元测试可以调用这些方法并测试它们是否返回正确的类型。 例如,如果方法是 GetAllTodos

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.ToArrayAsync());
}

单元测试代码可以验证是否从处理程序方法返回了 Ok<Todo[]> 类型的对象。 例如:

public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
    // Arrange
    var db = CreateDbContext();

    // Act
    var result = await TodosApi.GetAllTodos(db);

    // Assert: Check for the correct returned type
    Assert.IsType<Ok<Todo[]>>(result);
}

防止过度发布

目前,示例应用公开了整个 Todo 对象。 生产应用通常使用模型的子集来限制输入和返回的数据。 这背后有多种原因,但安全性是主要原因。 模型的子集通常称为数据传输对象 (DTO)、输入模型或视图模型。 本文使用的是 DTO

DTO 可用于:

  • 防止过度发布。
  • 隐藏客户端不应查看的属性。
  • 省略一些属性以缩减有效负载大小。
  • 平展包含嵌套对象的对象图。 对客户端而言,平展的对象图可能更方便。

若要演示 DTO 方法,请更新 Todo 类,使其包含机密字段:

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public string? Secret { get; set; }
}

此应用需要隐藏机密字段,但管理应用可以选择公开它。

确保可以发布和获取机密字段。

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

public class TodoItemDTO
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    public TodoItemDTO() { }
    public TodoItemDTO(Todo todoItem) =>
    (Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}

更新 Program.cs 中的代码以使用此 DTO 模型:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

RouteGroupBuilder todoItems = app.MapGroup("/todoitems");

todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);

app.Run();

static async Task<IResult> GetAllTodos(TodoDb db)
{
    return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}

static async Task<IResult> GetCompleteTodos(TodoDb db) {
    return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}

static async Task<IResult> GetTodo(int id, TodoDb db)
{
    return await db.Todos.FindAsync(id)
        is Todo todo
            ? TypedResults.Ok(new TodoItemDTO(todo))
            : TypedResults.NotFound();
}

static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
    var todoItem = new Todo
    {
        IsComplete = todoItemDTO.IsComplete,
        Name = todoItemDTO.Name
    };

    db.Todos.Add(todoItem);
    await db.SaveChangesAsync();

    todoItemDTO = new TodoItemDTO(todoItem);

    return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}

static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return TypedResults.NotFound();

    todo.Name = todoItemDTO.Name;
    todo.IsComplete = todoItemDTO.IsComplete;

    await db.SaveChangesAsync();

    return TypedResults.NoContent();
}

static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
    if (await db.Todos.FindAsync(id) is Todo todo)
    {
        db.Todos.Remove(todo);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    return TypedResults.NotFound();
}

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

后续步骤

配置 JSON 序列化选项

有关如何在最小 API 应用中配置 JSON 序列化的信息,请参阅配置 JSON 序列化选项

处理错误和异常

默认情况下,在开发环境中为最少 API 应用启用开发人员例外页。 有关如何处理错误和异常的信息,请参阅处理 ASP.NET Core API 中的错误

测试最小 API 应用

有关测试最小 API 应用的示例,请参阅此 GitHub 示例

使用 OpenAPI (Swagger)

有关如何将 OpenAPI 与最小 API 应用配合使用的信息,请参阅在最少 API 中使用 OpenAPI 支持

发布到 Azure

有关如何部署到 Azure 的信息,请参阅快速入门:部署 ASP.NET Web 应用

了解更多

有关最小 API 应用的详细信息,请参阅最小 API 快速参考

构建最小 API,以创建具有最小依赖项的 HTTP API。 它们非常适合于需要在 ASP.NET Core 中仅包括最少文件、功能和依赖项的微服务和应用。

本教程介绍使用 ASP.NET Core 生成最小 API 的基础知识。 有关基于包含更多功能的控制器创建 API 项目的教程,请参阅创建 Web API。 有关比较,请参阅本文档中最小 API 与具有控制器的 API 之间的差异

概述

本教程将创建以下 API:

API 描述 请求正文 响应正文
GET / 浏览器测试,“Hello World” Hello World!
GET /todoitems 获取所有待办事项 None 待办事项的数组
GET /todoitems/complete 获取已完成的待办事项 None 待办事项的数组
GET /todoitems/{id} 按 ID 获取项 None 待办事项
POST /todoitems 添加新项 待办事项 待办事项
PUT /todoitems/{id} 更新现有项 待办事项 None
DELETE /todoitems/{id}     删除项 None

先决条件

VS22 installer workloads

创建 API 项目

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

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

    • 在“搜索模板”搜索框中输入 API
    • 选择“ASP.NET Core Web API”模板,然后选择“下一步”。 Visual Studio Create a new project
  • 将项目命名为 TodoApi,然后选择“下一步”。

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

    • 选择“.NET 6.0 (长期支持)”
    • 删除“使用控制器(取消选中以使用最小 API)”
    • 选择“创建”

Additional information

检查代码

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

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

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
       (
           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(DateTime Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

项目模板创建了一个支持 SwaggerWeatherForecast API。 Swagger 用于为 API 生成有用的文档和帮助页面。

以下突出显示的代码添加了对 Swagger 的支持:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

运行应用

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

Visual Studio 会显示以下对话框:

This project is configured to use SSL. To avoid SSL warnings in the browser you can choose to trust the self-signed certificate that IIS Express has generated. Would you like to trust the IIS Express SSL certificate?

如果信任 IIS Express SSL 证书,请选择“是”

将显示以下对话框:

Security warning dialog

如果你同意信任开发证书,请选择“是”。

有关信任 Firefox 浏览器的信息,请参阅 Firefox SEC_ERROR_INADEQUATE_KEY_USAGE 证书错误

Visual Studio 启动 Kestrel Web s服务器

随即显示 Swagger 页面 /swagger/index.html。 选择 GET > Try it out> Execute。 页面将显示:

  • 用于测试 WeatherForecast API 的 Curl 命令。
  • 用于测试 WeatherForecast API 的 URL。
  • 响应代码、正文和标头。
  • 包含媒体类型、示例值和架构的下拉列表框。

将请求 URL 复制粘贴到浏览器中:https://localhost:<port>/WeatherForecast。 返回类似于以下项的 JSON:

[
  {
    "date": "2021-10-19T14:12:50.3079024-10:00",
    "temperatureC": 13,
    "summary": "Bracing",
    "temperatureF": 55
  },
  {
    "date": "2021-10-20T14:12:50.3080559-10:00",
    "temperatureC": -8,
    "summary": "Bracing",
    "temperatureF": 18
  },
  {
    "date": "2021-10-21T14:12:50.3080601-10:00",
    "temperatureC": 12,
    "summary": "Hot",
    "temperatureF": 53
  },
  {
    "date": "2021-10-22T14:12:50.3080603-10:00",
    "temperatureC": 10,
    "summary": "Sweltering",
    "temperatureF": 49
  },
  {
    "date": "2021-10-23T14:12:50.3080604-10:00",
    "temperatureC": 36,
    "summary": "Warm",
    "temperatureF": 96
  }
]

更新生成的代码

本教程重点介绍如何创建 API,以便删除 Swagger 代码和 WeatherForecast 代码。 将 Program.cs 文件的内容替换为以下内容:

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

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

app.Run();

以下突出显示的代码创建具有预配置默认值的 WebApplicationBuilderWebApplication

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

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

app.Run();

以下代码创建返回 Hello World! 的 HTTP GET 终结点 /

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

app.Run(); 运行应用。

Properties/launchSettings.json 文件中删除两个 "launchUrl": "swagger", 行。 如果未指定 launchUrl,Web 浏览器将请求 / 终结点。

运行应用。 此时将显示 Hello World!。 更新后的 Program.cs 文件包含一个最小但完整的应用。

添加 NuGet 包

必须添加 NuGet 包以支持本教程中使用的数据库和诊断。

  • 在“工具”菜单中,选择“NuGet 包管理器”>“管理解决方案的 NuGet 包”。
  • 在搜索框中输入“Microsoft.EntityFrameworkCore.InMemory”,然后选择 Microsoft.EntityFrameworkCore.InMemory
  • 选中右窗格中的“项目”复选框,然后选择“安装” 。
  • 按照上述说明添加 Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore 包。

添加 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();

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

模型和数据库上下文类

示例应用包含以下模型:

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

模型是一个表示应用管理的数据的类。 此应用的模型是 Todo 类。

示例应用包含以下数据库上下文类:

class TodoDb : DbContext
{
    public TodoDb(DbContextOptions<TodoDb> options)
        : base(options) { }

    public DbSet<Todo> Todos => Set<Todo>();
}

数据库上下文是为数据模型协调 Entity Framework 功能的主类。 此类由 Microsoft.EntityFrameworkCore.DbContext 类派生而来。

以下突出显示的代码将数据库上下文添加到依赖关系注入 (DI) 容器,并且允许显示与数据库相关的异常:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();

DI 容器提供对数据库上下文和其他服务的访问权限。

以下代码创建 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);
});

安装 Postman 以测试应用

本教程使用 Postman 测试 Web API。

  • 安装 Postman
  • 启动 Web 应用。
  • 启动 Postman。
  • 禁用 SSL 证书验证
    • 在“文件”>“设置”(“常规”选项卡)中,禁用“SSL 证书验证” 。

      警告

      在测试控制器之后重新启用 SSL 证书验证。

测试发布数据

以下说明将数据发布到应用:

  • 创建新的 HTTP 请求。

  • 将 HTTP 方法设置为 POST

  • 将 URI 设置为 https://localhost:<port>/todoitems。 例如: https://localhost:5001/todoitems

  • 选择“正文”选项卡。

  • 选择“raw”。

  • 将类型设置为 JSON。

  • 在请求正文中,输入待办事项的 JSON:

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • 选择SendPostman with Post request details

检查 GET 终结点

示例应用使用对 MapGet 的调用实现多个 GET 终结点:

API 描述 请求正文 响应正文
GET / 浏览器测试,“Hello World” Hello World!
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 终结点

通过从浏览器或 Postman 调用两个终结点来测试应用。 例如:

  • GET https://localhost:5001/todoitems
  • GET https://localhost:5001/todoitems/1

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

[
  {
    "id": 1,
    "name": "Item1",
    "isComplete": false
  }
]

使用 Postman 测试 GET 终结点

  • 创建新的 HTTP 请求。
  • 将 HTTP 方法设置为“GET”。
  • 将请求 URI 设置为 https://localhost:<port>/todoitems。 例如 https://localhost:5001/todoitems
  • 选择Send

此应用使用内存中数据库。 如果已重新启动应用,GET 请求不会返回任何数据。 如果未返回任何数据,则首先使用 POST 将数据发布到应用。

返回值

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 的待办事项并将其名称设置为 "feed fish"

{
  "id": 1,
  "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();
});

使用 Postman 删除待办事项:

  • 将方法设置为 DELETE
  • 设置要删除的对象的 URI,例如 https://localhost:5001/todoitems/1
  • 选择Send

防止过度发布

目前,示例应用公开了整个 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; }
}

此应用需要隐藏机密字段,但管理应用可以选择公开它。

确保可以发布和获取机密字段。

创建 DTO 模型:

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

更新代码以使用 TodoItemDTO

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 之间的差异

使用 JsonOptions

以下代码使用 JsonOptions

using Microsoft.AspNetCore.Http.Json;

var builder = WebApplication.CreateBuilder(args);

// Configure JSON options
builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapGet("/", () => new Todo { Name = "Walk dog", IsComplete = false });

app.Run();

class Todo
{
    // These are public fields instead of properties.
    public string? Name;
    public bool IsComplete;
}

以下代码使用 JsonSerializerOptions

using System.Text.Json;

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

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);

app.MapGet("/", () => Results.Json(new Todo {
                      Name = "Walk dog", IsComplete = false }, options));

app.Run();

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

上述代码使用 Web 默认值,它将属性名称转换为 camel 大小写。

测试最小 API

有关测试最小 API 应用的示例,请参阅此 GitHub 示例

发布到 Azure

有关部署到 Azure 的信息,请参阅快速入门:部署 ASP.NET Web 应用

其他资源