65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET Core 创建 API(第三日):在 ASP.NET Core API 中处理 HTTP 状态码、序列化器设置和内容协商

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (19投票s)

2017 年 6 月 6 日

CPOL

14分钟阅读

viewsIcon

36554

downloadIcon

392

在本文中,我们将继续探讨状态码的重要性以及实际示例。我们还将探讨资源创建和返回子资源。

引言

本系列文章“使用 ASP.NET Core 进行 Web API 开发”将重点介绍从 API 返回 HTTP 状态码及其重要性、返回子资源、序列化器字符串和内容协商等主题。在上 篇文章中,我们学习了如何在 ASP.NET Core 中创建 API 以及如何返回资源,并在状态码处暂停。本文将继续探讨状态码的重要性以及实际示例。我们还将探讨资源创建和返回子资源。我们可以使用与系列上 篇文章完成时相同的源代码。

HTTP 状态码

在调用 API 时,会发送一个 Http 请求,并返回一个响应,其中包含返回数据和一个 HTTP 状态码。HTTP 状态码很重要,因为它们告知消费者其请求具体发生了什么;错误的 HTTP 状态码会使消费者感到困惑。消费者应该知道(通过响应)其请求是否已得到处理,如果响应不符合预期,那么状态码应该告诉消费者问题出在哪里,是出在消费者级别还是 API 级别。

假设存在一种情况,即消费者收到状态码 200 的响应,但服务级别存在一些问题或故障,在这种情况下,消费者会错误地认为一切正常,而事实并非如此。因此,如果服务存在问题或服务器上发生错误,应向消费者发送状态码 500,以便消费者知道其发送的请求实际上有问题。通常,存在许多访问码。完整列表可在此处找到,但并非所有都如此重要,除了少数几个。服务执行的普通 CURD 操作经常使用一些状态码,因此服务不一定需要支持所有这些状态码。让我们快速了解一些重要的状态码。

当我们谈论状态码的级别时,有 5 个级别。100 级状态码更多是非正式的。200 级状态码专门用于请求发送成功。我们获得 200 状态码表示 GET 请求成功,201 表示新资源已成功创建。204 状态码也表示成功,但返回时什么也不返回,就像消费者执行了删除操作并且不期望任何返回一样。300 级 HTTP 状态码主要用于重定向,例如,告知消费者请求的资源(如页面、图像)已移动到另一个位置。400 级状态码用于表示错误或客户端错误,例如,状态码 400 表示“Bad Request”(错误请求),401 表示“Unauthorized”(未授权),即消费者提供了无效的身份验证凭据或详细信息,403 表示身份验证成功,但用户未获得授权。404 非常常见,我们经常遇到,表示请求的资源不可用。500 级用于服务器错误。“Internal Server Error”(内部服务器错误)异常非常常见,其代码为 500。此错误表示服务器上存在某些意外错误,客户端对此无能为力。我们将介绍如何在应用程序中使用这些 HTTP 状态码。

路线图

我们将遵循路线图来详细学习和涵盖 ASP.NET Core 的所有方面。以下是涵盖整个系列的路线图或文章列表。

  1. ASP.NET Core 创建 API(第一日):入门和 ASP.NET Core 请求管道
  2. ASP.NET Core 创建 API(第二日):在 ASP.NET Core 中创建 API 并返回资源
  3. ASP.NET Core 创建 API(第三日):在 ASP.NET Core API 中处理 HTTP 状态码、序列化器设置和内容协商
  4. ASP.NET Core 创建 API(第四日):理解 ASP.NET Core API 中的资源
  5. ASP.NET Core 创建 API(第五日):ASP.NET Core API 中的控制反转和依赖注入
  6. ASP.NET Core 创建 API(第六日):Entity Framework Core 入门
  7. ASP.NET Core 创建 API(第七日):ASP.NET Core API 中的 Entity Framework Core

HTTP 状态码实现

让我们尝试调整我们的实现,并尝试从 API 返回 HTTP 状态码。现在,在 EmployeesController 中,我们需要在响应中返回 JSON 结果以及状态码。目前我们只返回 JSON 结果,如下所示。

如果我们仔细查看 JsonResult 类并按 F12 查看其定义。我们会发现它派生自 ActionResult 类,该类专门将对象格式化为 JSON 对象。ActionResult 类实现了 IActionResult 接口。

理想情况下,API 不应总是只返回 JSON,它们应该返回消费者期望的内容,即通过检查请求标头,并且理想情况下,我们应该能够随结果一起返回状态码。

