2017年10月17日 星期二

C# HttpClient WebAPI : 3. GET 要求傳送至指定的 URI

在上一篇的文章中 C# HttpClient WebAPI : 2. 最簡單的方式使用 HttpClient 類別 中,我們使用 HttpClient client = new HttpClient(); 敘述,建立一個 HttpClient 類別的執行個體,接著就進行處理 GET 要求動作;這樣的過程看是很簡單與容易,可是對於我們實際進行專案開發上,會存在這許多問題需要克服,例如:要使用 await 的非同步方式來呼叫 client.GetStringAsync方法,還是要使用同步的方式, client.GetStringAsync("...URL...").Result,來啟動 Http 各種要求的動作;另外,對於得到的 Web API 呼叫結果,我們如何進行操作,是要使用動態解析的方式來處理,還是可以選擇較好用的強型別方式、對於 JSON 的序列與反序列化操作,又該如何設計這方面的程式碼呢?

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

使用 GET 要求傳送至指定的 URI

在這裡,我們會針對要使用 HttpClient 類別,進行各種 Web API 服務操作的程式碼寫作方式,提出一個程式碼設計模式,各位可以透過這樣的設計方法,建立出一個好用的 Http 服務存取程式。
在這篇文章中,我們還是一樣,要使用 HttpClient 類別建立出來的物件,呼叫遠端 Web API 的 GET 要求動作,底下是這樣需求的程式碼:
private static async Task<APIResult> HttpGetAsync()
{
    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";
                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");

                response = await client.GetAsync(fooFullUrl);
                #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;
}
關於上述程式碼的設計精神,如下所述:

採用非同步方法

在我們進行 C# 程式開發的時候,當面對到有 I/O 上面的需求,強烈建議需要採用非同步的方式來進行開發,因為,這樣會為您的開發專案帶來很多的好處,不過,再進行非同步方法設計的時候,您的方法回傳值必須為 Task 或 Task<T>,千萬不要使用 void 。關於這方面的詳細說明,可以參考 C# 非同步程式設計的 async void 與 async Task 的差異
 在進行非同步方法設計的時候,請不要使用 void 的回傳值。
在這個範例程式碼中,我們宣告了一個非同步方法,他的方法簽章為 async Task<APIResult> HttpGetAsync(),在這個方法內,將會使用 HttpClient.GetAsync 方法,來進行呼叫遠端 Web API 的 GET 要求動作;當我們自行設計的方法,屬於非同步的方法,請記得要在這個方法名稱之後,加入 Async 名稱,例如:HttpGetAsync,這樣,只要您一看到這樣的名稱,就知道他是個非同步的方法,並且可以使用 await 關鍵字來進行非同步方式的呼叫。
 在使用 .NET 基底類別庫的相關方法的時候,請務必要使用各類別提供的非同步方法來進行程式設計,也就是,該方法名稱會使用 Async 作為該函式名稱的結尾。
這個非同步方法的回傳值為 Task<APIResult>,也就是,在我們設計的方法中,需要回傳一個類別為 APIResult 類別產生的物件,底下是這個類別的定義。
/// <summary>
/// 呼叫 API 回傳的制式格式
/// </summary>
public class APIResult
{
    /// <summary>
    /// 此次呼叫 API 是否成功
    /// </summary>
    public bool Success { get; set; } = true;
    /// <summary>
    /// 呼叫 API 失敗的錯誤訊息
    /// </summary>
    public string Message { get; set; } = "";
    /// <summary>
    /// 呼叫此API所得到的其他內容
    /// </summary>
    public object Payload { get; set; }
}
在我們所做的一系列練習中,後端的 Web API 伺服器在接受用戶端(Client)的請求之後,會將要回報的結果,包裝在 APIResult 類別物件中,其中,在這個類別的屬性 Success 會標示這次呼叫 Web API 處理結果是否成功;而屬性 Message,則會標註這次呼叫 Web API 結果的說明文字內容;最後,若是這次呼叫 Web API,需要得到一些資料,則會將要回報的資料存放在 Payload 這個屬性中,不過,原則上,在 Payload 這個屬性中,將會儲存要回傳物件的 JSON 內容,因此,我們僅需要將這些字串內容,使用 JSON 反序列化的動作,就可以取得回報的結果物件。

