本文最后更新于 2021年4月4日 晚上
根据官方示例开发一个简单的 TODO API.
API 需求概述
要实现的是一个 TODO LIST 的后端, 需要实现如下 API:
- GET /api/TodoItems: 获取所有 TODO 列表
- GET /api/TodoItems/{id}: 获取指定 ID 的待办项
- POST /api/TodoItems: 添加一个待办事项
- PUT /api/TodoItems/{id}: 修改待办事项
- DELETE /api/TodoItems/{id}: 删除一个待办事项
且工程的结构按如下方式组织:
开始开发
根据结构设计, 按如下步骤实现整个项目:
- 实现数据访问层: 创建数据模型, 添加和使用数据库上下文.
- 实现表现层: 构建控制器(实现 API 端点), 创建 DTO 用于对外传递数据.
下面逐步进行实现.
实现数据访问层
数据访问层由如下两大内容构成:
- 模型类: 数据模型, 用于表示 APP 内部的数据.
- 数据库上下文: 用于访问数据库.
数据模型创建
本示例比较简单, 只有一个数据模型 TodoItem, 用于表示单个待办事项:
1 2 3 4 5 6 7 8 9 10
| namespace TodoApi.Models { public class TodoItem { 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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 方法中注入:
1 2 3 4 5 6
| public void ConfigureServices(IServiceCollection services) { services.AddDbContext<TodoContext>(opt => opt.UseInMemoryDatabase("TodoList")); services.AddControllers(); }
|
表现层开发
表现层用于对用户提供服务, 在这里需要实现一个提供 API 端点的控制器.
Todo 控制器实现
创建一个名为 TodoController 的控制器, 并用 [ApiController]
注解, 它派生自 ControllerBase
:
1 2 3 4 5 6 7 8 9 10 11 12
| [ApiController] [Route("api/[controller]")] public class TodoItemsController : ControllerBase { private readonly TodoContext _context;
public TodoItemsController(TodoContext context) { _context = context; } }
|
在其中主要实现四个方法, 即: 增删改查.
获取所有待办事项列表:
1 2 3 4 5
| [HttpGet] public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems() { return await _context.TodoItems.ToListAsync(); }
|
获取指定 ID 的待办事项:
1 2 3 4 5 6 7 8 9 10
| [HttpGet("{id}")] public async Task<ActionResult<TodoItem>> GetTodoItem(long id) { var todoItem = await _context.TodoItems.FindAsync(id); if (todoItem == null) { return NotFound(); } return todoItem; }
|
更新单个待办事项:
1 2 3 4 5 6 7 8 9 10
| [HttpPut("{id}")] public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem) { if (id != todoItem.Id) return BadRequest(); _context.Entry(todoItem).State = EntityState.Modified; await _context.SaveChangesAsync(); return NoContent(); }
|
创建待办事项:
1 2 3 4 5 6 7 8
| [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); }
|
删除待办事项:
1 2 3 4 5 6 7 8 9 10
| [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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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, 比如可以这样做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| [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 了.