练习 - 通过 HttpClient 使用 REST 服务

已完成

在工程师用于客户现场访问的应用中,你需要添加一个功能,使工程师能够查看电气组件的详细信息。 此信息将保存在数据库中,并通过 REST Web 服务进行访问。 你还需要提供一个界面,使管理员能够使用同一 REST Web 服务创建、删除和修改数据库中保存的部件的详细信息。

在本练习中,需要将 REST Web 服务部署到 Azure,然后验证是否可以使用 Web 浏览器访问该服务。 然后,向现有应用添加功能,该应用使用 REST Web 服务检索、添加、删除和更新电气组件的详细信息。

你将使用 Azure 沙盒执行本练习。

提示

可以使用“复制”按钮将命令复制到剪贴板。 要粘贴,请右键单击 Cloud Shell 终端中的新行,然后选择“粘贴”,或使用 Shift+Insert 键盘快捷方式(在 macOS 上为 ⌘+V)。

部署 Parts REST Web 服务

  1. 在 Cloud Shell 窗口中,运行以下命令以克隆包含本练习代码的存储库,包括 Parts REST Web 服务:

    git clone https://github.com/microsoftdocs/mslearn-dotnetmaui-consume-rest-services
    
  2. 移至 Consume-REST-services 文件夹

    cd mslearn-dotnetmaui-consume-rest-services/src
    
  3. 运行以下命令,使用 Azure Cloud Shell 沙盒部署 Parts Web 服务。 此命令使该服务通过唯一的 URL 可用。 当显示此 URL 时,请记下它。 你将配置应用,以使用该 URL 连接到 Web 服务。

    bash initenvironment.sh
    

检查 Web 服务的代码

注意

你将在本地开发计算机上执行本练习的其余部分。

  1. 在计算机上,打开命令提示符窗口并克隆此练习的存储库。 该代码位于 net-maui-learn-consume-rest-services 存储库中。

    git clone https://github.com/microsoftdocs/mslearn-dotnetmaui-consume-rest-services
    

    注意

    最好将练习内容克隆或下载到较短的文件夹路径,例如 C:\dev,以避免生成进程生成的文件超过最大路径长度。

  2. 转到克隆存储库中的 src\webservice\PartsServer 文件夹,并使用 Visual Studio 或 Visual Studio Code 中的文件夹打开 PartsServer.sln 解决方案。 此解决方案包含在上一过程中部署到 Azure 的 Web 服务的代码副本。

  3. 在“解决方案资源管理器”窗口中,展开 Models 文件夹。 此文件夹包含两个文件:

    • Part.cs。 Part 类表示由 REST Web 服务提供的部件。 这些字段包括部件 ID、部件名称、部件类型、可用性日期(首次提供部件的日期)和供应商列表。 Href 属性返回部件的相对 URI;REST 客户端可使用该 URI 引用 REST Web 服务中的此特定部件。 Suppliers 属性以字符串形式返回部件的供应商列表

    • PartsFactory.cs。 PartsFactory 类使用一小组硬编码值初始化由服务提供的部件列表。 在现实世界中,将从数据库中检索此数据。

  4. 在“解决方案资源管理器”窗口中,展开 Controllers 文件夹。 此文件夹包含以下文件:

    • PartsController.cs。 PartsController 类实现服务的 Web API。 它包括以下方法:使客户端应用程序能够检索所有部件的列表 (Get)、查找给定部件 ID 的特定部件的详细信息(Get 的已重载版本)、更新部件的详细信息 (Put)、将新部件添加到列表中 (Post),以及从列表中删除部件 (Delete)

    • LoginController.cs。 LoginController 类为 Web 服务实现了一种简单的身份验证形式。 应用必须向该控制器发送 HTTP GET 请求,这将返回授权令牌。 此授权令牌用于对发送到 PartsController 的请求进行身份验证

    • BaseController.cs。 BaseController 类包含用于验证请求的逻辑。 PartsController 类从该类继承。 如果客户端尝试在未提供有效身份验证令牌的情况下调用 PartsController 类中的方法,则这些方法将返回 HTTP 401(未获授权)响应

检查 .NET MAUI 客户端应用的代码

本模块使用 .NET 8.0 SDK。 通过在首选命令终端中运行以下命令,确保你已安装 .NET 8.0:

dotnet --list-sdks

将显示类似于以下示例的输出:

6.0.317 [C:\Program Files\dotnet\sdk]
7.0.401 [C:\Program Files\dotnet\sdk]
8.0.100 [C:\Program Files\dotnet\sdk]

确保列出了以 8 开头的版本。 如果未列出任何版本或未找到命令,请安装最新的 .NET 8.0 SDK

  1. 关闭 PartsServer 解决方案,然后打开克隆存储库中 src\client\PartsClient 文件夹中的 PartsClient 解决方案。 此解决方案包含使用 PartsServer Web 服务的 .NET MAUI 客户端应用的部分实现

  2. 在“解决方案资源管理器”窗口中,展开 Data 文件夹。 此文件夹包含两个类的代码:

    • PartsManager.cs。 PartsManager 类提供客户端应用与 REST Web 服务交互所使用的方法。 此类当前不完整;你将在本练习中添加必要的代码。 完成后,GetClient 方法会连接到 REST Web 服务。 GetAll 方法返回来自 REST Web 服务的部件列表。 Add 方法将新部件添加到由 REST Web 服务管理的部件列表中。 Update 方法修改了由 REST Web 服务存储的部件的详细信息,而 Delete 方法删除了该部件

    • Part.cs。 Part 类对存储在数据库中的部件进行建模。 该类公开应用程序可用于访问 PartID、PartName、PartAvailableDate、PartType 和 PartSuppliers 字段的属性。 该类还提供了一个名为 SupplierString 的实用工具方法,应用程序可以使用该方法检索包含供应商名称的格式化字符串

  3. 在“解决方案资源管理器”窗口中,展开 Pages 文件夹。 此文件夹包含两个页面的标记和代码:

    • PartsPage.xaml。 此页面使用带有 DataTemplate 的 CollectionView 布局,以显示以列表形式提供的部件的详细信息。 DataTemplate 使用数据绑定将显示的数据连接到从 Web 服务检索到的部件。 可以在 CollectionView 中选择一行来编辑 AddPartPage 中的部件,也可以选择“添加新部件”按钮来添加新部件

    • AddPartPage.xaml。 通过此页面用户可以输入和保存新部件的详细信息。 用户可以指定部件名称、部件类型和初始供应商。 部件 ID 和部件可用日期是自动生成的。

  4. 在“解决方案资源管理器”窗口中,展开 ViewModels 文件夹。 此文件夹包含两个类:AddPartViewModel.cs 和 PartsViewModel.cs。 这些是它们各自页面的视图模型,并包含页面显示和操作数据所需的属性和逻辑。

登录服务

REST 服务要求在登录后才能获取授权令牌。 没有用户身份验证。 首先调用特定终结点以获取授权令牌,然后在 HTTP 标头中存在每个后续请求时,将令牌发回服务器。

  1. 打开 Data 文件夹中的 PartsManager.cs 文件

  2. 将以下代码片段中定义的 BaseAddress 和 Url 静态字段添加到 PartsManager 类。 将“此处显示的 URL”文本替换为前面记下的 REST Web 服务的 URL

    public class PartsManager
    {
        static readonly string BaseAddress = "URL GOES HERE";
        static readonly string Url = $"{BaseAddress}/api/";
        ...
    }
    
  3. 在 Url 字段后面,将以下字段添加到该类中。 该字段将保存用户登录时返回的授权令牌:

    private static string authorizationKey;
    
  4. 查找 GetClient 方法。 此方法当前将引发 NotImplementedException 异常。 将此方法中的现有代码替换为以下代码。 此代码会创建一个 HttpClient 对象,然后向 REST Web 服务的登录终结点发送请求。 该服务应使用包含授权令牌的消息进行响应。 对该令牌进行反序列化,并将其添加为使用 HttpClient 对象发送的后续请求的默认授权请求头

    private static async Task<HttpClient> GetClient()
    {
        if (client != null)
            return client;
    
        client = new HttpClient();
    
        if (string.IsNullOrEmpty(authorizationKey))
        {                
            authorizationKey = await client.GetStringAsync($"{Url}login");
            authorizationKey = JsonSerializer.Deserialize<string>(authorizationKey);
        }
    
        client.DefaultRequestHeaders.Add("Authorization", authorizationKey);
        client.DefaultRequestHeaders.Add("Accept", "application/json");
    
        return client;
    }
    

执行 GET 操作以检索部件的信息

  1. 在 PartsManager.cs 文件中,找到 GetAll 方法。 该方法是异步方法,用于返回可枚举的部件列表。 此方法尚未实现。

  2. 在此方法中,删除引发 NotImplementedException 异常的代码

  3. 使用 Connectivity 类检查设备是否具有 Internet 连接。 如果 Internet 不存在,则返回一个空的 List<Part>

    if (Connectivity.Current.NetworkAccess != NetworkAccess.Internet)
        return new List<Part>();
    
  4. 调用 GetClient 方法以检索要使用的 HttpClient 对象。 请记住,GetClient 是异步方法,因此请使用 await 运算符来捕获由此方法返回的对象

  5. 调用 HttpClient 对象的 GetStringAsync 方法并提供基 URL,以从 REST Web 服务中检索一组部件。 数据以 JSON 字符串形式通过异步方法返回。

  6. 使用 JsonSerializer.Deserialize 方法将此方法返回的 JSON 字符串反序列化为 Part 对象列表。 将此列表返回给调用方。

    完整的方法应如下所示:

    public static async Task<IEnumerable<Part>> GetAll()
    {
        if (Connectivity.Current.NetworkAccess != NetworkAccess.Internet)
            return new List<Part>();
    
        var client = await GetClient();
        string result = await client.GetStringAsync($"{Url}parts");
    
        return JsonSerializer.Deserialize<List<Part>>(result, new JsonSerializerOptions
            {
                PropertyNameCaseInsensitive = true,
            });                     
    }
    
  7. 构建并运行应用。 当该应用启动时,将显示“部件列表”页面,并应显示通过 GetAll 方法检索到的部件列表。 下图显示在 Android 上运行的应用:

    在 Android 上运行的部件客户端应用的屏幕截图,其中显示了部件列表。

  8. 浏览完数据后,关闭该应用并返回到 Visual Studio 或 Visual Studio Code。

执行 POST 操作以将新部件添加到数据库

  1. 在 PartsManager 类中,找到 Add 方法。 此方法具有部件名称、供应商和部件类型的参数。 此方法是异步方法。 此方法的目的是在数据库中插入一个新部件,并返回表示新创建的项的 Part 对象

  2. 删除方法中的现有代码。

  3. 使用 Connectivity 类检查设备是否具有 Internet 连接。 如果 Internet 不存在,则返回一个空的 Part

    if (Connectivity.Current.NetworkAccess != NetworkAccess.Internet)
        return new Part();
    
  4. 创建新的 Part 对象。 使用传入的数据填充字段:

    • 将 PartID 字段设置为空字符串。 此 ID 将由 REST Web 服务生成。
    • 创建一个新的 List 来保存供应商的名称
    • 将 PartAvailableDate 字段设置为 DateTime.Now
    • 从 GetClient 方法获取 HTTP 客户端
    var part = new Part()
    {
        PartName = partName,
        Suppliers = new List<string>(new[] { supplier }),
        PartID = string.Empty,
        PartType = partType,
        PartAvailableDate = DateTime.Now.Date
    };
    
  5. 调用 GetClient 方法以检索要使用的 HttpClient 对象

  6. 创建一个 HttpRequestMessage 对象。 此对象用于对发送到 Web 服务的请求进行建模。 使用指示要使用的 HTTP 谓词和要与之通信的 Web 服务的 URL 的参数来启动该对象。

    var msg = new HttpRequestMessage(HttpMethod.Post, $"{Url}parts");
    
  7. 需使用要创建的 Part 信息将有效负载发送到 webservice。 此有效负载将被序列化为 JSON。 JSON 有效负载将被添加到 HttpRequestMessage.Content 属性,并使用 JsonContent.Create 方法进行序列化。

    msg.Content = JsonContent.Create<Part>(part);
    
  8. 现在使用 HttpClient.SendAsync 函数将消息发送到 Web 服务。 该函数将返回 HttpResponseMessage 对象,该对象保存有关服务器上操作的信息。 例如从服务器传回的 HTTP 响应代码和信息。

    var response = await client.SendAsync(msg);
    response.EnsureSuccessStatusCode();
    

    请注意,前面使用的是 response.EnsureSuccessStatusCode 方法。 如果返回了 2xx HTTP 状态代码以外的任何内容,这将引发错误。

  9. 如果 Web 服务返回信息(例如在 JSON 中序列化的对象),可以从 HttpResponseMessage 中读取它。 然后,可以使用 JsonSerializer.Deserialize 反序列化 JSON。

    var returnedJson = await response.Content.ReadAsStringAsync();
    
    var insertedPart = JsonSerializer.Deserialize<Part>(returnedJson, new JsonSerializerOptions
            {
                PropertyNameCaseInsensitive = true,
            });
    
  10. 最后,返回新插入的 Part

    return insertedPart;
    
  11. 构建并运行应用。 选择“添加新部件”按钮并输入名称、类型和供应商,以创建新部件。 选择“保存”。 将调用 PartsManager 类中的 Add 方法,从而在 Web 服务中创建新部件。 如果操作成功,部件列表页面将再次出现,其中新部件位于列表底部。

    添加新部件后运行的应用的屏幕截图。新部件位于列表底部。

  12. 浏览完数据后,关闭该应用并返回到 Visual Studio 或 Visual Studio Code。

执行 PUT 操作以更新数据库中部件的详细信息

  1. 在 PartsManager 类中,找到 Update 方法。 该方法是一个将 Part 对象作为参数的异步方法。 该方法没有显式的返回值。 但是,返回类型是 Task,以便将异常正确地返回给调用方。 让我们实现 PUT 功能

  2. 删除现有代码。

  3. 与之前一样,检查 Internet 连接。

    if (Connectivity.Current.NetworkAccess != NetworkAccess.Internet)
        return;
    
  4. 创建新的 HttpRequestMessage,此时指定 PUT 操作和用于更新部件的 URL

    HttpRequestMessage msg = new(HttpMethod.Put, $"{Url}parts/{part.PartID}");
    
  5. 使用 JsonContent.Create 函数和传递给该函数的 part 参数设置 HttpRequestMessageContent 属性

    msg.Content = JsonContent.Create<Part>(part);
    
  6. 从 GetClient 方法获取 HTTP 客户端

    var client = await GetClient();
    
  7. 使用 HttpClient 发送请求,然后确保该请求成功。

    var response = await client.SendAsync(msg);
    response.EnsureSuccessStatusCode();
    
  8. 构建并运行应用。 从列表中选择一个部件。 随即显示 AddPart 页面,此时其中的属性已填充。 更新所需内容。

  9. 选择“保存”。 这将调用 PartsManager 类中的 Update 方法,以将更改发送到 Web 服务。 如果成功,部件列表页面将再次出现并反映所做的更改。

    运行的应用的屏幕截图,其中列表中的第一项已更新。

    注意

    在上一个任务中添加的部件不会出现在“部件列表”页上。 应用所使用的数据被重置为每次应用运行时预定义部件的列表。 这是为了为测试应用提供一致性。

执行 DELETE 操作以从数据库中删除部件的详细信息

  1. 在 PartsManager 类中,找到 Delete 方法。 该方法是异步方法,它接受 partId 字符串并返回 Task

  2. 删除现有代码。

  3. 检查 Internet 连接。

    if (Connectivity.Current.NetworkAccess != NetworkAccess.Internet)
        return;
    
  4. 创建一个新的 HttpRequestMessage 对象。 现在只指定 DELETE HTTP 谓词和用于删除部件的 URL

    HttpRequestMessage msg = new(HttpMethod.Delete, $"{Url}parts/{partID}");
    
  5. 从 GetClient 方法获取 HTTP 客户端

    var client = await GetClient();
    
  6. 将请求发送到 Web 服务。 返回后检查是否成功。

    var response = await client.SendAsync(msg);
    response.EnsureSuccessStatusCode();
    
  7. 构建并运行应用。 从列表中选择某个部件,然后在“添加部件”页上,选择“删除”。 如果成功,“部件列表”页将再次出现,但已删除的部件将不再可见