天天看點

Web APi之捕獲請求原始内容的實作方法以及接受POST請求多個參數多種解決方案(十四)

前言

我們知道在Web APi中捕獲原始請求的内容是肯定是很容易的,但是這句話并不是完全正确,前面我們是不是讨論過,在Web APi中,如果對于字元串發出非Get請求我們則會出錯,為何?因為Web APi對于簡單的值不能很好的映射。之前我們談論過請求内容注意事項問題,本節我們将更加深入的來讨論這個問題,我們會循序漸進進行探讨,并給出可行的解決方案,。細細品,定讓你收貨多多!

捕獲複雜屬性值

Web APi對于複雜屬性值以JSON或者XML的形式成功發送到伺服器,基于這點是非常容易而且簡單的,如果我們想捕獲一個對象,我們隻需簡單的建立一個控制并在其方法上有一個對象參數即可,因為Web APi會自動以解碼JSON或者XML的處理形式到控制器上的方法參數對象中,如下:

[HttpPost]      public HttpResponseMessage PostPerson(Person person)     {     }      

對于上述我們不需要獲得person并進行解析,Web APi内部會自動檢測content type,并将其映射到MediaFormatter媒體格式并将其轉換為JSON或者XML格式,或者說我們配置的其他類型,并将其轉換為對應的格式。

如果我們是發出POST請求的表單資料,且表單資料以鍵值對的形式進行編碼,此時Web APi會利用模型綁定将其表單的鍵映射到對象的屬性中,是以由上知,對于複雜類型的映射那将是非常簡單的,這點和MVC模型綁定類似,以上就是複雜類型映射的一部分。接着我們将繼續進行讨論,請往下看。

捕獲原始請求内容

對于這個請求卻不如上述複雜類型的映射那麼簡單并且透明,例如,當我們想要通過簡單的參數如string、 number、DateTime等等。都說複雜的并不複雜,簡單的反而不簡單,從這裡看出,老外是不是也吸取了這句話的精華呢。因為Web APi是基于宿主約定,對于一些通過POST或者PUT請求的操作來捕獲其值,這是很容易的,但是就如以上複雜類型它不會進行自動檢測其類型進行映射,而且是不透明的。

我們可能會進行如下操作,并且認為結果會如我們所料,我們會認為擷取其值并進行映射到方法上的參數中。

[HttpPost]     public string PostRawContent(string content)     {         return content;     }      

如上,最終沒能如我們所願,并且還給我們任何提示,為何?因為此方法的參數簽名是有問題的。我們就不示範了,我們這裡可以總結出如下結論:

當我們發出POST值時,以下參數簽名是無效的。

(1)原始緩存資料内容

(2)帶有application/json content type的JSON字元串

(3)經過編碼的表單變量

(4)QueryString變量

事實上,我們在POST送出請求中字元串内容時,此時字元串總是空,這樣的結果對于Number、DateTime、byte[]皆是如此,在沒有添加特性的情況下都是不會進行映射,除了複雜類型比如對象、數組等。由此我們不得不想到在Web APi中對于參數的綁定,參數綁定預設情況下是利用了某種算法進行映射,且都是基于媒體類型例如(content-type header) ,當我們POST一個字元串或者位元組數組時,此時Web APi内部不知道如何去映射它,是将其映射到位元組數組?是将其映射到字元串?還是将其映射到表單資料?不得而知,是以需要對此作出一些處理才行。請繼續往下看。

為什麼JSON字元串無效?

我們其實應該将其解釋為原始字元串,而不是JSON字元串,令我們非常疑惑的是POST一個有application/json content type的JSON字元串将是無效的,像如下:

POST ......     Host: ......     Content-type: application/json; charset=utf-8      Content-Length: ......     "POST a JSON string"      

此上是一個驗證JSON的請求,但是結果是無法進行映射而失敗。  

添加【FromBody】特性到方法簽名的參數中 

我們可以通過參數綁定特性到方法簽名上的參數中,這樣就告訴Web APi這個内容的顯式來源,【FromBody】抑或【FromUrl】特性強迫POST請求的中的内容會被進行映射。例如:

[HttpPost]     public string PostRaw([FromBody] string text)     {         return text;     }      

這樣之後就允許來自Body中的内容以JSON或者XML形式進行映射,以上是示範字元串,對于其他簡單類型亦是如此,現在如果我們想POST,如下:

POST ......     Content-Type: application/json; charset=utf-8     Host: ......     Content-Length: ......     "POST a JSON string"      

現在我們就行獲得原始參數映射屬性,因為輸入的字元串是以JSON格式輸入。從此知,用【FromBody】特性标記參數能夠被映射,主要是對于要序列化的内容,例如:JSON或者XML。它要求資料以某種格式進行傳輸,【FromBody】當然也隻能在單一POST表單變量中有效,但是它的限制是僅僅隻能對于一個參數。

