ASP.NET Core 学习笔记(二):路由与控制器

写在前面

本文是 ASP.NET Core 系列第二篇,讲清楚三件事:URL 如何匹配到代码(路由)请求数据如何变成参数(模型绑定)参数如何校验(验证)。这是 API 开发每天用的东西。


一、路由

1.1 两种路由

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 特性路由(Attribute Routing)— API 推荐
[ApiController]
[Route("api/[controller]")]   // [controller] 占位符替换为控制器名
public class UsersController : ControllerBase
{
    [HttpGet("{id}")]          // GET /api/users/123
    public User Get(int id) => ...;

    [HttpPost]                 // POST /api/users
    public User Create([FromBody] User user) => ...;

    [HttpPut("{id}")]          // PUT /api/users/123
    public void Update(int id, [FromBody] User user) => ...;

    [HttpDelete("{id}")]       // DELETE /api/users/123
    public void Delete(int id) => ...;
}

// 约定路由(Convention Routing)— MVC/Razor Pages 用
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
1
2
API 用特性路由(清晰、灵活、RESTful)
传统 MVC 用约定路由

1.2 路由模板与约束

1
2
3
4
5
[HttpGet("users/{id:int}")]              // 只匹配整数 id
[HttpGet("users/{id:int:min(1)}")]       // id ≥ 1
[HttpGet("files/{name}.{ext}")]          // /files/a.txt
[HttpGet("search/{*query}")]             // * 是 catch-all,匹配剩余路径
[HttpGet("posts/{year:int}/{month:range(1,12)}")]
1
2
3
4
5
6
7
常用约束:
  :int :long :double :bool    类型
  :min() :max() :range()      范围
  :minlength() :maxlength()   长度
  :regex()                    正则
  :alpha                      字母
  :guid                       GUID

1.3 路由数据

1
2
3
4
5
6
// 路由参数自动绑定到方法参数(同名)
[HttpGet("{id}")]
public User Get(int id) { ... }   // id 从路由 {id} 取

// 也可从 HttpContext 取路由值
var id = RouteData.Values["id"];

1.4 路由匹配原理

1
2
3
4
5
请求进来 → UseRouting 匹配端点 → UseAuthorization 检查权限 → 执行端点

  路由匹配在 UseRouting 阶段完成(端点路由 Endpoint Routing)
  匹配出"端点"(Endpoint)= 控制器 + Action 元信息
  后续中间件可以基于匹配到的端点做事(如授权检查)

二、控制器

2.1 API 控制器

1
2
3
4
5
6
7
[ApiController]                       // 启用 API 专属行为
[Route("api/[controller]")]
public class UsersController : ControllerBase   // 注意是 ControllerBase 不是 Controller
{
    // ControllerBase 提供了 API 常用成员,不含 View 支持
    // Controller(MVC)才支持 View,API 不需要
}
1
2
3
4
5
6
[ApiController] 特性的作用(重要):
  ✓ 自动 400 响应(模型验证失败直接返回 400,不用手动检查 ModelState)
  ✓ 推断绑定源([FromBody] 等可省略)
  ✓ 多部分/form 绑定推断
  ✓ 问题详情(ProblemDetails)错误格式
  ✓ 必填路由参数检查

2.2 Action 返回类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 简单返回(直接返回对象,自动序列化为 JSON)
[HttpGet("{id}")]
public User Get(int id) => _service.Get(id);

// ActionResult(可返回不同状态码)
[HttpPost]
public ActionResult<User> Create(User user)
{
    var created = _service.Create(user);
    return CreatedAtAction(nameof(Get), new { id = created.Id }, created);
    // 返回 201 + Location 头 + body
}

// IActionResult(最灵活)
[HttpGet("{id}")]
public IActionResult Get(int id)
{
    var user = _service.Get(id);
    if (user == null) return NotFound();     // 404
    return Ok(user);                          // 200
}

// ActionResult<T>(推荐,类型安全 + 灵活)
public ActionResult<User> Get(int id)
{
    var user = _service.Get(id);
    return user == null ? NotFound() : user;
}

2.3 常用返回辅助方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Ok(obj)              200
Created(location,obj) 201(新建资源)
CreatedAtAction(...)  201 + Location
NoContent()         204(成功无内容)
NotFound()          404
BadRequest(obj)     400
Unauthorized()      401
Forbid()            403
Conflict(obj)       409
StatusCode(code,obj)  自定义状态码

三、Minimal API

.NET 6+ 引入的轻量写法,不用 Controller,直接在 Program.cs 写端点。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserService, UserService>();
var app = builder.Build();

