上一章講了系統如何將客戶端提交的請求數據格式化處理成我們想要的格式並綁定到對應的參數,本章講一下它的“逆過程”,如何將請求結果按照客戶端想要的格式返回去。(ASP.NET Core 系列目錄) 一、常見的返回類型 以系統模板預設生成的Home/Index這個Action來說,為什麼當請求它的時候回返 ...
上一章講了系統如何將客戶端提交的請求數據格式化處理成我們想要的格式並綁定到對應的參數,本章講一下它的“逆過程”,如何將請求結果按照客戶端想要的格式返回去。(ASP.NET Core 系列目錄)
一、常見的返回類型
以系統模板預設生成的Home/Index這個Action來說,為什麼當請求它的時候回返回一個Html頁面呢?除了這之外,還有JSON、文本等類型,系統是如何處理這些不同的類型的呢?
首先來說幾種常見的返回類型的例子,並用Fiddler請求這幾個例子看一下結果,涉及到的一個名為Book的類,代碼為:
public class Book { public string Code { get; set; } public string Name { get; set; } }
1.ViewResult類型
public class HomeController : Controller { public IActionResult Index() { return View(); } }
返回一個Html頁面,Content-Type值為: text/html; charset=utf-8
2.string類型
public string GetString() { return "Hello Core"; }
返回字元串“Hello Core”,Content-Type值為:text/plain; charset=utf-8
3.JSON類型
public JsonResult GetJson() { return new JsonResult(new Book() { Code = "1001", Name = "ASP" }); }
返回JSON,值為:
{"code":"1001","name":"ASP"}
Content-Type值為:Content-Type: application/json; charset=utf-8
4.直接返回實體類型
public Book GetModel() { return new Book() { Code = "1001", Name = "ASP" }; }
同樣是返回JSON,值為:
{"code":"1001","name":"ASP"}
Content-Type值同樣是:Content-Type: application/json; charset=utf-8
5.void類型
public void GetVoid() { }
沒有返回結果也沒有Content-Type值。
下麵看幾個非同步方法:
6.Task類型
public async Task GetTaskNoResult() { await Task.Run(() => { }); }
與void類型一樣,沒有返回結果也沒有Content-Type值。
7.Task<string>類型
public async Task<string> GetTaskString() { string rtn = string.Empty; await Task.Run(() => { rtn = "Hello Core"; }); return rtn; }
與string類型一樣,返回字元串“Hello Core”,Content-Type值為:text/plain; charset=utf-8
8.Task<JsonResult>類型
public async Task<JsonResult> GetTaskJson() { JsonResult jsonResult = null; await Task.Run(() => { jsonResult = new JsonResult(new Book() { Code = "1001", Name = "ASP" }); }); return jsonResult; }
與JSON類型一樣,返回JSON,值為:
[{"code":"1001","name":"ASP"},{"code":"1002","name":"Net Core"}]
Content-Type值為:Content-Type: application/json; charset=utf-8
還有其他類型,這裡暫不列舉了,總結一下:
- 返回結果有空、Html頁面、普通字元串、JSON字元串幾種。
- 對應的Content-Type類型有空、text/html、text/plain、application/json幾種。
- 非同步Action的返回結果,和其對應的同步Action返回結果類型一致。
下一節我們看一下系統是如何處理這些不同的類型的。
二、內部處理機制解析
1.總體流程
通過下圖 來看一下總體的流程:
圖1
這涉及三部分內容:
第一部分,在invoker的生成階段。在第14章講invoker的生成的時候,講到了Action的執行者的獲取,它是從一系列系統定義的XXXResultExecutor中篩選出來的,雖然它們名為XXXResultExecutor,但它們都是Action的執行者而不是ActionResult的執行者,都是ActionMethodExecutor的子類。以Action是同步還是非同步以及Action的返回值類型為篩選條件,具體這部分內容見圖 14‑2所示XXXResultExecutor列表及其後面的篩選邏輯部分。在圖 17‑1中,篩選出了被請求的Action對應的XXXResultExecutor,若以Home/Index這個預設的Action為例,這個XXXResultExecutor應該是SyncActionResultExecutor。
第二部分,在Action Filters的處理階段。這部分內容見16.5 Filter的執行,此處恰好以Action Filter為例講了Action Filter的執行方式及Action被執行的過程。在這個階段,會調用上文篩選出的SyncActionResultExecutor的Execute方法來執行Home/Index這個 Action。執行結果返回一個IActionResult。
第三部分,在Result Filters的處理階段。這個階段和Action Filters的邏輯相似,只不過前者的核心是Action的執行,後者的核心是Action的執行結果的執行。二者都分為OnExecuting和OnExecuted兩個方法,這兩個方法也都在其對應的核心執行方法前後執行。
整體流程是這樣,下麵看一下細節。
2. ActionMethodExecutor的選擇與執行
第一部分,系統為什麼要定義這麼多種XXXResultExecutor並且在請求的時候一個個篩選合適的XXXResultExecutor呢?從篩選規則是以Action的同步、非同步以及Action的返回值類型來看,這麼多種XXXResultExecutor就是為了處理不同的Action類型。
依然以Home/Index為例,在篩選XXXResultExecutor的時候,最終返回結果是SyncActionResultExecutor。它的代碼如下:
private class SyncActionResultExecutor : ActionMethodExecutor { public override ValueTask<IActionResult> Execute( IActionResultTypeMapper mapper, ObjectMethodExecutor executor, object controller, object[] arguments) { var actionResult = (IActionResult)executor.Execute(controller, arguments); EnsureActionResultNotNull(executor, actionResult); return new ValueTask<IActionResult>(actionResult); } protected override bool CanExecute(ObjectMethodExecutor executor) => !executor.IsMethodAsync && typeof(IActionResult).IsAssignableFrom(executor.MethodReturnType); }
XXXResultExecutor的CanExecute方法是篩選的條件,通過這個方法判斷它是否適合當前請求的目標Action。它要求這個Action不是非同步的並且返回結果類型是派生自IActionResult的。而Home/Index這個Action標識的返回結果是IActionResult,實際是通過View()這個方法返回的,這個方法的返回結果類型實際是IActionResult的派生類ViewResult。這樣的派生類還有常見的JsonResult和ContentResult等,他們都繼承了ActionResult,而ActionResult實現了IActionResult介面。所以如果一個Action是同步的並且返回結果是JsonResult或ContentResult的時候,對應的XXXResultExecutor也是SyncActionResultExecutor。
第二部分中,Action的執行是在XXXResultExecutor的Execute方法,它會進一步調用了ObjectMethodExecutor的Execute方法。實際上所有的Action的都是由ObjectMethodExecutor的Execute方法來執行執行的。而眾多的XXXResultExecutor方法的作用是調用這個方法並且對返回結果進行驗證和處理。例如SyncActionResultExecutor會通過EnsureActionResultNotNull方法確保返回的結果不能為空。
如果是sting類型呢?它對應的是SyncObjectResultExecutor,代碼如下:
private class SyncObjectResultExecutor : ActionMethodExecutor { public override ValueTask<IActionResult> Execute( IActionResultTypeMapper mapper, ObjectMethodExecutor executor, object controller, object[] arguments) { // Sync method returning arbitrary object var returnValue = executor.Execute(controller, arguments); var actionResult = ConvertToActionResult(mapper, returnValue, executor.MethodReturnType); return new ValueTask<IActionResult>(actionResult); } // Catch-all for sync methods protected override bool CanExecute(ObjectMethodExecutor executor) => !executor.IsMethodAsync; }
由於string不是IActionResult的子類,所以會通過ConvertToActionResult方法對返回結果returnValue進行處理。
private IActionResult ConvertToActionResult(IActionResultTypeMapper mapper, object returnValue, Type declaredType) { var result = (returnValue as IActionResult) ?? mapper.Convert(returnValue, declaredType); if (result == null) { throw new InvalidOperationException(Resources.FormatActionResult_ActionReturnValueCannotBeNull(declaredType)); } return result; }
如果returnValue是IActionResult的子類,則返回returnValue,否則調用一個Convert方法將returnValue轉換一下:
public IActionResult Convert(object value, Type returnType) { if (returnType == null) { throw new ArgumentNullException(nameof(returnType)); } if (value is IConvertToActionResult converter) { return converter.Convert(); } return new ObjectResult(value) { DeclaredType = returnType, }; }
這個方法會判斷returnValue是否實現了IConvertToActionResult介面,如果是則調用該介面的Convert方法轉換成IActionResult類型,否則會將returnValue封裝成ObjectResult。ObjectResult也是ActionResult的子類。下文有個ActionResult<T> 類型就是這樣,該例會介紹。
所以,針對不同類型的Action,系統設置了多種XXXResultExecutor來處理,最終結果無論是什麼,都會被轉換成IActionResult類型。方便在圖 17‑1所示的第三部分進行IActionResult的執行。
上一節列出了多種不同的Action,它們的處理在這裡就不一一講解了。通過下圖 17‑2看一下它們的處理結果:
圖 2
這裡有void類型沒有講到,它本身沒有返回結果,但它會被賦予一個結果EmptyResult,它也是ActionResult的子類。
圖 2被兩行虛線分隔為三行,第一行基本都介紹過了,第二行是第一行對應的非同步方法,上一節介紹常見的返回類的時候說過,這些非同步方法的返回結果和對應的同步方法是一樣的。不過通過圖 2可知,處理它們的XXXResultExecutor方法是不一樣的。
第三行的ActionResult<T> 類型是在ASP.NET Core 2.1 引入的,它支持IActionResult的子類也支持類似string和Book這樣的特定類型。
public sealed class ActionResult<TValue> : IConvertToActionResult { public ActionResult(TValue value) { if (typeof(IActionResult).IsAssignableFrom(typeof(TValue))) { var error = Resources.FormatInvalidTypeTForActionResultOfT(typeof(TValue), "ActionResult<T>"); throw new ArgumentException(error); } Value = value; } public ActionResult(ActionResult result) { if (typeof(IActionResult).IsAssignableFrom(typeof(TValue))) { var error = Resources.FormatInvalidTypeTForActionResultOfT(typeof(TValue), "ActionResult<T>"); throw new ArgumentException(error); } Result = result ?? throw new ArgumentNullException(nameof(result)); } /// <summary> /// Gets the <see cref="ActionResult"/>. /// </summary> public ActionResult Result { get; } /// <summary> /// Gets the value. /// </summary> public TValue Value { get; } public static implicit operator ActionResult<TValue>(TValue value) { return new ActionResult<TValue>(value); } public static implicit operator ActionResult<TValue>(ActionResult result) { return new ActionResult<TValue>(result); } IActionResult IConvertToActionResult.Convert() { return Result ?? new ObjectResult(Value) { DeclaredType = typeof(TValue), }; } }
TValue不支持IActionResult及其子類。它的值若是IActionResult子類,會被賦值給Result屬性,否則會賦值給Value屬性。它實現了IConvertToActionResult介面,想到剛講解string類型被處理的時候用到的Convert方法。當返回結果實現了IConvertToActionResult這個介面的時候,就會調用它的Convert方法進行轉換。它的Convert方法就是先判斷它的值是否是IActionResult的子類,如果是則返回該值,否則將該值轉換為ObjectResult後返回。
所以圖 2中ActionResult<T> 類型返回的結果被加上引號的意思就是結果類型可能是直接返回的IActionResult的子類,也有可能是string和Book這樣的特定類型被封裝後的ObjectResult類型。
3. Result Filter的執行
結果被統一處理為IActionResult後,進入圖 1所示的第三部分。這部分的主要內容有兩個,分別是Result Filters的執行和IActionResult的執行。Result Filter也有OnResultExecuting和OnResultExecuted兩個方法,分別在IActionResult執行的前後執行。
自定義一個MyResultFilterAttribute,代碼如下:
public class MyResultFilterAttribute : Attribute, IResultFilter { public void OnResultExecuted(ResultExecutedContext context) { Debug.WriteLine("HomeController=======>OnResultExecuted"); } public void OnResultExecuting(ResultExecutingContext context) { Debug.WriteLine("HomeController=======>OnResultExecuting"); } }
將它註冊到第一節JSON的例子中:
[MyResultFilter] public JsonResult GetJson() { return new JsonResult(new Book() { Code = "1001", Name = "ASP" }); }
可以看到這樣的輸出結果:
HomeController=======>OnResultExecuting
……Executing JsonResult……
HomeController=======>OnResultExecuted
在OnResultExecuting中可以通過設置context.Cancel = true;取消後面的工作的執行。
public void OnResultExecuting(ResultExecutingContext context) { //用於驗證的代碼略 context.Cancel = true; Debug.WriteLine("HomeController=======>OnResultExecuting"); }
再看輸出結果就至於的輸出了
HomeController=======>OnResultExecuting
同時返回結果也不再是JSON值了,返回結果以及Content-Type全部為空。所以這樣設置後,IActionResult以及OnResultExecuted都不再被執行。
在這裡除了可以做一些IActionResult執行之前的驗證,還可以對HttpContext.Response做一些簡單的操作,例如添加個Header值:
public void OnResultExecuting(ResultExecutingContext context) { context.HttpContext.Response.Headers.Add("version", "1.2"); Debug.WriteLine("HomeController=======>OnResultExecuting"); }
除了正常返回JSON結果外,Header中會出現新添加的version
Content-Type: application/json; charset=utf-8 version: 1.2
再看一下OnResultExecuted方法,它是在IActionResult執行之後執行。因為在這個方法執行的時候,請求結果已經發送給請求的客戶端了,所以在這裡可以做一些日誌類的操作。舉個例子,假如在這個方法中產生了異常:
public void OnResultExecuted(ResultExecutedContext context) { throw new Exception("exception"); Debug.WriteLine("HomeController=======>OnResultExecuted"); }
請求結果依然會返回正常的JSON,但從輸出結果中看到是不一樣的
HomeController=======>OnResultExecuting
……
System.Exception: exception
發生異常,後面的Debug輸出沒有 執行。但卻將正確的結果返回給了客戶端。
Result Filter介紹完了,看一下核心的IActionResult的執行。
4. IActionResult的執行
在ResourceInvoker的case State.ResultInside階段會調用IActionResult的執行方法InvokeResultAsync。該方法中參數IActionResult result會被調用ExecuteResultAsync方法執行。
protected virtual async Task InvokeResultAsync(IActionResult result) { var actionContext = _actionContext; _diagnosticSource.BeforeActionResult(actionContext, result); _logger.BeforeExecutingActionResult(result); try { await result.ExecuteResultAsync(actionContext); } finally { _diagnosticSource.AfterActionResult(actionContext, result); _logger.AfterExecutingActionResult(result); } }
由圖 2可知,雖然所有類型的Action的結果都被轉換成了IActionResult,但它們本質上還是有區別的。所以這個IActionResult類型的參數result實際上可能是JsonResult、ViewResult、EmptyResult等具體類型。下麵依然以第一節JSON的例子為例來看,它返回了一個JsonResult。在這裡就會調用JsonResult的ExecuteResultAsync方法,JsonResult的代碼如下:
public class JsonResult : ActionResult, IStatusCodeActionResult { //部分代碼略 public override Task ExecuteResultAsync(ActionContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } var services = context.HttpContext.RequestServices; var executor = services.GetRequiredService<JsonResultExecutor>(); return executor.ExecuteAsync(context, this); } }
在它的ExecuteResultAsync方法中會獲取依賴註入中設置的JsonResultExecutor,由JsonResultExecutor來調用ExecuteAsync方法執行後面的工作。JsonResultExecutor的代碼如下:
public class JsonResultExecutor { //部分代碼略 public virtual async Task ExecuteAsync(ActionContext context, JsonResult result) { //驗證代碼略 var response = context.HttpContext.Response; ResponseContentTypeHelper.ResolveContentTypeAndEncoding( result.ContentType, response.ContentType, DefaultContentType, out var resolvedContentType, out var resolvedContentTypeEncoding); response.ContentType = resolvedContentType; if (result.StatusCode != null) { response.StatusCode = result.StatusCode.Value; } var serializerSettings = result.SerializerSettings ?? Options.SerializerSettings; Logger.JsonResultExecuting(result.Value); using (var writer = WriterFactory.CreateWriter(response.Body, resolvedContentTypeEncoding)) { using (var jsonWriter = new JsonTextWriter(writer)) { jsonWriter.ArrayPool = _charPool; jsonWriter.CloseOutput = false; jsonWriter.AutoCompleteOnClose = false; var jsonSerializer = JsonSerializer.Create(serializerSettings); jsonSerializer.Serialize(jsonWriter, result.Value); } // Perf: call FlushAsync to call WriteAsync on the stream with any content left in the TextWriter's // buffers. This is better than just letting dispose handle it (which would result in a synchronous write). await writer.FlushAsync(); } } }
JsonResultExecutor的ExecuteAsync方法的作用就是將JsonResult中的值轉換成JSON串並寫入context.HttpContext.Response. Body中。至此JsonResult執行完畢。
ViewResult會有對應的ViewExecutor來執行,會通過相應的規則生成一個 Html頁面。
而EmptyResult的ExecuteResult方法為空,所以不會返回任何內容。
public class EmptyResult : ActionResult { /// <inheritdoc /> public override void ExecuteResult(ActionContext context) { } }
5. 下集預告
對於以上幾種類型返回結果的格式是固定的,JsonResult就會返回JSON格式,ViewResult就會返回Html格式。
但是從第一節的例子可知,string類型會返回string類型的字元串,而Book這樣的實體類型卻會返回JSON。
由圖 2可知這兩種類型在執行完畢後,都被封裝成了ObjectResult,那麼ObjectResult在執行的時候又是如何被轉換成string和JSON兩種格式的呢?
下一章繼續這個話題。