教學課程:使用 C 在 .NET 主控台應用程式中提出 HTTP 要求#

本教學課程會建置對 GitHub 上的 REST 服務發出 HTTP 要求的應用程式。 應用程式會以 JSON 格式讀取資訊,並將 JSON 轉換成 C# 物件。 從 JSON 轉換成 C# 物件稱為「還原序列化」。

本教學課程顯示如何:

  • 傳送 HTTP 要求。
  • 還原序列化 JSON 回應。
  • 使用屬性來設定還原序列化。

如果您偏好追蹤本教學課程的最終範例,則可以將其下載。 如需下載指示,請參閱範例和教學課程

必要條件

  • .NET SDK 6.0 或更新版本
  • Visual Studio Code (開放原始碼、跨平台編輯器) 這類程式碼編輯器。 您可以在 Windows、Linux、macOS 或 Docker 容器中執行範例應用程式。

建立用戶端應用程式

  1. 請開啟命令提示字元,然後為您的應用程式建立新目錄。 使該目錄成為目前的目錄。

  2. 在主控台視窗中輸入下列命令:

    dotnet new console --name WebAPIClient
    

    此命令會建立基本 "Hello World" 應用程式的起始檔案。 專案名稱為 "WebAPIClient"。

  3. 導覽至 "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 方法進行呼叫。

  1. 開啟專案目錄中的 Program.cs 檔案,並將其內容取代為下列內容:

    await ProcessRepositoriesAsync();
    
    static async Task ProcessRepositoriesAsync(HttpClient client)
    {
    }
    

    此程式碼:

    • Console.WriteLine 陳述式取代為使用 await 關鍵字的 ProcessRepositoriesAsync 呼叫。
    • 定義空的 ProcessRepositoriesAsync 方法。
  2. 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 擷取資訊的必要項目。
  3. 在 方法中 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 會列印至主控台。
  4. 請建置並執行應用程式。

    dotnet run
    

    沒有建置警告,因為 ProcessRepositoriesAsync 現在包含 await 運算子。 輸出是 JSON 文字的完整顯示。

還原序列化 JSON 結果

下列步驟會將 JSON 回應轉換成 C# 物件。 您可以使用 System.Text.Json.JsonSerializer 類別以將 JSON 還原序列化為物件。

  1. 建立名為 Repository.cs 的檔案,然後新增下列程式碼:

    public record class Repository(string name);
    

    上述程式碼定義類別來代表從 GitHub API 傳回的 JSON 物件。 您將使用此類別來顯示存放庫名稱清單。

    存放庫物件的 JSON 包含數十個屬性,但只會還原序列化 name 屬性。 序列化程式會自動忽略目標類別中沒有相符項目的 JSON 屬性。 此功能可讓您更輕鬆地建立類型,而類型只與大型 JSON 封包中的欄位子集搭配運作。

    C# 慣例是將屬性名稱的第一個字母大寫,但這裡的 name 屬性會以小寫字母開頭,因為這完全符合 JSON 中的內容。 您稍後將會看到如何使用不符合 JSON 屬性名稱的 C# 屬性名稱。

  2. 使用序列化程式,以將 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 運算式可以出現在您程式碼中幾乎任何一個地方,雖然到目前為止,您只在指派陳述式中看到它們。 另兩個參數 (JsonSerializerOptionsCancellationToken) 是選用參數,而且會在程式碼片段中予以省略。

    DeserializeAsync 方法是泛型,這表示您針對應該從 JSON 文字建立的物件類型提供類型引數。 在此範例中,您要還原序列化為 List<Repository>,這是另一個泛型物件 (System.Collections.Generic.List<T>)。 List<T> 類別可儲存物件的集合。 類型引數會宣告 List<T> 中所儲存的物件類型。 類型引數是您的 Repository 記錄,因為 JSON 文字代表存放庫物件的集合。

  3. 新增程式碼,以顯示每個存放庫的名稱。 將下列幾行:

    Console.Write(json);
    

    取代為下列程式碼:

    foreach (var repo in repositories ?? Enumerable.Empty<Repository>())
        Console.Write(repo.name);
    
  4. 下列 using 指示詞應該出現在檔案頂端:

    using System.Net.Http.Headers;
    using System.Text.Json;
    
  5. 執行應用程式。

    dotnet run
    

    輸出是屬於 .NET Foundation 一部分的存放庫名稱清單。

設定還原序列化

  1. Repository.cs 中,將檔案內容取代為下列 C#。

    using System.Text.Json.Serialization;
    
    public record class Repository(
        [property: JsonPropertyName("name")] string Name);
    

    此程式碼:

    • name 屬性的名稱變更為 Name
    • 新增 JsonPropertyNameAttribute,以指定此屬性在 JSON 中的顯示方式。
  2. Program.cs 中,更新程式碼以使用 Name 屬性的新大寫:

    foreach (var repo in repositories)
       Console.Write(repo.Name);
    
  3. 執行應用程式。

    輸出會相同。

重構程式碼

ProcessRepositoriesAsync 方法可以執行非同步工作,然後傳回儲存機制的集合。 變更該方法以傳回 Task<List<Repository>>,並將寫入至主控台的程式碼移至其呼叫端附近。

  1. 請變更 ProcessRepositoriesAsync 的簽章以傳回其結果為 Repository 物件清單的工作:

    static async Task<List<Repository>> ProcessRepositoriesAsync(HttpClient client)
    
  2. 在處理 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

  3. 修改 Program.cs 檔案,並將 ProcessRepositoriesAsync 呼叫取代為下列內容來擷取結果,然後將每個存放庫名稱寫入至主控台。

    var repositories = await ProcessRepositoriesAsync(client);
    
    foreach (var repo in repositories)
        Console.Write(repo.Name);
    
  4. 執行應用程式。

    輸出會相同。

還原序列化更多屬性

下列步驟會新增程式碼,以處理所收到 JSON 封包中的更多屬性。 您可能不想要處理每個屬性,但再新增一些屬性可示範其他 C# 功能。

  1. 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);
    

    Uriint 類型具有內建功能,可轉換成字串表示式以及轉換自字串表示式。 不需要額外的程式碼,即可將 JSON 字串格式還原序列化為這些目標類型。 如果 JSON 封包所包含的資料不會轉換成目標類型,則序列化動作會擲回例外狀況。

  2. 更新 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();
    }
    
  3. 執行應用程式。

    此清單現在會包括其他屬性。

新增日期屬性

在 JSON 回應中,會以此形式格式化最後一個推送作業的日期:

2016-02-08T21:27:00Z

此格式適用於國際標準時間 (UTC),因此還原序列化的結果是其 Kind 屬性為 UtcDateTime 值。

若要取得您時區中所代表的日期和時間,您必須撰寫自訂轉換方法。

  1. 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# 來建立「唯寫」屬性,但其值會受到限制)。

  2. 再次於 Program.cs 中新增另一個輸出陳述式:

    Console.WriteLine($"Last push: {repo.LastPush}");
    
  3. 完整的應用程式應該與下列 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();
    }
    
  4. 執行應用程式。

    輸出包括上次推送至每個存放庫的日期和時間。

下一步

在本教學課程中,您已建立應用程式來提出 Web 要求以及剖析結果。 您的應用程式版本現在應該與已完成範例相符。

若要深入瞭解如何設定 JSON 序列化,請參閱 如何在 .NET 中序列化和還原序列化 (封送處理和 unmarshal) JSON