2017年10月26日 星期四

C# HttpClient WebAPI : 13. 上傳本機文字檔案與相關欄位值到遠端伺服器上

當我們要進行圖片等二進位類型檔案的上傳時候,通常也想要將一些該應用程式的資料或者狀態,上傳到後端 Web API上,若您有關看到這一系列的前面文章,您已經想到了,這可以使用查詢字串、Header、Cookie來做到。
不過,在這裡,我們將會展示另外一種做法,因為我們在用戶端使用了 multipart/form-data 資料編碼技術,所以,我們可以把我們要上傳的各種欄位,物件、狀態等資料,也透過 MultipartFormDataContent 類別物件,一並傳送過伺服器那哩,在上一篇文章中,我們使用到了 StreamContent 的物件,在更早之前的文章,我們有展示過如何使用 StringContent 的物件 (還有一種式 ByteArrayContent 類別,各位可以自行試用看看),我們將會在這篇文章中一起使用。

上傳本機文字檔案與相關欄位值到遠端伺服器上

為了簡化練習與說明,在這裡我們上傳的檔案類行為文字檔案。
當要從用戶端上傳檔案到後端 Web API 伺服器上,我們需要使用 multipart/form-data 編碼方式,所以,我們使用 var content = new MultipartFormDataContent() 敘述,建立一個 MultipartFormDataContent 類別物件。
接者,我們使用 File.Open 方法,得到要上傳文字檔案的 FileStream 物件,然後,我們建立一個 StreamContent 的物件(需要把剛剛取得的 FileStream 物件設定為建構函式的引數),設定這個物件的 Content-Type 與 Content-Disposition 描述定義,這樣,就完成了上傳圖片檔案的準備工作。
var streamContent = new StreamContent(fs);
streamContent.Headers.Add("Content-Type", "text/plain");
streamContent.Headers.Add("Content-Disposition", "form-data; name=\"files\"; filename=\"" + fooSt + "\"");
content.Add(streamContent, "file", filename);
response = await client.PostAsync(fooFullUrl, content);
另外,我們也建立了許多 StringContent 物件,把要傳送的不同欄位名稱與欄位數值都設定到 StringContent 物件內,最後加入到 MultipartFormDataContent 物件內。
最後,使用 PostAsync 方法,把剛剛產生的 MultipartFormDataContent 物件,設定為該方法的引數即可。
public static async Task<APIResult> UploadImageAsync(string filename, LoginInformation loginInformation)
{
    APIResult fooAPIResult;
    using (HttpClientHandler handler = new HttpClientHandler())
    {
        using (HttpClient client = new HttpClient(handler))
        {
            try
            {
                #region 呼叫遠端 Web API
                //string FooUrl = $"http://localhost:53495/api/Upload/FileAndData";
                string FooUrl = $"http://vulcanwebapi.azurewebsites.net/api/Upload/FileAndData";
                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");

                #region 將剛剛拍照的檔案,上傳到網路伺服器上(使用 Multipart 的規範)
                // 規格說明請參考 https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
                using (var content = new MultipartFormDataContent())
                {

                    Dictionary<string, string> formDataDictionary = new Dictionary<string, string>()
                    {
                        { nameof(loginInformation.Account), loginInformation.Account },
                        { nameof(loginInformation.Password), loginInformation.Password },
                        { nameof(loginInformation.VerifyCode), loginInformation.VerifyCode }
                    };

                    foreach (var keyValuePair in formDataDictionary)
                    {
                        content.Add(new StringContent(keyValuePair.Value), keyValuePair.Key);
                    }

                    var rootPath = Directory.GetCurrentDirectory();
                    // 取得這個圖片檔案的完整路徑
                    var path = Path.Combine(rootPath, filename);

                    // 開啟這個圖片檔案,並且讀取其內容
                    using (var fs = File.Open(path, FileMode.Open))
                    {
                        var fooSt = $"My{filename}";
                        var streamContent = new StreamContent(fs);
                        streamContent.Headers.Add("Content-Type", "text/plain");
                        streamContent.Headers.Add("Content-Disposition", "form-data; name=\"files\"; filename=\"" + fooSt + "\"");
                        content.Add(streamContent, "file", filename);
                        // 上傳到遠端伺服器上
                        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/Upload/FileAndData ,此時,將會觸發 Web API 伺服器上的 Upload 控制器(Controller)的 public async Task<APIResult> FileAndData(List<IFormFile> files, LoginInformation loginInformation) 動作(Action),其該動作的原始碼如下所示。
這個 Web API 動作,將會回傳一個 APIData 的 JSON 資料。
[HttpPost("FileAndData")]
public async Task<APIResult> FileAndData(List<IFormFile> files, LoginInformation loginInformation)
{
    // https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads

    string webDatasRoot = Path.Combine(_HostingEnvironment.WebRootPath, "Datas");

    long size = files.Sum(f => f.Length);

    // full path to file in temp location
    if (files.Count > 0)
    {
        foreach (var formFile in files)
        {
            if (formFile.Length > 0)
            {
                using (var memoryStream = new MemoryStream())
                {
                    await formFile.CopyToAsync(memoryStream);
                    memoryStream.Seek(0, SeekOrigin.Begin);
                    var streamReader = new StreamReader(memoryStream);
                    var fooContent = streamReader.ReadToEnd();

                    fooAPIResult.Success = true;
                    fooAPIResult.Message = "檔案上傳成功";
                    fooAPIResult.Payload = new LoginInformation
                    {
                        Account = $">> {loginInformation.Account}",
                        Password = $">> {loginInformation.Account}",
                        VerifyCode = fooContent
                    };
                }
            }
        }
    }
    else
    {
        fooAPIResult.Success = false;
        fooAPIResult.Message = "沒有任何檔案上傳";
        fooAPIResult.Payload = null;
    }

    return fooAPIResult;
}

進行測試

在程式進入點函式,我們建立一個 LoginInformation 型別的物件,接著,設定該物件的相關屬性,這些屬性值,是我們要傳送到遠端伺服器端的資料,另外,也會指明要傳送到遠端伺服器上的文字檔案名稱。
static async Task Main(string[] args)
{
    var fooLoginInformation = new LoginInformation()
    {
        Account = "Vulcan",
        Password = "123",
        VerifyCode = "abc"
    };
    var foo = await UploadImageAsync("Readme.txt", fooLoginInformation);
    fooLoginInformation = JsonConvert.DeserializeObject<LoginInformation>(foo.Payload.ToString());
    Console.WriteLine($"使用 MultiPart/Form-Data 格式傳送文字檔案與資料、使用 Post 方法呼叫 Web API 的結果");
    Console.WriteLine($"結果狀態 : {foo.Success}");
    Console.WriteLine($"結果訊息 : {foo.Message}");
    Console.WriteLine($"Payload : {foo.Payload}");
    Console.WriteLine($"Account : {fooLoginInformation.Account}");
    Console.WriteLine($"Password : {fooLoginInformation.Password}");
    Console.WriteLine($"文字檔案內容 : {fooLoginInformation.VerifyCode}");
    Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
    Console.ReadKey();
}

執行結果

這個測試將會輸出底下內容
使用 MultiPart/Form-Data 格式傳送文字檔案與資料、使用 Post 方法呼叫 Web API 的結果
結果狀態 : True
結果訊息 : 檔案上傳成功
Payload : {
  "account": ">> Vulcan",
  "password": ">> Vulcan",
  "verifyCode": "這是一個文字類型檔案內容"
}
Account : >> Vulcan
Password : >> Vulcan
文字檔案內容 : 這是一個文字類型檔案內容
Press any key to Exist...

HTTP 傳送與接收原始封包

讓我們來看看,這個 Web API 的呼叫動作中,在請求 (Request) 與 反應 (Response) 這兩個階段,會在網路上傳送了那些 HTTP 資料
  • 請求 (Request)
    在這裡的第三行中,您將會看到 Http Header Content-Type 其設定值為 multipart/form-data; ,這表示了 這次的 POST 請求動作中,在 Http Body 封包內的資料,將會使用 multipart/form-data 編碼技術。
    從底下的 Http 封包中,我們可以看到了總共有四個欄位資料,使用了 multipart/form-data 編碼技術,而這四個欄位的定義內容,都會使用 Http Header 的 boundary 值,進行分割,這樣,後端伺服器才能夠清楚的分辨不同欄位的資料。
POST http://vulcanwebapi.azurewebsites.net/api/Upload/FileAndData HTTP/1.1
Accept: application/json
Content-Type: multipart/form-data; boundary="7e83396d-cea9-4d3f-9f03-02fb0dd30274"
Host: vulcanwebapi.azurewebsites.net
Content-Length: 631
Expect: 100-continue
Connection: Keep-Alive

--7e83396d-cea9-4d3f-9f03-02fb0dd30274
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=Account

Vulcan
--7e83396d-cea9-4d3f-9f03-02fb0dd30274
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=Password

123
--7e83396d-cea9-4d3f-9f03-02fb0dd30274
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=VerifyCode

abc
--7e83396d-cea9-4d3f-9f03-02fb0dd30274
Content-Type: text/plain
Content-Disposition: form-data; name="files"; filename="MyReadme.txt"

這是一個文字類型檔案內容
--7e83396d-cea9-4d3f-9f03-02fb0dd30274--
  • 反應 (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: Mon, 23 Oct 2017 02:15:14 GMT

9c
{"success":true,"message":"檔案上傳成功","payload":{"account":">> Vulcan","password":">> Vulcan","verifyCode":"這是一個文字類型檔案內容"}}
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 課程


沒有留言:

張貼留言