教程:在 .NET 控制台应用程序使用 C# 发出 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 foundation 组织下的所有存储库列表:

     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),因此反序列化的结果是 DateTime 值,其 Kind 属性为 Utc

若要获取你所在时区表示的日期和时间,必须写入自定义转换方法。

  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 访问器的 expression-bodied member 进行定义。 不存在 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 请求并分析结果的应用。 你的应用版本现在应与已完成的示例匹配。

若要详细了解如何在如何在 .NET 中序列化和反序列化(封送和拆收)JSON 中配置 JSON 序列化。