写在前面
本文是 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、基于策略的权限控制。