但是,假如我們想捕獲整個原始内容利用【FromBody】将是無效的,也就是說,如果資料不會經過JSON或者XML編碼的話,此時利用【FromBody】将毫無幫助。

捕獲請求原始内容 

如果我們不使用自定義擴充的參數綁定,我們還是有辦法來捕獲原始Http請求内容,但是此時無法将其原始捕獲值賦到一個參數上,利用這個是非常的簡單,代碼如下:

[HttpPost]     public async Task<string> PostRaw()     {         string result = await Request.Content.ReadAsStringAsync();                     return result;     }      

 ReadAsStringAsync 方法還有其他重載來捕獲如byte[]或者Stream等原始内容,似乎非常簡單。但是這樣就解決問題了嗎,如果是要捕獲其他類型的呢?難道我們寫重載方法嗎?就我們所描述的問題,這根本不是解決方案,而是解決問題。千呼萬喚始出來,最終解決方案出來了,請往下看。

建立自定義參數綁定 

為了解決我們上述所描述捕獲請求中的原始内容,我們不得的手動來實作的參數綁定,工作原理和【FromBody】實作方式類似,不過涉及Web APi中更多内容,感興趣話可以參考我最後給出有關Web APi的整個生命周期去進行了解。為了解決這個問題,我們需要實作兩點

(1)自定義參數綁定類

(2)自定義參數綁定特性來綁定參數

建立參數綁定類

首先,我們一個參數綁定特性類來擷取請求中的内容并将其可以應用到任何控制器上的方法的參數上。 預設情況下是使用基于媒體類型的綁定來處理來自JSON或者XML的模型綁定或者原始資料綁定,我們通過使用【FromBody】、【FromUrl】或者【自定義參數綁定特性】來覆寫預設的參數綁定行為,當Web APi解析控制器上的方法簽名時參數綁定會被調用。下面我們開始進行實作。

  • 定義一個自定義參數綁定類,并繼承于HttpParameterBinding
public class CustomParameterBinding : HttpParameterBinding         {             public CustomParameterBinding(HttpParameterDescriptor descriptor)                 : base(descriptor)             {             }             public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,                                                         HttpActionContext actionContext,                                                         CancellationToken cancellationToken)             {                 var binding = actionContext                     .ActionDescriptor                     .ActionBinding;                 if (binding.ParameterBindings.Length > 1 ||                     actionContext.Request.Method == HttpMethod.Get)                     return EmptyTask.Start();              }             ......         }      
  • 若參數綁定同樣隻适用一個參數并且是非GET請求,若不滿足,此時将執行一個空任務【EmptyTask】
public class EmptyTask         {             public static Task Start()             {                 var taskSource = new TaskCompletionSource<AsyncVoid>();                 taskSource.SetResult(default(AsyncVoid));                 return taskSource.Task as Task;             }             private struct AsyncVoid             {             }         }      
  • 當滿足條件後,則進行參數類型判斷并擷取原始内容
if (type == typeof(string))                 {                     return actionContext.Request.Content                             .ReadAsStringAsync()                             .ContinueWith((task) =>                             {                                 var stringResult = task.Result;                                 SetValue(actionContext, stringResult);                             });                 }                 else if (type == typeof(byte[]))                 {                     return actionContext.Request.Content                         .ReadAsByteArrayAsync()                         .ContinueWith((task) =>                         {                             byte[] result = task.Result;                             SetValue(actionContext, result);                         });                 }      
  • 綜上,整個代碼如下:
public class CustomParameterBinding : HttpParameterBinding         {             public CustomParameterBinding(HttpParameterDescriptor descriptor)                 : base(descriptor)             {             }             public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,                                                         HttpActionContext actionContext,                                                         CancellationToken cancellationToken)             {                 var binding = actionContext                     .ActionDescriptor                     .ActionBinding;                 if (binding.ParameterBindings.Length > 1 ||                     actionContext.Request.Method == HttpMethod.Get)                     return EmptyTask.Start();                 var type = binding                             .ParameterBindings[0]                             .Descriptor.ParameterType;                 if (type == typeof(string))                 {                     return actionContext.Request.Content                             .ReadAsStringAsync()                             .ContinueWith((task) =>                             {                                 var stringResult = task.Result;                                 SetValue(actionContext, stringResult);                             });                 }                 else if (type == typeof(byte[]))                 {                     return actionContext.Request.Content                         .ReadAsByteArrayAsync()                         .ContinueWith((task) =>                         {                             byte[] result = task.Result;                             SetValue(actionContext, result);                         });                 }                 throw new InvalidOperationException("Only string and byte[] are supported for [CustomParameterBinding] parameters");             }             public override bool WillReadBody             {                 get                 {                     return true;                 }             }         }      

