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

作者:Rick Anderson

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

本教程介绍使用 ASP.NET Core 生成最小 Web API 的基础知识。 有关基于包含更多功能的控制器创建 Web API 项目的教程,请参阅创建 Web 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}     删除项

先决条件

VS22 安装程序工作负载

创建 Web API 项目

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

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

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

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

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

其他信息

检查代码

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

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

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

将显示以下对话框:

安全警告对话

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

有关信任 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
  }
]

更新生成的代码

本教程重点介绍如何创建 Web 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.Ok(todo);
    }

    return Results.NotFound();
});

app.Run();

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

模型和数据库上下文类

示例应用包含以下模型:

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 方法设置为 POST

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

  • 选择“正文”选项卡。

  • 选择“raw”。

  • 将类型设置为 JSON。

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

    {
      "name":"walk dog",
      "isComplete":true
    }
    
  • 选择Send具有 Post 请求详细信息的 Postman

检查 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 方法设置为“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.Ok(todo);
    }

    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.Ok(new TodoItemDTO(todo));
    }

    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 应用

其他资源