1. 程式人生 > >ASP.NET Core 2.2 : 二十. Action的多資料返回格式處理機制

ASP.NET Core 2.2 : 二十. 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兩種格式的呢?

下一章繼續這個話