參數綁定方法 ExecuteBindingAsync() 方法用來處理參數的轉換,通過上述Web APi提供給我們的ActionContext來根據參數類型決定參數是否是我們需要處理的參數,若檢測到該請求為非GET請求并且參數隻有一個那将進行接下來的處理,讀取Body中的請求内容,最終調用SetValue()方法來設定其值到綁定參數上,否則将忽略綁定。稍微複雜一點的就是異步任務的操作邏輯,我們知道ExecuteBingdingAsync方法始終都要傳回一個Task但是不能傳回一個null或者不能獲得一個伺服器錯誤,是以當條件不滿足時我們需要繼續執行操作而不做任何其他事情,是以我們實作一個異步執行任務EmptyTask。

建立參數綁定特性 

我們知道自定義實作了參數綁定,我們需要一個機制讓Web APi知道一個參數需要這種綁定,是以我們需要将上述參數綁定類進行附加,此種自定義綁定作為預設綁定的話将作為最後一個綁定,但是這種情況下工作并不是很可靠,因為在執行到這裡之前如果content type沒有比對到已經注冊的媒體類型之一時,Web APi此時将會阻塞,是以一個明确的特性是可靠工作的唯一保證。  

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]         public sealed class CustomBodyAttribute : ParameterBindingAttribute         {             public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)             {                 if (parameter == null)                     throw new ArgumentException("Invalid parameter");                 return new CustomParameterBinding(parameter);             }         }      

上述CustomBodyAttribute特性繼承自ParameterBindingAttribute,此唯一的目的是動态的确定此種綁定将被應用在使用了特性的參數上,這一切無非就是為了建立了上述參數綁定類的執行個體,并進行傳遞參數。

使用自定義參數綁定特性驗證 

上述操作已經全部完成,接下來就是實作,如下:

[HttpPost]             public string PostRawContent([CustomBody]string rawContent)             {                 return rawContent;             }      

單元測試  

鑒于上述,我們利用單元測試來試試是否成功。我們利用Xunit來進行測試,代碼如下:

public class UnitTest1         {             [Fact]             public async Task TestMethod1()             {                 string url = "http://localhost:7114/api/product/PostRawContent";                 string post = "Hello World";                 var httpClient = new HttpClient();                 var content = new StringContent(post);                 var response = await httpClient.PostAsync(url, content);                 string result = await response.Content.ReadAsStringAsync();                 Xunit.Assert.Equal(result, "\"" + post + "\"");             }         }      

測試通過如下:

Web APi之捕獲請求原始内容的實作方法以及接受POST請求多個參數多種解決方案(十四)

  

總結 

【FromBody】隻适用于接受經過JSON序列化的值,并且僅僅隻能是一個參數,若我們想不經過JSON序列化而獲得其原始值,那麼用【FromBody】标記方法簽名的參數将無效。 

接受POST請求多個參數解決方案 

利用模型綁定不再叙述

利用JSON Formatter  

我們給出一個Person類,并在控制器上的方法中的參數中用此類變量來接受傳遞過來的值,如下:

public class User         {             public string Name { get; set; }             public int Age { get; set; }             public string Gender { get; set; }         }         public class ProductController : ApiController         {             [HttpPost]             public int PostUser(User user)             {                 return user.Age;             }           }      

前台進行傳遞參數:

var user = { Name: "xpy0928", Age: 12, Gender: "男" };             $("#btn").click(function () {                 $.ajax({                     type: "post",                     url: "http://localhost:7114/api/product/PostUser/1",                     dataType: "json",                     data: JSON.stringify(user),                     contentType: "application/json",                     cache: false,                     error: function (x, c, e) {                         if (c == "error") {                             $(this).val(c);                         }                     },                     success: function (r) {                         alert(r);                     }                 });             });      

總結如下:

我們隻需建立一個需要傳遞的參數對象,并利用JSON.stringfy将其序列化成JSON字元串即可

第三種解決方案

對于此種解決方案,我們需要首先來叙述下應用的場景,我們知道第一和第二種解決方案是類似的,這兩種解決方案隻不過在前台進行處理的方式不同而已,模型綁定總是有效主要是依靠一個單個的對象并将其映射到實體中,但是如果是如下的多個參數呢?

[HttpPost]             public int PostUser(User user,string userToken)             {}      

這樣的場景是很常見的,我們應該如何去求解呢?有如下幾種解決辦法

  • 利用POST和QueryString聯合解決,這就不再叙述