我们将看到,我们返回的 JsonResult 也可以做到这一点。因此,如果您将创建的 JSON 结果分配给任何变量(例如“employees”),您会发现该变量具有关联的 StatusCode 属性,我们可以设置该 StatusCode 属性。

所以我们可以将状态码设置为 200 并返回此变量,如下所示。

   [HttpGet()]
    public JsonResult GetEmployees()
    {
      var employees= new JsonResult(EmployeesDataStore.Current.Employees);
      employees.StatusCode = 200;
      return employees;
    }

但这将是一项繁琐的工作,需要每次都这样做。ASP.NET Core 中有预定义的方**法可以创建 IActionResult,并且所有这些方法都映射到正确的状态码(例如,如果资源不存在则为 NotFound,或对于有错误请求则为 BadRequest,等等)。

因此,将方法的返回类型替换为 IActionResult 而不是 JsonResult,并在 GetEmployee 方法中进行实现,就好像有一个 ID 为不存在的 Employee 的请求一样,我们可以返回 NotFound;否则,返回 Employee 数据以及状态码 OK。对于返回员工列表的第一个方法,我们可以直接返回 Ok 和数据。在此方法中没有 NotFound 的范围,因为即使没有记录,也可以返回一个空的列表,其 HTTP 状态码为 OK。因此,我们的代码如下所示,

using Microsoft.AspNetCore.Mvc;
using System.Linq;

namespace EmployeeInfo.API.Controllers
{
  [Route("api/employees")]
  public class EmployeesInfoController : Controller
  {
    [HttpGet()]
    public IActionResult GetEmployees()
    {
      return Ok(EmployeesDataStore.Current.Employees);
    }

    [HttpGet("{id}")]
    public IActionResult GetEmployee(int id)
    {
      var employee = EmployeesDataStore.Current.Employees.FirstOrDefault(emp => emp.Id == id);
      if (employee == null)
        return NotFound();
      return Ok(employee);

    }
  }
}

编译应用程序并运行它。现在转到 Postman 并尝试发出请求。在上 篇文章中,我们请求了 ID 为 8 的员工,该员工不存在,我们得到了以下结果。

结果显示为 null,这并非理想的正确响应。所以现在尝试使用我们所做的新实现发出相同的请求。在这种情况下,我们将获得正确的 404 状态码和 Not Found 消息,如下所示,

由于我们知道也可以从 Web 浏览器发送请求,因为浏览器支持 HTTP 请求。如果我们尝试通过浏览器访问我们的 API,我们将看到一个空白页,并在开发者工具中显示错误或响应,如下所示。

我们可以通过显示相同的信息来使响应更有意义,因为 ASP.NET Core 包含一个用于状态码的中间件。只需打开 Startup 类的 configure 方法,并将以下行添加到该方法中。

app.UseStatusCodePages();

也就是说,将其添加到我们的请求管道中。所以该方法将如下所示,

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
      loggerFactory.AddConsole();

      if (env.IsDevelopment())
      {
        app.UseDeveloperExceptionPage();
      }
      else
      {
        app.UseExceptionHandler();
      }

      app.UseStatusCodePages();

      app.UseMvc();


      //app.Run(async (context) =>
      //{
      //  throw new Exception("Test Dev Exception Page");
      //});

      //app.Run(async (context) =>
      //{
      //  await context.Response.WriteAsync("Hello World!");
      //});
    }

现在再次从浏览器发出相同的 API 请求,我们将在浏览器页面本身上看到以下消息。

返回子资源

在 EmployeeDto 中,我们有一个名为 NumberOfCompaniesWorkedWith 的属性,将其视为子资源或关联资源,我们也可能希望将其作为结果返回。让我们看看如何实现这一点。向 Model 文件夹添加一个名为 NumberOfCompaniesWorkedDto 的 DTO 类,并向这个新添加的类添加 Id、Name 和 Description。

NumberOfCompaniesWorkedDto.cs

namespace EmployeeInfo.API.Models
{
  public class NumberOfCompaniesWorkedDto
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
  }
}

现在,在 EmployeeDto 类中,添加一个相对于此 NumberOfCompaniesWorkedDto 的属性,该属性返回员工工作过的所有公司的集合。

EmployeeDto.cs

using System.Collections.Generic;

namespace EmployeeInfo.API.Models
{
  public class EmployeeDto
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Designation { get; set; }
    public string Salary { get; set; }
    public int NumberOfCompaniesWorkedWith
    {
      get
      {
        return CompaniesWorkedWith.Count;
      }
    }

    public ICollection<NumberOfCompaniesWorkedDto> CompaniesWorkedWith { get; set; } = new List<NumberOfCompaniesWorkedDto>();

  }
}

