2017年10月21日 星期六

C# HttpClient WebAPI : 6. 使用 POST 要求與 multipart/form-data MIME 類型編碼內容 呼叫 Web API

在這裡,我們來看看另外一種將資料從用戶端傳送到 Web API 伺服器端的方法:multipart/form-data,在這裡,我們需要將要傳送的資料,使用 MIME 格式進行封裝起來,並且透過 POST 的方式傳送到後端 Web API 主機上。這與我們上一篇文章中所用到的 application/x-www-form-urlencoded 方法的差異,在於所將要傳送過去的資料封裝技巧不同,我們可以從原始的 Http 封包內容中,看到兩者的差異。

了解更多關於 [HttpClient Class] 的使用方式
了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式

使用 POST 要求與 multipart/form-data 呼叫 Web API

在這個使用 multipart/form-data 編碼技術要呼叫的 Web API URL 也是 http://vulcanwebapi.azurewebsites.net/api/Values/FormUrlencodedPost,這是因為,後端使用 ASP.NET Core 所開發出來的 Web API,可以同時支援不同編碼技術。
我們建立一個 Dictionary<string, string> 泛型類別物件, formDataDictionary,將要傳送過的欄位名稱與欄位數值,分別加入到這個集合物件內。
我們使用 var content = new MultipartFormDataContent() 敘述,建立一個 content 物件,不過,因為 MultipartFormDataContent 類別以實作 IDisposable 介面,因此,我們使用 using 陳述式將其包起來,讓這個物件於使用完後,可以儘快的釋放掉非受管理的記憶體資源。
這個 MultipartFormDataContent 類別產生的物件,將會用來建立要傳送過去每筆資料紀錄,由於,在這裡我們要傳送過的都是欄位值,所以,我們使用了 StringContent 類別,將要傳送過去的欄位名稱與欄位值建立出來,再加入到 MultipartFormDataContent 集合物件內。
相關資料若都設定完成後,我們就可以呼叫 await client.PostAsync(fooFullUrl, content) 方法,將要傳送過去的資料,使用 multipart/form-data 編碼格式,送到 Web API 主機上對應的控制器與動作方法內。
private static async Task<APIResult> FormDataPostAsync(APIData apiData)
{
    APIResult fooAPIResult;
    using (HttpClientHandler handler = new HttpClientHandler())
    {
        using (HttpClient client = new HttpClient(handler))
        {
            try
            {
                #region 呼叫遠端 Web API
                string FooUrl = $"http://vulcanwebapi.azurewebsites.net/api/Values/FormUrlencodedPost";
                HttpResponseMessage response = null;

                #region  設定相關網址內容
                var fooFullUrl = $"{FooUrl}";

                // Accept 用於宣告客戶端要求服務端回應的文件型態 (底下兩種方法皆可任選其一來使用)
                //client.DefaultRequestHeaders.Accept.TryParseAdd("application/json");
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                // Content-Type 用於宣告遞送給對方的文件型態
                //client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");

                //https://docs.microsoft.com/zh-tw/dotnet/csharp/language-reference/keywords/nameof
                #region 使用 MultipartFormDataContent 產生要 Post 的資料
                // 準備要 Post 的資料
                Dictionary<string, string> formDataDictionary = new Dictionary<string, string>()
                {
                    {nameof(APIData.Id), apiData.Id.ToString() },
                    {nameof(APIData.Name), apiData.Name },
                    {nameof(APIData.Filename), apiData.Filename }
                };

                // https://msdn.microsoft.com/zh-tw/library/system.net.http.multipartformdatacontent(v=vs.110).aspx
                using (var content = new MultipartFormDataContent())
                {
                    foreach (var keyValuePair in formDataDictionary)
                    {
                        content.Add(new StringContent(keyValuePair.Value), keyValuePair.Key);
                    }
                    response = await client.PostAsync(fooFullUrl, content);
                }
                #endregion

                #endregion
                #endregion

                #region 處理呼叫完成 Web API 之後的回報結果
                if (response != null)
                {
                    if (response.IsSuccessStatusCode == true)
                    {
                        // 取得呼叫完成 API 後的回報內容
                        String strResult = await response.Content.ReadAsStringAsync();
                        fooAPIResult = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
                    }
                    else
                    {
                        fooAPIResult = new APIResult
                        {
                            Success = false,
                            Message = string.Format("Error Code:{0}, Error Message:{1}", response.StatusCode, response.RequestMessage),
                            Payload = null,
                        };
                    }
                }
                else
                {
                    fooAPIResult = new APIResult
                    {
                        Success = false,
                        Message = "應用程式呼叫 API 發生異常",
                        Payload = null,
                    };
                }
                #endregion
            }
            catch (Exception ex)
            {
                fooAPIResult = new APIResult
                {
                    Success = false,
                    Message = ex.Message,
                    Payload = ex,
                };
            }
        }
    }

    return fooAPIResult;
}