使用 C# 7.1 的新功能

在以往的主控台應用程式專案中的程式進入點 Main 方法,只能夠是 void 或者 int 型別,這樣會造成我們無法在 Main 方法內,使用非同步 await 關鍵字的方式來呼叫,不過,從 C# 7.1 開始,Main 的傳回型別可以是 void、int,或是 Task、Task\
可是,當你建立一個主控台應用專案之後,發現到你無法使用這樣的進入點函式宣告 static async Task Main(string[] args),因為,若你這樣宣告 Main 方法,就會產生編譯時期的錯誤。
要解決這樣的問題相當的簡單
  • 滑鼠雙擊這個專案的 Properties 這個節點
  • 切換到 建置 標籤頁次
  • 點選 進階 按鈕
  • 在 進階建置設定 對話窗中,點選 語言版本 這個下拉選單項目,選擇 C# 7.1 項目,此時,您就會看到剛剛的錯誤訊息消失了。
    C# 7.1

充分應用 using 陳述式

我們在用戶端進行 Web API 呼叫的時候,會用到許多類別物件,例如:HttpClientHandler / HttpClient,這些類別都有實作出 IDisposable 介面,為了要能夠讓在這些類別所生成的物件,可以加速釋放掉非受管理的記憶體,我們需要使用 using 陳述式,否則,您就需要自行呼叫這些物件的 Dispose()方法。
 若所建立的類別物件,會實作 IDisposable 介面,當您完成使用型別時,便應該直接或間接處置 ( dispose )它;若要直接處置型別,呼叫其 Dispose 方法在 try/catch 區塊;若要為其配置間接,建議請一定要養成習慣,使用語言建構例如 using。
例如,在底下,我們產生 HttpClientHandler / HttpClient 物件,並且搭配 using 陳述式的程式碼用法。
using (HttpClientHandler handler = new HttpClientHandler())
{
    using (HttpClient client = new HttpClient(handler))
    {
       ...
    }
}

存取 Web API 用的類別:HttpClientHandler / HttpClient

當我們要進行遠端 Web API 服務存取的時候,會使用到這兩個類別 HttpClientHandler / HttpClient
  • HttpClientHandler 類別
    HttpClient 所使用的預設訊息處理常式,其可以使用的方法與屬性,可以參考這裡 https://msdn.microsoft.com/zh-tw/library/system.net.http.httpclienthandler(v=vs.110).aspx.aspx)
    HttpClient 會使用 HttpMessageHandler 管道(pipeline) 來傳送與接收相關請求,而 HttpClientHandler 就是 HttpClient 用於 HttpMessageHandler 管道(pipeline)的預設訊息處理常式,關於更詳盡的運作原理,可以參考 HTTP Message Handlers in ASP.NET Web API
    因為,我們在某些情境下,會要使用 HttpClientHandler 來修正存取的行為與運作方式,因此,我們會把 HttpClientHandler 也放在這裡一併產生該物件出來。
  • HttpClient 類別
    提供基底類別,用來傳送 HTTP 要求,以及從 URI 所識別的資源接收 HTTP 回應,其可以使用的方法與屬性,可以參考這裡 https://msdn.microsoft.com/zh-tw/library/system.net.http.httpclient(v=vs.110).aspx.aspx)
    這個類別提供了這些執行緒安全的方法,讓我們可以存取遠端的 Web API 服務:DeleteAsync、GetAsync、GetByteArrayAsync、GetStreamAsync、GetStringAsync、PostAsync、PutAsync、SendAsync

使用例外異常捕捉任何異常錯誤

