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

注意

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

警告

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

重要

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

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

作者:Rick AndersonKirk Larkin

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

概述

本教程将创建以下 API:

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

下图显示了应用的设计。

客户端由左侧的框表示。它提交请求并接收来自应用程序的响应,由右侧的框表示。在应用程序框中,三个框分别代表控制器、模型和数据访问层。请求进入应用程序的控制器,在控制器和数据访问层之间发生读/写操作。模型被序列化并在响应中返回给客户端。

先决条件

创建 Web 项目

  • 从“文件”菜单中选择“新建”>“项目” 。
  • 在搜索框中输入“Web API”。
  • 选择“ASP.NET Core Web API”模板,然后选择“下一步”。
  • 在“配置新项目”对话框中,将项目命名为“TodoApi”,然后选择“下一步”
  • 在“其他信息”对话框中:
    • 确认“框架”为“.NET 8.0 (长期支持)”
    • 确认已选中“使用控制器(取消选中以使用最小 API)”。
    • 确认已选中“启用 OpenAPI 支持”复选框。
    • 选择“创建”。

添加 NuGet 包

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

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

说明

有关将包添加到 .NET 应用的指南,请参阅包使用工作流(NuGet 文档)中“安装和管理包”下的文章。 在 NuGet.org 中确认正确的包版本。

测试项目

项目模板创建了一个支持 SwaggerWeatherForecast API。

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

如果尚未将项目配置为使用 SSL,Visual Studio 显示以下对话:

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

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

将显示以下对话框:

安全警告对话

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

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

Visual Studio 将启动默认浏览器并导航到 https://localhost:<port>/swagger/index.html,其中 <port> 是在创建项目时设置的一个随机选择的端口号。

随即显示 Swagger 页面 /swagger/index.html。 选择 GET >“试用”>“执行” 。 页面将显示:

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

如果 Swagger 页面未显示,请参阅此 GitHub 问题

Swagger 用于为 Web API 生成有用的文档和帮助页面。 本教程使用 Swagger 测试应用。 有关 Swagger 的详细信息,请参阅包含 Swagger / OpenAPI 的 ASP.NET Core Web API 文档

将请求 URL 复制粘贴到浏览器中:https://localhost:<port>/weatherforecast

返回类似于以下示例的 JSON:

[
    {
        "date": "2019-07-16T19:04:05.7257911-06:00",
        "temperatureC": 52,
        "temperatureF": 125,
        "summary": "Mild"
    },
    {
        "date": "2019-07-17T19:04:05.7258461-06:00",
        "temperatureC": 36,
        "temperatureF": 96,
        "summary": "Warm"
    },
    {
        "date": "2019-07-18T19:04:05.7258467-06:00",
        "temperatureC": 39,
        "temperatureF": 102,
        "summary": "Cool"
    },
    {
        "date": "2019-07-19T19:04:05.7258471-06:00",
        "temperatureC": 10,
        "temperatureF": 49,
        "summary": "Bracing"
    },
    {
        "date": "2019-07-20T19:04:05.7258474-06:00",
        "temperatureC": -1,
        "temperatureF": 31,
        "summary": "Chilly"
    }
]

添加模型类

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

  • 在“解决方案资源管理器”中,右键单击项目。 选择“添加”>“新建文件夹”。 将该文件夹命名为 Models注册一个免费试用帐户。
  • 右键单击 Models 文件夹,然后选择“添加”>“类”。 将类命名为 TodoItem,然后选择“添加”。
  • 将模板代码替换为以下内容:
namespace TodoApi.Models;

public class TodoItem
{
    public long Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

Id 属性用作关系数据库中的唯一键。

模型类可位于项目的任意位置,但按照惯例会使用 Models 文件夹。

添加数据库上下文

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

  • 右键单击 Models 文件夹,然后选择“添加”>“类”。 将类命名为 TodoContext,然后单击“添加”。
  • 输入以下代码:

    using Microsoft.EntityFrameworkCore;
    
    namespace TodoApi.Models;
    
    public class TodoContext : DbContext
    {
        public TodoContext(DbContextOptions<TodoContext> options)
            : base(options)
        {
        }
    
        public DbSet<TodoItem> TodoItems { get; set; } = null!;
    }
    

注册数据库上下文

在 ASP.NET Core 中,服务(如数据库上下文)必须向依赖关系注入 (DI) 容器进行注册。 该容器向控制器提供服务。

使用以下突出显示的代码更新 Program.cs

using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddDbContext<TodoContext>(opt =>
    opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

前面的代码:

  • 添加 using 指令。
  • 将数据库上下文添加到 DI 容器。
  • 指定数据库上下文将使用内存中数据库。

构建控制器

  • 右键单击 Controllers 文件夹。

  • 选择 添加>New Scaffolded Item

  • 选择“其操作使用实体框架的 API 控制器”,然后选择“添加” 。

  • 在“添加其操作使用实体框架的 API 控制器”对话框中:

    • 在“模型类”中选择“TodoItem (TodoApi.Models)” 。
    • 在“数据上下文类”中选择“TodoContext (TodoAPI.Models)” 。
    • 选择“添加”。

    如果基架操作失败,请选择“添加”以第二次尝试使用基架。

生成的代码:

  • 使用 [ApiController] 属性标记类。 此属性指示控制器响应 Web API 请求。 有关该属性启用的特定行为的信息,请参阅使用 ASP.NET Core 创建 Web API
  • 使用 DI 将数据库上下文 (TodoContext) 注入到控制器中。 数据库上下文将在控制器中的每个 CRUD 方法中使用。

ASP.NET Core 模板:

  • 具有视图的控制器在路由模板中包含 [action]
  • API 控制器不在路由模板中包含 [action]

[action] 令牌不在路由模板中时,终结点中不包含 action 名称(方法名称)。 也就是说,不会在匹配的路由中使用操作的关联方法名称。

更新 PostTodoItem create 方法

更新 PostTodoItem 中的 return 语句,以使用 nameof 运算符:

[HttpPost]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
{
    _context.TodoItems.Add(todoItem);
    await _context.SaveChangesAsync();

    //    return CreatedAtAction("GetTodoItem", new { id = todoItem.Id }, todoItem);
    return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id }, todoItem);
}

上述代码是 HTTP POST 方法,如 [HttpPost] 属性所指示。 此方法从 HTTP 请求正文获取 TodoItem 的值。

有关详细信息,请参阅使用 Http [Verb] 特性的特性路由

CreatedAtAction 方法:

  • 如果成功,将返回 HTTP 201 状态代码HTTP 201 是在服务器上创建新资源的 HTTP POST 方法的标准响应。
  • 向响应添加位置标头。 Location 标头指定新建的待办事项的 URI。 有关详细信息,请参阅创建的 10.2.2 201
  • 引用 GetTodoItem 操作以创建 Location 标头的 URI。 C# nameof 关键字用于避免在 CreatedAtAction 调用中硬编码操作名称。

测试 PostTodoItem

  • 按 Ctrl+F5 运行应用。

  • 在 Swagger 浏览器窗口中,选择“POST /api/TodoItems”,然后选择“试用”。

  • 在“请求正文”输入窗口中,更新 JSON。 例如,

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

    Swagger POST

测试位置标头 URI

在上述 POST 中,Swagger UI 在“响应标头”下显示了位置标头。 例如 location: https://localhost:7260/api/TodoItems/1。 位置标头显示创建资源的 URI。

测试位置标头:

  • 在 Swagger 浏览器窗口中,选择“GET /api/TodoItems/{id}”,然后选择“试用”。

  • id 输入框中输入 1,然后选择“执行”。

    Swagger GET

检查 GET 方法

实现了两个 GET 终结点:

  • GET /api/todoitems
  • GET /api/todoitems/{id}