觸發的 Web API 動作

這個範例中,將會指向 URL http://vulcanwebapi.azurewebsites.net/api/Values/FormUrlencodedPost ,此時,將會觸發 Web API 伺服器上的 Values 控制器(Controller)的 public APIResult FormUrlencodedPost([FromForm]APIData value) 動作(Action),其該動作的原始碼如下所示。
這個 Web API 動作,將會回傳一個 APIData 的 JSON 資料。
[HttpPost("FormUrlencodedPost")]
public APIResult FormUrlencodedPost([FromForm]APIData value)
{
    APIResult foo;

    if (value.Id == 777)
    {
        foo = new APIResult()
        {
            Success = true,
            Message = "透過 post 方法,接收到 Id=777 資料",
            Payload = value
        };
    }
    else
    {
        foo = new APIResult()
        {
            Success = false,
            Message = "無法發現到指定的 ID",
            Payload = null
        };
    }
    return foo;
}

進行測試

在程式進入點函式,我們建立一個 APIData 型別的物件,接著,設定該物件的相關屬性,這些屬性值,是我們要傳送到遠端伺服器端的資料,由上面的程式碼中,我們可以知道,當 Id 這個屬性值為 777 的時候,該 Web API 動作將會回覆通知,這次的呼叫是成功的,否則,會回覆此次 Web API 呼叫失敗。
static async Task Main(string[] args)
{
    var fooAPIData = new APIData()
    {
        Id = 777,
        Name = "VulcanSource",
        Filename = "",
    };
    var foo = await FormDataPostAsync(fooAPIData);
    Console.WriteLine($"使用 multipart/form-data MIME 類型編碼 格式與使用 Post 方法呼叫 Web API 的結果");
    Console.WriteLine($"結果狀態 : {foo.Success}");
    Console.WriteLine($"結果訊息 : {foo.Message}");
    Console.WriteLine($"Payload : {foo.Payload}");
    Console.WriteLine($"");

    Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
    Console.ReadKey();

    fooAPIData = new APIData()
    {
        Id = 123,
        Name = "VulcanSource",
        Filename = "",
    };
    foo = await FormDataPostAsync(fooAPIData);
    Console.WriteLine($"使用 multipart/form-data MIME 類型編碼 格式與使用 Post 方法呼叫 Web API 的結果");
    Console.WriteLine($"結果狀態 : {foo.Success}");
    Console.WriteLine($"結果訊息 : {foo.Message}");
    Console.WriteLine($"Payload : {foo.Payload}");
    Console.WriteLine($"");

    Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
    Console.ReadKey();
}

執行結果

這個測試將會輸出底下內容
使用 multipart/form-data MIME 類型編碼 格式與使用 Post 方法呼叫 Web API 的結果
結果狀態 : True
結果訊息 : 透過 post 方法,接收到 Id=777 資料
Payload : {
  "id": 777,
  "name": "VulcanSource",
  "filename": null
}

Press any key to Exist...

使用 multipart/form-data MIME 類型編碼 格式與使用 Post 方法呼叫 Web API 的結果
結果狀態 : False
結果訊息 : 無法發現到指定的 ID
Payload :

Press any key to Exist...

HTTP 傳送與接收原始封包

