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
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...