上一节展示了 /api/todoitems/{id} 路由的示例。

按照 POST 说明添加另一待办事项,然后使用 Swagger 测试 /api/todoitems 路由。

此应用使用内存中数据库。 如果停止并启动应用,则前面的 GET 请求不会返回任何数据。 如果未返回任何数据,将数据 POST 到应用。

路由和 URL 路径

[HttpGet] 属性表示响应 HTTP GET 请求的方法。 每个方法的 URL 路径构造如下所示:

  • 在控制器的 Route 属性中以模板字符串开头:

    [Route("api/[controller]")]
    [ApiController]
    public class TodoItemsController : ControllerBase
    
  • [controller] 替换为控制器的名称,按照惯例,在控制器类名称中去掉“Controller”后缀。 对于此示例,控制器类名称为“TodoItems”控制器,因此控制器名称为“TodoItems”。 ASP.NET Core 路由不区分大小写。

  • 如果 [HttpGet] 属性具有路由模板(例如 [HttpGet("products")]),则将它追加到路径。 此示例不使用模板。 有关详细信息,请参阅使用 Http [Verb] 特性的特性路由

在下面的 GetTodoItem 方法中,"{id}" 是待办事项的唯一标识符的占位符变量。 调用 GetTodoItem 时,URL 中 "{id}" 的值会在 id 参数中提供给方法。

[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null)
    {
        return NotFound();
    }

    return todoItem;
}

返回值

GetTodoItemsGetTodoItem 方法的返回类型是 ActionResult<T> 类型。 ASP.NET Core 自动将对象序列化为 JSON,并将 JSON 写入响应消息的正文中。 此返回类型的响应代码为 200 OK(假设没有未处理的异常)。 未经处理的异常将转换为 5xx 错误。

ActionResult 返回类型可以表示大范围的 HTTP 状态代码。 例如,GetTodoItem 可以返回两个不同的状态值:

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

PutTodoItem 方法

检查 PutTodoItem 方法:

