第十五章我们学习了 EF Core,知道了如何用 C# 对象操作数据库。但现在的问题是:那些数据只有我们自己的程序能访问。如何让手机 App、网页、第三方程序都能访问我们的数据?这一章要学的 ASP.NET Core Web API 就是答案------它让我们构建 HTTP 服务,通过互联网提供数据访问接口。
16.1 什么是 Web API?
16.1.1 从一个问题说起
假设你开发了一个学生管理系统,里面有 10 万条学生数据。现在需求来了:
-
需要一个手机 App 查询学生信息
-
需要给第三方提供数据接口
-
需要一个网页展示学生统计图表
如果没有 Web API,每个客户端都要直接连接数据库------这非常危险(密码泄露、数据库暴露)、不可扩展。
16.1.2 Web API 的解决方案
text
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 手机App │ │ 网页 │ │ 第三方 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└───────────────┼───────────────┘
│ HTTP 请求/响应
▼
┌─────────────────┐
│ Web API 服务 │
│ (ASP.NET Core) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 数据库 │
└─────────────────┘
16.1.3 Web API vs 传统 Web 应用
| 特性 | 传统 Web 应用(MVC) | Web API |
|---|---|---|
| 返回内容 | HTML 页面 | JSON/XML 数据 |
| 客户端 | 浏览器 | 任何程序(App、网页、服务) |
| 用户界面 | 服务器端渲染 | 客户端独立负责 |
| 使用场景 | 普通网站 | 前后端分离、移动应用后端 |
16.2 创建第一个 Web API 项目
16.2.1 创建项目
bash
# 使用 dotnet CLI 创建
dotnet new webapi -n MyFirstApi
cd MyFirstApi
# 或使用 Visual Studio 创建
# 文件 → 新建 → 项目 → ASP.NET Core Web API
16.2.2 项目结构
text
MyFirstApi/
├── Controllers/ # 控制器(处理 HTTP 请求)
│ └── WeatherForecastController.cs
├── Program.cs # 程序入口和配置
├── appsettings.json # 配置文件
├── appsettings.Development.json # 开发环境配置
└── Properties/
└── launchSettings.json # 启动配置
16.2.3 运行项目
bash
dotnet run
默认访问:https://localhost:5001/swagger/index.html
16.2.4 第一个控制器
csharp
using Microsoft.AspNetCore.Mvc;
namespace MyFirstApi.Controllers;
// [ApiController]:启用 Web API 行为(模型验证、属性路由等)
[ApiController]
// [Route]:设置路由模板
[Route("api/[controller]")] // api/hello
public class HelloController : ControllerBase
{
// GET: api/hello
[HttpGet]
public IActionResult Get()
{
return Ok(new { message = "Hello, World!", time = DateTime.Now });
}
// GET: api/hello/zhangsan
[HttpGet("{name}")]
public IActionResult Get(string name)
{
return Ok(new { message = $"Hello, {name}!", time = DateTime.Now });
}
// POST: api/hello
[HttpPost]
public IActionResult Post([FromBody] HelloRequest request)
{
return Ok(new
{
message = $"收到消息:{request.Message}",
receivedAt = DateTime.Now
});
}
}
public class HelloRequest
{
public string Message { get; set; }
}
16.2.5 测试 API
使用 Swagger(自动生成的文档页面):
-
运行项目,打开
https://localhost:5001/swagger -
点击
GET /api/hello→ Try it out → Execute -
查看响应
或使用 curl:
bash
# GET 请求
curl https://localhost:5001/api/hello
# POST 请求
curl -X POST https://localhost:5001/api/hello \
-H "Content-Type: application/json" \
-d '{"message":"Hello API"}'
16.3 控制器与路由
16.3.1 路由基础
csharp
[ApiController]
[Route("api/[controller]")] // 模板:/api/Users
public class UsersController : ControllerBase
{
// GET: api/users
[HttpGet]
public IActionResult GetAll() { }
// GET: api/users/5
[HttpGet("{id}")]
public IActionResult GetById(int id) { }
// POST: api/users
[HttpPost]
public IActionResult Create([FromBody] User user) { }
// PUT: api/users/5
[HttpPut("{id}")]
public IActionResult Update(int id, [FromBody] User user) { }
// DELETE: api/users/5
[HttpDelete("{id}")]
public IActionResult Delete(int id) { }
}
16.3.2 HTTP 方法与语义
| HTTP 方法 | 语义 | 示例 |
|---|---|---|
| GET | 获取资源 | GET /api/users 获取所有用户 |
| GET | 获取单个资源 | GET /api/users/5 获取用户 5 |
| POST | 创建新资源 | POST /api/users 创建用户 |
| PUT | 完整更新 | PUT /api/users/5 完整更新用户 5 |
| PATCH | 部分更新 | PATCH /api/users/5 部分更新用户 |
| DELETE | 删除资源 | DELETE /api/users/5 删除用户 5 |
16.3.3 属性路由详解
csharp
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
// 静态路由
[HttpGet]
public IActionResult GetAll() { }
// 带参数的路由
[HttpGet("{id:int}")] // 约束 id 必须是整数
public IActionResult GetById(int id) { }
// 可选参数
[HttpGet("search/{category?}")]
public IActionResult Search(string category, [FromQuery] string keyword)
{
// URL: /api/products/search/electronics?keyword=phone
// category = "electronics", keyword = "phone"
return Ok();
}
// 自定义路由名称
[HttpGet("{id}/reviews", Name = "GetReviews")]
public IActionResult GetReviews(int id) { }
// 使用路由约束
[HttpGet("date/{date:datetime}")]
public IActionResult GetByDate(DateTime date) { }
// 多个 HTTP 方法
[HttpPost, HttpPut]
public IActionResult CreateOrUpdate() { }
}
16.3.4 参数来源
csharp
[ApiController]
[Route("api/[controller]")]
public class DemoController : ControllerBase
{
// [FromQuery]:从 URL 查询字符串获取
// GET /api/demo?page=1&size=10
[HttpGet]
public IActionResult GetList([FromQuery] int page = 1, [FromQuery] int size = 10)
{
return Ok(new { page, size });
}
// [FromRoute]:从路由参数获取(默认行为)
// GET /api/demo/5
[HttpGet("{id}")]
public IActionResult GetById([FromRoute] int id)
{
return Ok(new { id });
}
// [FromBody]:从请求体获取(JSON 格式)
// POST /api/demo
[HttpPost]
public IActionResult Create([FromBody] Product product)
{
return Ok(product);
}
// [FromHeader]:从请求头获取
[HttpGet("headers")]
public IActionResult GetHeaders([FromHeader] string userAgent, [FromHeader(Name = "X-Custom")] string custom)
{
return Ok(new { userAgent, custom });
}
// [FromForm]:从表单数据获取
[HttpPost("form")]
public IActionResult UploadForm([FromForm] string name, [FromForm] IFormFile file)
{
return Ok(new { name, fileLength = file?.Length });
}
// 复杂对象参数:默认从查询字符串绑定
// GET /api/demo/search?name=phone&minPrice=100&maxPrice=500
[HttpGet("search")]
public IActionResult Search([FromQuery] SearchCriteria criteria)
{
return Ok(criteria);
}
}
public class SearchCriteria
{
public string Name { get; set; }
public decimal MinPrice { get; set; }
public decimal MaxPrice { get; set; }
}
16.3.5 响应类型
csharp
[ApiController]
[Route("api/[controller]")]
public class ResponseController : ControllerBase
{
// 返回指定类型(自动序列化为 JSON)
[HttpGet("ok")]
public User GetOk()
{
return new User { Id = 1, Name = "张三" };
}
// 使用 IActionResult 控制状态码
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
if (id <= 0)
{
// 返回 400 Bad Request
return BadRequest("ID 必须大于 0");
}
var user = FindUser(id);
if (user == null)
{
// 返回 404 Not Found
return NotFound($"用户 {id} 不存在");
}
// 返回 200 OK
return Ok(user);
}
// 创建资源时返回 201 Created
[HttpPost]
public IActionResult Create([FromBody] User user)
{
user.Id = 100;
// 返回 201 Created,并包含 Location 头
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}
// 返回无内容(204 No Content)
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
// 执行删除
return NoContent();
}
// 自定义状态码
[HttpGet("custom")]
public IActionResult CustomStatus()
{
return StatusCode(418, "I'm a teapot");
}
}
16.4 依赖注入
16.4.1 什么是依赖注入?
依赖注入 = 让框架自动创建和管理对象,不用自己 new
csharp
// 没有依赖注入(紧耦合)
public class ProductController
{
private readonly ProductService _service;
public ProductController()
{
_service = new ProductService(); // 自己创建,难以测试
}
}
// 有依赖注入(松耦合)
public class ProductController
{
private readonly IProductService _service;
public ProductController(IProductService service) // 由框架注入
{
_service = service;
}
}
16.4.2 注册服务
csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// 添加服务到容器
// 1. 添加控制器服务
builder.Services.AddControllers();
// 2. 注册自定义服务(三种生命周期)
// Transient:每次都创建新实例
builder.Services.AddTransient<IEmailService, EmailService>();
// Scoped:每个 HTTP 请求一个实例
builder.Services.AddScoped<IUserService, UserService>();
// Singleton:整个应用生命周期一个实例
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();
// 3. 注册 DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
// 4. 注册 HttpClient
builder.Services.AddHttpClient();
// 5. 注册 Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
16.4.3 服务生命周期对比
| 生命周期 | 创建时机 | 适用场景 |
|---|---|---|
Transient |
每次请求都创建新实例 | 轻量级、无状态服务 |
Scoped |
每个 HTTP 请求一个实例 | DbContext、请求级别缓存 |
Singleton |
首次请求时创建,全程单例 | 配置、缓存、日志 |
16.4.4 在控制器中使用依赖注入
csharp
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
private readonly ILogger<UsersController> _logger;
private readonly AppDbContext _dbContext;
// 构造函数注入
public UsersController(
IUserService userService,
ILogger<UsersController> logger,
AppDbContext dbContext)
{
_userService = userService;
_logger = logger;
_dbContext = dbContext;
}
[HttpGet]
public async Task<IActionResult> GetAll()
{
_logger.LogInformation("获取所有用户");
var users = await _userService.GetAllAsync();
return Ok(users);
}
}
// 自定义服务接口
public interface IUserService
{
Task<List<User>> GetAllAsync();
Task<User> GetByIdAsync(int id);
Task<User> CreateAsync(User user);
}
public class UserService : IUserService
{
private readonly AppDbContext _dbContext;
public UserService(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<List<User>> GetAllAsync()
{
return await _dbContext.Users.ToListAsync();
}
public async Task<User> GetByIdAsync(int id)
{
return await _dbContext.Users.FindAsync(id);
}
public async Task<User> CreateAsync(User user)
{
_dbContext.Users.Add(user);
await _dbContext.SaveChangesAsync();
return user;
}
}
16.4.5 手动获取服务
csharp
// 在某些无法使用构造函数注入的地方,可以通过 IServiceProvider 获取
public class SomeClass
{
private readonly IServiceProvider _serviceProvider;
public SomeClass(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void DoSomething()
{
// 获取服务
var userService = _serviceProvider.GetService<IUserService>();
var logger = _serviceProvider.GetRequiredService<ILogger<SomeClass>>();
// 创建作用域
using (var scope = _serviceProvider.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 使用 dbContext
}
}
}
16.5 与 EF Core 集成
16.5.1 完整的 CRUD API 示例
csharp
// 实体类
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
// DTO(数据传输对象)— 不要直接把实体暴露给客户端
public class ProductDto
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
}
public class CreateProductDto
{
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
}
public class UpdateProductDto
{
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
}
// DbContext
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(200);
entity.Property(e => e.Description).HasMaxLength(1000);
entity.Property(e => e.Price).HasColumnType("decimal(18,2)");
});
}
}
// 控制器
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly AppDbContext _dbContext;
public ProductsController(AppDbContext dbContext)
{
_dbContext = dbContext;
}
// GET: api/products
[HttpGet]
public async Task<ActionResult<IEnumerable<ProductDto>>> GetProducts(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string search = null)
{
var query = _dbContext.Products.AsQueryable();
// 搜索过滤
if (!string.IsNullOrWhiteSpace(search))
{
query = query.Where(p => p.Name.Contains(search) ||
p.Description.Contains(search));
}
// 分页
var products = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
Stock = p.Stock
})
.ToListAsync();
// 返回总数
var totalCount = await query.CountAsync();
Response.Headers.Add("X-Total-Count", totalCount.ToString());
return Ok(products);
}
// GET: api/products/5
[HttpGet("{id}")]
public async Task<ActionResult<ProductDto>> GetProduct(int id)
{
var product = await _dbContext.Products.FindAsync(id);
if (product == null)
{
return NotFound();
}
return Ok(new ProductDto
{
Id = product.Id,
Name = product.Name,
Price = product.Price,
Stock = product.Stock
});
}
// POST: api/products
[HttpPost]
public async Task<ActionResult<ProductDto>> CreateProduct([FromBody] CreateProductDto createDto)
{
var product = new Product
{
Name = createDto.Name,
Description = createDto.Description,
Price = createDto.Price,
Stock = createDto.Stock,
CreatedAt = DateTime.UtcNow
};
_dbContext.Products.Add(product);
await _dbContext.SaveChangesAsync();
var productDto = new ProductDto
{
Id = product.Id,
Name = product.Name,
Price = product.Price,
Stock = product.Stock
};
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, productDto);
}
// PUT: api/products/5
[HttpPut("{id}")]
public async Task<IActionResult> UpdateProduct(int id, [FromBody] UpdateProductDto updateDto)
{
var product = await _dbContext.Products.FindAsync(id);
if (product == null)
{
return NotFound();
}
product.Name = updateDto.Name;
product.Description = updateDto.Description;
product.Price = updateDto.Price;
product.Stock = updateDto.Stock;
product.UpdatedAt = DateTime.UtcNow;
await _dbContext.SaveChangesAsync();
return NoContent();
}
// PATCH: api/products/5/stock
[HttpPatch("{id}/stock")]
public async Task<IActionResult> UpdateStock(int id, [FromBody] int stock)
{
var product = await _dbContext.Products.FindAsync(id);
if (product == null)
{
return NotFound();
}
product.Stock = stock;
product.UpdatedAt = DateTime.UtcNow;
await _dbContext.SaveChangesAsync();
return NoContent();
}
// DELETE: api/products/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProduct(int id)
{
var product = await _dbContext.Products.FindAsync(id);
if (product == null)
{
return NotFound();
}
_dbContext.Products.Remove(product);
await _dbContext.SaveChangesAsync();
return NoContent();
}
}
16.5.2 配置数据库连接
json
// appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=app.db",
"SqlServerConnection": "Server=localhost;Database=MyApiDb;Trusted_Connection=True;TrustServerCertificate=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// 配置 DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
// 或者使用 SQL Server
// builder.Services.AddDbContext<AppDbContext>(options =>
// options.UseSqlServer(builder.Configuration.GetConnectionString("SqlServerConnection")));
16.6 中间件
16.6.1 什么是中间件?
中间件 = 处理 HTTP 请求管道的组件,可以在请求到达控制器之前或之后执行代码
text
请求 → ┌─────────────────────────────────────────────────────────┐
│ 中间件管道 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 日志中间件│→│ 认证中间件│→│ 路由中间件│→│ 控制器 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ↓ ↓ ↓ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 响应中间件│←│ 响应中间件│←│ 响应中间件│ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────┘
↓
响应
16.6.2 内置中间件
csharp
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// 开发环境异常页面
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// 生产环境异常处理
app.UseExceptionHandler("/error");
// 静态文件
app.UseStaticFiles();
// HTTPS 重定向
app.UseHttpsRedirection();
// 路由
app.UseRouting();
// CORS(跨域)
app.UseCors();
// 认证
app.UseAuthentication();
// 授权
app.UseAuthorization();
// Swagger
app.UseSwagger();
app.UseSwaggerUI();
// 端点映射
app.MapControllers();
app.Run();
16.6.3 自定义中间件
csharp
// 方式1:使用 UseMiddleware 扩展方法
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// 请求进来时执行
_logger.LogInformation($"请求: {context.Request.Method} {context.Request.Path}");
// 记录请求开始时间
var startTime = DateTime.UtcNow;
// 调用下一个中间件
await _next(context);
// 响应返回时执行
var elapsed = DateTime.UtcNow - startTime;
_logger.LogInformation($"响应: {context.Response.StatusCode},耗时 {elapsed.TotalMilliseconds}ms");
}
}
// 注册中间件
app.UseMiddleware<RequestLoggingMiddleware>();
// 方式2:使用 Use 方法(简单中间件)
app.Use(async (context, next) =>
{
Console.WriteLine($"请求路径: {context.Request.Path}");
await next();
Console.WriteLine($"响应状态: {context.Response.StatusCode}");
});
// 方式3:条件中间件
app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), appBuilder =>
{
appBuilder.Use(async (context, next) =>
{
Console.WriteLine("API 请求处理中...");
await next();
});
});
16.6.4 全局异常处理中间件
csharp
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;
private readonly IWebHostEnvironment _env;
public GlobalExceptionMiddleware(
RequestDelegate next,
ILogger<GlobalExceptionMiddleware> logger,
IWebHostEnvironment env)
{
_next = next;
_logger = logger;
_env = env;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "未处理的异常");
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
var response = new ErrorResponse
{
StatusCode = StatusCodes.Status500InternalServerError,
Message = "服务器内部错误",
Path = context.Request.Path
};
if (_env.IsDevelopment())
{
response.Detail = exception.Message;
response.StackTrace = exception.StackTrace;
}
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(response);
}
}
public class ErrorResponse
{
public int StatusCode { get; set; }
public string Message { get; set; }
public string Detail { get; set; }
public string StackTrace { get; set; }
public string Path { get; set; }
}
// 注册
app.UseMiddleware<GlobalExceptionMiddleware>();
16.7 Swagger/OpenAPI 文档
16.7.1 什么是 Swagger?
Swagger 自动生成 API 文档,并提供测试界面
16.7.2 配置 Swagger
csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "我的 API",
Version = "v1",
Description = "这是一个示例 API",
Contact = new OpenApiContact
{
Name = "作者",
Email = "author@example.com"
}
});
// 启用 XML 注释
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
c.RoutePrefix = "swagger"; // 访问路径:/swagger
c.DocExpansion(DocExpansion.List); // 默认折叠
});
}
16.7.3 使用 XML 注释
csharp
// 在 .csproj 文件中启用 XML 注释
// <PropertyGroup>
// <GenerateDocumentationFile>true</GenerateDocumentationFile>
// <NoWarn>$(NoWarn);1591</NoWarn>
// </PropertyGroup>
/// <summary>
/// 用户控制器
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
/// <summary>
/// 获取所有用户
/// </summary>
/// <param name="page">页码</param>
/// <param name="pageSize">每页大小</param>
/// <returns>用户列表</returns>
/// <response code="200">成功返回用户列表</response>
/// <response code="400">参数错误</response>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<UserDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> GetUsers([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
// ...
}
/// <summary>
/// 根据 ID 获取用户
/// </summary>
/// <param name="id">用户 ID</param>
/// <returns>用户信息</returns>
[HttpGet("{id}")]
[ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetUser(int id)
{
// ...
}
}
16.8 综合示例:完整的博客 API
csharp
// ========== 实体类 ==========
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public string Slug { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public DateTime? PublishedAt { get; set; }
public bool IsPublished { get; set; }
// 关系
public int AuthorId { get; set; }
public User Author { get; set; }
public ICollection<Comment> Comments { get; set; }
public ICollection<Tag> Tags { get; set; }
}
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public string PasswordHash { get; set; }
public DateTime CreatedAt { get; set; }
public ICollection<Post> Posts { get; set; }
public ICollection<Comment> Comments { get; set; }
}
public class Comment
{
public int Id { get; set; }
public string Content { get; set; }
public DateTime CreatedAt { get; set; }
public int PostId { get; set; }
public Post Post { get; set; }
public int UserId { get; set; }
public User User { get; set; }
}
public class Tag
{
public int Id { get; set; }
public string Name { get; set; }
public string Slug { get; set; }
public ICollection<Post> Posts { get; set; }
}
// ========== DTO ==========
public class PostDto
{
public int Id { get; set; }
public string Title { get; set; }
public string Slug { get; set; }
public string Excerpt { get; set; }
public DateTime? PublishedAt { get; set; }
public AuthorDto Author { get; set; }
public List<string> Tags { get; set; }
public int CommentCount { get; set; }
}
public class PostDetailDto : PostDto
{
public string Content { get; set; }
public List<CommentDto> Comments { get; set; }
}
public class CreatePostDto
{
public string Title { get; set; }
public string Content { get; set; }
public List<string> Tags { get; set; }
public bool Publish { get; set; }
}
public class CommentDto
{
public int Id { get; set; }
public string Content { get; set; }
public DateTime CreatedAt { get; set; }
public string AuthorName { get; set; }
}
public class AuthorDto
{
public int Id { get; set; }
public string Username { get; set; }
}
// ========== DbContext ==========
public class BlogDbContext : DbContext
{
public BlogDbContext(DbContextOptions<BlogDbContext> options) : base(options) { }
public DbSet<Post> Posts { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<Comment> Comments { get; set; }
public DbSet<Tag> Tags { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 多对多关系配置
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(t => t.Posts)
.UsingEntity(j => j.ToTable("PostTags"));
// 唯一索引
modelBuilder.Entity<User>()
.HasIndex(u => u.Email)
.IsUnique();
modelBuilder.Entity<Post>()
.HasIndex(p => p.Slug)
.IsUnique();
// 默认值
modelBuilder.Entity<Post>()
.Property(p => p.CreatedAt)
.HasDefaultValueSql("CURRENT_TIMESTAMP");
}
}
// ========== 服务 ==========
public interface IPostService
{
Task<PostDto> GetPostBySlugAsync(string slug);
Task<List<PostDto>> GetPostsAsync(int page, int pageSize);
Task<PostDetailDto> CreatePostAsync(int userId, CreatePostDto dto);
}
public class PostService : IPostService
{
private readonly BlogDbContext _dbContext;
public PostService(BlogDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<PostDto> GetPostBySlugAsync(string slug)
{
return await _dbContext.Posts
.Where(p => p.Slug == slug && p.IsPublished)
.Select(p => new PostDto
{
Id = p.Id,
Title = p.Title,
Slug = p.Slug,
Excerpt = p.Content.Length > 200 ? p.Content.Substring(0, 200) + "..." : p.Content,
PublishedAt = p.PublishedAt,
Author = new AuthorDto { Id = p.Author.Id, Username = p.Author.Username },
Tags = p.Tags.Select(t => t.Name).ToList(),
CommentCount = p.Comments.Count
})
.FirstOrDefaultAsync();
}
public async Task<List<PostDto>> GetPostsAsync(int page, int pageSize)
{
return await _dbContext.Posts
.Where(p => p.IsPublished)
.OrderByDescending(p => p.PublishedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(p => new PostDto
{
Id = p.Id,
Title = p.Title,
Slug = p.Slug,
Excerpt = p.Content.Length > 200 ? p.Content.Substring(0, 200) + "..." : p.Content,
PublishedAt = p.PublishedAt,
Author = new AuthorDto { Id = p.Author.Id, Username = p.Author.Username },
Tags = p.Tags.Select(t => t.Name).ToList(),
CommentCount = p.Comments.Count
})
.ToListAsync();
}
public async Task<PostDetailDto> CreatePostAsync(int userId, CreatePostDto dto)
{
var post = new Post
{
Title = dto.Title,
Content = dto.Content,
Slug = GenerateSlug(dto.Title),
AuthorId = userId,
CreatedAt = DateTime.UtcNow,
IsPublished = dto.Publish,
PublishedAt = dto.Publish ? DateTime.UtcNow : null
};
// 处理标签
if (dto.Tags?.Any() == true)
{
var existingTags = await _dbContext.Tags
.Where(t => dto.Tags.Contains(t.Name))
.ToListAsync();
var newTagNames = dto.Tags.Except(existingTags.Select(t => t.Name)).ToList();
var newTags = newTagNames.Select(n => new Tag { Name = n, Slug = GenerateSlug(n) }).ToList();
_dbContext.Tags.AddRange(newTags);
post.Tags = existingTags.Concat(newTags).ToList();
}
_dbContext.Posts.Add(post);
await _dbContext.SaveChangesAsync();
return new PostDetailDto
{
Id = post.Id,
Title = post.Title,
Slug = post.Slug,
Content = post.Content,
PublishedAt = post.PublishedAt,
Tags = post.Tags.Select(t => t.Name).ToList()
};
}
private string GenerateSlug(string text)
{
var slug = text.ToLower().Replace(" ", "-");
slug = System.Text.RegularExpressions.Regex.Replace(slug, @"[^a-z0-9\-]", "");
return slug;
}
}
// ========== 控制器 ==========
[ApiController]
[Route("api/[controller]")]
public class PostsController : ControllerBase
{
private readonly IPostService _postService;
public PostsController(IPostService postService)
{
_postService = postService;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<PostDto>>> GetPosts(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10)
{
var posts = await _postService.GetPostsAsync(page, pageSize);
return Ok(posts);
}
[HttpGet("{slug}")]
public async Task<ActionResult<PostDetailDto>> GetPost(string slug)
{
var post = await _postService.GetPostBySlugAsync(slug);
if (post == null)
{
return NotFound();
}
return Ok(post);
}
[HttpPost]
public async Task<ActionResult<PostDetailDto>> CreatePost([FromBody] CreatePostDto dto)
{
// 这里应该从 JWT 中获取当前用户 ID
int userId = 1; // 示例
var post = await _postService.CreatePostAsync(userId, dto);
return CreatedAtAction(nameof(GetPost), new { slug = post.Slug }, post);
}
}
// ========== Program.cs ==========
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// 注册 DbContext
builder.Services.AddDbContext<BlogDbContext>(options =>
options.UseSqlite("Data Source=blog.db"));
// 注册服务
builder.Services.AddScoped<IPostService, PostService>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
// 确保数据库已创建
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<BlogDbContext>();
dbContext.Database.EnsureCreated();
}
app.Run();
16.9 常见错误与陷阱
错误1:循环引用导致 JSON 序列化失败
csharp
// ❌ 错误:实体之间有循环引用
public class Post
{
public User Author { get; set; }
}
public class User
{
public ICollection<Post> Posts { get; set; }
}
// 序列化 Post 时会包含 Author,Author 又包含 Posts...
// ✅ 解决方案1:使用 DTO
public class PostDto
{
public int AuthorId { get; set; }
public string AuthorName { get; set; }
// 不包含导航属性
}
// ✅ 解决方案2:配置忽略循环引用
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
错误2:异步方法不等待
csharp
// ❌ 错误:忘记 await
[HttpGet]
public IActionResult GetUsers()
{
var users = _dbContext.Users.ToListAsync(); // 返回 Task,不是结果
return Ok(users); // 返回 Task 对象,不是数据
}
// ✅ 正确
[HttpGet]
public async Task<IActionResult> GetUsers()
{
var users = await _dbContext.Users.ToListAsync();
return Ok(users);
}
错误3:依赖注入生命周期不匹配
csharp
// ❌ 错误:Singleton 依赖 Scoped
builder.Services.AddSingleton<ISingletonService, SingletonService>();
builder.Services.AddScoped<IScopedService, ScopedService>();
public class SingletonService : ISingletonService
{
private readonly IScopedService _scoped; // 危险!Singleton 持有 Scoped 引用
}
错误4:控制器中直接暴露实体
csharp
// ❌ 错误:直接返回实体(包含敏感信息)
[HttpGet]
public async Task<IActionResult> GetUsers()
{
return Ok(await _dbContext.Users.ToListAsync()); // 返回密码哈希等敏感字段
}
// ✅ 正确:使用 DTO
[HttpGet]
public async Task<IActionResult> GetUsers()
{
var users = await _dbContext.Users
.Select(u => new UserDto { Id = u.Id, Username = u.Username })
.ToListAsync();
return Ok(users);
}
16.10 本章总结
核心知识点导图
text
ASP.NET Core Web API
├── 基础概念
│ ├── 控制器(Controller)
│ ├── 路由(Route)
│ ├── HTTP 方法(GET, POST, PUT, DELETE)
│ └── 参数绑定
│
├── 依赖注入
│ ├── Transient、Scoped、Singleton
│ ├── 构造函数注入
│ └── 服务注册
│
├── EF Core 集成
│ ├── 注册 DbContext
│ ├── CRUD 操作
│ ├── 分页查询
│ └── DTO 使用
│
├── 中间件
│ ├── 内置中间件
│ ├── 自定义中间件
│ └── 全局异常处理
│
└── Swagger 文档
├── 自动生成文档
├── XML 注释
└── 在线测试
API 设计最佳实践
| 原则 | 说明 |
|---|---|
| 使用名词复数 | /api/users 而不是 /api/getUser |
| HTTP 方法语义 | GET(查)、POST(增)、PUT(改)、DELETE(删) |
| 使用 DTO | 不要直接暴露实体 |
| 状态码 | 200(成功)、201(创建)、400(参数错误)、404(未找到) |
| 版本控制 | /api/v1/users 或通过 Header |
| 分页 | ?page=1&pageSize=20 |
| 过滤和搜索 | ?search=keyword&category=tech |
16.11 练习题
基础题
-
创建一个简单的天气 API,返回未来 7 天的天气预报。
-
实现一个 Todo API,支持 Todo 项的增删改查。
-
在控制器中使用依赖注入,记录每次请求的日志。
应用题
-
实现一个完整的博客 API:
-
文章管理(增删改查)
-
分类管理
-
标签管理
-
评论功能
-
-
添加自定义中间件:
-
请求响应计时中间件
-
API 密钥验证中间件
-
挑战题
-
实现 JWT 认证:
-
登录接口返回 Token
-
需要认证的接口验证 Token
-
基于角色的授权
-
-
实现一个版本控制方案:
-
支持 URL 路径版本(
/api/v1/、/api/v2/) -
不同版本返回不同的数据结构
-