ASP.NET Core 中的 IActionResult深度解析
一、從一個(gè)問(wèn)題開(kāi)始
你寫了一個(gè) Web API,有時(shí)候要返回?cái)?shù)據(jù),有時(shí)候要返回 404,有時(shí)候要返回 400——這三種情況的返回值類型完全不同,一個(gè) C# 方法怎么能同時(shí)返回多種東西?
這就是 IActionResult 存在的根本原因。它的本質(zhì)是:封裝"如何把結(jié)果寫入 HTTP 響應(yīng)"的邏輯的統(tǒng)一抽象。
二、類型層次結(jié)構(gòu)
IActionResult(接口)
└── ActionResult(抽象類,默認(rèn)實(shí)現(xiàn))
├── StatusCodeResult
│ ├── NotFoundResult (404)
│ ├── OkResult (200)
│ └── BadRequestResult (400) ...
├── ObjectResult(帶 Body,最復(fù)雜)
│ ├── OkObjectResult (200)
│ ├── NotFoundObjectResult (404)
│ ├── BadRequestObjectResult (400)
│ ├── CreatedResult (201)
│ └── CreatedAtActionResult (201) ...
├── ContentResult
├── JsonResult
├── FileResult
├── RedirectResult
└── ViewResult ...
你調(diào)用
Ok()、NotFound()、BadRequest()返回的都是ObjectResult或StatusCodeResult的子類,它們最終都實(shí)現(xiàn)了IActionResult接口。
三、核心方法簽名逐層拆解
層一:IActionResult 接口
定義在 Microsoft.AspNetCore.Mvc.Abstractions.dll,整個(gè)接口只有一個(gè)方法:
// 命名空間: Microsoft.AspNetCore.Mvc
public interface IActionResult
{
// 由 MVC 框架調(diào)用,用于處理 Action 方法的返回結(jié)果
Task ExecuteResultAsync(ActionContext context);
}ActionContext 攜帶了執(zhí)行結(jié)果所需的全部上下文:
public class ActionContext
{
public HttpContext HttpContext { get; } // 整個(gè) HTTP 請(qǐng)求/響應(yīng)
public RouteData RouteData { get; } // 路由數(shù)據(jù)
public ActionDescriptor ActionDescriptor { get; } // Action 的元數(shù)據(jù)
public ModelStateDictionary ModelState { get; } // 模型驗(yàn)證狀態(tài)
}
層二:ActionResult 抽象類
定義在 Microsoft.AspNetCore.Mvc.Core.dll,是整個(gè)體系的基礎(chǔ):
public abstract class ActionResult : IActionResult
{
// 同步版本(供簡(jiǎn)單場(chǎng)景使用,子類可選擇重寫)
public virtual void ExecuteResult(ActionContext context) { }
// 異步版本:默認(rèn)實(shí)現(xiàn)調(diào)用同步方法,再返回 Task.CompletedTask
// 需要真正異步 I/O 的子類(如寫文件流)應(yīng)直接重寫此方法
public virtual Task ExecuteResultAsync(ActionContext context)
{
ExecuteResult(context);
return Task.CompletedTask;
}
}關(guān)鍵設(shè)計(jì): 默認(rèn)實(shí)現(xiàn)把異步路由到同步。子類按需選擇重寫哪一個(gè)——邏輯簡(jiǎn)單的重寫同步 ExecuteResult,有 I/O 操作的重寫異步 ExecuteResultAsync。
層三:StatusCodeResult(無(wú) Body 的結(jié)果)
public class StatusCodeResult : ActionResult, IStatusCodeActionResult
{
public int StatusCode { get; }
public StatusCodeResult(int statusCode) { StatusCode = statusCode; }
// 重寫同步方法,邏輯極簡(jiǎn):只設(shè)置狀態(tài)碼
public override void ExecuteResult(ActionContext context)
{
context.HttpContext.Response.StatusCode = StatusCode;
}
}
// 子類只做一件事:傳入固定的狀態(tài)碼
public class NotFoundResult : StatusCodeResult
{
public NotFoundResult() : base(StatusCodes.Status404NotFound) { }
}
public class OkResult : StatusCodeResult
{
public OkResult() : base(StatusCodes.Status200OK) { }
}層四:ObjectResult(帶 Body 的結(jié)果,最復(fù)雜)
public class ObjectResult : ActionResult, IStatusCodeActionResult
{
public object? Value { get; set; } // 要序列化的對(duì)象
public int? StatusCode { get; set; } // HTTP 狀態(tài)碼(可空)
public MediaTypeCollection ContentTypes { get; set; }
public FormatterCollection<IOutputFormatter> Formatters { get; set; }
// 重寫異步版本(序列化寫流是 I/O 操作)
public override async Task ExecuteResultAsync(ActionContext context)
{
// 子類在序列化前可修改狀態(tài)(如設(shè)置 StatusCode、寫 Header)
OnFormatting(context);
// 委托給 ObjectResultExecutor,它負(fù)責(zé):
// 1. 內(nèi)容協(xié)商:根據(jù) Accept 頭選擇 Formatter
// 2. 調(diào)用 IOutputFormatter 序列化 Value
// 3. 設(shè)置 Content-Type 和 StatusCode
var executor = context.HttpContext.RequestServices
.GetRequiredService<IActionResultExecutor<ObjectResult>>();
await executor.ExecuteAsync(context, this);
}
public virtual void OnFormatting(ActionContext context) { }
}所有"帶 Body"的子類只做一件事——在構(gòu)造函數(shù)里設(shè)置狀態(tài)碼,其余邏輯全部繼承自 ObjectResult:
public class OkObjectResult : ObjectResult
{
public OkObjectResult(object? value) : base(value)
{ StatusCode = StatusCodes.Status200OK; }
}
// CreatedAtActionResult 更特殊:重寫了 OnFormatting 來(lái)設(shè)置 Location Header
public class CreatedAtActionResult : ObjectResult
{
public string? ActionName { get; set; }
public string? ControllerName { get; set; }
public object? RouteValues { get; set; }
public override void OnFormatting(ActionContext context)
{
var url = urlHelper.Action(ActionName, ControllerName, RouteValues);
context.HttpContext.Response.Headers[HeaderNames.Location] = url;
}
}四、四種返回方式全景
1. 具體類型(最簡(jiǎn)單)
結(jié)果固定、無(wú)分支時(shí)使用:
[HttpGet]
public Task<List<Product>> Get() =>
_db.Products.OrderBy(p => p.Name).ToListAsync();2. IActionResult(靈活,需手動(dòng)標(biāo)注 Swagger 文檔)
當(dāng)一個(gè) Action 存在多種 HTTP 狀態(tài)碼分支時(shí)使用:
[HttpGet("{id}")]
[ProducesResponseType<Product>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetById(int id)
{
var product = _db.Products.Find(id);
return product == null ? NotFound() : Ok(product);
}3. ActionResult(現(xiàn)代推薦寫法)
兩大優(yōu)勢(shì):[ProducesResponseType] 的 Type 屬性可從泛型參數(shù)自動(dòng)推斷;支持直接 return T 而無(wú)需手動(dòng) Ok(T):
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<Product> GetById(int id)
{
var product = _db.Products.Find(id);
// 直接 return product,隱式轉(zhuǎn)換自動(dòng)包裝成 OkObjectResult
return product == null ? NotFound() : product;
}常見(jiàn)陷阱: C# 不支持接口的隱式轉(zhuǎn)換。當(dāng)泛型參數(shù)是接口(如
IEnumerable<Product>)時(shí),必須調(diào)用.ToList()轉(zhuǎn)為具體類型才能編譯通過(guò)。
4. Results<T1, T2>(跨場(chǎng)景共享,編譯期類型安全)
可省略所有 [ProducesResponseType],且框架會(huì)在編譯期檢查返回值是否合法:
[HttpGet("{id}")]
public Results<NotFound, Ok<Product>> GetById(int id)
{
var product = _db.Products.Find(id);
return product == null
? TypedResults.NotFound()
: TypedResults.Ok(product);
// 返回其他類型 → 編譯錯(cuò)誤!
}五、管道執(zhí)行流程
HTTP 請(qǐng)求
│
▼
中間件管道(UseRouting → UseAuthentication → UseEndpoints)
│
▼
ControllerActionInvoker(核心調(diào)度器)
│
├─── [1] Authorization Filter ──────────── OnAuthorizationAsync
│ 不通過(guò) → 直接短路,寫入 401/403
│
├─── [2] Resource Filter ────────────────── OnResourceExecutingAsync
│ 可短路(如緩存命中直接返回)
│
├─── [3] Model Binding
│ 綁定 Action 參數(shù);[ApiController] 時(shí)驗(yàn)證失敗自動(dòng)返回 400
│
├─── [4] Action Filter Before ───────────── OnActionExecutingAsync
│ 可修改參數(shù),或設(shè)置 context.Result 短路
│
├─── ★★★ [5] 執(zhí)行 Action 方法本體 ★★★
│ return Ok(product)
│ → 創(chuàng)建 OkObjectResult 對(duì)象,此時(shí)【尚未寫入任何響應(yīng)】
│
├─── [6] Action Filter After ────────────── OnActionExecutedAsync
│ 可攔截并替換 context.Result
│
├─── [7] Exception Filter
│ 處理未被捕獲的異常
│
├─── [8] Result Filter Before ───────────── OnResultExecutingAsync
│ 寫響應(yīng)前最后處理(如追加響應(yīng) Header)
│
├─── ★★★ [9] result.ExecuteResultAsync(actionContext) ★★★
│ 真正把狀態(tài)碼、Header、Body 寫入 HttpResponse
│
└─── [10] Result Filter After + Resource Filter After
OnResultExecutedAsync / OnResourceExecutedAsync
│
▼
HTTP 響應(yīng)返回客戶端最關(guān)鍵的一點(diǎn):
return Ok(product)并不立即寫響應(yīng)。它只是創(chuàng)建了一個(gè)OkObjectResult對(duì)象。真正的 HTTP 響應(yīng)寫入,發(fā)生在第 9 步——框架調(diào)用ExecuteResultAsync的時(shí)候。
六、一次 return Ok( product ) 的完整內(nèi)部旅程
public IActionResult GetById(int id)
{
var product = _db.Products.Find(id);
return product == null ? NotFound() : Ok(product);
}假設(shè) product 存在,展開(kāi)每一步:
[步驟 1] ControllerBase.Ok(product)
→ new OkObjectResult(product)
StatusCode = 200, Value = product 對(duì)象(內(nèi)存中的 C# 對(duì)象)
[步驟 2] Action 方法返回,ControllerActionInvoker 拿到 OkObjectResult
經(jīng)過(guò) Action Filter (OnActionExecuted)
經(jīng)過(guò) Result Filter (OnResultExecuting)
[步驟 3] 框架調(diào)用:
await result.ExecuteResultAsync(actionContext)
[步驟 4] ObjectResult.ExecuteResultAsync 內(nèi)部:
OnFormatting(context) // OkObjectResult 確認(rèn) StatusCode = 200
var executor = services.GetRequiredService<IActionResultExecutor<ObjectResult>>()
await executor.ExecuteAsync(context, this)
[步驟 5] ObjectResultExecutor.ExecuteAsync 內(nèi)部:
a. 內(nèi)容協(xié)商:檢查請(qǐng)求 Accept 頭(application/json? application/xml?)
b. 從 IOutputFormatter 列表中選擇匹配的 Formatter
→ 默認(rèn)是 SystemTextJsonOutputFormatter
c. Response.StatusCode = 200
d. Response.ContentType = "application/json; charset=utf-8"
e. formatter.WriteAsync(context)
→ 將 product 序列化為 JSON,寫入 Response.Body 流
OkObjectResult并沒(méi)有自己實(shí)現(xiàn)序列化邏輯——它把一切都交給父類ObjectResult,ObjectResult再把內(nèi)容協(xié)商和序列化交給ObjectResultExecutor。這是典型的職責(zé)分離:Result 對(duì)象只描述"要返回什么",Executor 負(fù)責(zé)"如何寫入"。
七、IActionResult vs IResult:兩套體系的本質(zhì)差異
// MVC 體系(Microsoft.AspNetCore.Mvc)
public interface IActionResult
{
Task ExecuteResultAsync(ActionContext context); // 參數(shù)是 ActionContext
}
// Minimal API 體系(Microsoft.AspNetCore.Http)
public interface IResult
{
Task ExecuteAsync(HttpContext httpContext); // 參數(shù)是 HttpContext,更輕量
}| 對(duì)比項(xiàng) | IActionResult(MVC) | IResult(Minimal API) |
|---|---|---|
| 接口方法 | ExecuteResultAsync | ExecuteAsync |
| 參數(shù)類型 | ActionContext | HttpContext |
| 調(diào)用方 | ControllerActionInvoker | RequestDelegateFactory |
| 內(nèi)容協(xié)商 | ? 支持(IOutputFormatter) | ? 不支持 |
| 序列化 | 可插拔 Formatter(JSON/XML/自定義) | 固定(WriteAsJsonAsync) |
| Filter 管道 | ? 完整管道 | ? 僅 EndpointFilter |
| 適用場(chǎng)景 | 傳統(tǒng) MVC / Web API Controller | Minimal API / 跨場(chǎng)景共享 |
IResult 的實(shí)現(xiàn)極其直接,以 Ok<T> 為例:
// Microsoft.AspNetCore.Http.HttpResults.Ok<TValue>
public sealed class Ok<TValue> : IResult, IStatusCodeHttpResult
{
public int? StatusCode => 200;
public TValue? Value { get; }
// 沒(méi)有 Formatter,沒(méi)有內(nèi)容協(xié)商,直接寫流
public async Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.StatusCode = 200;
if (Value is not null)
await httpContext.Response.WriteAsJsonAsync(Value);
}
}八、常用 ActionResult 速查
| 便捷方法 | 狀態(tài)碼 | 對(duì)應(yīng)類 | 有 Body |
|---|---|---|---|
Ok(data) | 200 | OkObjectResult | ? |
Ok() | 200 | OkResult | — |
Created(uri, data) | 201 | CreatedResult | ? + Location |
CreatedAtAction(...) | 201 | CreatedAtActionResult | ? + Location |
NoContent() | 204 | NoContentResult | — |
BadRequest() | 400 | BadRequestResult | — |
BadRequest(error) | 400 | BadRequestObjectResult | ? |
Unauthorized() | 401 | UnauthorizedResult | — |
Forbid() | 403 | ForbidResult | — |
NotFound() | 404 | NotFoundResult | — |
NotFound(data) | 404 | NotFoundObjectResult | ? |
Conflict() | 409 | ConflictResult | — |
StatusCode(code) | 自定義 | StatusCodeResult | — |
Content("text") | 200 | ContentResult | ? text/plain |
九、選型決策流程
需要返回多種 HTTP 狀態(tài)?
├── 否 ──→ 直接返回具體類型(最簡(jiǎn)單)
└── 是
│
├── 需要內(nèi)容協(xié)商或自定義 Formatter?
│ └── 是 ──→ ActionResult<T>(MVC 場(chǎng)景首選)
│
└── 需要和 Minimal API 共享代碼?
├── 是 ──→ Results<T1, T2>(編譯期類型安全,自動(dòng)推斷文檔)
└── 否 ──→ ActionResult<T>(現(xiàn)代 .NET 推薦寫法)一句話總結(jié): 新項(xiàng)目首選 ActionResult<T>,既有類型安全,又有 Swagger 文檔自動(dòng)推斷;需要跨 Minimal API 共享邏輯時(shí)換 Results<T1, T2>。刪除 Action 成功時(shí)無(wú)數(shù)據(jù)可返回,用 IActionResult 配合 NoContent() 更語(yǔ)義清晰。
到此這篇關(guān)于ASP.NET Core 中的 IActionResult深度解析的文章就介紹到這了,更多相關(guān)ASP.NET Core IActionResult內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 基于ASP.NET Core SignalR實(shí)現(xiàn)實(shí)時(shí)消息提醒與聊天功能
- ASP.NET Core + Layui實(shí)現(xiàn)聯(lián)動(dòng)選擇功能
- ASP.NET Core上傳文件到minio的實(shí)現(xiàn)示例
- ASP.NET Core生成ZIP壓縮包的終極實(shí)戰(zhàn)指南
- 深入理解?ASP.NET?Core?依賴注入(DI)的實(shí)現(xiàn)
- ASP.NET Core中ResourceFilter過(guò)濾器的實(shí)現(xiàn)
- ASP.NET Core中實(shí)現(xiàn)高效的文件上傳的示例代碼
- ASP.NET?Core?模型驗(yàn)證消息的本地化新姿勢(shì)詳解
相關(guān)文章
Linux上使用Docker部署ASP.NET?Core應(yīng)用程序
這篇文章介紹了使用Docker部署ASP.NET?Core應(yīng)用程序的方法,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-03-03
概述.net開(kāi)發(fā)過(guò)程中Bin目錄下面幾種文件格式
本篇文章主要對(duì)項(xiàng)目發(fā)布的時(shí)候,經(jīng)常用到的幾個(gè)文件:.pdb、.xsd、.vshost.exe、.exe、.exe.config、.vshost.exe.config的作用進(jìn)行介紹,具有一定的參考價(jià)值,需要的朋友可以看下2016-12-12
Mvc動(dòng)態(tài)注冊(cè)HttpModule詳解
本文主要介紹了Mvc動(dòng)態(tài)注冊(cè)HttpModule的方法。具有很好的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-03-03
ASP.NET?MVC5網(wǎng)站開(kāi)發(fā)顯示文章列表(九)
顯示文章列表分兩塊,管理員可以顯示全部文章列表,一般用戶只顯示自己的文章列表。文章列表的顯示采用easyui-datagrid,后臺(tái)需要與之對(duì)應(yīng)的action返回json類型數(shù)據(jù),感興趣的小伙伴們可以參考一下2015-09-09
.NET Core3.0 日志 logging的實(shí)現(xiàn)
這篇文章主要介紹了.NET Core3.0 日志 logging的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
ASPNET按鈕只執(zhí)行客戶端代碼不回送頁(yè)面實(shí)現(xiàn)思路
有些時(shí)候需要實(shí)現(xiàn)只執(zhí)行客戶端代碼不回送頁(yè)面,不過(guò)很多童鞋們不清楚如何實(shí)現(xiàn)呢,還好本文的出現(xiàn)將解決你的困擾,感興趣的朋友可以了解下,或許對(duì)你有所幫助2013-02-02
ASP.NET(C#)驗(yàn)證數(shù)字的兩種方法
ASP.NET(C#)驗(yàn)證數(shù)字的兩種方法,需要的朋友可以參考一下2013-06-06

