教程:使用 C 在 .NET 控制台应用中发出 HTTP 请求#

本教程构建了一个应用,该应用向 GitHub 上的 REST 服务发出 HTTP 请求。 应用以 JSON 格式读取信息,并将 JSON 转换为 C# 对象。 从 JSON 转换为 C# 对象称为 反序列化

本教程演示如何:

  • 发送 HTTP 请求。
  • 反序列化 JSON 响应。
  • 使用属性配置反序列化。

如果想要遵循本教程 的最后一个示例 ,可以下载它。 有关下载说明,请参阅 示例和教程

先决条件

创建客户端应用

  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语句替换为使用该关键字的ProcessRepositoriesAsyncawait调用。
    • 定义空 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 标头:
      • Accept接受 JSON 响应的标头
      • 标头 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 结果

以下步骤简化了提取数据和处理数据的方法。 将使用 GetFromJsonAsyncSystem.Net.Http.Json NuGet 包的📦扩展方法将 JSON 结果提取和反序列化为对象。

  1. 创建名为 Repository.cs 的文件并添加以下代码:

    public record class Repository(string Name);
    

    前面的代码定义一个类来表示从 GitHub API 返回的 JSON 对象。 你将使用此类来显示存储库名称的列表。

    存储库对象的 JSON 包含数十个属性,但只会 Name 反序列化该属性。 序列化程序会自动忽略目标类中不存在匹配项的 JSON 属性。 使用此功能可以更轻松地创建仅处理大型 JSON 数据包中的字段子集的类型。

    GetFromJsonAsync尽管在下一点中使用的方法在属性名称方面不区分大小写,但 C# 约定是将属性名称的第一个字母大写

  2. HttpClientJsonExtensions.GetFromJsonAsync使用该方法提取 JSON 并将其转换为 C# 对象。 将ProcessRepositoriesAsync方法中的调用GetStringAsync(String)替换为以下行:

    var repositories = await client.GetFromJsonAsync<List<Repository>>("https://api.github.com/orgs/dotnet/repos");
    

    更新后的代码替换为 GetStringAsync(String)HttpClientJsonExtensions.GetFromJsonAsync.

    方法的第一个参数 GetFromJsonAsync 是表达式 awaitawait 表达式几乎可以出现在代码中的任何位置,即使到目前为止,你只将其视为赋值语句的一部分。 下一个参数是可选的, requestUri 如果创建 client 对象时已指定,则无需提供此参数。 未向对象提供 client 要向其发送请求的 URI,因此现在指定了 URI。 最后一个可选参数, CancellationToken 代码片段中省略该参数。

    该方法 GetFromJsonAsync泛型方法,这意味着为应从提取的 JSON 文本创建对象的类型参数提供类型参数。 在此示例中,你将反序列化为 List<Repository>另一个泛型对象,即另一个 System.Collections.Generic.List<T>泛型对象。 该 List<T> 类存储对象的集合。 type 参数声明存储在 . 中的 List<T>对象的类型。 类型参数是记录 Repository ,因为 JSON 文本表示存储库对象的集合。

  3. 添加代码以显示每个存储库的名称。 替换读取的行:

    Console.Write(json);
    

    使用以下代码:

    foreach (var repo in repositories ?? Enumerable.Empty<Repository>())
        Console.WriteLine(repo.Name);
    
  4. 文件顶部应存在以下 using 指令:

    using System.Net.Http.Headers;
    using System.Net.Http.Json;
    
  5. 运行应用。

    dotnet run
    

    输出是属于 .NET Foundation 的存储库名称的列表。

重构代码

该方法 ProcessRepositoriesAsync 可以执行异步工作并返回存储库的集合。 更改该方法以返回 Task<List<Repository>>,并将写入到控制台的代码移到其调用方附近。

  1. 更改要返回其结果为对象列表的任务的Repository签名ProcessRepositoriesAsync

    static async Task<List<Repository>> ProcessRepositoriesAsync(HttpClient client)
    
  2. 处理 JSON 响应后返回存储库:

    var repositories = await client.GetFromJsonAsync<List<Repository>>("https://api.github.com/orgs/dotnet/repos");
    return repositories ?? new();
    

    编译器为返回值生成对象, Task<T> 因为已将此方法 async标记为 .

  3. 修改 Program.cs 文件,将调用 ProcessRepositoriesAsync 替换为以下内容,以捕获结果并将每个存储库名称写入控制台。

    var repositories = await ProcessRepositoriesAsync(client);
    
    foreach (var repo in repositories)
        Console.WriteLine(repo.Name);
    
  4. 运行应用。

    输出相同。

反序列化更多属性

以下步骤添加代码以处理接收的 JSON 数据包中的更多属性。 你可能不想处理每个属性,但添加更多属性演示了 C# 的其他功能。

  1. 将类的内容 Repository 替换为以下 record 定义:

    public record class Repository(
        string Name,
        string Description,
        Uri GitHubHomeUrl,
        Uri Homepage,
        int Watchers,
        DateTime LastPushUtc
    );
    

    Uriint类型具有用于转换到字符串表示形式和从字符串表示形式的内置功能。 无需额外的代码即可从 JSON 字符串格式反序列化到这些目标类型。 如果 JSON 数据包包含的数据未转换为目标类型,则序列化作将引发异常。

    JSON 通常对对象的名称使用小写形式,但是我们不需要进行任何转换,并且可以保留字段名称的大写,因为与前面的某个点一样, GetFromJsonAsync 扩展方法在属性名称方面不区分大小写。

  2. foreach更新Program.cs文件中的循环以显示属性值:

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

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

  1. Repository.cs中,为日期和时间的 UTC 表示形式添加属性,以及返回转换为本地时间的日期的只读 LastPush 属性,该文件应如下所示:

    public record class Repository(
        string Name,
        string Description,
        Uri GitHubHomeUrl,
        Uri Homepage,
        int Watchers,
        DateTime LastPushUtc
    )
    {
        public DateTime LastPush => LastPushUtc.ToLocalTime();
    }
    

    LastPush属性是使用访问器的 expression-bodied 成员get定义的。 没有 set 访问器。 省略 set 访问器是在 C# 中定义 只读 属性的一种方法。 (是的,可以在 C# 中创建 仅写 属性,但其值有限。

  2. Program.cs 中添加另一个输出语句: 再次:

    Console.WriteLine($"Last push: {repo.LastPush}");
    
  3. 完整的应用应类似于以下 Program.cs 文件:

    using System.Net.Http.Headers;
    using System.Net.Http.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)
    {
        var repositories = await client.GetFromJsonAsync<List<Repository>>("https://api.github.com/orgs/dotnet/repos");
        return repositories ?? new List<Repository>();
    }
    
  4. 运行应用。

    输出包括上次推送到每个存储库的日期和时间。

后续步骤

在本教程中,你创建了一个应用,用于发出 Web 请求并分析结果。 应用版本现在应与 已完成的示例匹配。

详细了解如何在 .NET 中配置 JSON 序列化,以及如何序列化和反序列化 (封送和取消序列化) JSON