ASP.NET Core 2.2 : 二十. Action的多種數據返回格式處理機制

来源:https://www.cnblogs.com/FlyLolo/archive/2019/09/11/ASPNETCore2_20.html
-Advertisement-
Play Games

上一章講了系統如何將客戶端提交的請求數據格式化處理成我們想要的格式並綁定到對應的參數,本章講一下它的“逆過程”,如何將請求結果按照客戶端想要的格式返回去。(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

還有其他類型,這裡暫不列舉了,總結一下:

  1. 返回結果有空、Html頁面、普通字元串、JSON字元串幾種。
  2. 對應的Content-Type類型有空、text/html、text/plain、application/json幾種。
  3. 非同步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兩種格式的呢?

下一章繼續這個話題。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • .Net core MVC 如何使用 .NET Core,最基本的入行,很多博客以及官網都有的太多太多的例子,但是大部分沒有人做到了真的讓一個小白一步一步的去學, 我第一次接觸的時候,連最基本的wwwroot都不知道是幹嘛用的。現在我們一起來看看它是幹嘛的~ 一 什麼是.NET Core,優點如何? ...
  • 前提 入行已經7,8年了,一直想做一套漂亮點的自定義控制項,於是就有了本系列文章。 GitHub:https://github.com/kwwwvagaa/NetWinformControl 碼雲:https://gitee.com/kwwwvagaa/net_winform_custom_contr ...
  • 在ASP.NET-WebForm程式中,添加SiteMapPath控制項時出現問題,如下圖所示: 解決辦法:找到上圖源文件指向的machine.config配置文件,將siteMap節點註釋即可。 ...
  • 我原創整理的powershell 6,7的新特性。基本可以說網上唯一。 1每個特性都註明瞭版本號,從這個版本開始,才支持這個特性。 2歡迎挑毛病,讓我更完善帖子。 3大都是ps6的新特性。ps7剛剛開始開發,新特性也只有一點點。 ...
  • 關於Oracle資料庫的連接失敗問題,有N種情況都會導致,這次遇到的是一般開發或者運維人員難以發現的 場景: 有一臺機A能夠正常連接資料庫並正常運行,機器B連接失敗 32位WebService程式基於.Net4.0開發,部署在IIS上,通過Oracle Client鏈接資料庫 問題排查: IIS啟用 ...
  • 序列化字元串後,值變成了"2018-02-05T00:00:00" 序列化時候 需要更改一下日期轉換方式: IsoDateTimeConverter timeConverter = new IsoDateTimeConverter { DateTimeFormat = "yyyy-MM-dd HH: ...
  • 前篇已經完成後端配置並獲取到api連接 https://www.cnblogs.com/ouyangkai/p/11504279.html 前端項目用的是VS code編譯器完成 vue 第一步 引入 編寫前端vue 註意以上是用 axios.get方式獲取後端api鏈接獲取數據,並利用vue渲染到 ...
  • 場景 在WInform中使用DevExpress時經常使用PanelControl控制項用來進行佈局設計,因此需要在代碼中生成控制項並添加子控制項。 實現 一種是設置要添加的自控制項的Parent屬性為容器控制項。 最常用的還是通過控制項的Controls屬性的Add方法將子控制項添加進來。 註: 博客首頁: h ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...