// 一行定义一个端点
app.MapGet("/api/users/{id}", async (int id, IUserService svc) =>
{
    var user = await svc.GetAsync(id);
    return user is null ? Results.NotFound() : Results.Ok(user);
});

app.MapPost("/api/users", async (User user, IUserService svc) =>
{
    var created = await svc.CreateAsync(user);
    return Results.Created($"/api/users/{created.Id}", created);
});

app.Run();
1
2
3
4
5
6
Controller vs Minimal API:
  Controller    — 适合大型项目、复杂业务、团队协作、AOP(过滤器)
  Minimal API   — 适合小型项目、微服务、快速原型

  共同点:都跑在端点路由上,模型绑定、DI 都一样
  Minimal API 性能略好(少一层 Controller 激活)

四、模型绑定

把请求数据(body / query / route / header)转换成方法参数。

4.1 绑定来源

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[HttpGet("{id}")]
public User Get([FromRoute] int id) { ... }              // 路由参数

[HttpGet]
public List<User> Search([FromQuery] string name, [FromQuery] int age) { ... }
// GET /api/users?name=张三&age=25

[HttpPost]
public User Create([FromBody] User user) { ... }          // 请求体(JSON)

[HttpGet]
public User Get([FromHeader] string token) { ... }        // 请求头

public void Do([FromServices] IUserService svc) { ... }   // 从 DI 容器
1
2
[ApiController] 启用后,复杂类型默认 FromBody,简单类型默认 FromQuery/Route
通常可省略特性,但显式写更清晰

4.2 复杂类型绑定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Query 自动绑定到对象
// GET /api/users?name=张三&age=25
[HttpGet]
public List<User> Search([FromQuery] UserQuery query) { ... }

public class UserQuery
{
    public string Name { get; set; }
    public int Age { get; set; }
}

4.3 自定义绑定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 自定义模型绑定器(复杂场景)
public class TrimModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (value != ValueProviderResult.None)
        {
            bindingContext.Result = ModelBindingResult.Success(value.ToString().Trim());
        }
        return Task.CompletedTask;
    }
}

五、参数验证

5.1 DataAnnotations(内置)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class CreateUserDto
{
    [Required] public string Username { get; set; }
    [Required][EmailAddress] public string Email { get; set; }
    [Required][StringLength(20, MinimumLength = 6)] public string Password { get; set; }
    [Range(0, 150)] public int Age { get; set; }
    [Url] public string Website { get; set; }
    [RegularExpression(@"^1\d{10}$")] public string Phone { get; set; }
}

[HttpPost]
public ActionResult<User> Create(CreateUserDto dto)
{
    // [ApiController] 会自动验证,失败直接 400,不用手动检查
    return _service.Create(dto);
}
1
2
3
4
常用验证特性:
  [Required] [StringLength] [Range] [MinLength] [MaxLength]
  [EmailAddress] [Url] [Phone] [RegularExpression]
  [Compare](和另一属性比较,如密码确认)

5.2 手动检查 ModelState(非 ApiController 时)

1
2
3
4
5
6
7
[HttpPost]
public ActionResult Create(CreateUserDto dto)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);   // 返回验证错误详情
    // ...
}

5.3 FluentValidation(复杂验证)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 安装:FluentValidation.AspNetCore
public class CreateUserDtoValidator : AbstractValidator<CreateUserDto>
{
    public CreateUserDtoValidator()
    {
        RuleFor(x => x.Username).NotEmpty().Length(3, 20);
        RuleFor(x => x.Email).NotEmpty().EmailAddress();
        RuleFor(x => x.Password).NotEmpty().MinimumLength(8)
            .Must(p => p.Any(char.IsDigit)).WithMessage("密码必须包含数字");
        RuleFor(x => x.Age).InclusiveBetween(0, 150);
    }
}
// 自动接入 ModelState,和 DataAnnotations 一样自动 400
1
2
3
4
DataAnnotations vs FluentValidation:
  DataAnnotations — 简单、内置、声明式、适合简单规则
  FluentValidation — 强大、流式、易测试、适合复杂业务规则
  大型项目推荐 FluentValidation

六、小结

  • 路由:API 用特性路由([Route] + [HttpGet]),支持模板和约束
  • 控制器:API 继承 ControllerBase,加 [ApiController] 享便利(自动 400、推断绑定)
  • Minimal API:轻量端点写法,适合小项目/微服务
  • 模型绑定[FromRoute]/[FromQuery]/[FromBody]/[FromHeader]/[FromServices]
  • 验证:DataAnnotations(简单)或 FluentValidation(复杂),自动 400

下一篇讲认证与授权:JWT、Cookie、基于策略的权限控制。