C# 零基础到精通教程 - 第十六章:ASP.NET Core Web API——构建现代 Web 服务

第十五章我们学习了 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(自动生成的文档页面):

  1. 运行项目,打开 https://localhost:5001/swagger

  2. 点击 GET /api/hello → Try it out → Execute

  3. 查看响应

或使用 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 练习题

基础题

  1. 创建一个简单的天气 API,返回未来 7 天的天气预报。

  2. 实现一个 Todo API,支持 Todo 项的增删改查。

  3. 在控制器中使用依赖注入,记录每次请求的日志。

应用题

  1. 实现一个完整的博客 API:

    • 文章管理(增删改查)

    • 分类管理

    • 标签管理

    • 评论功能

  2. 添加自定义中间件:

    • 请求响应计时中间件

    • API 密钥验证中间件

挑战题

  1. 实现 JWT 认证:

    • 登录接口返回 Token

    • 需要认证的接口验证 Token

    • 基于角色的授权

  2. 实现一个版本控制方案:

    • 支持 URL 路径版本(/api/v1//api/v2/

    • 不同版本返回不同的数据结构

相关推荐
游乐码2 小时前
unity基础(八)协程
游戏·unity·c#·游戏引擎
basketball6162 小时前
Go语言介绍
开发语言·go
霸道流氓气质2 小时前
Spring Data JPA 完全指南
开发语言·数据库
dualven_in_csdn2 小时前
cmd切换到powershell (一)
服务器·开发语言·php
会编程的土豆2 小时前
Go 里的 init() 到底是什么(彻底理解)
开发语言·后端·golang
z落落2 小时前
C#ArrayList 和 List<T>核心对比和数组对比
开发语言·c#·list
Cheng小攸2 小时前
实验九:防火墙安全认证和审计实验
开发语言·安全·php
不会C语言的男孩3 小时前
C++ Primer Plus 第8章:函数探幽
开发语言·c++
方也_arkling10 小时前
【Java-Day08】static / final / 枚举
java·开发语言