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

前言

咱們知道在Web APi中捕獲原始請求的內容是確定是很容易的,可是這句話並非徹底正確,前面咱們是否是討論過,在Web APi中,若是對於字符串發出非Get請求咱們則會出錯,爲什麼?由於Web APi對於簡單的值不能很好的映射。以前咱們談論過請求內容注意事項問題,本節咱們將更加深刻的來討論這個問題,咱們會按部就班進行探討,並給出可行的解決方案,。細細品,定讓你收貨多多!ajax

捕獲請求原始內容實現方法

捕獲複雜屬性值

Web APi對於複雜屬性值以JSON或者XML的形式成功發送到服務器,基於這點是很是容易並且簡單的,若是咱們想捕獲一個對象,咱們只需簡單的建立一個控制並在其方法上有一個對象參數便可,由於Web APi會自動以解碼JSON或者XML的處理形式到控制器上的方法參數對象中,以下:算法

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

對於上述咱們不須要得到person並進行解析,Web APi內部會自動檢測content type,並將其映射到MediaFormatter媒體格式並將其轉換爲JSON或者XML格式,或者說咱們配置的其餘類型,並將其轉換爲對應的格式。json

若是咱們是發出POST請求的表單數據,且表單數據以鍵值對的形式進行編碼,此時Web APi會利用模型綁定將其表單的鍵映射到對象的屬性中,因此由上知,對於複雜類型的映射那將是很是簡單的,這點和MVC模型綁定相似,以上就是複雜類型映射的一部分。接着咱們將繼續進行討論,請往下看。api

捕獲原始請求內容

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

咱們可能會進行以下操做,而且認爲結果會如咱們所料,咱們會認爲獲取其值並進行映射到方法上的參數中。緩存

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

如上,最終沒能如咱們所願,而且還給咱們任何提示,爲什麼?由於此方法的參數簽名是有問題的。咱們就不演示了,咱們這裏能夠總結出以下結論:服務器

當咱們發出POST值時,如下參數簽名是無效的。app

(1)原始緩存數據內容異步

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

(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 + "\"");
        }
    }

測試經過以下:

  

總結 

【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);
                }
            });
        });

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

  • 利用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);

        }
  • 前臺調用不變
  • 瞧瞧驗證結果

總結

以上對於POST請求獲取多個參數的方式可能不是最好的解決方法,將一堆參數串聯起來供Web APi來調用,在理想狀況下,Web APi是隻接受單一的個參數,可是這並不意味着在任何場景下咱們不須要應用上述方法,當咱們須要傳遞幾個對象到服務器上時有以上幾種方式在不一樣場景下供咱們選擇而且是有效的。

 

說明 

最近找工做中,因此博客暫時中止更新,Web APi原理還剩下參數綁定、模型綁定原理解析未更新,後續有時間再進行更新,下面給出Web APi整個生命週期的示意圖,有想學習而不知從何學Web APi的原理的園友,能夠藉助此示意圖進行參考學習。

示意圖連接地址:Web APi生命週期示意圖(ASP.NET Web APi Poster.PDF)

相關文章
相關標籤/搜索