在上面的代码中,我们添加了一个新属性,它返回员工工作过的公司列表。对于 NumberOfCompaniesWorkedWith 属性,我们从集合中计算了计数。我们将 CompaniesWorkedWith 属性初始化为一个空列表,以避免出现空引用异常。现在向 EmployeesDataStore 类添加一些 CompaniesWorkedWith 的模拟数据。

EmployeesDataStore.cs

using EmployeeInfo.API.Models;
using System.Collections.Generic;

namespace EmployeeInfo.API
{
  public class EmployeesDataStore
  {
    public static EmployeesDataStore Current { get; } = new EmployeesDataStore();
    public List<EmployeeDto> Employees { get; set; }

    public EmployeesDataStore()
    {
      //Dummy data
      Employees = new List<EmployeeDto>()
            {
                new EmployeeDto()
                {
                     Id = 1,
                     Name = "Akhil Mittal",
                     Designation = "Technical Manager",
                     Salary="$50000",
                     CompaniesWorkedWith=new List<NumberOfCompaniesWorkedDto>()
                     {
                       new NumberOfCompaniesWorkedDto()
                       {
                         Id=1,
                         Name="Eon Technologies",
                         Description="Financial Technologies"
                       },
                       new NumberOfCompaniesWorkedDto()
                       {
                         Id=2,
                         Name="CyberQ",
                         Description="Outsourcing"
                       },
                       new NumberOfCompaniesWorkedDto()
                       {
                         Id=3,
                         Name="Magic Software Inc",
                         Description="Education Technology and Fin Tech"
                       }
                     }
                },
                new EmployeeDto()
                {
                     Id = 2,
                     Name = "Keanu Reaves",
                     Designation = "Developer",
                     Salary="$20000",
                     CompaniesWorkedWith=new List<NumberOfCompaniesWorkedDto>()
                     {
                       new NumberOfCompaniesWorkedDto()
                       {
                         Id=1,
                         Name="Eon Technologies",
                         Description="Financial Technologies"
                       }
                     }
                },
                 new EmployeeDto()
                {
                     Id = 3,
                     Name = "John Travolta",
                     Designation = "Senior Architect",
                     Salary="$70000",
                     CompaniesWorkedWith=new List<NumberOfCompaniesWorkedDto>()
                     {
                       new NumberOfCompaniesWorkedDto()
                       {
                         Id=1,
                         Name="Eon Technologies",
                         Description="Financial Technologies"
                       },
                       new NumberOfCompaniesWorkedDto()
                       {
                         Id=2,
                         Name="CyberQ",
                         Description="Outsourcing"
                       }
                     }
                },
                  new EmployeeDto()
                {
                     Id = 4,
                     Name = "Brad Pitt",
                     Designation = "Program Manager",
                     Salary="$80000",
                     CompaniesWorkedWith=new List<NumberOfCompaniesWorkedDto>()
                     {
                       new NumberOfCompaniesWorkedDto()
                       {
                         Id=1,
                         Name="Infosys Technologies",
                         Description="Financial Technologies"
                       },
                       new NumberOfCompaniesWorkedDto()
                       {
                         Id=2,
                         Name="Wipro",
                         Description="Outsourcing"
                       },
                       new NumberOfCompaniesWorkedDto()
                       {
                         Id=3,
                         Name="Magic Software Inc",
                         Description="Education Technology and Fin Tech"
                       }
                     }
                },
                   new EmployeeDto()
                {
                     Id = 5,
                     Name = "Jason Statham",
                     Designation = "Delivery Head",
                     Salary="$90000",
                     CompaniesWorkedWith=new List<NumberOfCompaniesWorkedDto>()
                     {
                       new NumberOfCompaniesWorkedDto()
                       {
                         Id=1,
                         Name="Fiserv",
                         Description="Financial Technologies"
                       },
                       new NumberOfCompaniesWorkedDto()
                       {
                         Id=2,
                         Name="Wipro",
                         Description="Outsourcing"
                       },
                       new NumberOfCompaniesWorkedDto()
                       {
                         Id=3,
                         Name="Magic Software Inc",
                         Description="Education Technology and Fin Tech"
                       }
                       ,
                       new NumberOfCompaniesWorkedDto()
                       {
                         Id=4,
                         Name="Sapient",
                         Description="Education Technology and Fin Tech"
                       }
                     }
                }
            };

    }
  }
}

现在,以与上 篇文章中创建 EmployeesController 相同的方式,在 CompaniesWorkedWithController 中添加一个新控制器。新控制器应派生自 Controller 类。