此種方式隻能說暫時解決了問題,對于一個簡單的參數用QueryString還可以,如果是多個複雜類型對象的話,這種方式将無效,因為QueryString不支援複雜類型映射,僅僅隻對于簡單類型才有效。

  • 利用單個對象将兩個參數進行包裹

我們簡單的想象一下,如果如上述要接受這樣的參數,我們可以将其作為一個對象來擷取,就如同數學中的整體思想,将上述兩個參數封裝為一個對象來實作,一般來看的話,當我們發出POST請求最終肯定是要獲得此請求的結果或者說是請求成功的狀态,換言之,也就是我們輸入應該包裹輸入的多個參數,并且輸出最終的結果值,也就是說利用Request和Response來獲得其請求并作出響應。如下:

  • 使用者類依然不變
public class User {             public string Name { get; set; }             public int Age { get; set; }             public string Gender { get; set; }         }      
  • 包裹請求的兩個參數
public class UserRequest         {             public User User { get; set; }             public string UserToken { get; set; }         }      
  • 最後響應結果
public class UserResponse         {             public string Result { get; set; }             public int StatusCode { get; set; }             public string ErrorMessage { get; set; }         }      
  • 控制器方法接受傳入參數
[HttpPost]             public UserResponse PostUser(UserRequest userRequest)             {                 var name = userRequest.User.Name;                 var age = userRequest.User.Age;                 var userToken = userRequest.UserToken;                 return new UserResponse()                 {                     StatusCode = 200,                     Result = string.Format("name:{0},age:{1},userToken:{2}", name, age, userToken)                 };             }      
  • 前台進行傳遞參數并将其序列化 
var user = { Name: "xpy0928", Age: 12, Gender: "男" };             var userToken = "xpy09284356fd765fdf";             $("#btn").click(function () {                 $.ajax({                     type: "post",                     url: "http://localhost:7114/api/product/PostUser/1",                     dataType: "json",                     data: JSON.stringify({ User: user, UserToken: userToken }),                     contentType: "application/json",                     cache: false,                     error: function (x, c, e) {                         if (c == "error") {                             $(this).val(c);                         }                     },                     success: function (r) {                         alert(r);                     }                 });             });      

接下來我們進行驗證,是否接受成功

Web APi之捕獲請求原始内容的實作方法以及接受POST請求多個參數多種解決方案(十四)
  • 利用JObject解析多個屬性(完美解決方案,你值得擁有)  

上述似乎成功了解決了問題,但是我們不得不為方法簽名建立使用者接受和響應的對象,如果上述兩個參數是頻繁要用到,我們是不是就得每次都這樣做,這樣的話,我們就不能偷懶了,我們所說的懶,不是偷工減料而是有沒有做成代碼可複用的可能。我們想想,難道就不能将參數抽象成一個單個的對象并且為所有方法進行複用嗎?好像很複雜的樣子,确實,在JSON.NET未出世之前确實令人頭疼,但是現在一切都将變得如此簡單。

直接在Web APi上進行全自動包裝是不可能的,但是有了JSON.NET代替JSON.Serializer我們就再也不用擔心了,我們利用JObject來接受一個靜态的JSON結果,并最終将JObject的子對象進行動态轉換為強類型對象即可

  • 控制器方法改造
[HttpPost]             public string PostUser(JObject jb)             {                 dynamic json = jb;  //獲得動态對象                 JObject userJson = json.User; //擷取動态對象中子對象                 string userToken = json.UserToken;                 var user = userJson.ToObject<User>();  //将其轉換為強類型對象                 return string.Format("name:{0},age:{1},userToken:{2}", user.Name, user.Age, userToken);             }      
  • 前台調用不變
  • 瞧瞧驗證結果
Web APi之捕獲請求原始内容的實作方法以及接受POST請求多個參數多種解決方案(十四)

總結

以上對于POST請求擷取多個參數的方式可能不是最好的解決方法,将一堆參數串聯起來供Web APi來調用,在理想情況下,Web APi是隻接受單一的個參數,但是這并不意味着在任何場景下我們不需要應用上述方法,當我們需要傳遞幾個對象到伺服器上時有以上幾種方式在不同場景下供我們選擇并且是有效的。

說明 

最近找工作中,是以部落格暫時停止更新,Web APi原理還剩下參數綁定、模型綁定原了解析未更新,後續有時間再進行更新,下面給出Web APi整個生命周期的示意圖,有想學習而不知從何學Web APi的原理的園友,可以借助此示意圖進行參考學習。

示意圖連結位址:Web APi生命周期示意圖(ASP.NET Web APi Poster.PDF)

所有的選擇不過是為了下一次選擇做準備