3.2 控制器与操作结果


文档摘要

3.2 控制器与操作结果 控制器是路由选中的业务处理单元:一个按约定命名的类,一组公共方法即动作;动作返回 ActionResult 而非裸字符串,是为了把"业务决定"与"响应怎么写"解耦——返回的是意图,框架负责落笔。本节观察控制器被发现、被选中、返回结果的全部过程。 路由在上一节选好了终点站,请求流到控制器门口。观察哨本节看三件事:框架凭什么把一个普通类认作控制器、动作是怎么被精确选中的、以及为什么动作都爱返回 IActionResult 而不是直接写字符串。 控制器是什么:一个按约定命名的类 MVC 模板里控制器集中在 Controllers 目录,但目录本身没有魔力——框架认的是两条约定:类以 Controller 结尾(或继承 Controller 基类),公共非静态方法即动作。

3.2 控制器与操作结果

控制器是路由选中的业务处理单元:一个按约定命名的类,一组公共方法即动作;动作返回 ActionResult 而非裸字符串,是为了把"业务决定"与"响应怎么写"解耦——返回的是意图,框架负责落笔。本节观察控制器被发现、被选中、返回结果的全部过程。

路由在上一节选好了终点站,请求流到控制器门口。观察哨本节看三件事:框架凭什么把一个普通类认作控制器、动作是怎么被精确选中的、以及为什么动作都爱返回 IActionResult 而不是直接写字符串。

控制器是什么:一个按约定命名的类

MVC 模板里控制器集中在 Controllers 目录,但目录本身没有魔力——框架认的是两条约定:类以 Controller 结尾(或继承 Controller 基类),公共非静态方法即动作。控制器名的取法是"去掉 Controller 后缀":

// ProductsController 的控制器名是 Products // 访问 /Products/List 即选中此类的 List 方法 public class ProductsController : Controller { // 动作一:返回视图,视图引擎默认找 Views 下的 Products 目录 public IActionResult List() { var items = new[] { "键盘", "显示器", "鼠标" }; return View(items); // 把模型交给视图,3.4 节展开 } // 动作二:不经过视图,直接产出内容 public IActionResult Count() => Content("共 3 件商品"); }

控制器基类提供的不是生存能力,而是一批"返回意图"的快捷方法:View、Content、Redirect、NotFound、StatusCode、Json。继承 Controller 不是必须的——普通类加 Controller 后缀同样会被发现,基类的价值是少敲键盘。构造函数参数随意声明,容器会按第 2 章的注入规则填满,这是控制器保持"薄"的关键:它编排流程,重活委托给注入的服务。

动作选择:HTTP 方法与参数的博弈

路由选定控制器后,还有一层"动作选择"要过。同名的多个动作,框架按 HTTP 方法谓词与参数情况裁决:

public class ReportsController : Controller { [HttpGet] // GET 进这个 public IActionResult Export() => Content("展示导出表单"); [HttpPost] // POST 进这个 public IActionResult Export(IFormFile file) => Content($"收到文件 {file.FileName}"); [AcceptVerbs("GET", "HEAD")] // 显式声明接受多个谓词 public IActionResult Preview() => Content("预览报告"); } // GET /Reports/Export → 展示导出表单 // POST /Reports/Export(带文件)→ 收到文件 xxx // 同时匹配失败(如同为 GET 且同名同参)→ 启动期直接抛异常,AmbiguousMatch

谓词限定是管线里的隐形安检:写错谓词的请求在动作选择这一步被拒(405),根本进不了业务代码。动作选择器还会参考路由数据里的 action 值、可选参数个数——规则细节多,但设计意图单一:让"该进哪个方法"在编译期就尽量明确,运行期零猜测。

返回类型为什么统一用 IActionResult?因为动作的职责是表达意图:"渲染视图""重定向""返回 404",而不是亲自操纵响应流。看 ActionResult 家族的全貌:

图 3.2-1 ActionResult 家族图谱

图 3.2-1 ActionResult 家族图谱

案例:编辑动作的四种返回意图

背景:商品编辑页提交后要处理四种结局:数据无效回到表单、保存成功跳到列表、商品不存在返回 404、无权限返回 403。新手常在一个动作里手工写响应,代码越堆越乱。

操作:全部用返回意图表达,动作体只剩判断逻辑:

[HttpPost] public async Task<IActionResult> Edit(ProductInput input) { if (!ModelState.IsValid) return View(input); // 结局一:带验证错误回显表单 var product = await _repo.FindAsync(input.Id); if (product is null) return NotFound(); // 结局二:明确 404 if (!User.CanEdit(product)) return Forbid(); // 结局三:403,第 7 章展开 await _repo.SaveAsync(input); return RedirectToAction("List"); // 结局四:PRG 重定向,刷新不再重复提交 } // 四种返回四种意图,单元测试各断言一个类型即可

结果:动作方法二十四行清爽收场,测试里直接断言 result is RedirectToActionResult,不用启动服务器。页面行为同步变好:保存后刷新浏览器不再产生重复提交。

解读:这就是"意图与落笔分离"的收益——动作决定做什么,结果对象在出站时决定怎么写。加新结局(比如冲突 409)只是多一个分支返回,管线其余部分不动。

变式:接口型控制器(第 6 章)把 View 换成 Ok、Created,家族图谱不变,思维模型完全复用;需要携带正文的失败(校验错误清单)用 BadRequest 带模型状态,或用 ProblemDetails 标准化错误体。

⚠️ 常见坑:动作里返回 string 或自定义对象也能工作(框架会包装成结果),但混用后过滤器与单元测试的类型断言会失去一致性,团队项目里统一返回 IActionResult 利大于弊。另一个坑:在动作里直接操作 Response 写流后还返回 View,两类写笔冲突,出站时抛"响应已启动"。

💡 关键直觉:把动作想象成车站值班员——他不亲自开车,只决定这趟车进哪条股道。开车的活留给结果对象在出站段执行。

本节要点回顾

  • 两条约定:Controller 后缀定控制器名,公共方法即动作,目录只是习惯;
  • 动作选择按谓词与参数裁决,二义性在启动期报错,不让运行期猜;
  • 返回意图族谱:视图、重定向、状态码、内容四族,动作返回对象而非字节;
  • 结果对象出站才执行,这是可测试性与 PRG 模式的共同基础;
  • 控制器保持薄:流程编排留下,业务与数据委托注入的服务,重头戏在第 5 章。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U