讓我們來看看,這個 Web API 的呼叫動作中,在請求 (Request) 與 反應 (Response) 這兩個階段,會在網路上傳送了那些 HTTP 資料
  • 請求 (Request)
    在底下為原始的 Http 請求封包內容,我們看到了 Content-Type 這個 Http Header 欄位的值設定為 multipart/form-data ,這就表示了這次 POST 要求動作將會是使用 multipart/form-data 編碼方式進行傳送資料;會有這樣的結果產生,那是因為,我們有產生一個 new MultipartFormDataContent()這樣的物件,並且使用 PostAsync 方法,將這個物件傳送到這個非同步方法內。
    而 Content-Length 這個 Http Header 欄位,C# 的 HttpClient 類別,會自動幫我們計算出,此次 Postmultipart/form-data 編碼封包的總共大小。boundary 這個欄位,則是宣告了每個資料的分隔識別代碼是甚麼,這個值,在每次呼叫的時候,都會產生不同的字串出來。
    因此,對於這樣的資料
--8f545aed-8b67-40e7-996c-a357fa37d5a2
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=Id

777
這就表示了,這裡傳送了欄位為 Id (form-data; name=Id),他的數值為 777,並且這個欄位的資料編碼使用的是 text/plain; charset=utf-8
對於完整的 Http multipart/form-data 的封包使用與定義規格,請參考相關說明文件。
POST http://vulcanwebapi.azurewebsites.net/api/Values/FormUrlencodedPost HTTP/1.1
Accept: application/json
Content-Type: multipart/form-data; boundary="8f545aed-8b67-40e7-996c-a357fa37d5a2"
Host: vulcanwebapi.azurewebsites.net
Content-Length: 443
Expect: 100-continue
Connection: Keep-Alive

--8f545aed-8b67-40e7-996c-a357fa37d5a2
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=Id

777
--8f545aed-8b67-40e7-996c-a357fa37d5a2
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=Name

VulcanSource
--8f545aed-8b67-40e7-996c-a357fa37d5a2
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=Filename


--8f545aed-8b67-40e7-996c-a357fa37d5a2--
  • 反應 (Response)
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8
Server: Kestrel
X-Powered-By: ASP.NET
Set-Cookie: ARRAffinity=9d3635139ab6649f453417d1e9047b7ed7a79b7bef031b04afeb6a2c58b33d4e;Path=/;HttpOnly;Domain=vulcanwebapi.azurewebsites.net
Date: Sat, 21 Oct 2017 10:06:55 GMT

84
{"success":true,"message":"透過 post 方法,接收到 Id=777 資料","payload":{"id":777,"name":"VulcanSource","filename":null}}
0
  • 請求 (Request)
POST http://vulcanwebapi.azurewebsites.net/api/Values/FormUrlencodedPost HTTP/1.1
Accept: application/json
Content-Type: multipart/form-data; boundary="6b46064d-728d-4f76-a0e0-d1c7a2135a85"
Host: vulcanwebapi.azurewebsites.net
Content-Length: 443
Expect: 100-continue

--6b46064d-728d-4f76-a0e0-d1c7a2135a85
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=Id

123
--6b46064d-728d-4f76-a0e0-d1c7a2135a85
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=Name

VulcanSource
--6b46064d-728d-4f76-a0e0-d1c7a2135a85
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=Filename


--6b46064d-728d-4f76-a0e0-d1c7a2135a85--
  • 反應 (Response)
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8
Server: Kestrel
X-Powered-By: ASP.NET
Set-Cookie: ARRAffinity=9d3635139ab6649f453417d1e9047b7ed7a79b7bef031b04afeb6a2c58b33d4e;Path=/;HttpOnly;Domain=vulcanwebapi.azurewebsites.net
Date: Sat, 21 Oct 2017 10:06:56 GMT

48
{"success":false,"message":"無法發現到指定的 ID","payload":null}
0

相關文章索引

C# HttpClient WebAPI 系列文章索引


了解更多關於 [HttpClient Class] 的使用方式
了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式

關於 Xamarin 在台灣的學習技術資源

Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程


沒有留言:

張貼留言