【C# 10 和 .NET 6】构建和使用 Web 服务(第16章)

918c9e7bb70fc0854ef788c2bad2cd43.png

e685fee7c814873a95c7e0a66da23c5f.png

Building and Consuming Web Services 构建和使用 Web 服务

本章介绍如何使用 ASP.NET Core Web API 构建 Web 服务(也称为 HTTP 或 REST 服务),以及如何使用 HTTP 客户端使用 Web 服务,这些客户端可以是任何其他类型的 .NET 应用程序,包括网站或移动设备或 桌面应用程序。本章需要您在第 10 章“使用 Entity Framework Core 处理数据”和第 13 至 15 章中学习的知识和技能,这些知识和技能涉及 C# 和 .NET 的实际应用以及使用 ASP.NET Core 构建网站。

在本章中,我们将讨论以下主题:

  • 使用 ASP.NET Core Web API 构建网络服务Web services

  • 使用 HTTP 客户端使用网络服务Web services

  • 实现网络服务的高级功能

  • 使用最少的 API 构建网络服务

一、使用 ASP.NET Core Web API 构建 Web 服务

在我们构建现代 Web 服务之前,我们需要介绍一些背景知识来设置本章的上下文。

1.1 了解 Web 服务首字母缩略词(acronyms)

尽管 HTTP 最初的设计目的是用 HTML 和其他资源来请求和响应以供人类查看,但它也有利于构建服务
Roy Fielding 在他的博士论文中描述了 Representational State Transfer(REST)表征状态转移 架构风格,HTTP 标准有利于构建服务,因为它定义了以下内容

  • 用于唯一标识资源的URI,例如 https://localhost:5001/api/products/23

  • 对这些资源执行常见任务的方法,如GET、POST、PUT 和DELETE

  • 能够协商在请求和响应中交换的内容的媒体类型,例如 XML 和 JSON。当客户端指定一个请求标头时,就会发生内容协商,例如 Accept: application/xml,*/*;q=0.8。 ASP.NET Core Web API 使用的默认响应格式是 JSON,这意味着响应标头之一将是 Content-Type: application/json;字符集=utf-8

Web services使用 HTTP 通信标准,因此有时也称为 HTTP 或 RESTful 服务。HTTP 或 RESTful 服务是本章的内容。
Web services也可以表示实现某些 WS-* standards的Simple Object Access Protocol (SOAP)简单对象访问协议 (SOAP) 服务。这些标准使在不同系统上实现的客户端和服务能够相互通信。 WS-* standards最初由 IBM 在其他公司(如 Microsoft)的输入下定义。

1.2 了解 Windows Communication Foundation (WCF)

.NET Framework 3.0 及更高版本包括名为 Windows Communication Foundation (WCF) 的远程过程调用 (RPC) 技术。 RPC 技术使一个系统上的代码能够通过网络在另一个系统上执行代码。
WCF 使开发人员可以轻松创建服务,包括实现 WS-* standards的 SOAP 服务。后来它还支持构建 Web/HTTP/REST 风格的服务,但如果这就是您所需要的,那么它的设计就太过分了。
如果您有现有的 WCF 服务,并且希望将它们移植到现代 .NET,那么有一个开源项目在 2021 年 2 月发布了第一个通用版本 (GA)。您可以在以下链接中阅读相关信息:
https://corewcf.github.io/blog/2021/02/19/corewcf-ga-release

1.3 WCF 的替代品

Microsoft 推荐的 WCF 替代方案是 gRPC。gRPC 是由 Google 创建的现代跨平台开源 RPC 框架(gRPC 中的非官方“g”)。您将在第 18 章构建和使用专业服务中了解有关 gRPC 的更多信息。

1.4 了解 Web API 的 HTTP 请求和响应

HTTP 定义了请求的标准类型和指示响应类型的标准代码。其中大部分可用于实现 Web API 服务。
最常见的请求类型是 GET,用于检索由唯一路径标识的资源,以及其他选项,例如可接受的媒体类型,设置为请求标头,如以下示例所示:

GET /path/to/resource 
Accept: application/json

常见的响应包括成功和多种类型的失败,如下表所示:

Status code Description
200 OK 路径形成正确,资源成功找到,序列化为可接受的媒体类型,然后在响应正文中返回。响应标头指定 Content-Type、Content-Length 和 Content-Encoding,例如 GZIP。
301 Moved Permanently 随着时间的推移,Web 服务可能会更改其资源模型,包括用于标识现有资源的路径。Web 服务可以通过返回此状态代码和具有新路径的名为 Location 的响应标头来指示新路径。
302 Found 类似于301.
304 Not Modified 如果请求包含 If-Modified-Since 标头,则 Web 服务可以使用此状态代码进行响应。响应主体为空,因为客户端应该使用其缓存的资源副本。
400 Bad Request 该请求无效,例如,它使用了一个产品路径,该路径使用了一个缺少 ID 值的整数 ID。
401 Unauthorized 请求有效,已找到资源,但客户端未提供凭据或无权访问该资源。重新验证可以启用访问,例如,通过添加或更改授权请求标头。
403 Forbidden 请求有效,已找到资源,但客户端无权访问该资源。重新验证不会解决问题。
404 Not Found 请求有效,但未找到资源。如果稍后重复请求,则可能会找到该资源。要指示永远找不到资源,请返回 410 Gone。
406 Not Acceptable 如果请求具有仅列出 Web 服务不支持的媒体类型的 Accept 标头。例如,如果客户端请求 JSON 但 Web 服务只能返回 XML。
451 Unavailable for Legal Reasons 在美国托管的网站可能会针对来自欧洲的请求返回此信息,以避免必须遵守通用数据保护条例 (GDPR)。选择这个数字是为了参考小说《华氏 451 度》,其中书籍被禁止和焚烧。
500 Server Error 请求有效,但在处理请求时服务器端出现问题。稍后重试可能会奏效。
503 Service Unavailable Web 服务正忙,无法处理请求。稍后再试可能会奏效。

其他常见类型的 HTTP 请求包括创建、修改或删除资源的 POST、PUT、PATCH 或 DELETE
创建新资源,您可以使用包含新资源的正文发出 POST 请求,如以下代码所示:

POST /path/to/resource 
Content-Length: 123 
Content-Type: application/json

创建新资源或更新现有资源,您可以使用包含现有资源的全新版本的主体发出 PUT 请求,如果该资源不存在,则创建它,或者如果它存在,则它 被替换(有时称为 upsert 操作),如以下代码所示:

PUT /path/to/resource 
Content-Length: 123 
Content-Type: application/json

要更有效地更新现有资源,您可以使用包含仅需要更改属性的对象的主体发出 PATCH 请求,如以下代码所示:

PATCH /path/to/resource 
Content-Length: 123 
Content-Type: application/json

删除现有资源,您可以发出 DELETE 请求,如以下代码所示:

DELETE /path/to/resource

除了上表中显示的 GET 请求响应外,创建、修改或删除资源的所有类型的请求都有其他可能的常见响应,如下表所示:

Status code Description
201 Created 新资源创建成功,名为 Location 的响应头中包含其路径,响应体中包含新创建的资源。立即获取资源应该返回 200。
202 Accepted 无法立即创建新资源,因此请求排队等候稍后处理,并立即获取资源可能返回 404。正文可以包含指向某种形式的状态检查器的资源或资源何时可用的估计 .
204 No Content 通常用于响应 DELETE 请求,因为删除后返回正文中的资源通常没有意义!如果客户端不需要确认请求是否已正确处理,有时用于响应 POST、PUT 或 PATCH 请求。
405 Method Not Allowed 当请求使用不支持的方法时返回。例如,设计为只读的 Web 服务可能会明确禁止 PUT、DELETE 等操作。
415 Unsupported Media Type 当请求正文中的资源使用 Web 服务无法处理的媒体类型时返回。例如,如果正文包含 XML 格式的资源,但 Web 服务只能处理 JSON。

1.5 创建 ASP.NET Core Web API 项目

我们将构建一个 Web 服务,该服务提供一种使用 ASP.NET Core 处理 Northwind 数据库中数据的方法以便可以发出 HTTP 请求和接收 HTTP 响应的任何平台上的任何客户端应用程序都可以使用该数据

  1. 使用您喜欢的代码编辑器添加一个新项目,如下表所定义:

    1. 项目模板:ASP.NET Core Web API / webapi

    2. 工作区/解决方案文件和文件夹:PracticalApps

    3. 项目文件和文件夹:Northwind.WebApi

    4. 其他 Visual Studio 选项:身份验证类型:无,为 HTTPS 配置:选中,启用 Docker:清除,启用 OpenAPI 支持:选中

  2. 在 Visual Studio Code 中,选择 Northwind.WebApi 作为活动的 OmniSharp 项目。

  3. 构建 Northwind.WebApi 项目。

  4. Controllers文件夹中,打开并查看 WeatherForecastController.cs,如下代码所示:

    using Microsoft.AspNetCore.Mvc;
    
    namespace Northwind.WebApi.Controllers;
    
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {//“冻结”、“支撑”、“寒冷”、“凉爽”、“温和”、“温暖”、“温暖”、“热”、“闷热”、“灼热”
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };
    
        private readonly ILogger<WeatherForecastController> _logger;
    
        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }
    
        // GET /weatherforecast  操作方法和URL 路径
        [HttpGet]  //默认5日的天气
        public IEnumerable<WeatherForecast> Get() // original method
        {
            return Get(5); //调用新的Get方法,参数5   
        }
    
        // GET /weatherforecast/7   操作方法和URL 路径
        [HttpGet("{days:int}")]  //随机天气 7日的 将 days 参数限制为 int 值的路由格式模式 {days:int}
        public IEnumerable<WeatherForecast> Get(int days) // new method
        {
            return Enumerable.Range(1, days).Select(index =>
            new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }

    查看前面的代码时,请注意以下事项:

  • Controller 类继承自 ControllerBase。这比MVC 中使用的控制器类简单,因为它没有像 View 这样的方法来通过将视图模型传递给 Razor 文件来生成 HTML 响应。

  • [Route] 属性为 客户端注册 /weatherforecast 相对URL,用于发出将由该控制器处理的HTTP 请求。例如,对 https://localhost:5001/weatherforecast/ 的 HTTP 请求将由该控制器处理。一些开发人员喜欢在控制器名称前加上 api/ 前缀,这是在混合项目中区分 MVC 和 Web API 的约定。如果您使用 [controller] 如图所示,它会使用类名称中 Controller 之前的字符,在本例中为 WeatherForecast,或者您可以简单地输入一个不带方括号的不同名称,例如 [Route("api/forecast" )]。

  • [ApiController] 属性是在 ASP.NET Core 2.1 中引入的,它为控制器启用了 REST 特定的行为,例如对无效模型的自动 HTTP 400 响应,您将在本章后面看到。

  • [HttpGet] 属性在 Controller 类中注册 Get 方法以响应 HTTP GET 请求,其实现使用共享的 Random 对象返回一组 WeatherForecast 对象,其中包含未来五天的随机温度和摘要,如 Bracing 或 Balmy 天气。

添加第二个Get方法,允许调用通过实施以下方法指定预测应该提前多少天:

  • 在原始方法上方添加注释以显示其响应的操作方法和URL 路径

  • 添加一个带有名为days 的整数参数的新方法。

  • 将原来的Get 方法实现代码语句剪切并粘贴到新的Get 方法中。

  • 修改新方法以创建最多为请求days 的整数的 IEnumerable,并修改原始 Get 方法以调用新的 Get 方法并传递值 5。您的方法应如以下代码中突出显示的那样:

在 [HttpGet] 属性中,注意将 days 参数限制为 int 值的路由格式模式 {days:int}。

1.6 审查 web service's 功能

现在,我们将测试 Web 服务的功能:

  1. 如果您使用的是 Visual Studio,请在“属性”中打开 launchSettings.json 文件,请注意,默认情况下,它将启动浏览器并导航至 /swagger 相对 URL 路径,如以下标记中突出显示的所示:

    "profiles": {
    "Northwind.WebApi": {
        "commandName": "Project",
        "dotnetRunMessages": "true",
        "launchBrowser": false,
        "launchUrl": "swagger",
        "applicationUrl": "https://localhost:5001;http://localhost:5000",
        "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
        }
    },
  2. 修改名为 Northwind.WebApi 的配置文件,将 launchBrowser 设置为 false。

  3. 对于 applicationUrl,将 HTTP 的随机端口号更改为 5000,将 HTTPS 的随机端口号更改为 5001。

  4. 启动 Web 服务项目。

  5. 启动浏览器。

  6. 导航到 https://localhost:5001/ 并注意您将收到 404 状态代码响应,因为我们没有启用静态文件并且没有 index.html,也没有配置路由的 MVC 控制器,要么 . 请记住,该项目不是为人类查看和交互而设计的,因此这是 Web 服务的预期行为

    GitHub 上的解决方案配置为使用端口 5002,因为我们将在本书后面更改其配置。
    Program.cs中:builder.WebHost.UseUrls("https://localhost:5002/");

  7. 在 Chrome 中,显示开发人员工具。

    导航到 https://localhost:5002/weatherforecast 并注意 Web API 服务应该返回一个 JSON 文档,其中包含数组中的五个随机天气预报对象,如图 16.1 所示:

    4cb1deff85206f0bd14b7af992669a3b.png

  8. 关闭开发者工具。

  9. 导航到 https://localhost:5002/weatherforecast/14 并注意请求两周天气预报时的响应,如图 16.2 所示:

    348f4619bb1a2755aeffceefa2e82eea.png

  10. 关闭 Chrome 并关闭网络服务器。

1.7 为 Northwind 数据库创建 Web 服务

与 MVC 控制器不同,Web API 控制器不会调用 Razor 视图来返回 HTML 响应,供网站访问者在浏览器中查看。相反,他们与发出 HTTP 请求的客户端应用程序进行内容协商,以在其 HTTP 响应中返回 XML、JSON 或 X-WWW-FORM-URLENCODED 等格式的数据

然后客户端应用程序必须反序列化协商格式的数据。现代 web services 最常用的格式是 JavaScript 对象表示法 (JSON),因为它很紧凑,并且在使用 Angular、React 和 Vue 等客户端技术构建 单页应用程序 (SPA) 时,它可以在浏览器中以原生方式与 JavaScript 配合使用。我们将引用您在第 13 章“介绍 C# 和 .NET 的实际应用”中创建的 Northwind 数据库的 Entity Framework Core 实体数据模型:

  1. Northwind.WebApi项目中,为SQLite 或 SQL Server 添加一个项目引用Northwind.Common.DataContext,如以下标记所示:

    <ItemGroup>
        <!--ProjectReference Include="..\Northwind.Common.DataContext.SqlServer\Northwind.Common.DataContext.SqlServer.csproj" /-->
         <!--使用sqlite数据库 -->
        <ProjectReference Include="..\Northwind.Common.DataContext.Sqlite\Northwind.Common.DataContext.Sqlite.csproj" />
    </ItemGroup>
    
    </Project>
  2. 构建项目并修复代码中的任何编译错误。

  3. 打开 Program.cs 并导入命名空间以使用 Web 媒体格式化程序web media formatters 和 shared Packt 类,如以下代码所示:

    using Microsoft.AspNetCore.Mvc.Formatters;
    using Packt.Shared; // AddNorthwindContext extension method
    
    using static System.Console;
  4. 在调用 AddControllers 之前添加一条语句以注册 Northwind 数据库上下文类(它将使用 SQLite 或 SQL Server,具体取决于您在项目文件中引用的数据库提供程序),如以下代码所示:

    builder.Services.AddNorthwindContext();
  5. 在对 AddControllers 的调用中,添加一个带有语句的 lambda 块,以将默认输出格式化程序的名称和支持的媒体类型写入控制台,然后添加 XML 序列化程序格式化程序,如以下代码所示:

    builder.Services.AddControllers(options =>
    {//此代码块用于配置MVC的输出格式化程序
        WriteLine("Default output formatters:");//输出默认格式化程序
        //遍历MVC中注册的每个输出格式化程序
        foreach (IOutputFormatter formatter in options.OutputFormatters)
        {
            OutputFormatter? mediaFormatter = formatter as OutputFormatter;//检查formatter是否是真正的输出格式化程序
            if (mediaFormatter == null)
            {//如果不是,则仅打印其类型名称
                WriteLine($"  {formatter.GetType().Name}");
            }
            else // OutputFormatter class has SupportedMediaTypes
            {//如果是真正的输出格式化程序,则打印其类型和支持的媒体类型。
                WriteLine("  {0}, Media types: {1}",
                arg0: mediaFormatter.GetType().Name,
                arg1: string.Join(", ",
                    mediaFormatter.SupportedMediaTypes));
            }
        }
    })
    .AddXmlDataContractSerializerFormatters() //添加XML DataContractSerializer 格式化程序
    .AddXmlSerializerFormatters(); 添加 XML 序列化程序格式化程序
  6. 启动网络服务。

  7. 在命令提示符或终端中,请注意有四种默认输出格式化程序,包括将null转换为 204 No Content 的格式化程序和支持纯文本、字节流和 JSON 响应的格式化程序,如以下输出所示 :

    Default output formatters:
      HttpNoContentOutputFormatter
      StringOutputFormatter, Media types: text/plain
      StreamOutputFormatter
      SystemTextJsonOutputFormatter, Media types: application/json, text/json, application/*+json
  8. 关闭网络服务器。

1.8 为实体(entities)创建数据存储库

定义和实现数据存储库以提供 CRUD 操作是一种很好的做法。CRUD 首字母缩略词包括以下操作:

  • C 表示创建 Create

  • R 表示检索(或读取) Retrieve (or Read)

  • U 表示更新 Update

  • D 表示删除 Delete

我们将为 Northwind 中的 Customers 表创建一个数据存储库。这张表只有91个客户,所以我们会在内存中保存一份全表的副本,以提高读取客户记录时的可扩展性和性能。

良好实践:在真正的 web service中,您应该使用像 Redis 这样的分布式缓存,这是一种开源数据结构存储,可用作高性能、高可用性数据库、缓存或消息代理。

我们将遵循现代良好实践并使存储库 API 异步。它将由使用构造函数参数注入的 Controller 类实例化,因此将创建一个新实例来处理每个 HTTP 请求

  1. 在 Northwind.WebApi 项目中,创建一个名为 Repositories 的文件夹

  2. 将两个类文件添加到名为ICustomerRepository.cs 和CustomerRepository.cs 的Repositories 文件夹中。

  3. ICustomerRepository 接口会定义五个方法,如下代码所示:

    using Packt.Shared; // Customer
    
    namespace Northwind.WebApi.Repositories;
    
    public interface ICustomerRepository
    { 
        Task<Customer?> CreateAsync(Customer c);
        Task<IEnumerable<Customer>> RetrieveAllAsync();
        Task<Customer?> RetrieveAsync(string id);
        Task<Customer?> UpdateAsync(string id, Customer c);
        Task<bool?> DeleteAsync(string id);
    }
  4. CustomerRepository 类会实现这5个方法,记住里面使用await的方法一定要标记为async,如下代码所示:

    using Microsoft.EntityFrameworkCore.ChangeTracking; // EntityEntry<T>
    using Packt.Shared; // Customer
    using System.Collections.Concurrent; // ConcurrentDictionary
    
    namespace Northwind.WebApi.Repositories;
    
    public class CustomerRepository : ICustomerRepository
    {
        //使用静态线程安全字典字段来缓存客户
        private static ConcurrentDictionary
        <string, Customer>? customersCache;
    
        // 使用实例数据上下文字段,因为由于其内部缓存,不应缓存它
        private NorthwindContext db;
    
        public CustomerRepository(NorthwindContext injectedContext)
        {
            db = injectedContext;
            //以 CustomerId 为键从数据库预加载客户作为普通字典,然后转换为线程安全的 ConcurrentDictionary
            if (customersCache is null)
            {
                customersCache = new ConcurrentDictionary<string, Customer>(
                db.Customers.ToDictionary(c => c.CustomerId));
            }
        }
        //创建客户
        public async Task<Customer?> CreateAsync(Customer c)
        {
            // 将 CustomerId 标准化为大写
            c.CustomerId = c.CustomerId.ToUpper();
    
            // 使用EF Core将其添加到数据库
            EntityEntry<Customer> added = await db.Customers.AddAsync(c);
            int affected = await db.SaveChangesAsync();
            if (affected == 1)
            {
                if (customersCache is null) return c;
                //如果客户是新客户,将其添加到缓存中,否则调用 UpdateCache 方法
                return customersCache.AddOrUpdate(c.CustomerId, c, UpdateCache);
            }
            else
            {
                return null;
            }
        }
        //检索所有客户
        public Task<IEnumerable<Customer>> RetrieveAllAsync()
        {
            //为了性能,从缓存中获取
            return Task.FromResult(customersCache is null
            ? Enumerable.Empty<Customer>() : customersCache.Values);
        }
        //检索指定id客户
        public Task<Customer?> RetrieveAsync(string id)
        {
            //为了性能,从缓存中获取
            id = id.ToUpper();
            if (customersCache is null) return null!;
            customersCache.TryGetValue(id, out Customer? c);
            return Task.FromResult(c);
        }
        //更新高速缓存
        private Customer UpdateCache(string id, Customer c)
        {
            Customer? old;
            if (customersCache is not null)
            {   //如果高速缓存中有旧值
                if (customersCache.TryGetValue(id, out old))
                {   //如果尝试使用新值更新高速缓存成功
                    //如果带有键的现有值等于比较值,则将与键关联的值更新为新值。
                    if (customersCache.TryUpdate(id, c, old))
                    {
                        return c;//返回新值
                    }
                }
            }
            return null!;
        }
        //标准化客户ID    更新客户的ID为大写
        public async Task<Customer?> UpdateAsync(string id, Customer c)
        {
            // 标准化客户 ID
            id = id.ToUpper();
            c.CustomerId = c.CustomerId.ToUpper();
    
            // 在数据库中更新
            db.Customers.Update(c);
            int affected = await db.SaveChangesAsync();
            if (affected == 1)
            {
                //在缓存中更新
                return UpdateCache(id, c);
            }
            return null;
        }
        //从数据库中删除
        public async Task<bool?> DeleteAsync(string id)
        {
            id = id.ToUpper();
    
            // 查找并删除数据库中的 Customer 对象
            Customer? c = db.Customers.Find(id);
            if (c is null) return null;
            db.Customers.Remove(c);
            int affected = await db.SaveChangesAsync();
            // 如果删除成功,则从 customersCache 中移除指定 id 的 Customer 对象
            if (affected == 1)
            {
                if (customersCache is null) return null;
                //从缓存中删除
                return customersCache.TryRemove(id, out c);
            }
            else
            {
                return null;
            }
        }
    }

    这个客户存储库类展示了一些最佳实践:
    使用 ConcurrentDictionary 并发字典 缓存客户,以改善检索操作的性能。
    使用Entity Framework Core与数据库交互。
    从数据库获取数据后,填充缓存。
    在创建、更新或删除客户时,保持缓存与新数据同步,通过添加、更新或移除客户。
    将客户ID规范为大写,以简化缓存查找。
    使用静态缓存避免为每个存储库实例从数据库重新获取相同的数据。
    使用实例数据上下文避免EF Core的内部缓存问题

    总的来说,它做到了:
    通过缓存来提高性能
    在数据更改时更新缓存
    规范化键以简化缓存查找
    将缓存与数据库上下文的生命周期分离开来

    要点是:
    使用 ConcurrentDictionary 缓存客户,以实现线程安全
    将 CustomerId 规范为大写
    在Create/Update/Delete方法被调用时更新缓存
    使用静态缓存和实例数据上下文

1.9 实现 Web API 控制器

有一些有用的属性和方法可用于实现返回数据而不是 HTML 的控制器。
对于 MVC 控制器,像 /home/index 这样的路由告诉我们控制器类名和操作方法名,例如 HomeController 类和 Index 操作方法
对于 Web API 控制器,像 /weatherforecast 这样的路由只告诉我们控制器类名,例如 WeatherForecastController。 要确定要执行的操作方法名称,我们必须将 GET 和 POST 等 HTTP 方法映射到控制器类中的方法
您应该使用以下属性修饰控制器方法,以指示它们将响应的 HTTP 方法:

  • [HttpGet]、[HttpHead]:这些操作方法响应 GET 或 HEAD 请求以检索资源并返回资源及其响应标头或仅返回响应标头

  • [HttpPost]:此操作方法响应 POST 请求以创建新资源或执行服务定义的某些其他操作

  • [HttpPut]、[HttpPatch]:这些操作方法响应 PUT 或 PATCH 请求,通过替换现有资源或更新其属性的子集来更新现有资源

  • [HttpDelete]:此操作方法响应DELETE 请求以删除资源

  • [HttpOptions]:此操作方法响应 OPTIONS 请求。

1.10 了解操作方法返回类型

action方法可以返回 .NET 类型,如单个字符串值、由类、记录或结构定义的复杂对象,或复杂对象的集合。如果已经注册了合适的序列化程序,ASP.NET Core Web API 会将它们序列化为 HTTP 请求 Accept 标头中设置的请求数据格式,例如 JSON。

为了更好地控制响应,有一些辅助方法返回围绕 .NET 类型的 ActionResult 包装器。
如果操作方法可以根据输入或其他变量返回不同的返回类型,则将操作方法的返回类型声明为 IActionResult
如果操作方法仅返回单一类型但具有不同的状态代码,则将操作方法的返回类型声明为 ActionResult

最佳实践:使用 [ProducesResponseType] 属性修饰操作方法,以指示客户端应在响应中期望的所有已知类型和 HTTP 状态代码。然后可以公开此信息以记录客户端应如何与您的 Web 服务交互。将其视为正式文档的一部分。在本章的后面,您将学习如何安装代码分析器,以便在您没有像这样装饰您的操作方法时向您发出警告。

例如,一个基于 id 参数获取产品的操作方法将使用三个属性进行修饰——一个表示它响应 GET 请求并具有一个 id 参数,另外两个用于指示当它成功处理或客户端提供无效产品ID时的行为,如以下代码所示:

[HttpGet("{id}")]
[ProducesResponseType(200, Type = typeof(Product))]  
[ProducesResponseType(404)] 
public IActionResult Get(string id)

ControllerBase 类具有可以轻松返回不同响应的方法,如下表所示:

Method Description
Ok 返回 200 状态代码和转换为客户端首选格式(如 JSON 或 XML)的资源。通常用于响应 GET 请求。
CreatedAtRoute 返回 201 状态代码和新资源的路径。通常用于响应 POST 请求以创建可以快速执行的资源。
Accepted 返回 202 状态代码以指示正在处理请求但尚未完成。通常用于响应触发需要很长时间才能完成的后台进程的 POST、PUT、PATCH 或 DELETE 请求。
NoContentResult 返回 204 状态代码和空响应正文。当响应不需要包含受影响的资源时,常用于响应 PUT、PATCH 或 DELETE 请求。
BadRequest 返回 400 状态代码和包含更多详细信息的可选消息字符串。
NotFound 返回 404 状态代码和自动填充的 ProblemDetails 正文(需要 2.2 或更高版本的兼容版本)。

1.11 配置客户存储库和 Web API 控制器

现在您将配置存储库以便可以从 Web API 控制器中调用它。
当 Web 服务启动时,您将为存储库注册作用域依赖服务实现,然后使用构造函数参数注入将其获取到新的 Web API 控制器中,以便与客户合作。
为了展示使用路由区分 MVC 和 Web API 控制器的示例,我们将为客户控制器使用通用的 /api URL 前缀约定:

  1. 打开 Program.cs 并导入 Northwind.WebApi.Repositories 命名空间。2. 在调用 Build 方法之前添加一条语句,它将在运行时 将CustomerRepository 注册为作用域依赖项,如以下代码中突出显示的那样:

    builder.Services.AddScoped<ICustomerRepository, CustomerRepository>();
    var app = builder.Build();

    良好实践:我们的存储库使用注册为作用域依赖项的数据库上下文。您只能在其他作用域依赖项中使用作用域依赖项,因此我们不能将存储库注册为单例。您可以在以下链接中阅读更多相关信息:https://docs.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#scoped

  2. 在 Controllers 文件夹中,添加一个名为 CustomersController.cs 的新类。

  3. 在 CustomersController 类文件中,添加语句定义一个 Web API 控制器类来处理客户,如下代码所示:

    using Microsoft.AspNetCore.Mvc; // [Route], [ApiController], ControllerBase
    using Packt.Shared; // Customer
    using Northwind.WebApi.Repositories; // ICustomerRepository
    
    namespace Northwind.WebApi.Controllers;
    
    // base address: api/customers  基地址
    [Route("api/[controller]")]
    [ApiController]
    public class CustomersController : ControllerBase
    {
        private readonly ICustomerRepository repo; //客户存储库,在program.cs中将CustomerRepository 注册为作用域依赖项
    
        // 构造函数注入 在 Startup 中注册的 客户存储库
        public CustomersController(ICustomerRepository repo)
        {
            this.repo = repo;
        }
    
        // GET: api/customers
        // GET: api/customers/?country=[country]  带参数country
        // 这将始终返回客户列表(但它可能是空的)
        [HttpGet]
        [ProducesResponseType(200, Type = typeof(IEnumerable<Customer>))]
        public async Task<IEnumerable<Customer>> GetCustomers(string? country)
        {
            if (string.IsNullOrWhiteSpace(country))
            {//国家参数不存在,返回所有客户
                return await repo.RetrieveAllAsync();
            }
            else
            {//返回指定国家的客户
                return (await repo.RetrieveAllAsync())
                .Where(customer => customer.Country == country);
            }
        }
    
        // GET: api/customers/[id]
        [HttpGet("{id}", Name = nameof(GetCustomer))] //命名路由  named route
        [ProducesResponseType(200, Type = typeof(Customer))]
        [ProducesResponseType(404)]
        public async Task<IActionResult> GetCustomer(string id)
        {
            Customer? c = await repo.RetrieveAsync(id); //异步检索客户
            if (c == null)
            {
                return NotFound(); // 404 Resource not found
            }
            return Ok(c); // 200 OK with customer in body
        }
    
        // POST: api/customers
        // BODY: Customer (JSON, XML)
        [HttpPost]
        [ProducesResponseType(201, Type = typeof(Customer))]
        [ProducesResponseType(400)]
        public async Task<IActionResult> Create([FromBody] Customer c) //添加客户
        {
            if (c == null)
            {
                return BadRequest(); // 400 Bad request
            }
    
            Customer? addedCustomer = await repo.CreateAsync(c);//异步创建客户
    
            if (addedCustomer == null)
            {
                return BadRequest("Repository failed to create customer.");
            }
            else
            {
                return CreatedAtRoute( // 201 Created
                routeName: nameof(GetCustomer), //路由名称
                routeValues: new { id = addedCustomer.CustomerId.ToLower() },
                value: addedCustomer);
            }
        }
    
        // PUT: api/customers/[id]
        // BODY: Customer (JSON, XML)
        [HttpPut("{id}")]
        [ProducesResponseType(204)]
        [ProducesResponseType(400)]
        [ProducesResponseType(404)]
        public async Task<IActionResult> Update(
        string id, [FromBody] Customer c)
        {
            id = id.ToUpper();
            c.CustomerId = c.CustomerId.ToUpper();
    
            if (c == null || c.CustomerId != id)
            {
                return BadRequest(); // 400 Bad request
            }
    
            Customer? existing = await repo.RetrieveAsync(id);
            if (existing == null)
            {
                return NotFound(); // 404 Resource not found
            }
    
            await repo.UpdateAsync(id, c);
    
            return new NoContentResult(); // 204 No content
        }
    
        // DELETE: api/customers/[id]
        [HttpDelete("{id}")]
        [ProducesResponseType(204)]
        [ProducesResponseType(400)]
        [ProducesResponseType(404)]
        public async Task<IActionResult> Delete(string id) //删除客户
        {
            //控制问题细节 take control of problem details
            if (id == "bad")
            {
                ProblemDetails problemDetails = new()
                {
                    Status = StatusCodes.Status400BadRequest,
                    Type = "https://localhost:5001/customers/failed-to-delete",
                    Title = $"Customer ID {id} found but failed to delete.",
                    Detail = "More details like Company Name, Country and so on.",
                    Instance = HttpContext.Request.Path
                };
                return BadRequest(problemDetails); // 400 Bad Request
            }
    
            Customer? existing = await repo.RetrieveAsync(id);//检索
            if (existing == null)
            {
                return NotFound(); // 404 Resource not found
            }
    
            bool? deleted = await repo.DeleteAsync(id);//删除
    
            if (deleted.HasValue && deleted.Value) // short circuit AND
            {
                return new NoContentResult(); // 204 No content
            }
            else
            {
                return BadRequest( // 400 Bad request
                $"Customer {id} was found but failed to delete.");
            }
        }
    }

    查看此 Web API 控制器类时,请注意以下事项:

  • Controller 类注册一个以 api/ 开头并包含控制器名称的路由,即api/customers

  • 构造函数使用依赖注入来获取用于与客户合作的注册存储库

  • 有五种对客户执行 CRUD 操作的操作方法— 两种GET 方法(针对所有客户或一个客户)、POST(创建)、PUT(更新)和DELETE

  • GetCustomers 方法可以传递带有国家/地区名称的字符串参数。如果丢失,则返回所有客户。如果存在,则用于按国家/地区过滤客户。

  • GetCustomer 方法有一个显式命名为GetCustomer 的路由,因此它可用于在插入新客户后生成URL

  • Create 和 Update 方法都用 [FromBody] 修饰客户参数,以告诉模型绑定器用 POST 请求正文中的值填充它。

  • Create 方法返回一个使用 GetCustomer 路由的响应,以便客户端知道将来如何获取新创建的资源。我们正在匹配两种方法来创建和获取客户。

  • Create 和 Update 方法不需要检查在 HTTP 请求正文中传递的客户模型状态,如果无效则返回包含模型验证错误详细信息的 400 Bad Request,因为控制器用 [ ApiController],它会为你做这件事。

当服务接收到 HTTP 请求时,它将创建 Controller 类的实例,调用适当的操作方法,以客户端首选的格式返回响应,并释放控制器使用的资源,包括存储库及其数据上下文。

1.12 指定问题详细信息

ASP.NET Core 2.1 及更高版本中添加的一项功能是用于指定问题详细信息的 Web 标准的实现。
在启用了 ASP.NET Core 2.2 或更高版本兼容性的项目中装饰有 [ApiController] 的 Web API 控制器中,返回 IActionResult 并返回客户端错误状态代码(即 4xx)的操作方法将自动包含 ProblemDetails 的序列化实例 响应主体中的类。
如果您想控制,那么您可以自己创建一个 ProblemDetails 实例并包含其他信息。
让我们模拟一个需要将自定义数据返回给客户端的错误请求

  1. Delete方法的实现顶部,添加语句来检查id是否匹配文字字符串值“bad”,如果是,则返回一个自定义的问题详情对象,如下代码所示:

    //控制问题细节 take control of problem details
    if (id == "bad")
    {
        ProblemDetails problemDetails = new()
        {
            Status = StatusCodes.Status400BadRequest,
            Type = "https://localhost:5001/customers/failed-to-delete",
            Title = $"Customer ID {id} found but failed to delete.",
            Detail = "More details like Company Name, Country and so on.",
            Instance = HttpContext.Request.Path
        };
        return BadRequest(problemDetails); // 400 Bad Request
    }
  2. 稍后您将测试此功能。

1.13 控制 XML 序列化

在 Program.cs 中,我们添加了 XmlSerializer,以便我们的 Web API 服务可以在客户端请求时返回 XML 和 JSON
但是,XmlSerializer 不能序列化接口,我们的实体类使用 ICollection 来定义相关的子实体。这会在运行时引发警告,例如,针对 Customer 类及其 Orders 属性,如以下输出所示:

warn: Microsoft.AspNetCore.Mvc.Formatters.XmlSerializerOutputFormatter[1] An error occurred while trying to create an XmlSerializer for the type 'Packt. Shared.Customer'.
System.InvalidOperationException: There was an error reflecting type 'Packt. Shared.Customer'.
---> System.InvalidOperationException: Cannot serialize member 'Packt. Shared.Customer.Orders' of type 'System.Collections.Generic.ICollection`1[[Packt.  Shared.Order, Northwind.Common.EntityModels, Version=1.0.0.0, Culture=neutral,  PublicKeyToken=null]]', see inner exception for more details.

我们可以通过在将 Customer 序列化为 XML 时排除 Orders 属性来防止此警告:

  1. 在Northwind.Common.EntityModels.Sqlite和Northwind.Common.EntityModels.SqlServer 项目,打开 Customers.cs。

  2. 导入System.Xml.Serialization命名空间,这样我们就可以使用 [XmlIgnore]属性

  3. 用属性修饰 Orders 属性以在序列化时忽略它,如以下代码中突出显示的所示:

    [InverseProperty(nameof(Order.Customer))]
    [XmlIgnore]
    public virtual ICollection<Order> Orders { get; set; }
    
    [ForeignKey("CustomerId")]
    [InverseProperty("Customers")]
    [XmlIgnore]
    public virtual ICollection<CustomerDemographic> CustomerTypes { get; set; }
  4. 在 Northwind.Common.EntityModels.SqlServer 项目中,也用 [XmlIgnore] 修饰 CustomerTypes 属性

二、记录和测试网络服务

您可以通过使用浏览器发出 HTTP GET 请求来轻松测试 Web 服务。要测试其他 HTTP 方法,我们需要更高级的工具。

2.1 使用浏览器测试 GET 请求

您将使用 Chrome 测试 GET 请求的三种实现方式——针对所有客户针对特定国家/地区的客户以及针对使用其唯一客户 ID 的单个客户

  1. 启动 Northwind.WebApi 网络服务。

  2. 启动浏览器。

  3. 导航到 https://localhost:5002/api/customers 并注意返回的 JSON 文档,其中包含 Northwind 数据库中的所有 91 个客户(未排序),如图 16.3 所示:

    b10a730ef939cc43a9c71577008f3c07.png

  4. 导航至 https://localhost:5002/api/customers/?country=Germany 并注意返回的 JSON 文档,其中仅包含德国的客户,如图 16.4 所示:

    30132535422694bb835800c89b103560.png

    如果返回一个空数组,请确保您使用正确的大小写输入了国家/地区名称,因为数据库查询区分大小写。例如,比较 uk 和 UK 的结果。

  5. 导航至 https://localhost:5002/api/customers/alfki 并注意返回的 JSON 文档仅包含名为 Alfreds Futterkiste 的客户,如图 16.5 所示:

    7d6519291677020f47cad82539911993.png

与国家名称不同,我们不需要担心客户 ID 值的大小写问题,因为在控制器类中,我们在代码中将字符串值规范化为大写。
但是我们如何测试其他 HTTP 方法,例如 POST、PUT 和 DELETE? 我们如何记录我们的 Web 服务,以便任何人都可以轻松理解如何与之交互?为了解决第一个问题,我们可以安装一个名为 REST Client 的 Visual Studio Code 扩展。要解决第二个问题,我们可以使用 Swagger,这是世界上最流行的记录和测试 HTTP API 的技术。但首先,让我们看看 Visual Studio Code 扩展可以做什么。

Web API 的测试工具有很多,例如 Postman。尽管 Postman 很流行,但我更喜欢 REST Client,因为它不会隐藏实际发生的事情。我觉得 Postman 太 GUI-y 了。但我鼓励您探索不同的工具并找到适合您风格的工具。您可以通过以下链接了解有关 Postman 的更多信息:https://www.postman.com/

2.2 使用 REST 客户端扩展测试 HTTP 请求

REST Client 是一个扩展,允许您发送任何类型的 HTTP 请求并在 Visual Studio Code 中查看响应。即使您更喜欢使用 Visual Studio 作为代码编辑器,安装 Visual Studio Code 以使用像 REST Client 这样的扩展也很有用。

2.2.1 使用 REST 客户端发出 GET 请求

我们将从创建用于测试 GET 请求的文件开始:

  1. 如果你还没有安装Huachao Mao的REST客户端(humao.rest-client),那么现在就在Visual Studio Code中安装它。

  2. 在您首选的代码编辑器中,启动 Northwind.WebApi 项目 Web 服务。

  3. 在 Visual Studio Code 的 PracticalApps 文件夹中,创建一个 RestClientTests 文件夹,然后打开该文件夹。

  4. 在 RestClientTests 文件夹中,创建一个名为 get-customers.http 的文件,并修改其内容以包含一个 HTTP GET 请求以检索所有客户,如以下代码所示:
    GET https://localhost:5001/api/customers/ HTTP/1.1

  5. 在 Visual Studio Code 中,导航到查看 | Command Palette,输入rest client,选择命令Rest Client: Send Request,回车,如图16.6所示:

    41851ca1841dbe7f1957386feeb2152c.png

  6. 请注意,Response 在一个新的选项卡式窗口窗格中垂直显示,您可以通过拖放选项卡将打开的选项卡重新排列为水平布局。

  7. 输入更多的GET请求,每个请求之间用三个井号隔开,测试获取各个国家的客户和使用他们的ID获取单个客户,如下代码所示:

    ###
    GET https://localhost:5001/api/customers/?country=Germany HTTP/1.1  
    ###
    GET https://localhost:5001/api/customers/?country=USA HTTP/1.1  Accept: application/xml 
    ###
    GET https://localhost:5001/api/customers/ALFKI HTTP/1.1  
    ###
    GET https://localhost:5001/api/customers/abcxy HTTP/1.1
  8. 点击每个请求上方的 Send Request 链接发送;例如,具有请求头的 GET 以 XML 而非 JSON 形式请求美国客户,如图 16.7 所示:

    b90459ecc4c75dee5470c8b3c6742628.png

2.2.2 使用 REST 客户端发出其他请求

接下来,我们将创建一个文件来测试其他请求,如 POST:

  1. 在RestClientTests文件夹下,新建一个名为create-customer.http的文件,修改其内容,定义一个创建新客户的POST请求,注意REST Client会在你输入普通的HTTP请求时提供IntelliSense,如下图 代码:

    POST https://localhost:5001/api/customers/ HTTP/1.1  
    Content-Type: application/json 
    Content-Length: 301 
    {
    "customerID": "ABCXY",   
    "companyName": "ABC Corp",   
    "contactName": "John Smith",   
    "contactTitle": "Sir",   
    "address": "Main Street",   
    "city": "New York",   
    "region": "NY",   
    "postalCode": "90210",   
    "country":  "USA",   
    "phone": "(123) 555-1234",   
    "fax": null,   
    "orders": null 
    }
  2. 由于不同操作系统的行尾不同,Content Length header 的值在 Windows 和 macOS 或 Linux 上会有所不同。 如果值错误,则请求将失败。要确定正确的内容长度,请选择请求的正文,然后在状态栏中查看字符数,如图 16.8 所示:

    c77e8c7f6167baa7ef68af43760b0e5b.png

  3. 发送请求并注意响应是 201 Created。另请注意新建客户的位置(即URL)为 https://localhost:5002/api/Customers/abcxy ,并将新建客户包含在响应体中,如图16.9所示:

    38ab10cd1d41a6b538fe559093161d34.png

我会给你留下一个可选的挑战来创建测试更新客户(使用 PUT)和删除客户(使用 DELETE)的 REST 客户端文件。在确实存在的客户和不存在的客户身上试用它们。解决方案位于本书的 GitHub 存储库中。既然我们已经看到了一种快速简便的方法来测试我们的服务,这也恰好是学习 HTTP 的好方法,那么外部开发人员呢?我们希望它尽可能容易让他们学习,然后调用我们的服务。为此,我们将使用 Swagger。

2.3 了解Swagger

Swagger 最重要的部分是 OpenAPI 规范,它为您的 API 定义了 REST 风格的契约,以人类和机器可读的格式详细说明其所有资源和操作,以便于开发、发现和集成。开发人员可以使用 Web API 的 OpenAPI 规范以他们首选的语言或库自动生成强类型的客户端代码。
对我们来说,另一个有用的功能是 Swagger UI,因为它使用内置的可视化测试功能自动为您的 API 生成文档。
让我们回顾一下如何使用 Swashbuckle 包为我们的 Web 服务启用 Swagger

  1. 如果网络服务正在运行,关闭网络服务器。

  2. 打开 Northwind.WebApi.csproj 并注意 Swashbuckle.AspNetCore 的包引用,如以下标记所示:

    <ItemGroup>
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
    </ItemGroup>
  3. Swashbuckle.AspNetCore包的版本更新到最新,比如写这篇文章的时候是2021年9月,是6.2.1。

  4. 在Program.cs中,注意导入微软的OpenAPI模型命名空间,如下代码所示:

    using Microsoft.OpenApi.Models;
  5. 导入Swashbuckle的SwaggerUI命名空间,如下代码所示:

    using Swashbuckle.AspNetCore.SwaggerUI; // SubmitMethod
  6. Program.cs 下到一半左右,注意添加Swagger支持包括Northwind服务文档的语句,说明这是你的服务的第一个版本,把标题Title改一下,如下代码高亮显示:

    builder.Services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new() { Title = "Northwind Service API", Version = "v1" });
    });
  7. 在配置 HTTP 请求管道的部分,注意在开发模式下使用 Swagger 和 Swagger UI 的语句,并为 OpenAPI 规范 JSON 文档定义一个端点

  8. 添加代码以明确列出我们希望在 Web 服务中支持的 HTTP 方法并更改端点名称,如以下代码中突出显示的所示:

    var app = builder.Build();
    if (builder.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI(c =>
        {//添加 Swagger JSON 端点。可以是完全限定的或相对于 Ul 页面
            c.SwaggerEndpoint("/swagger/v1/swagger.json",
            "Northwind Service API Version 1");
            //启用了试用功能的 HTTP 方法列表。空数组会禁用所有操作的试用。这不会从显示中过滤操作
            c.SupportedSubmitMethods(new[] {
        SubmitMethod.Get, SubmitMethod.Post,
        SubmitMethod.Put, SubmitMethod.Delete });
        });
    }

2.4 使用 Swagger UI 测试请求

您现在已准备好使用 Swagger 测试 HTTP 请求

  1. 启动 Northwind.WebApi 网络服务。

  2. 在 Chrome 中,导航至 https://localhost:5002/swagger/ 并注意已发现并记录了 Customers 和 WeatherForecast Web API 控制器,以及 API 使用的模式

  3. 单击 GET /api/Customers/{id} 展开该端点并记下客户 ID 所需的参数,如图 16.10 所示:

    42458f67c77781570e7248c2cbfbed00.png

  4. 点击Try it out,输入一个 id 为 ALFKI,然后点击宽蓝色的 Execute 按钮,如图16.11所示:

    3e9ab9418ec58d696a7a8d3d20cba890.png

  5. 向下滚动并注意 Request URL、带有代码的Server response 以及包括Response body和Response headers在内的Details,如图 16.12 所示:

    92836c2a51930151214b6c0d32282b2a.png

  6. 向上滚动到页面顶部,单击 POST /api/Customers 展开该部分,然后单击试用。

  7. 点击 Request body 框内部,修改 JSON 定义新客户,如下 JSON 所示:

    {
        "customerID": "SUPER",   
        "companyName": "Super Company",   
        "contactName": "Rasmus Ibensen",   
        "contactTitle": "Sales Leader",   
        "address": "Rotterslef 23",   
        "city": "Billund",   
        "region": null,
        "postalCode": "4371",   
        "country": "Denmark",   
        "phone": "31 21 43 21",  
        "fax": "31 21 43 22" 
    }
  8. 点击 Execute ,注意 Request URL,Server response with Code,Details including Response body 和Response headers,注意响应码为201表示客户创建成功,如图16.13所示:

    ce38850180861f1a4342c006af082d27.png

  9. 向上滚动到页面顶部,点击 GET /api/Customers,点击 Try it out,在 country 参数中输入 Denmark,点击 Execute,确认新客户已添加到数据库中,如图 图 16.14:

    f2cc2320ac6dac06c9f394b842051215.png

  10. 点击 DELETE /api/Customers/{id},点击Try it out,id输入super,点击Execute,注意Server response Code为204,说明删除成功,如图16.15所示:

    d8a601b919c49a07fd23338a386fbdab.png

  11. 再次点击 Execute ,注意Server response Code为404,说明客户不存在了,Response body包含一个问题详情JSON文档,如图16.16所示:

    0e14870ac1aa953eb53f72ea385c4d48.png

  12. id输入bad,再次点击Execute,注意Server response Code为400,说明客户确实存在但是删除失败(本例是因为web服务模拟了这个错误),然后 响应体包含一个自定义的问题详情JSON文档,如图16.17所:

    e4915d7eb2820c1ae8cd48513e54db3d.png

  13. 使用 GET 方法确认新客户已从数据库中删除(Denmark 丹麦原来只有两个客户)。

    我将通过使用 PUT 将测试更新留给现有客户

  14. 关闭 Chrome 并关闭网络服务器。

2.5 启用 HTTP 日志记录

HTTP 日志记录是一个可选的中间件组件,用于记录有关 HTTP 请求和 HTTP 响应的信息,包括以下内容:

  • 有关 HTTP 请求request的信息

  • 标头 Headers

  • 正文 Body

  • 有关 HTTP 响应 response的信息

这在用于 审计 auditing 和 调试 debugging场景的 Web 服务中很有价值,但要小心,因为它会对性能产生负面影响。您还可能会记录(personally identifiable information)个人身份信息 (PII),这在某些司法管辖区可能会导致合规性问题
让我们看看实际的 HTTP 日志记录

  1. 在 Program.cs 中,导入用于处理 HTTP 日志记录的命名空间,如以下代码所示:

    using Microsoft.AspNetCore.HttpLogging; // HttpLoggingFields
  2. 在服务配置(services configuration)部分,添加配置HTTP日志记录的语句,如下代码所示:

    builder.Services.AddHttpLogging(options =>
    {
        options.LoggingFields = HttpLoggingFields.All;
        options.RequestBodyLogLimit = 4096; // default is 32k
        options.ResponseBodyLogLimit = 4096; // default is 32k
    });
  3. 在HTTP管道配置部分,在调用use routing之前添加一条添加HTTP日志记录的语句,如下代码所示:

    app.UseHttpLogging();
  4. 启动 Northwind.WebApi 网络服务。

  5. 启动浏览器。

  6. 导航到 https://localhost:5002/api/customers。

  7. 在命令提示符或终端中,注意已记录请求和响应,如以下输出所示:

    info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[1]
        Request:
        Protocol: HTTP/2
        Method: GET
        Scheme: https
        PathBase:
        Path: /favicon.ico
        Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
        Host: localhost:5002
        User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36
        :method: [Redacted]
        Accept-Encoding: gzip, deflate, br
        Accept-Language: zh-CN,zh;q=0.9
        Referer: [Redacted]
        sec-ch-ua: [Redacted]
        sec-ch-ua-mobile: [Redacted]
        sec-ch-ua-platform: [Redacted]
        sec-fetch-site: [Redacted]
        sec-fetch-mode: [Redacted]
        sec-fetch-dest: [Redacted]
    info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
        Response:
        StatusCode: 404
        super-secure: [Redacted]
    info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
        Request finished HTTP/2 GET https://localhost:5002/favicon.ico - - - 404 0 - 8.2746ms
  8. 关闭 Chrome 并关闭网络服务器。

您现在已准备好构建使用您的 Web 服务的应用程序。

三、使用 HTTP 客户端使用 Web 服务

现在我们已经构建并测试了 Northwind 服务,我们将学习如何使用 HttpClient 类及其工厂从任何 .NET 应用程序调用它。

3.1 了解 HttpClient

使用 Web 服务的最简单方法是使用 HttpClient 类。然而,许多人错误地使用它,因为它实现了 IDisposable 并且微软自己的文档显示它的用法很差。请参阅 GitHub 存储库中的书籍链接,以获取对此进行更多讨论的文章。
通常,当一个类型实现 IDisposable 时,您应该在 using 语句中创建它以确保它尽快被释放。 HttpClient 是不同的,因为它是共享的、可重入的和部分线程安全的。
问题与必须如何管理底层网络套接字有关。最重要的是,您应该为应用程序生命周期中使用的每个 HTTP 端点使用它的单个实例。这将允许每个 HttpClient 实例设置适合其使用的端点的默认值,同时有效地管理底层网络套接字。

3.2 使用 HttpClientFactory 配置 HTTP 客户端

微软意识到了这个问题,并在 ASP.NET Core 2.1 中引入了 HttpClientFactory 以鼓励最佳实践;这就是我们将要使用的技术。
在下面的示例中,我们将使用 Northwind MVC 网站作为 Northwind Web API 服务的客户端。由于两者都需要同时托管在一个web服务器上,我们首先需要配置它们使用不同的端口号,如下列表所示:

  • Northwind Web API service 将使用HTTPS 侦听端口5002

  • Northwind MVC website 将继续使用 HTTP 侦听端口 5000 并使用 HTTPS 侦听端口 5001

让我们配置这些端口:

  1. Northwind.WebApi项目中,在Program.cs中,添加调用UseUrls的扩展方法,为HTTPS指定5002端口,如下代码高亮显示:

    var builder = WebApplication.CreateBuilder(args);
    builder.WebHost.UseUrls("https://localhost:5002/");
  2. Northwind.Mvc项目中,打开Program.cs,导入HTTP客户端工厂工作的命名空间,如下代码所示:

    using System.Net.Http.Headers; // MediaTypeWithQualityHeaderValue
  3. 添加一条语句,使具有命名客户端的 HttpClientFactory 能够在端口 5002 上使用 HTTPS 调用 Northwind Web API 服务,并请求 JSON 作为默认响应格式,如下代码所示:

    builder.Services.AddHttpClient(name: "Northwind.WebApi",
    configureClient: options =>
    {
        options.BaseAddress = new Uri("https://localhost:5002/");
        options.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue(
        "application/json", 1.0));
    });

3.3 在控制器中以 JSON 形式获取客户

我们现在可以创建一个 MVC 控制器操作方法,该方法使用工厂创建 HTTP 客户端,为客户发出 GET 请求,并使用 .NET 5 在 System.Net.Http.Json 程序集和命名空间中引入的便利扩展方法反序列化 JSON 响应 :

  1. 打开Controllers/HomeController.cs,声明一个字段用于存放HTTP客户端工厂,如下代码所示:

    private readonly IHttpClientFactory clientFactory;
  2. 在构造函数中设置字段,如下代码高亮显示:

    public HomeController(ILogger<HomeController> logger,
    NorthwindContext injectedContext,
    IHttpClientFactory httpClientFactory)//
    {
        _logger = logger;
        db = injectedContext;
        clientFactory = httpClientFactory;//
    }
  3. 创建一个新的操作方法来调用 Northwind Web API 服务获取所有客户,并将它们传递给视图,如以下代码所示:

    public async Task<IActionResult> Customers(string country)
    {
        string uri;
        // 检查传入的country参数是否为空或空字符串
        if (string.IsNullOrEmpty(country))
        {// 如果country参数为空或空字符串,则设置ViewData["Title"]为"All Customers Worldwide"
            ViewData["Title"] = "All Customers Worldwide";
            uri = "api/customers/";
        }
        else
        {  // 如果country参数不为空,则设置ViewData["Title"]为"Customers in {country}"
            ViewData["Title"] = $"Customers in {country}";
            uri = $"api/customers/?country={country}";
        }
        // 使用HttpClientFactory创建一个名为"Northwind.WebApi"的HttpClient实例
        HttpClient client = clientFactory.CreateClient(
            name: "Northwind.WebApi");
        // 创建一个GET请求,其请求URI为上面设置的uri
        HttpRequestMessage request = new(
            method: HttpMethod.Get, requestUri: uri);
        // 使用创建的HttpClient实例发送请求并获取响应
        HttpResponseMessage response = await client.SendAsync(request);
        // 将响应中的内容反序列化为一个IEnumerable<Customer>类型的对象
        IEnumerable<Customer>? model = await response.Content
            .ReadFromJsonAsync<IEnumerable<Customer>>();
        // 返回一个视图结果,其中包含从Web API获取的客户数据作为模型
        return View(model);
    }
  4. 在 Views/Home 文件夹中,创建一个名为 Customers.cshtml 的 Razor 文件。

  5. 修改 Razor 文件以呈现客户,如以下标记所示:

    @using Packt.Shared
    @model IEnumerable<Customer>
    <h2>@ViewData["Title"]</h2>
    <table class="table">
    <thead>
        <tr>
        <th>Company Name</th>
        <th>Contact Name</th>
        <th>Address</th>
        <th>Phone</th>
        </tr>
    </thead>
    <tbody>
        @if (Model is not null)
        {
        @foreach (Customer c in Model)
        {
            <tr>
            <td>
                @Html.DisplayFor(modelItem => c.CompanyName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => c.ContactName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => c.Address)
                @Html.DisplayFor(modelItem => c.City)
                @Html.DisplayFor(modelItem => c.Region)
                @Html.DisplayFor(modelItem => c.Country)
                @Html.DisplayFor(modelItem => c.PostalCode)
            </td>
            <td>
                @Html.DisplayFor(modelItem => c.Phone)
            </td>
            </tr>
        }
        }
    </tbody>
    </table>
  6. 在 Views/Home/Index.cshtml 中,在呈现访问者计数后添加一个表单,以允许访问者输入国家/地区并查看客户,如以下标记所示:

    <h3>Query customers from a service</h3>
    <form asp-action="Customers" method="get">
        <input name="country" placeholder="Enter a country" /> <!-- 输入框 -->
        <input type="submit" />  <!-- 提交按钮 -->
    </form>

3.4 启用跨源资源共享

跨源资源共享(Cross-Origin Resource Sharing) (CORS) 是一种基于 HTTP 标头(HTTP-header-based)的标准,用于在客户端和服务器位于不同域domains (origins)(源)时保护 Web 资源。它允许服务器指示除其自身之外的哪些来源(由域domain, 方案scheme或端口port的组合定义)将允许从中加载资源。

由于我们的 Web 服务托管在端口 5002 上,而我们的 MVC 网站托管在端口 5000 和 5001 上,因此它们被认为是不同的来源origins,因此无法共享资源

在服务器上启用 CORS 并将我们的 Web 服务配置为仅允许来自 MVC 网站的请求会很有用:

  1. 在 Northwind.WebApi 项目中,打开 Program.cs

  2. 服务配置部分添加一条语句,添加对CORS的支持,如下代码所示:

    builder.Services.AddCors();
  3. 在 HTTP 管道配置部分添加一条语句,在调用 UseEndpoints 之前,使用 CORS 并允许来自任何网站的 GET、POST、PUT 和 DELETE 请求,例如 Northwind MVC,其来源origin为 https://localhost:5001,如 如下代码所示:

    app.UseCors(configurePolicy: options =>
    {
        options.WithMethods("GET", "POST", "PUT", "DELETE");
        options.WithOrigins(
        "https://localhost:5001" // allow requests from the MVC client
        );
    });
  4. 启动 Northwind.WebApi 项目并确认Web 服务仅在端口 5002 上侦听,如以下输出所示:

    info: Microsoft.Hosting.Lifetime[14]
        Now listening on: https://localhost:5002
  5. 启动 Northwind.Mvc 项目并确认网站正在侦听端口 5000 和 5002,如下输出所示:

    info: Microsoft.Hosting.Lifetime[14]
        Now listening on: https://localhost:5001
    info: Microsoft.Hosting.Lifetime[14]
        Now listening on: http://localhost:5000
  6. 启动浏览器。

  7. 在客户表单中,输入一个国家,如德国、英国或美国(Germany, UK, or USA),点击提交,并记下客户列表,如图16.18所示:

    0a4067d158537de9e1260fe332c93702.png

    c707d985be35725e23cafa19deb31b03.png

  8. 单击浏览器中的后退按钮,清除国家/地区文本框,单击提交,并注意全球客户列表。

  9. 在命令提示符或终端中,注意 HttpClient 写入它发出的每个 HTTP 请求和它接收的 HTTP 响应,如以下输出所示:

    info: System.Net.Http.HttpClient.Northwind.WebApi.LogicalHandler[100]
        Start processing HTTP request GET https://localhost:5002/api/customers/?country=USA
    info: System.Net.Http.HttpClient.Northwind.WebApi.ClientHandler[100]
        Sending HTTP request GET https://localhost:5002/api/customers/?country=USA
    info: System.Net.Http.HttpClient.Northwind.WebApi.ClientHandler[101]
        Received HTTP response headers after 2385.9676ms - 200
    info: System.Net.Http.HttpClient.Northwind.WebApi.LogicalHandler[101]
        End processing HTTP request after 2397.3037ms - 200
  10. 关闭 Chrome 并关闭网络服务器。

已成功构建 Web 服务并从 MVC 网站调用它

四、为 Web 服务实现高级功能

现在您已经了解了构建 Web 服务然后从客户端调用它的基础知识,让我们看看一些更高级的功能

4.1 实施健康检查 API

有许多付费服务执行站点可用性测试,这些测试是基本的 ping,其中一些对 HTTP 响应进行更高级的分析。
ASP.NET Core 2.2 及更高版本可以轻松实现更详细的网站健康检查。例如,您的网站可能已经上线,但准备好了吗?它可以从数据库中检索数据吗?
让我们为我们的网络服务添加基本的健康检查功能

  1. Northwind.WebApi项目中,添加项目引用以启用Entity Framework Core 数据库健康检查,如以下标记所示:

    <ItemGroup>
        <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.11" />
        </ItemGroup>
  2. 构建项目。

  3. Program.cs中,在服务配置部分的底部,添加一条语句来添加健康检查,包括到Northwind数据库上下文中,如下代码所示:

    builder.Services.AddHealthChecks()
    .AddDbContextCheck<NorthwindContext>()
    // execute SELECT 1 using the specified connection string
    .AddSqlServer("Data Source=.;Initial Catalog=Northwind;Integrated Security=true;");

    默认情况下,数据库上下文检查调用 EF Core 的 CanConnectAsync 方法。您可以通过调用 AddDbContextCheck 方法自定义运行的操作

  4. HTTP管道配置部分,在调用MapControllers之前,添加使用基本健康检查的语句,如下代码所示:

    app.UseHealthChecks(path: "/howdoyoufeel");
  5. 启动web service网络服务。

  6. 启动浏览器。

  7. 导航到 https://localhost:5002/howdoyoufeel 并注意 Web 服务以纯文本响应响应:Healthy。

  8. 在命令提示符或终端,记下为测试数据库健康状况而执行的 SQL 语句,如以下输出所示:

    Level: Debug, Event Id: 20100, State: Executing DbCommand [Parameters=[],  CommandType='Text', CommandTimeout='30'] SELECT 1
  9. 关闭 Chrome 并关闭网络服务器。

4.2 实施 Open API 分析器(analyzers)和约定(conventions)

在本章中,您学习了如何通过手动为控制器类添加属性启用 Swagger 来记录 Web 服务
在 ASP.NET Core 2.2 或更高版本中,有API分析器可以自动反射带有[ApiController]属性的控制器类来进行文档记录。分析器假定了一些API约定。
要使用它,您的项目必须启用 OpenAPI 分析器,如以下标记中突出显示的那样:

<PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <IncludeOpenAPIAnalyzers>true</IncludeOpenAPIAnalyzers>
</PropertyGroup>

安装后,未正确装饰的控制器应该有警告(绿色波浪线)和编译源代码时的警告。例如,WeatherForecastController 类。
然后,自动代码修复可以添加适当的 [Produces] 和 [ProducesResponseType] 属性,尽管这目前仅适用于 Visual Studio。在 Visual Studio Code 中,你会看到有关分析器认为你应该在何处添加属性的警告,但你必须自己添加它们。

4.3 实施瞬态故障处理

当客户端应用程序或网站调用 Web 服务时,它可能来自世界的另一端。客户端和服务器之间的网络问题可能会导致与您的实现代码无关的问题。如果客户端调用失败,应用程序不应直接放弃。如果再次尝试,问题现在可能已经解决。我们需要一种方法来处理这些临时故障
为了处理这些瞬态故障,微软建议您使用第三方库 Polly 来实现指数退避的自动重试。您定义一个策略,库处理其他一切。

良好实践:您可以在以下链接中阅读更多关于 Polly 如何使您的 Web 服务更可靠的信息:https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly

4.4 添加安全 HTTP 标头

ASP.NET Core 内置了对 HSTS 等常见安全 HTTP 标头的支持。但是您应该考虑实施更多的 HTTP 标头。添加这些标头的最简单方法是使用中间件类

  1. 在 Northwind.WebApi 项目/文件夹中,创建一个名为SecurityHeadersMiddleware.cs 并修改其语句,如下代码所示:

    using Microsoft.Extensions.Primitives; // StringValues
    
    public class SecurityHeaders
    {
        private readonly RequestDelegate next;
    
        public SecurityHeaders(RequestDelegate next)
        {
            this.next = next;
        }
    
        public Task Invoke(HttpContext context)
        {
            // 在此处添加您想要的任何 HTTP 响应标头
            context.Response.Headers.Add(
            "super-secure", new StringValues("enable"));
    
            return next(context);
        }
    }
  2. 在Program.cs中,在HTTP管道配置部分,在调用UseEndpoints之前添加一条注册中间件的语句,如下代码所示:

    app.UseMiddleware<SecurityHeaders>();
  3. 启动网络服务。

  4. 启动浏览器。

  5. 显示开发人员工具(Developer tools)及其网络(Network )选项卡以记录请求和响应。

  6. 导航到 https://localhost:5002/weatherforecast

  7. 注意我们添加的名为 super-secure 的自定义 HTTP 响应标头,如图 16.19 所示

    06294549c36d2562f9cbeb27bceea571.png

五、使用最少的 API 构建 Web 服务

对于 .NET 6,Microsoft 投入大量精力为 C#10 语言添加新功能并简化 ASP.NET Core 库,以支持使用最少的 API 创建 Web 服务。
您可能还记得 Web API 项目模板中提供的天气预报服务。它展示了如何使用控制器类使用伪造的数据返回五天的天气预报。我们现在将使用最少的 API 重新创建该天气服务
首先,天气服务有一个类来表示单个天气预报。我们需要在多个项目中使用这个类,所以让我们为此创建一个类库

  1. 使用您喜欢的代码编辑器添加一个新项目,如下表所定义:

    1. 项目模板:Class Library / classlib

    2. 工作区/解决方案文件和文件夹:PracticalApps

    3. 项目文件和文件夹:Northwind.Common

  2. 将 Class1.cs 重命名为 WeatherForecast.cs。

  3. 修改WeatherForecast.cs,如下代码所示:

    namespace Northwind.Common
    {
    public class WeatherForecast
    {
        public static readonly string[] Summaries = new[]
        {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };
    
        public DateTime Date { get; set; }
    
        public int TemperatureC { get; set; }
    
        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    
        public string? Summary { get; set; }
    }
    }

5.1 使用最少的 API 构建天气服务

现在让我们使用最少的 API 重新创建该天气服务。它将监听端口 5003 并启用 CORS 支持,以便请求只能来自 MVC 网站并且只允许 GET 请求

  1. 使用您喜欢的代码编辑器添加一个新项目,如下表所定义:

    1. 项目模板:ASP.NET Core Empty / web

    2. 工作区/解决方案文件和文件夹:PracticalApps

    3. 项目文件和文件夹:Minimal.WebApi

    4. 其他 Visual Studio 选项:身份验证类型Authentication Type:无,为 HTTPS 配置Configure for HTTPS:选中,启用 Docker(Enable Docker):清除,启用 OpenAPI 支持(Enable OpenAPI support:):选中。

  2. 在 Visual Studio Code 中,选择 Minimal.WebApi 作为活动的 OmniSharp 项目。

  3. 在 Minimal.WebApi 项目中,添加对 Northwind.Common 项目的项目引用,如以下标记所示:

    <ItemGroup>
        <ProjectReference Include="..\Northwind.Common\Northwind.Common.csproj" />
    </ItemGroup>
  4. 构建 Minimal.WebApi 项目。

  5. 修改Program.cs,如下代码高亮显示:

    using Northwind.Common; // 引入 Northwind.Common 命名空间,它包含了 WeatherForecast 类
    
    var builder = WebApplication.CreateBuilder(args); // 创建一个 WebApplication 构建器,传入命令行参数
    
    builder.WebHost.UseUrls("https://localhost:5003"); // 设置 WebHost 的 URL 为 https://localhost:5003
    
    builder.Services.AddCors(); // 向服务集合中添加跨域资源共享(CORS)服务
    
    var app = builder.Build(); // 从构建器中构建一个 WebApplication 实例
    
    // 只允许 MVC 客户端和 GET 请求
    app.UseCors(configurePolicy: options =>
    {
    options.WithMethods("GET"); // 指定允许的 HTTP 方法为 GET
    options.WithOrigins("https://localhost:5001" // 指定允许的源为 https://localhost:5001
    );
    });
    
    app.MapGet("/api/weather", () => // 映射一个 GET 请求到 /api/weather 路径,返回一个委托函数
    {
    return Enumerable.Range(1, 5).Select(index => // 返回一个序列,包含五个 WeatherForecast 对象
        new WeatherForecast // 创建一个 WeatherForecast 对象
        {
        Date = DateTime.Now.AddDays(index), // 设置日期为当前日期加上索引值
        TemperatureC = Random.Shared.Next(-20, 55), // 设置摄氏温度为随机数
        Summary = WeatherForecast.Summaries[ // 设置天气概况为随机选取的一个字符串
            Random.Shared.Next(WeatherForecast.Summaries.Length)]
        })
        .ToArray(); // 将序列转换为数组
    });
    
    app.Run(); // 运行 WebApplication 实例
  6. 在属性中,修改 launchSettings.json 以配置 Minimal.WebApi 配置文件以使用 URL 中的端口 5003 启动浏览器,如以下标记中突出显示的所示:

    "profiles": {
        "Minimal.WebApi": {
        "commandName": "Project",
        "dotnetRunMessages": "true",
        "launchBrowser": true,
        "applicationUrl": "https://localhost:5003/api/weather",
        "environmentVariables": {
            "ASPNETCORE_ENVIRONMENT": "Development"
        }
        }

5.2 测试最小天气服务

为服务创建客户端之前,让我们测试它是否以 JSON 格式返回预测:
1.启动web服务项目。
2. 如果您使用的不是 Visual Studio 2022,请启动 Chrome 并导航至 https://localhost:5003/api/weather
3. 请注意,Web API 服务应返回一个 JSON 文档,其中包含数组中的五个随机天气预报对象。
4. 关闭 Chrome 并关闭网络服务器。

5.3 将天气预报添加到 Northwind 网站主页

最后,让我们为 Northwind 网站添加一个 HTTP 客户端,以便它可以调用天气服务并在主页上显示预报

  1. 在 Northwind.Mvc 项目中,添加对Northwind.Common 的项目引用,如以下标记中突出显示的所示:

    <ItemGroup>
        <!-- change Sqlite to SqlServer if you prefer -->
        <ProjectReference Include="..\Northwind.Common.DataContext.Sqlite\Northwind.Common.DataContext.Sqlite.csproj" />
        <ProjectReference Include="..\Northwind.Common\Northwind.Common.csproj" />
    </ItemGroup>
  2. 在Program.cs中,添加配置HTTP客户端调用5003端口最小服务的语句,如下代码所示:

    builder.Services.AddHttpClient(name: "Minimal.WebApi",
    configureClient: options =>
    {
        options.BaseAddress = new Uri("https://localhost:5003/");
        options.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue(
        "application/json", 1.0));
    });
  3. 在 HomeController.cs 中,导入Northwind.Common命名空间,在Index方法中,添加语句获取并使用HTTP客户端调用天气服务获取天气预报,并存储在ViewData中,如下代码所示:

    try // 使用 try 块来捕获可能发生的异常
    {
    HttpClient client = clientFactory.CreateClient( // 使用 clientFactory 创建一个 HttpClient 实例
    name: "Minimal.WebApi"); // 指定客户端的名称为 "Minimal.WebApi"
    
    HttpRequestMessage request = new( // 创建一个 HttpRequestMessage 对象
    method: HttpMethod.Get, requestUri: "api/weather"); // 指定请求方法为 GET,请求 URI 为 "api/weather"
    
    HttpResponseMessage response = await client.SendAsync(request); // 使用 HttpClient 发送请求,并等待响应
    
    ViewData["weather"] = await response.Content // 将响应内容
    .ReadFromJsonAsync<WeatherForecast[]>(); // 异步读取为 WeatherForecast 数组,并赋值给 ViewData["weather"]
    }
    catch (Exception ex) // 如果发生异常,执行 catch 块
    {
    _logger.LogWarning($"The Minimal.WebApi service is not responding. Exception: {ex.Message}"); // 使用 _logger 记录警告信息,包含异常的消息
    ViewData["weather"] = Enumerable.Empty<WeatherForecast>().ToArray(); // 将 ViewData["weather"] 赋值为空的 WeatherForecast 数组
    }
  4. 在 Views/Home 的 Index.cshtml 中,导入 Northwind.Common 命名空间,然后在顶部代码块中从 ViewData 字典中获取天气预报,如以下标记所示:

    @using Packt.Shared
    @using Northwind.Common
    @model HomeIndexViewModel //小写model    
    @{
        ViewData["Title"] = "Home Page";
        string currentItem = "";
        WeatherForecast[]? weather = ViewData["weather"] as WeatherForecast[];
    }
  5. 在第一个 <div> 中,在呈现当前时间后,添加标记以枚举天气预报(除非没有),并将它们呈现在表格中,如下标记所示:

    <p>
        <h4>Five-Day Weather Forecast</h4>
        @if ((weather is null) || (!weather.Any()))
        {
        <p>No weather forecasts found.</p>
        }
        else
        {
            <table class="table table-info">
            <tr>
                    @foreach (WeatherForecast w in weather)
                    {
                        <td>@w.Date.ToString("ddd d MMM") will be @w.Summary</td>
                    }
            </tr>
            </table>
        }
    </p>
  6. 启动Minimal.WebApi服务。

  7. 启动 Northwind.Mvc 网站。

  8. 进入https://localhost:5001/,注意天气预报,如图16.20:

    12421a3b0f9a88e2c2c9e8d0eef68eec.png

  9. 查看 MVC 网站的命令提示符或终端,并注意指示请求已在大约 83 毫秒内发送到最小 API Web 服务 api/weather 端点的信息消息,如以下输出所示:

    info: System.Net.Http.HttpClient.Minimal.WebApi.LogicalHandler[100]
        Start processing HTTP request GET https://localhost:5003/api/weather
    info: System.Net.Http.HttpClient.Minimal.WebApi.ClientHandler[100]
        Sending HTTP request GET https://localhost:5003/api/weather
    info: System.Net.Http.HttpClient.Minimal.WebApi.ClientHandler[101]
        Received HTTP response headers after 57.6561ms - 200
    info: System.Net.Http.HttpClient.Minimal.WebApi.LogicalHandler[101]
        End processing HTTP request after 70.1459ms - 200
  10. 停止Minimal.WebApi服务,刷新浏览器,注意几秒后出现MVC网站首页,没有天气预报。

  11. 关闭 Chrome 并关闭网络服务器。

六、 实践与探索

通过回答一些问题来测试您的知识和理解力,进行一些动手实践,并通过更深入的研究探索本章的主题。

练习 16.1 – 测试你的知识

回答下列问题:


  1. 你应该继承哪个类来为 ASP.NET Core Web API 服务创建控制器类?
    要创建一个 ASP.NET Core Web API 服务的控制器类,你应该继承自 ControllerBase 类??。控制器类的名称必须以 “Controller” 结尾,例如 HomeController 或 StudentController?。控制器类中的所有公共方法都被称为操作方法?,用于处理 Web API 请求。


  1. 如果你用 [ApiController] 属性装饰你的控制器类以获得默认行为,比如对无效模型自动 400 响应,你还必须做什么?
    在使用 [ApiController] 特性(attribute)来装饰控制器类时,以获得默认行为,例如:对无效模型自动返回 400 响应。你还需要执行以下操作:

    引入命名空间:在控制器类文件的顶部,引入 Microsoft.AspNetCore.Mvc 命名空间,以获取 ApiController 特性。

    using Microsoft.AspNetCore.Mvc;

    添加特性:在控制器类上添加 [ApiController] 特性。这将启用一些默认行为,如模型验证和自动返回 400 响应。

    [ApiController]
    public class MyController : ControllerBase
    {
        // 控制器方法
    }

    继承 ControllerBase:确保你的控制器类继承自 ControllerBase 类,而不是 Controller 类。因为 ApiController 特性仅适用于继承自 ControllerBase 的控制器。

    添加路由:为了使控制器生效,还需要在控制器类上添加路由特性(例如:[Route])。这将定义控制器方法的访问路径。路由模板可以包含控制器、操作和/或参数。

    [ApiController]
    [Route("api/[controller]")]
    public class MyController : ControllerBase
    {
        // 控制器方法
    }

    模型绑定和验证:当使用 [ApiController] 特性时,ASP.NET Core 会自动处理模型绑定和验证。如果一个客户端请求的数据无法绑定到操作方法的参数或模型验证失败,框架将自动返回 400 Bad Request 响应。在控制器方法中,你可以使用 ModelState.IsValid 属性检查模型是否有效。

    public IActionResult Post([FromBody] MyModel model)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
    
        // 处理已验证的模型
    }

    通过执行以上操作,你可以在控制器中使用 [ApiController] 特性并获得相关的默认行为


  1. 你必须做什么来指定哪个控制器操作方法将被执行以响应 HTTP 请求?
    要指定响应 HTTP 请求时将执行哪个控制器动作方法,您需要执行以下操作:

    在控制器类中编写动作方法,并为每个方法指定唯一的名称。
    在控制器方法上使用特性(Attribute)来指定响应的 HTTP 请求的方法和 URL 路径。例如,使用 [HttpGet]、[HttpPost]、[HttpPut]、[HttpDelete] 等特性来指定 GET、POST、PUT、DELETE 等 HTTP 方法,使用 [Route] 特性来指定 URL 路径。
    在 Web 应用程序的启动代码中配置路由规则,以便将传入的请求映射到相应的控制器和动作方法。
    在 ASP.NET Core 中,您可以使用 MapControllerRoute 方法或更具体的 MapGet、MapPost、MapPut、MapDelete 等方法来配置路由规则。

    通过这些步骤,您可以指定响应 HTTP 请求时将执行哪个控制器动作方法,并将请求映射到该方法。


  1. 在调用动作方法时,你必须做什么来指定预期的响应?
    要指定调用控制器动作方法时应该期望什么响应,您需要执行以下操作:

    在控制器方法的签名中指定返回类型,例如 IActionResult、JsonResult、ViewResult 等,以指示应该返回什么类型的响应。
    在控制器方法中编写逻辑代码,生成要返回的响应结果。
    在返回响应结果时,使用合适的返回类型进行包装,例如使用 OkResult、BadRequestResult、JsonResult、ViewResult 等类型进行包装。
    在需要时,使用返回状态码、响应头和响应正文等方式对响应进行更具体的定制
    在 ASP.NET Core 中,您可以使用各种类型的 ActionResult(例如 IActionResult、JsonResult、ViewResult 等)来返回响应结果,并使用对应的 OkResult、BadRequestResult、StatusCodeResult、NotFoundResult 等类型进行包装,以指示返回的状态码和响应内容。

    通过这些步骤,您可以指定控制器动作方法应该返回什么类型的响应,以及如何对响应进行更具体的定制。


  1. 列出三种可以调用以返回具有不同状态代码的响应的方法。
    以下是可以调用的三个方法,用于返回不同状态码的响应:

    StatusCode 方法:该方法允许您返回任意的 HTTP 状态码,例如 200、400、404、500 等。您可以使用该方法来返回自定义的状态码和响应内容。

    Ok 方法:该方法返回状态码为 200 的 OK 响应。当您要返回成功的响应时,可以使用该方法。例如,返回 JSON 数据时,您可以使用 Ok 方法将数据包装成 JsonResult 类型的响应。

    BadRequest 方法:该方法返回状态码为 400 的 BadRequest 响应。当您需要指示客户端提供的请求有错误或不完整时,可以使用该方法。例如,当模型验证失败时,您可以使用 BadRequest 方法返回 400 响应,同时提供有关错误的详细信息。

    这些方法是 ASP.NET Core 中常用的返回响应的方法之一,它们可以帮助您快速返回不同状态码的响应,并提供有关响应内容的更多控制。


  1. 列出四种测试 Web 服务的方法。
    以下是四种测试 Web 服务的方法:

    单元测试:单元测试是一种测试方法,用于测试应用程序的单个组件或模块。在测试 Web 服务时,您可以使用单元测试来测试控制器的动作方法,检查它们是否正确地返回所期望的响应。

    集成测试:集成测试是一种测试方法,用于测试应用程序中多个组件之间的交互。在测试 Web 服务时,您可以使用集成测试来测试整个应用程序的行为,包括控制器、模型、视图和路由等方面。

    UI 测试:UI 测试是一种测试方法,用于测试应用程序的用户界面。在测试 Web 服务时,您可以使用 UI 测试来测试 Web 应用程序的前端功能和用户体验,例如测试 HTML、CSS 和 JavaScript 等方面。

    性能测试:性能测试是一种测试方法,用于测试应用程序的性能和可伸缩性。在测试 Web 服务时,您可以使用性能测试来测试 Web 应用程序的负载能力和响应时间,以确保它能够在预期的负载下正常运行。

    使用这些测试方法,您可以测试 Web 服务的不同方面,包括功能、交互、用户体验和性能等方面。


  1. 为什么不将对 HttpClient 的使用包装在 using 语句中以便在完成后处理它,即使它实现了 IDisposable 接口,您应该使用什么来代替?
    虽然 HttpClient 实现了 IDisposable 接口,但是在使用 HttpClient 时不应该使用 using 语句来释放它。这是因为 HttpClient 是一种被设计为重用的对象,使用 using 语句释放它会导致与同一主机的后续请求出现延迟,从而导致性能问题。

    相反,应该使用 HttpClientFactory 来管理 HttpClient 实例的生命周期。HttpClientFactory 可以创建和管理 HttpClient 实例,并确保它们在使用后得到正确地释放和重用。使用 HttpClientFactory,您可以通过在 Startup.cs 文件中注册和配置 HttpClientFactory 来管理 HttpClient 实例的生命周期。然后,在需要使用 HttpClient 的地方,您可以使用 HttpClientFactory 来获取 HttpClient 实例。

    使用 HttpClientFactory 可以确保 HttpClient 实例在使用后得到正确释放和重用,从而提高应用程序的性能和可靠性。


  1. 首字母缩略词 CORS 代表什么,为什么在 Web 服务中启用它很重要?
    CORS 的首字母缩写代表“跨源资源共享”(Cross-Origin Resource Sharing)。在 Web 服务中启用 CORS 是非常重要的,因为它允许浏览器从不同的源请求资源,并允许服务器向不同的源提供资源。如果不启用 CORS,浏览器将会拒绝从不同源请求资源,这将限制 Web 服务的访问性和可用性。

    CORS 机制是基于浏览器的同源策略(Same-Origin Policy)的,同源策略指浏览器只允许页面从同一个源加载资源。源指协议、主机名和端口号的组合。当 Web 应用程序使用 AJAX、WebSocket 或其他跨域技术时,同源策略将会导致请求被浏览器拒绝。

    通过启用 CORS,Web 服务可以向不同的源提供资源,从而允许跨域请求。要启用 CORS,您需要在 Web 服务中添加 CORS 中间件,并配置允许访问的跨域资源。CORS 中间件可以通过添加适当的响应标头(例如 Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers 等)来允许跨域请求。

    启用 CORS 可以帮助您提高 Web 服务的访问性和可用性,使其可以与其他源交互,并支持 AJAX、WebSocket 和其他跨域技术。


  1. 如何使客户端能够使用 ASP. NET Core 2.2 and later 检测您的 Web 服务是否健康?
    在 ASP.NET Core 2.2 及更高版本中,您可以使用 Health Checks 功能来启用客户端检测 Web 服务的健康状态。Health Checks 可以让客户端发现 Web 服务是否处于正常运行状态,并及时采取措施来解决问题。

    要启用 Health Checks,您可以执行以下步骤:

    添加 Microsoft.AspNetCore.Diagnostics.HealthChecks 包到您的项目中。

    在 Startup.cs 文件中注册 Health Checks 服务,例如:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHealthChecks();
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseHealthChecks("/health");
    }

    在客户端中使用 Health Checks 端点(例如 /health)来检测 Web 服务的健康状态。如果服务正常运行,则返回一个包含成功状态和其他有用信息的 JSON 响应。如果服务处于异常状态,则返回一个包含错误信息的 JSON 响应。

    HTTP GET /health
    Response: 
    {
    "status": "Healthy" 
    }

    也可以给/health端点添加更丰富的健康检查,例如数据库连接,缓存连接等。ASP.NET Core会汇总所有检查的结果一起返回。

    建议客户端定期(每30秒或1分钟)调用/health端点来验证Web服务是否正常运行。如果连续几次调用都没有正常响应,则可以判定Web服务出现了问题。


  1. 端点路由有什么好处?
    Endpoint Routing(终结点路由)提供了以下几个优势:

    更灵活的路由:Endpoint Routing 可以根据请求的 URL、HTTP 方法和其他条件来决定使用哪个控制器动作方法处理请求,从而提供更灵活的路由控制。您可以使用多种方式来配置 Endpoint Routing,例如使用特性路由、约定路由和区域路由等。

    更高效的路由:Endpoint Routing 使用更高效的路由匹配算法,可以提高路由性能和响应速度。通过使用 Endpoint Routing,可以减少路由表的大小,并快速确定应该使用哪个控制器动作方法来处理请求。

    更好的可扩展性:Endpoint Routing 提供了更好的可扩展性,可以轻松地添加新的路由策略和路由约束。您可以使用自定义路由策略和路由约束来扩展 Endpoint Routing,从而满足更复杂的业务需求。

    更好的测试:Endpoint Routing 可以让您更轻松地对控制器动作方法进行单元测试。由于 Endpoint Routing 提供了更灵活的路由控制,您可以更容易地对控制器动作方法的单元测试进行模拟和验证。

    通过使用 Endpoint Routing,您可以提高路由性能、增强路由控制、提供更好的可扩展性和更好的测试支持,从而提高 Web 应用程序的可靠性和可维护性。


练习 16.2 – 练习使用 HttpClient 创建和删除客户

扩展 Northwind.Mvc 网站项目,使访问者可以在其中填写表单以创建新客户,或搜索客户然后将其删除。MVC 控制器应该调用 Northwind Web 服务来创建和删除客户

练习 16.3 – 探索主题

使用下一页上的链接了解有关本章所涵盖主题的更多详细信息:
https://github.com/markjprice/cs10dotnet6/blob/main/book-links.md#chapter-16--building-and-consuming-web-services

七、 总结

在本章中,您学习了如何构建一个 ASP.NET Core Web API 服务,该服务可以被任何平台上的任何应用程序调用,可以发出 HTTP 请求并处理 HTTP 响应。您还学习了如何使用 Swagger 测试和记录 Web 服务 API,以及如何有效地使用服务。
在下一章中,您将学习使用 Blazor 构建用户界面,Blazor 是 Microsoft 很酷的新组件技术,它使开发人员能够使用 C# 而不是 JavaScript 为网站构建客户端单页应用程序 (SPA)、桌面混合应用程序,以及 潜在的移动应用程序。

The End

猜你喜欢

转载自blog.csdn.net/cxyhjl/article/details/131160617