[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
    if (id != todoItem.Id)
    {
        return BadRequest();
    }

    _context.Entry(todoItem).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!TodoItemExists(id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return NoContent();
}

PutTodoItemPostTodoItem 类似,但使用的是 HTTP PUT。 响应是 204(无内容)。 根据 HTTP 规范,PUT 请求需要客户端发送整个更新的实体,而不仅仅是更改。 若要支持部分更新,请使用 HTTP PATCH

测试 PutTodoItem 方法

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

使用 Swagger UI,使用 PUT 按钮更新 Id = 1 的 TodoItem 并将其名称设置为 "feed fish"。 请注意,响应为 HTTP 204 No Content

DeleteTodoItem 方法

检查 DeleteTodoItem 方法:

[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTodoItem(long id)
{
    var todoItem = await _context.TodoItems.FindAsync(id);
    if (todoItem == null)
    {
        return NotFound();
    }

    _context.TodoItems.Remove(todoItem);
    await _context.SaveChangesAsync();

    return NoContent();
}

测试 DeleteTodoItem 方法

使用 Swagger UI 删除 Id = 1 的 TodoItem。 请注意,响应为 HTTP 204 No Content

使用其他工具进行测试

还有许多其他工具可用于测试 Web API,例如:

有关详细信息,请参阅:

防止过度发布

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

DTO 可用于:

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

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

namespace TodoApi.Models
{
    public class TodoItem
    {
        public long Id { get; set; }
        public string? Name { get; set; }
        public bool IsComplete { get; set; }
        public string? Secret { get; set; }
    }
}

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

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

创建 DTO 模型:

namespace TodoApi.Models;

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

更新 TodoItemsController 以使用 TodoItemDTO

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace TodoApi.Controllers;

[Route("api/[controller]")]
[ApiController]
public class TodoItemsController : ControllerBase
{
    private readonly TodoContext _context;

    public TodoItemsController(TodoContext context)
    {
        _context = context;
    }

    // GET: api/TodoItems
    [HttpGet]
    public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
    {
        return await _context.TodoItems
            .Select(x => ItemToDTO(x))
            .ToListAsync();
    }

    // GET: api/TodoItems/5
    // <snippet_GetByID>
    [HttpGet("{id}")]
    public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
    {
        var todoItem = await _context.TodoItems.FindAsync(id);

        if (todoItem == null)
        {
            return NotFound();
        }

        return ItemToDTO(todoItem);
    }
    // </snippet_GetByID>

    // PUT: api/TodoItems/5
    // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
    // <snippet_Update>
    [HttpPut("{id}")]
    public async Task<IActionResult> PutTodoItem(long id, TodoItemDTO todoDTO)
    {
        if (id != todoDTO.Id)
        {
            return BadRequest();
        }

        var todoItem = await _context.TodoItems.FindAsync(id);
        if (todoItem == null)
        {
            return NotFound();
        }

        todoItem.Name = todoDTO.Name;
        todoItem.IsComplete = todoDTO.IsComplete;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException) when (!TodoItemExists(id))
        {
            return NotFound();
        }

        return NoContent();
    }
    // </snippet_Update>

    // POST: api/TodoItems
    // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
    // <snippet_Create>
    [HttpPost]
    public async Task<ActionResult<TodoItemDTO>> PostTodoItem(TodoItemDTO todoDTO)
    {
        var todoItem = new TodoItem
        {
            IsComplete = todoDTO.IsComplete,
            Name = todoDTO.Name
        };

        _context.TodoItems.Add(todoItem);
        await _context.SaveChangesAsync();

        return CreatedAtAction(
            nameof(GetTodoItem),
            new { id = todoItem.Id },
            ItemToDTO(todoItem));
    }
    // </snippet_Create>

    // DELETE: api/TodoItems/5
    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteTodoItem(long id)
    {
        var todoItem = await _context.TodoItems.FindAsync(id);
        if (todoItem == null)
        {
            return NotFound();
        }

        _context.TodoItems.Remove(todoItem);
        await _context.SaveChangesAsync();

        return NoContent();
    }

    private bool TodoItemExists(long id)
    {
        return _context.TodoItems.Any(e => e.Id == id);
    }

    private static TodoItemDTO ItemToDTO(TodoItem todoItem) =>
       new TodoItemDTO
       {
           Id = todoItem.Id,
           Name = todoItem.Name,
           IsComplete = todoItem.IsComplete
       };
}

确保无法发布或获取机密字段。

使用 JavaScript 调用 Web API

请参阅教程:使用 JavaScript 调用 ASP.NET Core Web API

Web API 视频系列

请参阅视频:初学者系列:Web API

可靠的 Web 应用模式

请观看《适用于 .NET 的可靠 Web 应用模式》YouTube 视频文章,了解如何创建新式、可靠、高性能、可测试、经济高效且可缩放的 ASP.NET Core 应用,无论是从头开始创建还是重构现有应用

向 Web API 添加身份验证支持

ASP.NET Core Identity 将用户界面 (UI) 登录功能添加到 ASP.NET Core Web 应用。 若要保护 Web API 和 SPA,请使用以下项之一:

Duende Identity Server 是适用于 ASP.NET Core 的 OpenID Connect 和 OAuth 2.0 框架。 Duende Identity Server 支持以下安全功能:

  • 身份验证即服务 (AaaS)
  • 跨多个应用程序类型的单一登录/注销 (SSO)
  • API 的访问控制
  • Federation Gateway

重要事项

Duende Software 可能会要求你为 Duende Identity Server 的生产使用支付许可证费用。 有关详细信息,请参阅从 ASP.NET Core 5.0 迁移到 6.0

有关详细信息,请参阅 Duende Identity Server 文档(Duende Software 网站)

发布到 Azure

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

其他资源

查看或下载本教程的示例代码。 请参阅如何下载

有关更多信息,请参见以下资源: