REST 客户端REST client

此教程将介绍 .NET Core 和 C# 语言的许多功能。This tutorial teaches you a number of features in .NET Core and the C# language. 你将了解:You’ll learn:

  • .NET Core CLI 的基础知识。The basics of the .NET Core CLI.
  • C# 语言功能概述。An overview of C# Language features.
  • 如何使用 NuGet 管理依赖项Managing dependencies with NuGet
  • HTTP 通信HTTP Communications
  • 如何处理 JSON 信息Processing JSON information
  • 如何管理含特性的配置。Managing configuration with Attributes.

将生成一个应用程序,向 GitHub 上的 REST 服务发出 HTTP 请求。You’ll build an application that issues HTTP Requests to a REST service on GitHub. 需要读取 JSON 格式的信息,并将相应的 JSON 数据包转换成 C# 对象。You'll read information in JSON format, and convert that JSON packet into C# objects. 最后将了解如何使用 C# 对象。Finally, you'll see how to work with C# objects.

此教程中介绍了多项功能。There are many features in this tutorial. 我们将逐个生成这些功能。Let’s build them one by one.

如果想要按照针对本主题的最终示例操作,可以下载它。If you prefer to follow along with the final sample for this topic, you can download it. 有关下载说明,请参阅示例和教程For download instructions, see Samples and Tutorials.

先决条件Prerequisites

必须将计算机设置为运行 .Net Core。You’ll need to set up your machine to run .NET core. 有关安装说明,请访问 .NET Core 下载页。You can find the installation instructions on the .NET Core Downloads page. 可以在 Windows、Linux、macOS 或 Docker 容器中运行此应用程序。You can run this application on Windows, Linux, macOS or in a Docker container. 必须安装常用的代码编辑器。You’ll need to install your favorite code editor. 在以下说明中,我们使用的是开放源代码跨平台编辑器 Visual Studio CodeThe descriptions below use Visual Studio Code, which is an open source, cross platform editor. 不过,你可以使用习惯使用的任意工具。However, you can use whatever tools you are comfortable with.

创建应用程序Create the Application

第一步是新建应用程序。The first step is to create a new application. 打开命令提示符,然后新建应用程序的目录。Open a command prompt and create a new directory for your application. 将新建的目录设为当前目录。Make that the current directory. 在控制台窗口中输入以下命令:Enter the following command in a console window:

dotnet new console --name WebApiClient

这将为基本的“Hello World”应用程序创建起始文件。This creates the starter files for a basic "Hello World" application. 项目名称为“WebApiClient”。The project name is "WebApiClient". 这是一个新项目,因此没有部署任何依赖项。As this is a new project, none of the dependencies are in place. 第一次运行时将下载 .NET Core 框架、安装开发证书并运行 NuGet 包管理器来还原缺少的依赖项。The first run will download the .NET Core framework, install a development certificate, and run the NuGet package manager to restore missing dependencies.

在开始修改之前,在命令提示符中键入 dotnet run参见注释)以运行应用程序。Before you start making modifications, type dotnet run (see note) at the command prompt to run your application. 如果环境缺少依赖项,则 dotnet run 会自动执行 dotnet restoredotnet run automatically performs dotnet restore if your environment is missing dependencies. 如果需要重新生成应用程序,它还会执行 dotnet buildIt also performs dotnet build if your application needs to be rebuilt. 初始设置完成后,只需在对项目有意义的情况下运行 dotnet restoredotnet buildAfter your initial setup, you will only need to run dotnet restore or dotnet build when it makes sense for your project.

添加新的依赖项Adding New Dependencies

.NET Core 的主要设计目标之一是最小化 .NET 安装的大小。One of the key design goals for .NET Core is to minimize the size of the .NET installation. 如果应用程序需要使用其他库来生成某些功能,请将这些依赖项添加到 C# 项目 (*.csproj) 文件中。If an application needs additional libraries for some of its features, you add those dependencies into your C# project (*.csproj) file. 对于我们的示例,将需要添加 System.Runtime.Serialization.Json 包,以便应用程序可以处理 JSON 响应。For our example, you'll need to add the System.Runtime.Serialization.Json package, so your application can process JSON responses.

你将需要此应用程序的 System.Runtime.Serialization.Json 包。You'll need the System.Runtime.Serialization.Json package for this application. 通过运行以下 .NET CLI 命令,将其添加到项目:Add it to your project by running the following .NET CLI command:

dotnet add package System.Text.Json

发出 Web 请求Making Web Requests

现在,可以开始检索 Web 数据了。Now you're ready to start retrieving data from the web. 在此应用程序中,需要读取 GitHub API 返回的信息。In this application, you'll read information from the GitHub API. 让我们在 .NET Foundation 的保护下读取项目信息。Let's read information about the projects under the .NET Foundation umbrella. 先向 GitHub API 发出请求,以检索项目信息。You'll start by making the request to the GitHub API to retrieve information on the projects. 将使用终结点 https://api.github.com/orgs/dotnet/reposThe endpoint you'll use is: https://api.github.com/orgs/dotnet/repos. 由于要检索这些项目的所有信息,因此将发出 HTTP GET 请求。You want to retrieve all the information about these projects, so you'll use an HTTP GET request. 此外,浏览器也使用 HTTP GET 请求,以便你可以将相应的 URL 粘贴到浏览器,查看将要收到并处理的信息。Your browser also uses HTTP GET requests, so you can paste that URL into your browser to see what information you'll be receiving and processing.

使用 HttpClient 类发出 Web 请求。You use the HttpClient class to make web requests. 与所有新式 .NET API 一样,HttpClient 只支持长时间运行 API 的异步方法。Like all modern .NET APIs, HttpClient supports only async methods for its long-running APIs. 从异步方法入手。Start by making an async method. 将一边填充实现代码,一边生成应用程序功能。You'll fill in the implementation as you build the functionality of the application. 首先,打开项目目录中的 program.cs 文件,然后向 Program 类添加以下方法:Start by opening the program.cs file in your project directory and adding the following method to the Program class:

private static async Task ProcessRepositories()
{
}

需要在 Main 方法的最上面添加 using 指令,以便 C# 编译器能够识别 Task 类型:You'll need to add a using directive at the top of your Main method so that the C# compiler recognizes the Task type:

using System.Threading.Tasks;

如果此时生成项目,将会看到系统针对此方法生成的警告,因为其中不含任何 await 运算符,将以同步方式运行。If you build your project at this point, you'll get a warning generated for this method, because it does not contain any await operators and will run synchronously. 暂且忽略此警告;将在填充方法时添加 await 运算符。Ignore that for now; you'll add await operators as you fill in the method.

接下来,将 namespace 语句中定义的命名空间从默认名称 ConsoleApp 重命名为 WebAPIClientNext, rename the namespace defined in the namespace statement from its default of ConsoleApp to WebAPIClient. 我们稍后将在此命名空间中定义 repo 类。We'll later define a repo class in this namespace.

接下来,将 Main 方法更新为调用此方法。Next, update the Main method to call this method. ProcessRepositories 方法会返回一个任务,不应在此任务完成前退出程序。The ProcessRepositories method returns a task, and you shouldn't exit the program before that task finishes. 因此,必须更改 Main 的签名。Therefore, you must change the signature of Main. 添加 async 修饰符,并将返回类型更改为 TaskAdd the async modifier, and change the return type to Task. 然后,在方法的主体中,添加对 ProcessRepositories 的调用。Then, in the body of the method, add a call to ProcessRepositories. 向该方法调用添加 await 关键字:Add the await keyword to that method call:

static async Task Main(string[] args)
{
    await ProcessRepositories();
}

现在,生成了一个不执行任何操作但具备异步功能的程序。Now, you have a program that does nothing, but does it asynchronously. 现在进行改进。Let's improve it.

首先需要一个能从 Web 检索数据的对象;可使用 HttpClient 执行此操作。First you need an object that is capable to retrieve data from the web; you can use a HttpClient to do that. 此对象负责处理请求和响应。This object handles the request and the responses. 在 Program.cs 文件内的 Program 类中初始化该类型的一个实例。 Instantiate a single instance of that type in the Program class inside the Program.cs file.

namespace WebAPIClient
{
    class Program
    {
        private static readonly HttpClient client = new HttpClient();

        static async Task Main(string[] args)
        {
            //...
        }
    }
}

让我们回到 ProcessRepositories 方法,并填充它的第一个版本:Let's go back to the ProcessRepositories method and fill in a first version of it:

private static async Task ProcessRepositories()
{
    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 stringTask = client.GetStringAsync("https://api.github.com/orgs/dotnet/repos");

    var msg = await stringTask;
    Console.Write(msg);
}

此外,为了让编译能够顺利进行,还需要在文件的最上面添加两个新的 using 指令:You'll need to also add two new using directives at the top of the file for this to compile:

using System.Net.Http;
using System.Net.Http.Headers;

第一个版本发出 Web 请求来读取 .NET Foundation 组织下的所有存储库列表。This first version makes a web request to read the list of all repositories under the dotnet foundation organization. (.NET Foundation 的 gitHub ID 为“dotnet”)。(The gitHub ID for the .NET Foundation is 'dotnet'). 前几行代码针对该请求设置 HttpClientThe first few lines set up the HttpClient for this request. 在第一行中,它被配置为接受 GitHub JSON 响应。First, it is configured to accept the GitHub JSON responses. 此格式仅为 JSON。This format is simply JSON. 下一代码行将用户代理标头添加到此对象发出的所有请求中。The next line adds a User Agent header to all requests from this object. 这两个标头均由 GitHub 服务器代码进行检查,必须使用它们,才能检索 GitHub 中的信息。These two headers are checked by the GitHub server code, and are necessary to retrieve information from GitHub.

配置 HttpClient 后,发出 Web 请求并检索响应。After you've configured the HttpClient, you make a web request and retrieve the response. 在此第一个版本中,将使用 HttpClient.GetStringAsync(String) 便捷方法。In this first version, you use the HttpClient.GetStringAsync(String) convenience method. 此便捷方法先执行发出 Web 请求的任务,然后当返回请求时读取响应流,并从流中提取内容。This convenience method starts a task that makes the web request, and then when the request returns, it reads the response stream and extracts the content from the stream. 响应正文以 String 的形式返回。The body of the response is returned as a String. 此字符串在任务完成时可用。The string is available when the task completes.

此方法的最后两行代码用于等待任务完成,然后在控制台中打印输出响应。The final two lines of this method await that task, and then print the response to the console. 生成并运行应用程序。Build the app, and run it. 此时,生成警告不再显示,因为 ProcessRepositories 现在的确包含 await 运算符。The build warning is gone now, because the ProcessRepositories now does contain an await operator. 将看到很长的 JSON 格式文本。You'll see a long display of JSON formatted text.

处理 JSON 结果Processing the JSON Result

此时,你已编写用于检索来自 Web 服务器的响应,并显示响应文本的代码。At this point, you've written the code to retrieve a response from a web server, and display the text that is contained in that response. 接下来,让我们将相应的 JSON 响应转换成 C# 对象。Next, let's convert that JSON response into C# objects.

System.Text.Json.JsonSerializer 类将对象序列化为 JSON,并将 JSON 反序列化为对象。The System.Text.Json.JsonSerializer class serializes objects to JSON and deserializes JSON into objects. 首先定义一个类,用于表示从 GitHub API 返回的 repo JSON 对象:Start by defining a class to represent the repo JSON object returned from the GitHub API:

using System;

namespace WebAPIClient
{
    public class Repository
    {
        public string name { get; set; }
    }
}

将以上代码添加到“repo.cs”新文件中。Put the above code in a new file called 'repo.cs'. 此版本的类表示处理 JSON 数据的最简单路径。This version of the class represents the simplest path to process JSON data. 类名和成员名称与 JSON 数据包中使用的名称一致,而不是遵循以下 C# 约定。The class name and the member name match the names used in the JSON packet, instead of following C# conventions. 稍后将通过提供某些配置特性进行修复。You'll fix that by providing some configuration attributes later. 此类展示了 JSON 序列化和反序列化的另一个重要功能:并非 JSON 数据包中的所有字段都属于此类。This class demonstrates another important feature of JSON serialization and deserialization: Not all the fields in the JSON packet are part of this class. JSON 序列化程序将忽略所使用的类类型未包含的信息。The JSON serializer will ignore information that is not included in the class type being used. 借助此功能,可更轻松地创建仅处理一部分 JSON 数据包字段的类型。This feature makes it easier to create types that work with only a subset of the fields in the JSON packet.

至此,你已创建类型,让我们来进行反序列化吧。Now that you've created the type, let's deserialize it.

接下来,使用序列化程序将 JSON 转换成 C# 对象。Next, you'll use the serializer to convert JSON into C# objects. 使用以下行替换 ProcessRepositories 方法中对 GetStringAsync(String) 的调用:Replace the call to GetStringAsync(String) in your ProcessRepositories method with the following lines:

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

你使用的是新命名空间,因此,还需要将其添加到文件顶部:You're using a new namespace, so you'll need to add it at the top of the file as well:

using System.Text.Json;

请注意,现使用 GetStreamAsync(String),而不是 GetStringAsync(String)Notice that you're now using GetStreamAsync(String) instead of GetStringAsync(String). 序列化程序使用流(而不是字符串)作为其源。The serializer uses a stream instead of a string as its source. 让我们来看看前面第二行代码段中所使用的多项 C# 语言功能。Let's explain a couple features of the C# language that are being used in the second line of the preceding code snippet. JsonSerializer.DeserializeAsync<TValue>(Stream, JsonSerializerOptions, CancellationToken) 的第一个自变量是 await 表达式。The first argument to JsonSerializer.DeserializeAsync<TValue>(Stream, JsonSerializerOptions, CancellationToken) is an await expression. (其他两个参数为可选,并在代码段中省略。)Await 表达式可以出现在代码中的几乎任何位置,尽管到目前为止,你只在赋值语句中看到过它们。(The other two parameters are optional and are omitted in the code snippet.) Await expressions can appear almost anywhere in your code, even though up to now, you've only seen them as part of an assignment statement. Deserialize 方法为泛型 ,这意味着必须为应从 JSON 文本创建的对象的类型提供类型参数。The Deserialize method is generic, which means you must supply type arguments for what kind of objects should be created from the JSON text. 在此示例中,你要反序列化到 List<Repository>,这是另一个泛型对象,即 System.Collections.Generic.List<T>In this example, you're deserializing to a List<Repository>, which is another generic object, the System.Collections.Generic.List<T>. List<> 类存储对象的集合。The List<> class stores a collection of objects. 类型参数声明存储在 List<> 中的对象的类型。The type argument declares the type of objects stored in the List<>. JSON 文本表示存储库对象的集合,因此类型参数为 RepositoryThe JSON text represents a collection of repo objects, so the type argument is Repository.

即将完成此部分的操作。You're almost done with this section. 至此,你已将 JSON 数据转换成 C# 对象,让我们来显示每个存储库的名称。Now that you've converted the JSON to C# objects, let's display the name of each repository. 将以下代码行:Replace the lines that read:

var msg = await stringTask;   //**Deleted this
Console.Write(msg);

替换为:with the following:

foreach (var repo in repositories)
    Console.WriteLine(repo.name);

编译并运行该应用程序。Compile and run the application. 将打印输出属于 .NET Foundation 的存储库的名称。It will print out the names of the repositories that are part of the .NET Foundation.

控制序列化Controlling Serialization

在添加更多功能之前,让我们通过使用 [JsonPropertyName] 特性来处理 name 属性。Before you add more features, let's address the name property by using the [JsonPropertyName] attribute. 对 repo.cs 中的 name 字段声明执行以下更改:Make the following changes to the declaration of the name field in repo.cs:

[JsonPropertyName("name")]
public string Name { get; set; }

若要使用 [JsonPropertyName] 属性,需要将 System.Text.Json.Serialization 命名空间添加到 using 指令:To use [JsonPropertyName] attribute, you will need to add the System.Text.Json.Serialization namespace to the using directives:

using System.Text.Json.Serialization;

此更改意味着需要更改用于在 program.cs 中写入每个存储库名称的代码:This change means you need to change the code that writes the name of each repository in program.cs:

Console.WriteLine(repo.Name);

执行 dotnet run,以确保生成正确映射。Execute dotnet run to make sure you've got the mappings correct. 应能看到与之前一样的输出。You should see the same output as before.

让我们在添加新功能前执行另一项更改。Let's make one more change before adding new features. ProcessRepositories 方法可以执行异步工作,并返回一组存储库。The ProcessRepositories method can do the async work and return a collection of the repositories. 我们将使用此方法返回 List<Repository>,并将用于写入信息的代码移到 Main 方法中。Let's return the List<Repository> from that method, and move the code that writes the information into the Main method.

更改 ProcessRepositories 的签名,以返回可生成 Repository 对象列表的任务:Change the signature of ProcessRepositories to return a task whose result is a list of Repository objects:

private static async Task<List<Repository>> ProcessRepositories()

然后,在处理 JSON 响应后仅返回存储库:Then, just return the repositories after processing the JSON response:

var streamTask = client.GetStreamAsync("https://api.github.com/orgs/dotnet/repos");
var repositories = await JsonSerializer.DeserializeAsync<List<Repository>>(await streamTask);
return repositories;

编译器将生成返回内容的 Task<T> 对象,因为你已将此方法标记为 asyncThe compiler generates the Task<T> object for the return because you've marked this method as async. 然后,我们将把 Main 方法修改为,捕获这些结果并将每个存储库名称写入控制台。Then, let's modify the Main method so that it captures those results and writes each repository name to the console. 现在,Main 方法如下所示:Your Main method now looks like this:

public static async Task Main(string[] args)
{
    var repositories = await ProcessRepositories();

    foreach (var repo in repositories)
        Console.WriteLine(repo.Name);
}

读取详细信息Reading More Information

最后,让我们来处理 GitHub API 发送的 JSON 数据包中的其他一些属性。Let's finish this by processing a few more of the properties in the JSON packet that gets sent from the GitHub API. 虽然你不想面面俱到,但添加其他一些属性将展示更多的 C# 语言功能。You won't want to grab everything, but adding a few properties will demonstrate a few more features of the C# language.

首先,将其他一些简单类型添加到 Repository 类定义中。Let's start by adding a few more simple types to the Repository class definition. 将这些属性添加到此类:Add these properties to that class:

[JsonPropertyName("description")]
public string Description { get; set; }

[JsonPropertyName("html_url")]
public Uri GitHubHomeUrl { get; set; }

[JsonPropertyName("homepage")]
public Uri Homepage { get; set; }

[JsonPropertyName("watchers")]
public int Watchers { get; set; }

这些属性内置转换功能,可从字符串类型(即 JSON 数据包所含)转换成目标类型。These properties have built-in conversions from the string type (which is what the JSON packets contain) to the target type. 你可能是刚开始接触 Uri 类型。The Uri type may be new to you. 它代表 URI(或在此示例中,代表 URL)。It represents a URI, or in this case, a URL. 在有 Uriint 类型的情况下,如果 JSON 数据包内的数据未转换成目标类型,那么序列化操作将会抛出异常。In the case of the Uri and int types, if the JSON packet contains data that does not convert to the target type, the serialization action will throw an exception.

添加这些元素后,将 Main 方法更新为显示它们:Once you've added these, update the Main method to display those elements:

foreach (var repo in repositories)
{
    Console.WriteLine(repo.Name);
    Console.WriteLine(repo.Description);
    Console.WriteLine(repo.GitHubHomeUrl);
    Console.WriteLine(repo.Homepage);
    Console.WriteLine(repo.Watchers);
    Console.WriteLine();
}

最后一步,让我们添加最后一次推送操作的信息。As a final step, let's add the information for the last push operation. 此信息按如下方式在 JSON 响应中进行格式化:This information is formatted in this fashion in the JSON response:

2016-02-08T21:27:00Z

该格式为协调世界时 (UTC),因此,你将获得一个 DateTime 值,该值的 Kind 属性为 UtcThat format is in Coordinated Universal Time (UTC) so you'll get a DateTime value whose Kind property is Utc. 如果你倾向于以时区表示的日期,则需要编写自定义转换方法。If you prefer a date represented in your time zone, you'll need to write a custom conversion method. 首先,定义一个 public 属性,该属性将保存 Repository 类中日期和时间的 UTC 表示形式,并定义一个 LastPush readonly 属性,该属性返回转换为本地时间的日期:First, define a public property that will hold the UTC representation of the date and time in your Repository class and a LastPush readonly property that returns the date converted to local time:

[JsonPropertyName("pushed_at")]
public DateTime LastPushUtc { get; set; }

public DateTime LastPush => LastPushUtc.ToLocalTime();

让我们来看一下刚定义的新构造。Let's go over the new constructs we just defined. LastPush 属性使用 get 访问器的 expression-bodied member 进行定义。The LastPush property is defined using an expression-bodied member for the get accessor. 不存在 set 访问器。There is no set accessor. 省略 set 访问器就是在 C# 中定义只读 属性的方式。Omitting the set accessor is how you define a read-only property in C#. (是的,可以在 C# 中创建只写属性,但属性值受限。)(Yes, you can create write-only properties in C#, but their value is limited.)

最后,在控制台中再添加一个输出语句,然后就可以再次生成并运行此应用程序:Finally, add one more output statement in the console, and you're ready to build and run this app again:

Console.WriteLine(repo.LastPush);

你的版本现在应与已完成的示例匹配。Your version should now match the finished sample.

结束语Conclusion

此教程介绍了如何发出 Web 请求、分析结果,以及如何显示这些结果的属性。This tutorial showed you how to make web requests, parse the result, and display properties of those results. 你也已经在项目中将新的包添加为依赖项,You've also added new packages as dependencies in your project. 并已了解一些支持面向对象的技术的 C# 语言功能。You've seen some of the features of the C# language that support object-oriented techniques.

备注

从 .NET Core 2.0 SDK 开始,无需运行 dotnet restore,因为它由所有需要还原的命令隐式运行,如 dotnet newdotnet builddotnet runStarting with .NET Core 2.0 SDK, you don't have to run dotnet restore because it's run implicitly by all commands that require a restore to occur, such as dotnet new, dotnet build and dotnet run. 在执行显式还原有意义的某些情况下,例如 Azure DevOps Services 中的持续集成生成中,或在需要显式控制还原发生时间的生成系统中,它仍然是有效的命令。It's still a valid command in certain scenarios where doing an explicit restore makes sense, such as continuous integration builds in Azure DevOps Services or in build systems that need to explicitly control the time at which the restore occurs.