由于 CompaniesWorkedWith 直接与 Employees 相关,如果我们将其设置为默认路由“api/companiesworkedwith”,则看起来不美观且不合理。如果这与员工相关,API 的 URI 也应该反映这一点。CompaniesWorkedWith 可以被视为 Employees 的子资源或 Employee 的关联资源。因此,员工工作过的公司作为资源应该通过 employees 访问,因此 URI 将类似于“api/employees//companiesworkedwith”。因此,控制器路由将是“api/employees”,因为它将是所有操作的通用部分。

由于子资源依赖于父资源,因此我们获取父资源的 ID 来获取子资源。

以下是返回员工公司列表的实际实现。

    [HttpGet("{employeeid}/companiesworkedwith")]
    public IActionResult GetCompaniesWorkedWith(int employeeId)
    {
      var employee = EmployeesDataStore.Current.Employees.FirstOrDefault(emp => emp.Id == employeeId);
      if (employee == null) return NotFound();
      return Ok(employee.CompaniesWorkedWith);
    }

如果我们查看上面的代码,我们首先找到具有传入 ID 的员工,我们必须这样做,因为如果不存在具有该 ID 的员工,那么可以理解其工作过的公司也不存在,这使我们能够发送 NotFound 状态码;否则,如果我们找到员工,我们可以将 companiesworkedwith 发送给消费者。

以类似的方式,我们编写了获取单个公司工作信息中的代码。在这种情况下,我们应该传递两个 ID,一个用于员工,另一个用于公司,如下面的代码所示。

   [HttpGet("{employeeid}/companyworkedwith/{id}")]
    public IActionResult GetCompanyWorkedWith(int employeeId, int Id)
    {
      var employee = EmployeesDataStore.Current.Employees.FirstOrDefault(emp => emp.Id == employeeId);
      if (employee == null) return NotFound();

      var companyWorkedWith = employee.CompaniesWorkedWith.FirstOrDefault(comp => comp.Id == Id);
      if (companyWorkedWith == null) return NotFound();
      return Ok(companyWorkedWith);
    }

因此,在上面的代码中,我们首先获取传入 ID 的员工。如果员工存在,我们继续获取具有传入 ID 的公司;否则,我们返回 Not Found。如果公司不存在,我们再次返回 NotFound;否则,我们返回 OK 状态码以及公司对象。我们可以尝试在 Postman 上检查这些实现。

现有员工的 Companiesworkedwith

不存在员工的 Companiesworkedwith

特定员工的特定 Companiesworkedwith

特定员工的不存在的 Companyworkedwith

获取所有员工

所以我们看到我们得到了预期的结果。例如,以与 API 编写方式类似的方式,在那些情况下,消费者也获得了正确的响应,并且响应不会使消费者感到困惑。我们还看到,如果我们请求获取所有员工,我们也会获得相关的或子资源。是的,这种情况并非对所有请求都理想。例如,消费者可能只想要员工,而不想要相关的公司工作数据。是的,我们也可以通过 Entity Framework 来控制这一点,我们将在本系列的后续文章中介绍。请注意,我们还可以控制返回的 JSON 数据的首字母大小写,因为消费者可能期望属性是大写字母,我们也可以通过稍后将介绍的序列化器设置来做到这一点。

ASP.NET Core API 中的序列化器设置

默认情况下,ASP.NET Core 使用 JSON 进行序列化和反序列化,但最好的地方在于我们也可以在代码中进行配置。我们在 Startup 类中使用 ConfigureServices 方法来配置容器使用的服务,同样,我们也可以为 MVC 配置。

我们可以将 JSON 选项添加到已添加的 MVC 服务中。

由于 AddJsonOptions 方法需要一个操作,我们在其中提供一个 lambda 表达式作为选项参数,通过该参数我们可以轻松访问序列化器设置,如上图所示。我们获取 contract resolver,因为我们将覆盖默认的命名策略设置。行 var resolver = opt.SerializerSettings.ContractResolver as DefaultContractResolver; 提取 contract resolver 并将其转换为默认 contract resolver,现在我们可以覆盖其 NamingStrategy 属性并将其设置为 null。因此,它现在应该不会像默认情况下那样遵循小写字母约定。我们使用 DefaultContractResolver 类,该类属于 Newtonsoft.Json.Serialization,所以我们也需要添加它的命名空间。代码如下所示

public void ConfigureServices(IServiceCollection services)
    {
      services.AddMvc().AddJsonOptions(opt =>
      {
        if (opt.SerializerSettings.ContractResolver != null)
        {
          var resolver = opt.SerializerSettings.ContractResolver as DefaultContractResolver;
          resolver.NamingStrategy = null;
        }
      });
    }

