背景
問題的起因是這樣的。群里面一個哥們兒發現在使用 ASP.NET WebAPI 時,不能在同一個方法簽名中使用多次 FromBodyAttribute 這個 Attribute 。正好我也在用 WebAPI,不過我還沒有這種需求。所以就打算研究一下。
異常信息
當使用多個 FromBodyAttribute 時,會收到下面的異常信息:
{ "Message": "An error has occurred.", "ExceptionMessage": "Can't bind multiple parameters ('a' and 'b') to the request's content.", "ExceptionType": "System.InvalidOperationException", "StackTrace": " 在 System.Web.Http.Controllers.HttpActionBinding.ExecuteBindingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)\r\n 在 System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.MoveNext()\r\n--- 引發異常的上一位置中堆棧跟蹤的末尾 ---\r\n 在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n 在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n 在 System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()" }
意思就是不能參數 a 和 b 綁定到當前請求。
源代碼追蹤
通過異常信息可以發現是在 HttpActionBinding 這個類里面拋出了這個異常。立馬去源代碼中找這個類。下面是源代碼:
通過 1,2,3 這三個點,發現是參數綁定類里面的驗證失敗。接着看了 HttpParameterBinding 是個抽象類。沒有看到太多可用信息。去 FromBodyAttribute 里面看看有沒有什么可用信息。
發現這里需要提供一個 HttpParameterBinding 的實例。從箭頭標記的方法根進去接着看。
這里可以看到返回了一個 FormatterParameterBinding 的實例。並且需要三個參數。
- parameter:從命名可以看出來是參數描述信息;
- formatters:這個應該比較熟悉了,是格式化器;
- bodyModelValidator:這個是對應參數的驗證器;
以上三個參數的意義基本就是看命名+大概閱讀源代碼得到的(所以寫代碼,命名很重要)。接着進入 FormatterParameterBinding 的源代碼。這個類里面的代碼也就 100 多行,邏輯就是從 HttpContent 中讀取內容並設置為當前參數的值。
到了這兒算是理清了一點:原來在參數上打的這些 Attribute 都是從 ParameterBindingAttribute 繼承的,又通過實現 GetBinding(HttpParameterDescriptor parameter); 方法將請求的參數與方法的參數進行綁定。
但是,在哪兒標記了不能使用多個 FromBodyAttribute 呢?既然是在 HttpActionBinding 中進行的驗證,那就順着 HttpActionBinding 往上找。通過 HttpActionBinding 的構造函數,發現只有 DefaultActionValueBinder 調用了它。接着往下看,看誰使用了這個 new 出來的實例。緊挨着就看到了 EnsureOneBodyParameter 這個方法,有點兒可疑,進去看一下。
這個地方的 WillReadBody 如果為 true 並且 idxFromBody 大於 0 ,就會給 ParameterBinding 設置錯誤消息。看了一下消息內容,就是最上面的異常消息的模板。到這里應該算是找到根兒上了。
現在來梳理一下:也就是說 HttpParameterBinding 的 WillReadBody 如果返回 true 就不能在一個方法的簽名中使用多次,一旦使用多次,就會把這個錯誤。剛才上面看到的 FormatterParameterBinding 里面的 WillReadBody 是直接返回的 true ,而且是只讀的,並且在執行綁定時是直接讀取的 HttpContent 的內容,設置為當前參數的值了。假如要執行的 action 的方法簽名中有多個參數就綁定不成功了。
定制開發
知道了這個原理,那么想在一個有多個參數的 action 中進行參數的靈活綁定,就有了辦法。分兩步走:
- 自定義一個 Attribute 從 ParameterBindingAttribute 繼承;
- 自定義一個 ParameterBinding 從 HttpParameterBinding 繼承;在 ExecuteBindingAsync 方法中綁定 action 的參數的值。並把這個自定義的類的 WillReadBody 設置為 false 。