根据官方示例开发一个简单的 TODO API.

API 需求概述

要实现的是一个 TODO LIST 的后端, 需要实现如下 API:

  1. GET /api/TodoItems: 获取所有 TODO 列表
  2. GET /api/TodoItems/{id}: 获取指定 ID 的待办项
  3. POST /api/TodoItems: 添加一个待办事项
  4. PUT /api/TodoItems/{id}: 修改待办事项
  5. DELETE /api/TodoItems/{id}: 删除一个待办事项

且工程的结构按如下方式组织:

开始开发

根据结构设计, 按如下步骤实现整个项目:

  1. 实现数据访问层: 创建数据模型, 添加和使用数据库上下文.
  2. 实现表现层: 构建控制器(实现 API 端点), 创建 DTO 用于对外传递数据.

下面逐步进行实现.

实现数据访问层

数据访问层由如下两大内容构成:

  1. 模型类: 数据模型, 用于表示 APP 内部的数据.
  2. 数据库上下文: 用于访问数据库.

数据模型创建

本示例比较简单, 只有一个数据模型 TodoItem, 用于表示单个待办事项:

cs
namespace TodoApi.Models
{
    public class TodoItem
    {
        /// Id 作为关系数据库的唯一键, 即 Key.
        public long Id { get; set; }
        public string Name { get; set; }
        public bool IsComplete { get; set; }
    }
}

数据库上下文实现及数据库连接

数据库上下文类用于和 Entity Framework 协同工作, 派生自 Microsoft.EntityFrameworkCore.DbContext.

在开始开发阶段,可以使用内存数据库来测试,这样可以提高开发速度。

本示例准备使用 SQL Server 作为数据库, 故需要引入 Microsoft.EntityFrameworkCore.SqlServer 包, 且由于使用 ASP.NET Core 5.0, 所以需要对应 5.0 以上版本的依赖.

创建上下文 TodoContext:

cs
using Microsoft.EntityFrameworkCore;

namespace TodoApi.Models
{
    public class TodoContext : DbContext
    {
        public TodoContext(DbContextOptions<TodoContext> options)
            : base(options)
        {
        }

        public DbSet<TodoItem> TodoItems { get; set; }
    }
}

要使用数据库上下文, 需要进行依赖注入(DI), 在 Startup 的 ConfigureServices 方法中注入:

cs
public void ConfigureServices(IServiceCollection services)
{
    // 使用内存数据库进行测试, 可以自己配置 SQL Server 数据库
    services.AddDbContext<TodoContext>(opt => opt.UseInMemoryDatabase("TodoList"));
    services.AddControllers();
}

表现层开发

表现层用于对用户提供服务, 在这里需要实现一个提供 API 端点的控制器.

Todo 控制器实现

创建一个名为 TodoController 的控制器, 并用 [ApiController] 注解, 它派生自 ControllerBase:

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

    // 构造方法注入数据库上下文
    public TodoItemsController(TodoContext context)
    {
        _context = context;
    }
}

在其中主要实现四个方法, 即: 增删改查.

获取所有待办事项列表:

cs
[HttpGet]
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
{
    return await _context.TodoItems.ToListAsync();
}

获取指定 ID 的待办事项:

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

更新单个待办事项:

cs
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
    // 保证不会修改 item 的 Id
    if (id != todoItem.Id) return BadRequest();
    // 使用 Entry 接口来更新数据
    _context.Entry(todoItem).State = EntityState.Modified;
    await _context.SaveChangesAsync();
    return NoContent();
}

创建待办事项:

cs
[HttpPost]
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
{
    _context.TodoItems.Add(todoItem);
    await _context.SaveChangesAsync();
    // 不直接返回对象的好处有很多,也算作一种原则。
    return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id }, todoItem);
}

删除待办事项:

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

至此实现了对外的完整功能.

使用 Postman 检查接口

通过 Postman 检查接口是否正常即可.

防止过度发布: 使用 DTO

目前上述内容中对外公布的是完整的 TodoItem, 但生产应用通常使用数据模型的子集来限制输入和返回数据, 主要原因是安全性, 另外还有诸多原因. 模型的子集也叫 DTO/输入模型或视图模型, 即数据传输对象, 顾名思义, 就是用于在不同物理层间传递数据的对象.

使用 DTO 可以:

  • 防止过度发布
  • 隐藏客户端不该看到的数据属性
  • 省略一些属性以减少负载大小
  • 方便客户端使用

针对上面的例子, 比如在 TodoItem 中包含一个 Secret 属性, 但不想对客户端展示, 则可以创建一个 DTO:

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

// ...

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

创建 DTO 后, 就可以在表现层将数据模型替换为 DTO, 比如可以这样做:

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

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

这样客户端得到的就是 DTO 了.