现在让我们用 Postman 测试一下。在上一个请求中,我们得到的 JSON 属性是小写字母,如下所示

现在运行应用程序,并从 Postman 作为消费者发出对同一 API 的请求。

现在我们得到的 JSON 属性是大写字母(即由我们的实现覆盖)。此实现是按需的,或者基于消费者想要的 JSON 类型。这里就出现了内容协商的概念,即根据消费者的请求参数发送响应。

ASP.NET Core API 中的内容协商和格式化程序

内容协商是我们开发 API 的重要概念之一。它使 API 能够在存在多种表示形式时为所需的响应选择最佳表示形式。假设我们为多个消费者构建了一个 API,我们不确定所有客户端是否都能使用 API 发送的默认表示形式(JSON)。一些消费者可能期望 XML 作为响应或任何其他格式。在这种情况下,消费者将 JSON 而非 XML 理解和处理将很困难。

消费者总有一个选项可以通过指定 Accept 标头中请求的媒体类型来请求特定格式。例如,如果 Accept 标头中请求的格式是 XML,API 应该以 XML 格式发送响应;如果格式是 JSON,API 应该以 JSON 格式发送响应。如果没有指定标头,API 可以自由地以其默认格式发送响应。例如,在我们的情况下是 JSON。ASP.NET Core 也通过输出格式化程序支持此功能。输出格式化程序(顾名思义)主要处理输出。在这种情况下,消费者可以通过将 Accept 标头设置为请求的媒体类型(如 Application/JSON 或 Application/XML)来请求任何特定类型的输出。它也以输入格式的方式工作,假设有一个 POST 请求到 API 以创建资源,输入媒体类型以及请求随附的内容然后由请求中的 Content-Type 标头识别。让我们通过实际实现来涵盖这一点。让我们尝试使用 JSON Accept 标头请求员工列表。因此,我们将标头键设置为“Accept”,值为“application/json”,我们得到 JSON,如下所示。

现在用 Accept 标头 application/xml 发出请求,我们仍然得到 JSON。

但理想情况下,API 应该返回 XML。让我们看看如何配置我们的服务以返回相同的内容。如果我们转到 Startup 类的 ConfigureService 方法,我们可以选择向服务添加 MvcOptions,它又具有添加输入和输出格式化程序的选项。

如果我们想使用 XML 输出格式化程序,我们将不得不安装一个名为 Microsoft.AspNetCore.MVC.Formatters.Xml 的 Nuget 包,如下所示。所以右键单击项目,转到管理 Nuget 包并在网上搜索此包并安装它。

现在我们可以添加 XML 输出格式化程序,如以下代码所示

services.AddMvc()
        .AddMvcOptions(opt => opt.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter()));

类似地,也可以使用 XmlDataContractSerializerInputFormatter 添加输入格式化程序。所以,构建解决方案,运行项目,然后现在再次尝试请求员工列表。

首先使用默认的或 JSON 格式化程序,我们得到 JSON,如下所示。

现在使用 XML Accept 标头,我们得到 XML,如下所示。

因此,现在消费者可以请求所需格式的响应,我们的 API 也能够提供。

我们 Startup 类的代码如下所示

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Mvc.Formatters;

namespace EmployeeInfo.API
{
  public class Startup
  {
    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
      services.AddMvc()
        .AddMvcOptions(opt => opt.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter()));
      //.AddJsonOptions(opt =>
      //{
      //  if (opt.SerializerSettings.ContractResolver != null)
      //  {
      //    var resolver = opt.SerializerSettings.ContractResolver as DefaultContractResolver;
      //    resolver.NamingStrategy = null;
      //  }
      //});
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
      loggerFactory.AddConsole();

      if (env.IsDevelopment())
      {
        app.UseDeveloperExceptionPage();
      }
      else
      {
        app.UseExceptionHandler();
      }

      app.UseStatusCodePages();

      app.UseMvc();

      //app.Run(async (context) =>
      //{
      //  throw new Exception("Test Dev Exception Page");
      //});

      //app.Run(async (context) =>
      //{
      //  await context.Response.WriteAsync("Hello World!");
      //});
    }
  }
}

结论

在本文中,我们学习了 HTTP 状态码及其重要性,以及如何配置我们的服务来使用 HTTP 状态码。我们还重点介绍了如何通过 API 发送子资源或关联资源。我们学习了序列化器设置,最重要的是格式化程序,以及如何使 API 支持内容协商。在学习 ASP.NET Core API 的下一篇文章中,我们将执行一些更实际的操作,例如执行 CRUD 操作。

<< 上一篇

Github 上的源代码

源代码

参考

© . All rights reserved.