針對 .NET / CLR / C# / Blazor / MAUI / Xamarin / .NET Core / .NET Framework / OOP / Design Pattern 等相關程式開發議題進行研究與寫成相關心得筆記。
2017年10月18日 星期三
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 的 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
關鍵字來進行非同步方式的呼叫。
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
項目,此時,您就會看到剛剛的錯誤訊息消失了。
充分應用 using 陳述式
我們在用戶端進行 Web API 呼叫的時候,會用到許多類別物件,例如:
HttpClientHandler
/ HttpClient
,這些類別都有實作出 IDisposable
介面,為了要能夠讓在這些類別所生成的物件,可以加速釋放掉非受管理的記憶體,我們需要使用 using
陳述式,否則,您就需要自行呼叫這些物件的 Dispose()
方法。
例如,在底下,我們產生
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 服務,此時,你的程式將會發出例外異常,若您沒有做任何處置,則將會造成您的程式異常中斷,無法繼續執行下去。

在這裡,只要使用 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 需求處理。
Accept
的 Header,填入 Http 請求的 Header 中;在底下的程式碼中,我們將會告知 Web API 伺服器,期望使用結果為 JSON 格式回傳到呼叫的用戶端中。client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

在這裡的原始碼中,我們看到了這個 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 格式。
當然,還有其他的前置處理動作可以在這裡設定,例如,設定這次 Web API 呼叫的最長逾期秒數,超過這個秒數,將會視為這次的 Web API 要求失敗。類似這樣的應用,您將會在本系列文章的其他部份看到如何進行這樣的程式設計。
進行 Web API 要求動作
您可以依據您的需求,選擇要執行的 Web API 要求動作,例如:GET / POST / PUT / DELETE 等等;另外,請記得要使用非同步的方式來進行這些函式呼叫。
任何 Web API 呼叫的要求方法中,會有不同的回傳結果物件,讓我們做更多進一步的操作,在這系列文章中,大部分的範例將會得到一個
HttpResponseMessage
類別的物件,我們將會透過這個物件,確認 Web API 執行結果是否正確與取得伺服器回傳的 JSON 內容。response = await client.GetAsync(fooFullUrl);

response.Content.Headers.FirstOrDefault(x => x.Key == "Content-Type").Value.FirstOrDefault();
,就可以得到了;在下圖,是我們在除錯模式下,設定的執行中斷的情況。
檢查 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 / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程
訂閱:
文章 (Atom)
Query String
的方法;至於其他傳送資料的使用技術方法,會在其他文章中做說明,在這裡,我們將會說明如何在 GET 要求的時候,使用查詢字串,呼叫遠端 Web API 的指定動作。使用 GET 要求與 QueryString 呼叫 Web API
private static async Task<APIResult> GetQueryStringAsync(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/QueryStringGet"; 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 組合 QueryString Dictionary<string, string> dic = new Dictionary<string, string>(); dic.Add(nameof(apiData.Id), apiData.Id.ToString()); dic.Add(nameof(apiData.Name), apiData.Name.ToString()); dic.Add(nameof(apiData.Filename), apiData.Filename.ToString()); string queryString = "?"; foreach (string key in dic.Keys) { queryString += key + "=" + dic[key] + "&"; } queryString = queryString.Remove(queryString.Length - 1, 1); #endregion response = await client.GetAsync(fooFullUrl+queryString); #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; }
APIData
的參數物件,我們需要將這個物件組合成為查詢字串 (Query String),並且組合到我們要呼叫的 Web API URL 後;從底下的程式碼中,我們宣告了Dictionary<string, string>
型別變數,dic
,我們使用這樣的程式碼敘述:Dictionary<string, string> dic = new Dictionary<string, string>();
,接著我們收到的方法參數物件內的每個屬性,加入到 dic 中。Dictionary<string, string>
型別變數中,他的鍵值 (Key),我們需要指定這個參數屬性的名稱,我們採用強型別的方式寫法,以避免撰寫這類程式碼而造成執行時期的錯誤,所以,我們使用nameof(apiData.Id)
這樣的寫法,其中,nameof 運算子關鍵字的功用將是:用來取得變數、類型或成員的簡單字串名稱;而在鍵值的相對應的數值,我們就需要設定這個屬性的實際物件內容。string queryString = "?"; foreach (string key in dic.Keys) { queryString += key + "=" + dic[key] + "&"; } queryString = queryString.Remove(queryString.Length - 1, 1);
觸發的 Web API 動作
http://vulcanwebapi.azurewebsites.net/api/values/QueryStringGet
,此時,將會觸發 Web API 伺服器上的 Values 控制器(Controller)的public APIResult QueryStringGet([FromQuery] APIData value)
動作(Action),其該動作的原始碼如下所示。[HttpGet("QueryStringGet")] public APIResult QueryStringGet([FromQuery] APIData value) { APIResult foo; if (value.Id == 777) { foo = new APIResult() { Success = true, Message = "透過 Get 方法,接收到 Id=777", Payload = new APIData() { Id = 777, Name = "Vulcan by QueryStringGet" } }; } 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 = "Vulcan", Filename = "", }; var foo = await GetQueryStringAsync(fooAPIData); Console.WriteLine($"使用 Get 與 QueryString 方法呼叫 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 = "Vulcan", Filename = "", }; foo = await GetQueryStringAsync(fooAPIData); Console.WriteLine($"使用 Get 與 QueryString 方法呼叫 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(); }
執行結果
HTTP 傳送與接收原始封包
相關文章索引
關於 Xamarin 在台灣的學習技術資源