在進行電腦程式設計的時候,務必要記得,當要進行資源(記憶體、檔案、網路等等)存取的時候,一定要將這些存取敘述放在 try { } 區塊內;例如,若網路突然斷線,造成無法繼續存取遠端的 Web API 服務,此時,你的程式將會發出例外異常,若您沒有做任何處置,則將會造成您的程式異常中斷,無法繼續執行下去。
 若我們將這些關鍵程式碼放在 try 區塊內,這樣,我們就可以在 catch 區塊內根據不同的例外異常訊息,進行相對應的處理。
在這裡,只要使用 HttpClient 物件進行後端 Web API 呼叫過程中,發生了任何例外異常狀況,我們將會在 catch 區塊內,產生一個 APIResult 物件,並且設定此次 Web API 呼叫為失敗的,並且將例外異常訊息放到 APIResult.Message 屬性上。透過這樣的設計,只要 APIResult.Success 不為 true,那叫表示呼叫 Web API 過程失敗或者不成功,可以將 APIResult.Message 顯示給使用者,讓他們知道發生了甚麼問題;最重要的是,您的應用程式不會異常終止。
catch (Exception ex)
{
    fooAPIResult = new APIResult
    {
        Success = false,
        Message = ex.Message,
        Payload = ex,
    };
}

呼叫 Web API 前置處理動作

我們繼續往這個程式碼看下去,現在已經使用 using 陳述式,建立了 HttpClientHandler / HttpClient 的物件,現在,我們就可以使用 HttpClient 的物件,進行 Web API 需求處理。
 通常,我們需要告訴後端 Web API 伺服器,告知需要將結果使用甚麼樣的 MIME 格式回傳到用戶端中,在這裡,我們需要將這樣的需求,使用名稱為 Accept 的 Header,填入 Http 請求的 Header 中;在底下的程式碼中,我們將會告知 Web API 伺服器,期望使用結果為 JSON 格式回傳到呼叫的用戶端中。
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
 想要知道更多的 MIME 類型描述,可以參考 Incomplete list of MIME types 的文件說明
在這裡的原始碼中,我們看到了這個 C# 敘述,client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");,被註解起來,這個 Content-Type Http Header,是用來告知遠端的 Web API 伺服器,此次傳送過去的 Http Body,其 MIME 的型別是甚麼?這個 C# 敘述,特別適合當您需要使用 JSON 格式文字,傳送到遠端 Web API 伺服器中,這樣,Web API 伺服器將會知道您送過來的資料,是屬於 JSON 的MIME 格式。
 想要知道更多的 Http Header 的可用名稱與其意義和用法,可以參考 HTTP headers 的文件說明
當然,還有其他的前置處理動作可以在這裡設定,例如,設定這次 Web API 呼叫的最長逾期秒數,超過這個秒數,將會視為這次的 Web API 要求失敗。類似這樣的應用,您將會在本系列文章的其他部份看到如何進行這樣的程式設計。

進行 Web API 要求動作

您可以依據您的需求,選擇要執行的 Web API 要求動作,例如:GET / POST / PUT / DELETE 等等;另外,請記得要使用非同步的方式來進行這些函式呼叫。
任何 Web API 呼叫的要求方法中,會有不同的回傳結果物件,讓我們做更多進一步的操作,在這系列文章中,大部分的範例將會得到一個 HttpResponseMessage 類別的物件,我們將會透過這個物件,確認 Web API 執行結果是否正確與取得伺服器回傳的 JSON 內容。
response = await client.GetAsync(fooFullUrl);
 若您想知道此次 Web API 伺服器回傳的內容,是屬於甚麼樣的 MIME 格式,我們可以在取得 response 物件之後,使用這個表示式,response.Content.Headers.FirstOrDefault(x => x.Key == "Content-Type").Value.FirstOrDefault();,就可以得到了;在下圖,是我們在除錯模式下,設定的執行中斷的情況。
HTTP Response Content-type

檢查 Web API 回傳結果

最後,我們需要檢查此次 Web API 所回傳的 Http 狀態碼,是否為正常(不只有狀態碼為 200),因此,我們使用了這個屬性 IsSuccessStatusCode 確認 Http 狀態碼是否為成功。
若 Http 狀態碼為成功,此時,我們使用 await response.Content.ReadAsStringAsync(); 非同步呼叫敘述,將伺服器回傳的 JSON 字串取得,接著,透過 JSON.NET 套件,使用強型別的 JsonConvert.DeserializeObject<APIResult> 泛型方法,把 JSON 字串,反序列化成為 C# 的物件,也就是 APIResult 類別的物件。
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,
    };
}

觸發的 Web API 動作

這個範例中,將會指向 URL http://vulcanwebapi.azurewebsites.net/api/values ,此時,將會觸發 Web API 伺服器上的 Values 控制器(Controller)的 public APIResult Get() 動作(Action),其該動作的原始碼如下所示。
這個 Web API 動作,將會回傳一個 APIData 的集合 JSON 資料。
[HttpGet]
public APIResult Get()
{
    APIResult foo = new APIResult()
    {
        Success = true,
        Message = "成功得所有資料集合",
        Payload = new List<APIData>()
        {
            new APIData()
            {
                Id =777,
                Name = "Vulcan01"
            },
            new APIData()
            {
                Id =234,
                Name ="Vulcan02"
            }
        }
    };
    return foo;
}

進行測試

在程式進入點函式,我們呼叫了非同步方法 HttpGetAsync(),這個方法會使用 HttpClient 物件,使用 GET 要求,與使用 http://vulcanwebapi.azurewebsites.net/api/values 這個 URL,進行後端 Web API 的呼叫。(你也可以使用瀏覽器,輸入這個網址 http://vulcanwebapi.azurewebsites.net/api/values ,也可看到 Web API 的回傳 JSON 文字內容)
不論此次 Web API 呼叫是否有成功,都會得到 APIResult 類別物件,我們可以從這個物件,得到更加詳細的 Web API 回傳結果;在這個範例中,將會得到一個集合 List<APIData> ,所以,我們從 APIResult.Payload 屬性中,將其反序列化回來,並且逐一將 APIData 物件值顯示在螢幕上。
static async Task Main(string[] args)
{
    var foo = await HttpGetAsync();
    Console.WriteLine($"使用 Get 方法呼叫 Web API 的結果");
    Console.WriteLine($"結果狀態 : {foo.Success}");
    Console.WriteLine($"結果訊息 : {foo.Message}");
    var fooAPIData = JsonConvert.DeserializeObject<List<APIData>>(foo.Payload.ToString());
    foreach (var item in fooAPIData)
    {
        Console.WriteLine($"Id : {item.Id}");
        Console.WriteLine($"Name : {item.Name}");
        Console.WriteLine($"Filename : {item.Filename}");
    }
    Console.WriteLine($"");

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

}

執行結果

這個測試將會輸出底下內容
使用 Get 方法呼叫 Web API 的結果
結果狀態 : True
結果訊息 : 成功得所有資料集合
Id : 777
Name : Vulcan01
Filename :
Id : 234
Name : Vulcan02
Filename :

Press any key to Exist...

HTTP 傳送與接收原始封包

讓我們來看看,這個 Web API 的呼叫動作中,在請求 (Request) 與 反應 (Response) 這兩個階段,會在網路上傳送了那些 HTTP 資料
  • 請求 (Request)
    在這個階段,您將會看到我們使用的敘述 client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 所產生的 Http Header,有出現在傳送的 Http 封包內容中。
GET http://vulcanwebapi.azurewebsites.net/api/values HTTP/1.1
Accept: application/json
Host: vulcanwebapi.azurewebsites.net
Connection: Keep-Alive
  • 反應 (Response)
    在這個階段,我們也看到了, Web API 伺服器,告知用戶端,他回傳的 Http 封包內容,是使用 MIME application/json; charset=utf-8 的格式內容,也就是 JSON 內容。
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: Wed, 18 Oct 2017 15:54:09 GMT

9e
{"success":true,"message":"成功得所有資料集合","payload":[{"id":777,"name":"Vulcan01","filename":null},{"id":234,"name":"Vulcan02","filename":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 課程


沒有留言:

張貼留言