教學課程:使用 C 在 .NET 主控台應用程式中提出 HTTP 要求#
本教學課程會建置對 GitHub 上的 REST 服務發出 HTTP 要求的應用程式。 應用程式會以 JSON 格式讀取資訊,並將 JSON 轉換成 C# 物件。 從 JSON 轉換成 C# 物件稱為「還原序列化」。
本教學課程顯示如何:
- 傳送 HTTP 要求。
- 還原序列化 JSON 回應。
- 使用屬性來設定還原序列化。
如果您偏好追蹤本教學課程的最終範例,則可以將其下載。 如需下載指示,請參閱範例和教學課程。
必要條件
- .NET SDK 6.0 或更新版本
- Visual Studio Code (開放原始碼、跨平台編輯器) 這類程式碼編輯器。 您可以在 Windows、Linux、macOS 或 Docker 容器中執行範例應用程式。
建立用戶端應用程式
請開啟命令提示字元,然後為您的應用程式建立新目錄。 使該目錄成為目前的目錄。
在主控台視窗中輸入下列命令:
dotnet new console --name WebAPIClient
此命令會建立基本 "Hello World" 應用程式的起始檔案。 專案名稱為 "WebAPIClient"。
導覽至 "WebAPIClient" 目錄,然後執行應用程式。
cd WebAPIClient
dotnet run
dotnet run
會自動執行dotnet restore
以還原應用程式所需的任何相依性。 其也會視需要執行dotnet build
。 您應該會看到應用程式輸出"Hello, World!"
。 在您的終端機中,按 Ctrl+C 以停止應用程式。
發出 HTTP 要求
此應用程式會呼叫 GitHub API ,以取得 .NET Foundation 底下專案的相關資訊。 端點為 https://api.github.com/orgs/dotnet/repos。 若要擷取資訊,其會提出 HTTP GET 要求。 瀏覽器也會提出 HTTP GET 要求,因此您可以將該 URL 貼入瀏覽器網址列,以查看您將接收和處理的資訊。
使用 HttpClient 類別來提出 HTTP 要求。 HttpClient 只支援其長時間執行 API 的非同步方法。 因此,下列步驟會建立非同步方法,並從 Main 方法進行呼叫。
開啟專案目錄中的
Program.cs
檔案,並將其內容取代為下列內容:await ProcessRepositoriesAsync(); static async Task ProcessRepositoriesAsync(HttpClient client) { }
此程式碼:
- 將
Console.WriteLine
陳述式取代為使用await
關鍵字的ProcessRepositoriesAsync
呼叫。 - 定義空的
ProcessRepositoriesAsync
方法。
- 將
在
Program
類別中,使用 HttpClient 來處理要求和回應,方法是將內容取代為下列 C#。using System.Net.Http.Headers; using HttpClient client = new(); client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json")); client.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter"); await ProcessRepositoriesAsync(client); static async Task ProcessRepositoriesAsync(HttpClient client) { }
此程式碼:
- 設定所有要求的 HTTP 標頭:
- 要接受 JSON 回應的
Accept
標頭 User-Agent
標題。 這些標頭受到 GitHub 伺服器程式碼的檢查,而且是從 GitHub 擷取資訊的必要項目。
- 要接受 JSON 回應的
- 設定所有要求的 HTTP 標頭:
在 方法中
ProcessRepositoriesAsync
,呼叫 GitHub 端點,以傳回 .NET 基礎組織下所有存放庫的清單:static async Task ProcessRepositoriesAsync(HttpClient client) { var json = await client.GetStringAsync( "https://api.github.com/orgs/dotnet/repos"); Console.Write(json); }
此程式碼:
- 等候呼叫 HttpClient.GetStringAsync(String) 方法所傳回的工作。 此方法會將 HTTP GET 要求傳送至指定的 URI。 回應主體會傳回為 String,而這是在工作完成時提供。
- 回應字串
json
會列印至主控台。
請建置並執行應用程式。
dotnet run
沒有建置警告,因為
ProcessRepositoriesAsync
現在包含await
運算子。 輸出是 JSON 文字的完整顯示。
還原序列化 JSON 結果
下列步驟會將 JSON 回應轉換成 C# 物件。 您可以使用 System.Text.Json.JsonSerializer 類別以將 JSON 還原序列化為物件。
建立名為 Repository.cs 的檔案,然後新增下列程式碼:
public record class Repository(string name);
上述程式碼定義類別來代表從 GitHub API 傳回的 JSON 物件。 您將使用此類別來顯示存放庫名稱清單。
存放庫物件的 JSON 包含數十個屬性,但只會還原序列化
name
屬性。 序列化程式會自動忽略目標類別中沒有相符項目的 JSON 屬性。 此功能可讓您更輕鬆地建立類型,而類型只與大型 JSON 封包中的欄位子集搭配運作。C# 慣例是將屬性名稱的第一個字母大寫,但這裡的
name
屬性會以小寫字母開頭,因為這完全符合 JSON 中的內容。 您稍後將會看到如何使用不符合 JSON 屬性名稱的 C# 屬性名稱。使用序列化程式,以將 JSON 轉換成 C# 物件。 將
ProcessRepositoriesAsync
方法中的 GetStringAsync(String) 呼叫取代為下列各行:await using Stream stream = await client.GetStreamAsync("https://api.github.com/orgs/dotnet/repos"); var repositories = await JsonSerializer.DeserializeAsync<List<Repository>>(stream);
更新過的程式碼會將 GetStringAsync(String) 取代為 GetStreamAsync(String)。 此序列化程式方法會使用資料流 (而非字串) 作為其來源。
JsonSerializer.DeserializeAsync<TValue>(Stream, JsonSerializerOptions, CancellationToken) 的第一個引數是
await
運算式。await
運算式可以出現在您程式碼中幾乎任何一個地方,雖然到目前為止,您只在指派陳述式中看到它們。 另兩個參數 (JsonSerializerOptions
和CancellationToken
) 是選用參數,而且會在程式碼片段中予以省略。DeserializeAsync
方法是泛型,這表示您針對應該從 JSON 文字建立的物件類型提供類型引數。 在此範例中,您要還原序列化為List<Repository>
,這是另一個泛型物件 (System.Collections.Generic.List<T>)。List<T>
類別可儲存物件的集合。 類型引數會宣告List<T>
中所儲存的物件類型。 類型引數是您的Repository
記錄,因為 JSON 文字代表存放庫物件的集合。新增程式碼,以顯示每個存放庫的名稱。 將下列幾行:
Console.Write(json);
取代為下列程式碼:
foreach (var repo in repositories ?? Enumerable.Empty<Repository>()) Console.Write(repo.name);
下列
using
指示詞應該出現在檔案頂端:using System.Net.Http.Headers; using System.Text.Json;
執行應用程式。
dotnet run
輸出是屬於 .NET Foundation 一部分的存放庫名稱清單。
設定還原序列化
在 Repository.cs 中,將檔案內容取代為下列 C#。
using System.Text.Json.Serialization; public record class Repository( [property: JsonPropertyName("name")] string Name);
此程式碼:
- 將
name
屬性的名稱變更為Name
。 - 新增 JsonPropertyNameAttribute,以指定此屬性在 JSON 中的顯示方式。
- 將
在 Program.cs 中,更新程式碼以使用
Name
屬性的新大寫:foreach (var repo in repositories) Console.Write(repo.Name);
執行應用程式。
輸出會相同。
重構程式碼
ProcessRepositoriesAsync
方法可以執行非同步工作,然後傳回儲存機制的集合。 變更該方法以傳回 Task<List<Repository>>
,並將寫入至主控台的程式碼移至其呼叫端附近。
請變更
ProcessRepositoriesAsync
的簽章以傳回其結果為Repository
物件清單的工作:static async Task<List<Repository>> ProcessRepositoriesAsync(HttpClient client)
在處理 JSON 回應之後,傳回存放庫:
await using Stream stream = await client.GetStreamAsync("https://api.github.com/orgs/dotnet/repos"); var repositories = await JsonSerializer.DeserializeAsync<List<Repository>>(stream); return repositories ?? new();
編譯器會產生用於傳回值的
Task<T>
物件,因為您已將此方法標記為async
。修改 Program.cs 檔案,並將
ProcessRepositoriesAsync
呼叫取代為下列內容來擷取結果,然後將每個存放庫名稱寫入至主控台。var repositories = await ProcessRepositoriesAsync(client); foreach (var repo in repositories) Console.Write(repo.Name);
執行應用程式。
輸出會相同。
還原序列化更多屬性
下列步驟會新增程式碼,以處理所收到 JSON 封包中的更多屬性。 您可能不想要處理每個屬性,但再新增一些屬性可示範其他 C# 功能。
將
Repository
類別的內容取代為下列record
定義:using System.Text.Json.Serialization; public record class Repository( [property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("description")] string Description, [property: JsonPropertyName("html_url")] Uri GitHubHomeUrl, [property: JsonPropertyName("homepage")] Uri Homepage, [property: JsonPropertyName("watchers")] int Watchers);
Uri 和
int
類型具有內建功能,可轉換成字串表示式以及轉換自字串表示式。 不需要額外的程式碼,即可將 JSON 字串格式還原序列化為這些目標類型。 如果 JSON 封包所包含的資料不會轉換成目標類型,則序列化動作會擲回例外狀況。更新 Program.cs 檔案中的
foreach
迴圈,以顯示內容值:foreach (var repo in repositories) { Console.WriteLine($"Name: {repo.Name}"); Console.WriteLine($"Homepage: {repo.Homepage}"); Console.WriteLine($"GitHub: {repo.GitHubHomeUrl}"); Console.WriteLine($"Description: {repo.Description}"); Console.WriteLine($"Watchers: {repo.Watchers:#,0}"); Console.WriteLine(); }
執行應用程式。
此清單現在會包括其他屬性。
新增日期屬性
在 JSON 回應中,會以此形式格式化最後一個推送作業的日期:
2016-02-08T21:27:00Z
此格式適用於國際標準時間 (UTC),因此還原序列化的結果是其 Kind 屬性為 Utc 的 DateTime 值。
若要取得您時區中所代表的日期和時間,您必須撰寫自訂轉換方法。
在 Repository.cs 中,新增日期和時間 UTC 表示法的屬性,以及可傳回轉換為當地時間之日期的唯讀
LastPush
屬性,而檔案看起來應該與下面類似:using System.Text.Json.Serialization; public record class Repository( [property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("description")] string Description, [property: JsonPropertyName("html_url")] Uri GitHubHomeUrl, [property: JsonPropertyName("homepage")] Uri Homepage, [property: JsonPropertyName("watchers")] int Watchers, [property: JsonPropertyName("pushed_at")] DateTime LastPushUtc) { public DateTime LastPush => LastPushUtc.ToLocalTime(); }
LastPush
屬性是使用get
存取子的「運算式主體成員」所定義。 沒有set
存取子。 省略set
存取子是在 C# 中定義「唯讀」屬性的一種方式。 (是的,您可以用 C# 來建立「唯寫」屬性,但其值會受到限制)。再次於 Program.cs 中新增另一個輸出陳述式:
Console.WriteLine($"Last push: {repo.LastPush}");
完整的應用程式應該與下列 Program.cs 檔案類似:
using System.Net.Http.Headers; using System.Text.Json; using HttpClient client = new(); client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json")); client.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter"); var repositories = await ProcessRepositoriesAsync(client); foreach (var repo in repositories) { Console.WriteLine($"Name: {repo.Name}"); Console.WriteLine($"Homepage: {repo.Homepage}"); Console.WriteLine($"GitHub: {repo.GitHubHomeUrl}"); Console.WriteLine($"Description: {repo.Description}"); Console.WriteLine($"Watchers: {repo.Watchers:#,0}"); Console.WriteLine($"{repo.LastPush}"); Console.WriteLine(); } static async Task<List<Repository>> ProcessRepositoriesAsync(HttpClient client) { await using Stream stream = await client.GetStreamAsync("https://api.github.com/orgs/dotnet/repos"); var repositories = await JsonSerializer.DeserializeAsync<List<Repository>>(stream); return repositories ?? new(); }
執行應用程式。
輸出包括上次推送至每個存放庫的日期和時間。
下一步
在本教學課程中,您已建立應用程式來提出 Web 要求以及剖析結果。 您的應用程式版本現在應該與已完成範例相符。
若要深入瞭解如何設定 JSON 序列化,請參閱 如何在 .NET 中序列化和還原序列化 (封送處理和 unmarshal) JSON。