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

測試通過如下:

  

總結 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM