本章包括

  • 创建Web API控制器以向客户端返回JSON
  • 使用属性路由自定义URL
  • 使用内容协商生成响应
  • 使用[ApiController]属性应用通用约定

在前五章中,您已经完成了服务器端渲染ASP.NET Core应用程序的每一层,使用RazorPages将HTML渲染到浏览器。在本章中,您将看到对ASP.NET Core应用程序的不同理解。我们将探索Web API,而不是使用Razor Pages,它作为客户端SPA和移动应用程序的后端。

您可以将您已经学到的许多知识应用于Web API;它们使用相同的MVC设计模式,路由、模型绑定和验证的概念都贯穿始终。与传统web应用程序的区别主要体现在MVC的视图部分。它们不返回HTML,而是以JSON或XML形式返回数据,客户端应用程序使用这些数据来控制其行为或更新UI。

在本章中,您将学习如何定义控制器和操作,并了解它们与您已经知道的Razor Pages和控制器有多相似。您将学习如何创建一个API模型,以客户端应用程序能够理解的方式响应请求返回数据和HTTP状态代码。

在探索MVC设计模式如何应用于WebAPI之后,您将看到一个相关主题如何与WebAPI一起工作:路由。我们将研究属性路由如何重用第5章中的许多相同概念,但将它们应用于您的操作方法,而不是RazorPages。

ASP.NET Core 2.1中添加的一个重要特性是[ApiController]属性。该属性应用了Web API中使用的几种常见约定,从而减少了必须自己编写的代码量。在第9.5节中,您将了解如何对无效请求、模型绑定参数推断和ProblemDetails对象自动生成400个错误请求,从而使构建API更容易、更一致。

您还将学习如何使用内容协商格式化操作方法返回的API模型,以确保生成调用客户端可以理解的响应。作为这一部分,您将学习如何添加对其他格式类型(如XML)的支持,以便在客户端请求时生成XML响应。

ASP.NET Core的一个重要方面是您可以使用它创建各种各样的应用程序。轻松构建通用HTTP Web API的能力使ASP.NET Core能够在比仅使用传统Web应用程序更广泛的情况下使用。但是你应该构建一个Web API吗?为什么?在本章的第一节中,我将介绍您可能或不希望创建Web API的一些原因。

9.1 什么是Web API?您应该何时使用?

传统的web应用程序通过向用户返回HTML来处理请求,HTML显示在web浏览器中。您可以使用RazorPages轻松构建这种性质的应用程序,以使用Razor模板生成HTML,正如您在最近几章中看到的那样。这种方法很常见,也很容易理解,但现代应用程序开发人员还需要考虑许多其他可能性,如图9.1所示。

近年来,随着Angular、React和Vue等框架的发展,客户端单页应用程序(SPA)变得流行起来。这些框架通常使用在用户的web浏览器中运行的JavaScript来生成他们看到并与之交互的HTML。当用户第一次访问应用程序时,服务器将这个初始JavaScript发送到浏览器。用户的浏览器在从服务器加载任何应用程序数据之前加载JavaScript并初始化SPA。

注:Blazor WebAssembly是一个令人兴奋的新SPA框架。Blazor允许您编写一个与其他SPA一样在浏览器中运行的SPA,但它使用C#和Razor模板,而不是使用新的web标准WebAssembly的JavaScript。我在这本书中没有涉及Blazor,所以为了了解更多信息,我推荐Chris Sainty(Manning,2021)的《Blazor in Action》。

图9.1 现代开发人员必须考虑其应用程序的许多不同消费者。与使用web浏览器的传统用户一样,这些用户可以是SPA、移动应用程序或其他应用程序。

一旦SPA加载到浏览器中,与服务器的通信仍然通过HTTP进行,但服务器端应用程序不会响应请求直接向浏览器发送HTML,而是向客户端应用程序发送数据(通常为JSON格式)。SPA然后解析数据并生成适当的HTML以显示给用户,如图9.2所示。客户端与之通信的服务器端应用程序端点有时称为Web API。

定义:Web API公开了多个URL,可用于访问或更改服务器上的数据。它通常使用HTTP访问。

图9.2 使用Blazor WebAssembly的客户端SPA示例。最初的请求将SPA文件加载到浏览器中,随后的请求从Web API获取数据,格式为JSON。

如今,移动应用程序很常见,从服务器应用程序的角度来看,类似于客户端SPA。移动应用程序通常会使用HTTPWebAPI与服务器应用程序通信,以常见格式(如JSON)接收数据,就像SPA一样。然后,它根据收到的数据修改应用程序的UI。

WebAPI的最后一个用例是,您的应用程序设计为部分或完全由其他后端服务使用。假设您已经构建了一个用于发送电子邮件的web应用程序。通过创建Web API,您可以通过向您发送电子邮件地址和消息,允许其他应用程序开发人员使用您的电子邮件服务。实际上,所有语言和平台都可以访问HTTP库,它们可以使用该库从代码访问您的服务。

这就是Web API的全部功能。它公开了许多端点(URL),客户端应用程序可以向其发送请求并从中检索数据。这些用于增强客户端应用程序的行为,以及提供客户端应用程序向用户显示正确界面所需的所有数据。

是否需要或希望为ASP.NET Core应用程序创建Web API取决于要构建的应用程序类型。如果您熟悉客户端框架,需要开发移动应用程序,或者您已经配置了SPA构建管道,那么您很可能希望添加Web API,以便他们可以使用这些API访问您的应用程序。

使用WebAPI的一个卖点是它可以作为所有客户端应用程序的通用后端。例如,您可以从构建使用Web API的客户端应用程序开始。稍后,您可以添加一个使用相同Web API的移动应用程序,而无需对ASP.NET Core代码进行修改。

如果您是web开发的新手,您不需要从web浏览器外部调用应用程序,或者您不想或不需要配置客户端应用程序所需的工作,那么您最初可能不需要web API。你可以坚持使用RazorPages生成用户界面,毫无疑问,你会非常高效!

注意:尽管行业已经向客户端框架结构转变,但使用Razor的服务器端渲染仍然是相关的。您选择哪种方法在很大程度上取决于您对以传统方式构建HTML应用程序的偏好,而不是在客户端使用JavaScript。

话虽如此,在应用程序中添加Web API并不是您必须提前担心的事情。稍后添加它们很简单,因此您可以在开始时忽略它们,并在需要时添加它们。在许多情况下,这将是最好的方法。

带有ASP.NET Core的SPA

ASP.NET Core的跨平台和轻量级设计意味着它很适合作为SPA框架的后端。考虑到本书的重点以及总体上的SPAs的广泛范围,我不会在这里讨论Angular、React或其他SPAs。相反,我建议查看适合您选择的SPA的资源。Manning提供了所有常见客户端JavaScript框架以及Blazor的书籍:

    • 马克·泰伦斯·托马斯(Mark Tielens Thomas)的《React in Action》(Manning,2018)
    • 杰里米·威尔肯(Jeremy Wilken)的《Angular in Action》(Manning,2018)
    • Erik Hanchett与Benjamin Listwon的《Vue.js in Action》(曼宁,2018)
    • 《Blazor in Action》,Chris Sainty著(曼宁,2021)

一旦你确定你的应用程序需要一个WebAPI,创建一个很容易,因为它内置于ASP.NET Core。在下一节中,您将看到如何创建WebAPI项目和第一个API控制器。

9.2 创建第一个Web API项目

在本节中,您将学习如何创建ASP.NET Core Web API项目并创建第一个Web API控制器。您将看到如何使用控制器操作方法处理HTTP请求,以及如何使用ActionResults生成响应。

有些人认为MVC设计模式只适用于直接呈现其UI的应用程序,如您在前几章中看到的Razor视图。在ASP.NET Core中,MVC模式在构建Web API时同样适用,但MVC的视图部分涉及生成机器友好的响应,而不是用户友好的响应。

与此并行,您在ASP.NET Core中创建WebAPI控制器的方式与创建传统MVC控制器的方式相同。从代码角度来看,它们唯一的区别是它们返回的数据类型。MVC控制器通常返回ViewResult;Web API控制器通常从其操作方法或IActionResult(如StatusCodeResult)返回原始.NET对象,如第4章所示。

注意:这与以前版本的ASP.NET不同,在ASP.NET中MVC和Web API堆栈是完全独立的。ASP.NET Core将两个堆栈统一为一种方法(并将RazorPages添加到混合方法中),这使得在项目中使用任何选项都变得轻松!

为了让您初步了解所使用的内容,图9.3显示了从浏览器调用WebAPI端点的结果。与友好的HTML UI不同,您接收的数据可以在代码中轻松使用。在本例中,当您请求URL/fruit时,Web API将返回字符串水果名称列表,格式为JSON。

图9.3 通过访问浏览器中的URL测试Web API。向/fruit URL发出GET请求,该URL返回一个已被JSON编码为字符串数组的List<string>。

提示:Web API通常由SPA或移动应用程序从代码访问,但通过直接访问Web浏览器中的URL,您可以查看API返回的数据。

ASP.NET Core 5.0应用程序还包括一个有用的端点,用于测试和探索开发中的Web API项目,称为Swagger UI,如图9.4所示。这允许您浏览应用程序中的端点,查看预期的响应,并通过发送请求进行实验。

注意:Swagger UI基于行业标准OpenAPI规范(以前称为Swagger,www.openapis.org),该规范在Web API应用程序中默认启用。OpenAPI提供了一种记录API的方法,这样您就可以自动生成客户端,以便用几十种不同的语言与之交互。有关ASP.NET Core应用程序中OpenAPI和Swagger的更多信息,请参阅Microsoft文档:http://mng.bz/QmjR.

图9.4 默认ASP.NET Core 5.0 Web API模板配置为使用Swagger UI。Swagger UI提供了一个方便的网页,用于探索和与web API交互。默认情况下,此UI仅在开发中启用,但如果您愿意,也可以在生产中启用。

您可以使用在第2章中看到的相同的NewProject过程在VisualStudio中创建新的WebAPI项目。创建一个新的ASP.NET Core应用程序,提供项目名称,当您到达NewProject对话框时,选择ASP.NET Core Web API模板,如图9.5所示。如果使用的是CLI,则可以使用dotnet new webapi-o WebApplication1创建类似的模板。

图9.5 web应用程序模板屏幕。此屏幕位于“配置您的项目”对话框之后,允许您自定义将生成应用程序的模板。选择ASP.NET Core Web API模板以创建Web API项目。

API模板仅为Web API控制器配置ASP.NET Core项目。此配置出现在Startup.cs文件中,如清单9.1所示。如果将此模板与RazorPages项目进行比较,您将看到WebAPI项目在ConfigureServices方法中使用AddControllers()而不是AddRazorPage()。此外,通过在UseEndpoints方法中调用MapControllers()来添加API控制器而不是RazorPages。默认Web API模板还添加了Swagger UI所需的Swagger服务和端点,如图9.4所示。如果在项目中同时使用RazorPages和WebAPI,则需要将所有这些方法调用添加到应用程序中。

清单9.1 默认Web API项目的Startup类

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();     //AddControllers将API控制器的必要服务添加到应用程序中。
        //添加生成Swagger/OpenAPI规范文档所需的服务
        services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { 
              Title = "DefaultApiTemplate", Version = "v1" });
        });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        //添加Swagger UI中间件以探索Web API 
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseSwagger();
            app.UseSwaggerUI(c => c.SwaggerEndpoint( "/swagger/v1/swagger.json", "DefaultApiTemplate v1"));
        }
        app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();    //MapControllers将应用程序中的API控制器操作配置为端点。
        });
    }
}

清单9.1中的Startup.cs文件指示应用程序查找应用程序中的所有API控制器,并在EndpointMiddleware中配置它们。当RoutingMiddleware将传入URL映射到动作方法时,每个动作方法都会成为一个端点,并可以接收请求。

注意:您不必为RazorPages和WebAPI控制器使用单独的项目,但我更愿意在可能的情况下这样做。采用这种方法可以使某些方面(例如错误处理和身份验证)变得更容易。当然,运行两个独立的应用程序有其自身的困难!

您可以通过在项目中的任何位置创建一个新的.cs文件,将Web API控制器添加到项目中。传统上,这些文件放在名为Controllers的文件夹中,但这不是技术要求。清单9.2显示了用于创建图9.3所示的Web API的代码。这个微不足道的例子突出了与传统MVC控制器的相似性。

清单9.2 一个简单的Web API控制器

[ApiController]    //Web API控制器通常使用[ApiController]属性来选择加入通用约定。
public class FruitController : ControllerBase    //ControllerBase类为创建IActionResults提供了几个有用的函数。
{
    //返回的数据通常会从真实应用程序中的应用程序模型中获取。
    List<string> _fruit = new List<string>
    {
        "Pear",
        "Lemon", "Peach"
    };

    [HttpGet("fruit")]    //[HttpGet]属性定义用于调用操作的路由模板。
    public IEnumerable<string> Index()    //操作方法的名称Index不用于路由。它可以是你喜欢的任何东西。
    {
        return _fruit;    ////控制器公开一个返回水果列表的单一操作方法。
    }
}

 Web API通常在API控制器上使用[ApiController]属性(在.NET Core 2.1中引入),并从ControllerBase类派生。基类提供了几个用于生成结果的助手方法,[ApiController]属性自动应用一些常见的约定,如第9.5节所示。

提示:还有一个Controller基类,通常在使用Razor视图的MVC控制器时使用。这对于WebAPI控制器来说是不必要的,所以ControllerBase是更好的选择。

在清单9.2中,您可以看到action方法Index直接从action方法返回字符串列表。当您从这样的操作返回数据时,您就为请求提供了API模型。客户端将接收此数据。它被格式化为一个适当的响应,如图9.3所示,是列表的JSON表示,并用一个200OK状态代码发送回浏览器。

提示:默认情况下,ASP.NET Core将数据格式化为JSON。您将在第9.6节中看到如何以其他方式格式化返回的数据。

Web API控制器操作公开的URL的处理方式与传统MVC控制器和Razor Pages使用路由相同。应用于Index方法的[HttpGet(“fruit”)]属性表示该方法应使用路由模板“fruit”,并应响应HTTP GET请求。您将在第9.5节中了解有关属性路由的更多信息。

在清单9.2中,数据直接从action方法返回,但不必这样做。您可以自由地返回ActionResult,这通常是必需的。根据所需的API行为,有时您可能希望返回数据,而有时您可能需要返回原始HTTP状态代码,指示请求是否成功。例如,如果进行API调用,请求不存在的产品的详细信息,则可能需要返回404 not Found状态代码。

清单9.3显示了这样一个例子。它显示了与之前相同的FruitController上的另一个操作。此方法为客户端提供了一种通过id获取特定水果的方法,在本例中,我们将假设它是您在上一个列表中定义的_fruit列表中的索引。模型绑定用于设置请求中id参数的值。

注意:API控制器使用模型绑定,如您在第6章中所见,将动作方法参数绑定到传入请求。模型绑定和验证的工作方式与RazorPages完全相同:您可以将请求绑定到简单的原语,也可以绑定到复杂的C#对象。唯一的区别是没有一个PageModel具有[BindProperty]属性,您只能绑定到操作方法参数。

清单9.3 返回IActionResult以处理错误条件的Web API操作

[HttpGet("fruit/{id}")]    //定义操作方法的路由模板
public ActionResult<string> View(int id)    //action方法返回ActionResult<string>,因此它可以返回字符串或ActionResult。
{
    if (id >= 0 && id < _fruit.Count)    //只有当id值是有效的_fruit元素索引时,才能返回元素。
    {
        return _fruit[id];    //直接返回数据将返回具有200状态代码的数据。
    }
    return NotFound();    //NotFound返回NotFoundResult,它将发送404状态代码。
}

在action方法的成功路径中,id参数的值大于0,小于_fruit中的元素数。如果这是真的,元素的值将返回给调用者。如清单9.2所示,这是通过直接返回数据来实现的,它生成一个200状态代码并返回响应体中的元素,如图9.6所示。您也可以使用OkResult返回数据,通过返回Ok(_fruit[id]),使用引擎盖下ControllerBase类上的Ok-helper方法,结果是相同的。

图9.6 从动作方法返回的数据被序列化到响应体中,并生成状态代码为200OK的响应。

如果id在_fruit列表的边界之外,则该方法调用NotFound以创建NotFoundResult。执行时,此方法生成404 Not Found状态代码响应。[ApiController]属性自动将响应转换为标准ProblemDetails实例,如图9.7所示。

图9.7 [ApiController]属性将错误响应(在本例中为404响应)转换为标准ProblemDetails格式。

定义:ProblemDetails是一个为HTTPAPI提供机器可读错误的web规范。您将在第9.5节中了解更多信息。

从清单9.3中您可能会感到困惑的一个方面是,对于成功的情况,我们返回了一个字符串,但View的方法签名表示我们返回了ActionResult<string>。这怎么可能?为什么不是编译器错误?

通用的ActionResult<T>使用一些带有隐式转换的花哨的C#体操来实现这一点。使用ActionResult<T>有两个好处:

  • 您可以从同一方法返回T的实例或ActionResult实现(如NotFoundResult)。这可能很方便,如清单9.3所示。
  • 它可以更好地与ASP.NET Core的OpenAPI支持集成。

您可以自由地从Web API控制器返回任何类型的ActionResult,但通常会返回StatusCodeResult实例,该实例将响应设置为特定的状态代码,无论是否有关联数据。例如,NotFoundResult和OkResult都源自StatusCodeResult。另一个常用的状态代码是400BadRequest,通常在请求中提供的数据未通过验证时返回。这可以使用BadRequestResult生成。在许多情况下,[ApiController]属性可以自动为您生成400个响应,如第9.5节所示。

提示:您在第4章中了解了各种ActionResults。BadRequest-Result、OkResult和NotFoundResult都继承自StatusCodeResult,并为其类型设置适当的状态代码(分别为200、404和400)。使用这些包装器类比依赖其他开发人员了解各种状态代码编号的意义更清楚地了解代码的意图。

从控制器返回ActionResult(或其他对象)后,它将被序列化为适当的响应。这有几种方式,具体取决于

  • 应用程序支持的格式化程序
  • 从方法返回的数据
  • 请求客户端可以处理的数据格式

在第9.6节中,您将了解更多关于格式化程序和序列化数据的信息,但在我们进一步讨论之前,有必要缩小一点,探索传统服务器端渲染应用程序和Web API端点之间的相似之处。这两者是相似的,因此建立它们共享的模式以及它们的不同之处很重要。

9.3 将MVC设计模式应用于Web API

在ASP.NET的早期版本中,Microsoft使用通用术语“Web API”来创建ASP.NET Web API框架。正如您所料,这个框架用于创建HTTP端点,这些端点可以响应请求返回格式化的JSON或XML。

ASP.NET Web API框架与MVC框架完全分离,尽管它使用了类似的对象和范例。它们的底层网络堆栈完全不同,无法互操作。

在ASP.NET Core中,一切都变了。现在,您有了一个可以用来构建传统web应用程序和web API的框架。相同的底层框架与WebAPI控制器、RazorPages和带有视图的MVC控制器一起使用。你自己已经看到了这一点;您在9.2节中创建的WebAPI FruitController看起来与您在前几章中快速看到的MVC控制器非常相似。

因此,即使您构建的应用程序完全由Web API组成,不使用HTML的服务器端呈现,MVC设计模式仍然适用。无论您是构建传统的web应用程序还是web API,您都可以几乎相同地构建应用程序。

我希望,现在您已经非常熟悉ASP.NET Core如何处理请求。但如果您不是,图9.8显示了框架在通过中间件管道后如何处理典型的RazorPages请求。此示例显示了在传统杂货店网站上查看可用水果的请求的外观。

图9.8 处理对传统RazorPages应用程序的请求,其中视图生成一个HTML响应并发送回用户。这个图现在应该很熟悉了!

RoutingMiddleware将查看苹果类别中列出的所有水果的请求路由到fruit.cshtml Razor页面。EndpointMiddleware然后构造一个绑定模型,对其进行验证,将其设置为Razor Page的PageModel上的属性,并在PageModel基类上设置ModelState属性,其中包含任何验证错误的详细信息。页面处理程序通过调用服务、与数据库对话以及获取任何必要的数据来与应用程序模型交互。

最后,RazorPage使用PageModel执行其Razor视图以生成HTML响应。响应通过中间件管道返回到用户的浏览器。

如果请求来自客户端或移动应用程序,这会发生什么变化?如果您希望提供机器可读的JSON而不是HTML,有什么不同?如图9.9所示,答案是“很少”。主要的变化与从Razor Pages切换到控制器和操作有关,但正如您在第4章中看到的,两种方法都使用相同的通用范式。

图9.9 对电子商务ASP.NET Core Web应用程序中Web API端点的调用。图中的重影部分与图9.8相同。

与之前一样,路由中间件根据传入的URL选择要调用的端点。对于API控制器,这是一个控制器和操作,而不是Razor页面。

路由之后是模型绑定,其中绑定器创建一个绑定模型,并用请求中的值填充它。Web API通常接受比Razor Pages更多的格式的数据,例如XML,但在其他方面,模型绑定过程与Razor Page请求相同。验证也以相同的方式进行,ControllerBase基类上的ModelState属性将填充任何验证错误。

注意:Web API使用输入格式器接受以各种格式发送给它们的数据。通常这些格式是JSON或XML,但您可以为任何类型(如CSV)创建输入格式器。我在第9.6节中演示了如何启用XML输入格式化程序。您可以在http://mng.bz/e5gG.

action方法相当于RazorPage处理程序;它以完全相同的方式与应用程序模型交互。这一点很重要;通过将应用程序的行为分离到应用程序模型中,而不是将其合并到页面和控制器本身中,您可以使用多个UI范式重用应用程序的业务逻辑。

提示:尽可能保持页面处理程序和控制器尽可能简单。将所有的业务逻辑决策转移到构成应用程序模型的服务中,并让RazorPages和API控制器专注于与用户交互的机制。

在应用程序模型返回了服务请求所需的数据(苹果类别中的水果对象)之后,您将看到API控制器和RazorPages之间的第一个显著区别。action方法创建了一个API模型,而不是将值添加到要在Razor视图中使用的PageModel中。这类似于PageModel,但它不包含用于生成HTML视图的数据,而是包含将在响应中发送回的数据。

定义视图模型和页面模型包含构建响应所需的数据和关于如何构建响应的元数据。API模型通常只包含要在响应中返回的数据。

当我们查看Razor Pages应用程序时,我们将PageModel与Razor视图模板结合使用来构建最终响应。对于WebAPI应用程序,我们将API模型与输出格式器结合使用。顾名思义,输出格式器将API模型序列化为机器可读的响应,如JSON或XML。输出格式器通过选择要返回的数据的适当表示形式,在MVC的Web API版本中形成“V”。

最后,对于RazorPages应用程序,生成的响应然后通过中间件管道发送回,通过每个配置的中间件组件,并返回到原始调用方。

希望RazorPages和WebAPI之间的相似之处很明显;大多数行为都是相同的,只是反应不同。从请求到达到与应用程序模型的交互,范式之间的一切都是相似的。

RazorPages和WebAPI之间的大部分差异与框架在幕后的工作方式无关,而是与如何使用不同的范式有关。例如,在下一节中,您将学习如何使用属性路由将第5章中学习的路由构造与WebAPI一起使用。

9.4 属性路由:将动作方法链接到URL

在本节中,您将了解属性路由:将API控制器动作与给定路由模板关联的机制。您将看到如何将控制器操作与特定的HTTP谓词(如GET和POST)相关联,以及如何避免模板中的重复。

我们在第5章RazorPages的上下文中深入介绍了路由模板,您将很高兴知道您使用的API控制器完全相同的路由模板。唯一的区别是如何指定模板:在RazorPages中使用@page指令,而在API控制器中使用路由属性。

注意:RazorPages和API控制器都在后台使用属性路由。替代的常规路由通常与传统的MVC控制器和视图一起使用。如前所述,我不建议使用这种方法,因此我在本书中不涉及传统的路由。

使用属性路由,您可以用一个属性来修饰API控制器中的每个操作方法,并为该操作方法提供关联的路由模板,如下面的列表所示。

清单9.4 属性路由示例

public class HomeController: Controller
{
    [Route("")]    ////当请求/URL时,将执行索引操作。
    public IActionResult Index()
    {
        /* method implementation*/
    }

    [Route("contact")]    //当请求/Contact URL时,将执行Contact操作。
    public IActionResult Contact()
    {
        /* method implementation*/
    }
}

每个[Route]属性都定义了一个应该与操作方法关联的路由模板。在提供的示例中,/URL直接映射到Index方法,/contact URL映射到contact方法。

属性路由将URL映射到特定的操作方法,但单个操作方法仍然可以有多个路由模板,因此可以对应多个URL。每个模板都必须用自己的RouteAttribute声明,如清单所示,其中显示了赛车游戏的Web API框架。

清单9.5 具有多个属性的属性路由

public class CarController
{
     //当这些路由模板匹配时,将执行Start方法。
    [Route("car/start")] 
    [Route("car/ignition")] 
    [Route("start-car")]
    public IActionResult Start()    //操作方法的名称对路由模板没有影响。
    {
        /* method implementation*/
    }
    //RouteAttribute模板可以包含路由参数,在本例中为{speed}。
    [Route("car/speed/{speed}")] 
    [Route("set-speed/{speed}")]
    public IActionResult SetCarSpeed(int speed)
    {
        /* method implementation*/
    }
}

该列表显示了两种不同的操作方法,这两种方法都可以从多个URL访问。例如,当请求以下任何URL时,将执行Start方法:

  • /car/start
  • /car/ignition
  • /start-car

这些URL完全独立于控制器和操作方法名称;只有RouteAttribute中的值才重要。

注意:默认情况下,当使用RouteAttributes时,控制器和操作名称与URL或路由模板无关。

路线属性中使用的模板是标准路线模板,与第5章中使用的相同。您可以使用文字段,并且可以自由定义将从URL中提取值的路由参数,如前一列表中的SetCarSpeed方法所示。该方法定义了两个路由模板,这两个模板都定义了一个路由参数{speed}。

提示:在本例中,我对每个操作使用了多个[Route]属性,但最好在一个URL中公开您的操作。这将使您的API更容易理解,并让其他应用程序使用。

路由参数的处理方式与Razor Pages的处理方式完全相同,它们代表了URL的一段,该段可能会有所不同。对于Razor Pages,RouteAttribute模板中的路由参数可以

  • 可选
  • 具有默认值
  • 使用管线约束

例如,您可以更新上一列表中的SetCarSpeed方法,将{speed}约束为整数,并默认为20,如下所示:

[Route("car/speed/{speed=20:int}")] 
[Route("set-speed/{speed=20:int}")]
public IActionResult SetCarSpeed(int speed)

注:如第5章所述,不要使用路线约束进行验证。例如,如果使用无效的速度值/set speed/oops调用前面的“set speed/{speed=20:int}”路由,则会得到404 Not Found响应,因为该路由不匹配。如果没有int约束,您将收到更合理的400BadRequest响应。

如果您在第5章中设法绕过了路由,那么使用API控制器进行路由不会给您带来任何惊喜。当您开始对API控制器使用属性路由时,您可能会注意到一件事,那就是您自己重复的次数。RazorPages通过使用约定基于RazorPage的文件名计算路由模板,消除了大量重复。

幸运的是,有一些功能可以让你的生活更轻松。特别是,结合路由属性和令牌替换可以帮助减少代码中的重复。

9.4.1 组合路由属性以保持路线模板干燥

将路由属性添加到所有API控制器可能会有点乏味,特别是如果您主要遵循路由具有标准前缀(如“API”或控制器名称)的约定。通常,当涉及到这些字符串时,您需要确保不重复自己(DRY)。下面的列表显示了两个具有多个[Route]属性的操作方法。(这仅用于演示目的。如果可以,请坚持每个动作一个!)

清单9.6 RouteAttribute模板中的重复

public class CarController
{
    //多个路线模板使用相同的“api/car”前缀。 
    [Route("api/car/start")] 
    [Route("api/car/ignition")] 
    [Route("/start-car")]
    public IActionResult Start()
    {
        /* method implementation*/
    }

    [Route("api/car/speed/{speed}")] 
    [Route("/set-speed/{speed}")]
    public IActionResult SetCarSpeed(int speed)
    {
    /* method implementation*/
    }
}

这里有很多重复,你在大多数路线上都添加了“api/car”。据推测,如果你决定将其更改为“api/viders”,你必须检查每个属性并更新它!

为了减轻这种痛苦,除了操作方法之外,还可以将RouteAttributes应用于控制器,正如您在第5章中简要介绍的那样。当控制器和动作方法都具有路由属性时,通过组合这两个模板来计算该方法的总体路由模板。

清单9.7 组合RouteAttribute模板

[Route("api/car")]
public class CarController
{
    [Route("start")]    //组合以提供“api/car/start”模板
    [Route("ignition")]    //组合以提供“api/car/ignition”模板
    [Route("/start-car")]    //不组合,因为它以/开头;提供“启动汽车”模板
    public IActionResult Start()
    {
        /* method implementation*/
    }
    [Route("speed/{speed}")]	 //组合以提供“api/car/speed/{speed}”模板
    [Route("/set-speed/{speed}")]    //不组合,因为它以/开头;提供“set-speed/{speed}”模板
    public IActionResult SetCarSpeed(int speed)
    {
        /* method implementation*/
    }
}

以这种方式组合属性可以减少路线模板中的一些重复,并使添加或更改多个动作方法的前缀(例如将“car”切换为“vehicle”)更加容易。若要忽略控制器上的RouteAttribute并创建绝对路由模板,请使用斜线(/)启动操作方法路由模板。使用控制器RouteAttribute可以减少大量重复,但通过使用令牌替换可以做得更好。

9.4.2 使用令牌替换减少属性路由中的重复

组合属性路由的功能很方便,但如果您使用控制器的名称作为路由前缀,或者如果您的路由模板始终使用操作名称,则仍会出现一些重复。如果你愿意,你可以进一步简化!

属性路由支持自动替换属性路由中的[action]和[controller]标记。这些将分别替换为操作名称和控制器(不带“控制器”后缀)。在合并所有属性后,将替换标记,因此当您具有控制器继承层次结构时,这非常有用。此列表显示了如何创建BaseController类,您可以使用该类将一致的路由模板前缀应用于应用程序中的所有API控制器。

清单9.8 RouteAttributes中的令牌替换

[Route("api/[controller]")]    //您可以将属性应用于基类,派生类将继承它们。
public abstract class BaseController { }    //令牌替换发生在最后,因此[控制器]被替换为“car”而不是“base”。
public class CarController : BaseController
{
    [Route("[action]")]    //组合并替换令牌以提供“api/car/start”模板
    [Route("ignition")]    //组合并替换令牌以提供“api/car/ignition”模板
    [Route("/start-car")]    //不与基本属性组合,因为它以/开头,所以它保持为“start car”
    public IActionResult Start()
    {
        /* method implementation*/
    }
}		

警告:如果您对[control]或[action]使用令牌替换,请记住重命名类和方法将更改您的公共API。如果这让你担心,你可以坚持使用静态字符串,比如“car”。

结合您在第5章中学习的所有内容,我们已经涵盖了关于属性路由的几乎所有知识。还有一件事需要考虑:处理不同的HTTP请求类型,如GET和POST。

9.4.3 使用属性路由处理HTTP谓词

在RazorPages中,HTTP动词(如GET或POST)不是路由过程的一部分。RoutingMiddleware仅基于与Razor页面关联的路由模板来确定要执行哪个Razor页。只有当Razor Page即将执行时,HTTP动词才用于决定要执行哪个页面处理程序:例如,GET动词为OnGet,POST动词为OnPost。

使用API控制器,情况会有所不同。对于API控制器,HTTP动词本身参与路由过程,因此GET请求可以被路由到一个动作,POST请求可以被发送到不同的动作,即使请求使用相同的URL。这种模式在HTTP API设计中很常见,其中HTTP动词是路由的重要组成部分。

例如,假设您正在构建一个API来管理日历。您希望能够列出和创建约会。好吧,传统的HTTP REST服务可以定义以下URL和HTTP动词来实现这一点:

  • GET /appointments—列出所有appointments
  • POST /appointments—创建新appointments

注意,这两个端点使用相同的URL;只有HTTP动词不同。到目前为止,我们使用的[Route]属性响应所有HTTP动词,这对我们没有好处。在这里,我们希望根据URL和动词的组合选择不同的操作。这种模式在构建WebAPI时很常见,幸运的是,它很容易在ASP.NET Core中建模。

ASP.NET Core提供了一组属性,您可以使用这些属性来指示动作应响应哪个动词。例如,

  • [HttpPost]仅处理POST请求。
  • [HttpGet]仅处理GET请求。
  • [HttpPut]仅处理PUT请求。

所有标准HTTP动词都有类似的属性,如DELETE和OPTIONS。您可以使用这些属性而不是[Route]属性来指定动作方法应对应于单个动词,如以下列表所示。

清单9.9 使用带有属性路由的HTTP动词属性

public class AppointmentController
{
    //仅在响应GET/appointments时执行 
    [HttpGet("/appointments")]
    public IActionResult ListAppointments()
    {
        /* method implementation */
    }

    //仅在响应POST/appointments时执行 
    [HttpPost("/appointments")]
    public IActionResult CreateAppointment()
    {
        /* method implementation */
    }
}

如果应用程序收到的请求与操作方法的路由模板匹配,但与所需的HTTP谓词不匹配,则会收到405方法不允许的错误响应。例如,如果您向上一个列表中的/a约会URL发送DELETE请求,您将得到405错误响应。

自ASP.NET Core的第一天起,属性路由就被用于API控制器,因为它允许对应用程序公开的URL进行严格控制。当您构建API控制器时,您会发现自己在重复编写一些代码。ASP.NET Core 2.1中引入的[ApiController]属性旨在为您处理其中一些问题,并减少所需的样板数量。

9.5 使用[ApiController]属性的通用约定

在本节中,您将了解[ApiController]属性,以及它如何减少创建一致的Web API控制器所需的代码量。您将了解它适用的约定,它们为什么有用,以及如何在需要时关闭它们。

在.NET Core 2.1中引入了[ApiController]属性,以简化创建Web API控制器的过程。为了理解它的作用,可以看看一个示例,说明如何在没有[ApiController]属性的情况下编写WebAPI控制器,并将其与使用该属性实现相同功能所需的代码进行比较。

清单9.10 创建不带[ApiController]属性的Web API控制器

public class FruitController : ControllerBase
{
    //在此示例中,字符串列表用作应用程序模型。
    List<string> _fruit = new List<string>
    {
        "Pear", "Lemon", "Peach"
    };

    [HttpPost("fruit")]    //Web API使用属性路由来定义路由模板。
    public ActionResult Update([FromBody] UpdateModel model)    //[FromBody]属性指示参数应绑定到请求主体。
    {
        //您需要检查模型验证是否成功,如果失败,则返回400响应。
        if (!ModelState.IsValid)
        {
            return BadRequest(
            new ValidationProblemDetails(ModelState));
        }

        if (model.Id < 0 || model.Id > _fruit.Count)
        {
            //如果发送的数据不包含有效ID,则返回404 ProblemDetails响应。 
            return NotFound(new ProblemDetails()
            {
                Status = 404,
                Title = "Not Found",
                Type = "https://tools.ietf.org/html/rfc7231"
                + "#section-6.5.4",
            });
        }
        //更新模型并返回200响应。 
        _fruit[model.Id] = model.Name; 
        return Ok();
    }

    public class UpdateModel
    {
        //UpdateModel仅在提供Name值时有效,该值由[必需]属性设置。 
        public int Id { get; set; }
        [Required]
        public string Name { get; set; }
    }
}

此示例演示了Web API控制器使用的许多常见功能和模式:

  • Web API控制器从请求主体读取数据,通常以JSON形式发送。为了确保将主体读取为JSON而不是表单值,必须将[FromBody]属性应用于方法参数,以确保其正确绑定模型。
  • 如第6章所述,在模型绑定之后,模型将被验证,但这取决于您对验证结果采取行动。如果提供的值未通过验证,则应返回400错误请求响应。您通常希望提供请求无效原因的详细信息:这在清单9.10中通过在响应主体中返回ValidationProblemDetails对象来完成,该对象是从Model-State构建的。
  • 每当您返回错误状态(如404 Not Found)时,如果可能,您应该返回问题的详细信息,以便调用方诊断问题。ProblemDetails类是ASP.NET Core中的推荐方法。

清单9.10中的代码代表了在.NETCore 2.1之前的ASP.NET Core API控制器中可能看到的内容。在.NETCore2.1中引入[ApiController]属性(以及后续版本中的改进),使相同的代码更加简单,如下表所示。

清单9.11 使用[ApiController]属性创建Web API控制器

[ApiController]    //添加[ApiController]属性将应用API控制器常见的几种约定。
public class FruitController : ControllerBase
{
    List<string> _fruit = new List<string>
    {
        "Pear", "Lemon", "Peach"
    };
 
    [HttpPost("fruit")]
    public ActionResult Update(UpdateModel model)    //对于复杂的操作方法参数,假定使用[FromBody]属性。
    {
        //自动检查模型验证,如果无效,则返回400响应。
        if (model.Id < 0 || model.Id > _fruit.Count)
        {
            return NotFound();    //错误状态代码将自动转换为ProblemDetails对象。
        }

        _fruit[model.Id] = model.Name;
        return Ok();
    }

    public class UpdateModel
    {
        public int Id { get; set; }

        [Required]
        public string Name { get; set; }
    }
}

如果将清单9.10与清单9.11进行比较,您将看到清单9.10中的所有粗体代码都可以删除,并用清单9.11中的[ApiController]属性替换。[ApiController]属性自动将几种约定应用于控制器:

  • 属性路由——必须对控制器使用属性路由;你不能使用传统的路由。不是这样,因为我们只讨论了API控制器的这种方法。
  • 自动400响应——我在第6章中说过,您应该在Razor Page处理程序和MVC操作中始终检查ModelState.IsValid的值,但[ApiController]属性通过添加过滤器为您实现这一点。我们将在第13章详细介绍过滤器。
  • 模型绑定源推断——如果没有[ApiController]属性,则假定复杂类型作为表单值在请求主体中传递。对于Web API,更常见的是以JSON形式传递数据,这通常需要添加[FromBody]属性。[ApiController]属性为您处理这些问题。
  • 错误代码的问题详细信息——当API中发生错误时,您通常希望返回一组一致的数据。ProblemDetails是一种基于web标准的类型,用作此一致数据。[ApiController]属性将拦截控制器返回的任何错误状态代码(例如,404 Not Found响应),并将其自动转换为ProblemDetails类型。

[ApiController]属性的一个关键功能是使用Problem Details格式在所有控制器中以一致的格式返回错误。3典型的ProblemDetails对象如下所示,其中显示了为无效请求自动生成的ValidationProblemDetails对象示例:

{
  type: "https://tools.ietf.org/html/rfc7231#section-6.5.1" title: "One or more validation errors occurred."
  status: 400
  traceId: "|17a2706d-43736ad54bed2e65." 
  errors: {
    name: ["The name field is required."]
  }
}

[ApiController]约定可以显著减少需要编写的样板代码量。它们还可以确保整个应用程序的一致性。例如,当收到错误请求时,您可以确保所有控制器将返回相同的错误类型ValidationProblemDetails(ProblemDetails的子类型)。

将所有错误转换为ProblemDetails
[ApiController]属性可确保API控制器返回的所有错误响应都转换为ProblemDetails对象,从而使应用程序中的错误响应保持一致。

唯一的问题是你的API控制器并不是唯一可能产生错误的东西。例如,如果收到的URL与控制器中的任何操作都不匹配,我们在第3章中讨论的管道中间件的结尾将生成404 Not Found响应。由于此错误是在API控制器之外生成的,因此不会使用ProblemDetails。类似地,当代码抛出异常时,您也希望将其作为ProblemDetails对象返回,但默认情况下不会发生这种情况。

在第3章中,我描述了几种类型的错误处理中间件,您可以使用它们来处理这些情况,但处理所有边缘情况可能会很复杂。我更喜欢使用社区创建的包Hellang.Middleware.ProblemDetails,它可以为您处理这些问题。您可以在我的博客上阅读如何使用此软件包http://mng.bz/Gx7D.

正如ASP.NET Core中常见的那样,如果您遵循惯例而不是试图与之对抗,那么您将最有效率。然而,如果您不喜欢某些惯例,或者想要自定义它们,您可以很容易地这样做。

通过对Startup.cs文件中AddControllers()方法返回的IMvcBuilder对象调用ConfigureApiBehaviorOptions(),可以自定义应用程序使用的约定。例如,您可以禁用验证失败时的自动400响应,如以下列表所示。

清单9.12 自定义[ApiAttribute]行为

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers()
            .ConfigureApiBehaviorOptions(options =>    //通过提供配置lambda来控制应用哪些约定。
            {
            options.SuppressModelStateInvalidFilter = true;    //这将禁用无效请求的自动400响应。
            })
    }

    // ...
}

自定义ASP.NET Core的每个方面的能力是使其与以前版本的ASP.NET不同的特性之一。ASP.NET Core在将服务添加到应用程序时,使用两种机制之一的依赖注入或通过配置Options对象来配置其内部组件的大部分,如第10章(依赖注入)和第11章(Options对象)所示。

在下一节中,您将学习如何控制Web API控制器返回的数据的格式,无论是JSON、XML还是其他自定义格式。

9.6 从模型生成响应

这将我们带到本章的最后一个主题:格式化响应。现在,API控制器返回JSON很常见,但情况并非总是如此。在本节中,您将了解内容协商以及如何启用其他输出格式(如XML)。您还将了解ASP.NET Core 3.0中JSON的重要变化。

考虑这个场景:您创建了一个WebAPI动作方法,用于返回汽车列表,如下所示。它调用应用程序模型上的一个方法,该方法将数据列表交还给控制器。现在您需要格式化响应并将其返回给调用者。

清单9.13 返回汽车列表的Web API控制器

[ApiController]
public class CarsController : Controller
{
    [HttpGet("api/cars")]    //该操作通过请求GET/api/cars来执行。
    public IEnumerable<string> ListCars()    //包含数据的API模型是一个IEnumerable<string>。
    {
        return new string[] { "Nissan Micra", "Ford Focus" };    //该数据通常从应用程序模型中获取。
    }
}

您在第9.2节中看到,可以直接从操作方法返回数据,在这种情况下,中间件对其进行格式化并将格式化后的数据返回给调用者。但是中间件如何知道使用哪种格式?毕竟,您可以将其序列化为JSON、XML,甚至可以使用简单的ToString()调用。

确定要发送给客户端的数据格式的过程通常称为内容协商(conneg)。在高级别上,客户端发送一个标头,指示它可以理解的内容类型。Accept标头,服务器从中选择一个,格式化响应,并在响应中发送一个内容类型标头,指示其选择的类型。

Accept和内容类型标题
Accept标头由客户端作为请求的一部分发送,以指示客户端可以处理的内容类型。它由多个MIME类型组成,a具有可选权重(从0到1),以指示首选哪种类型。例如,application/json、text/xml;q=0.9,文本/普通;q=0.6标头表示客户端可以接受JSON、XML和纯文本,权重分别为1.0、0.9和0.6。JSON的权重为1.0,因为没有提供明确的权重。在内容协商期间,可以使用权重为双方选择最佳代表。
内容类型标头描述请求或响应中发送的数据。它包含数据的MIME类型以及可选的字符编码。例如,application/json;charset=utf-8头将指示请求或响应的主体是JSON,使用utf-8编码。

你不会被迫只发送客户端期望的内容类型,在某些情况下,你甚至可能无法处理它请求的类型。如果请求规定只能接受Excel电子表格,该怎么办?即使这是请求包含的唯一内容类型,您也不太可能支持这一点。

当您从操作方法返回API模型时,无论是直接(如清单9.13所示)还是通过OkResult或其他StatusCodeResult,ASP.NET Core都会返回一些东西。如果它不能接受Accept头中规定的任何类型,则默认情况下将返回JSON。图9.10显示,即使请求了XML,API控制器也将响应格式化为JSON。

注意:在ASP.NET的早期版本中,使用PascalCase将对象序列化为JSON,其中属性以大写字母开头。在ASP.NET Core中,默认情况下使用camelCase序列化对象,其中属性以小写字母开头。

图9.10 尽管请求是用文本/xml的Accept头发出的,但返回的响应是JSON,因为服务器没有配置为返回xml。

无论数据是如何发送的,它都由IOutputFormatter实现序列化。ASP.NET Core提供了数量有限的现成输出格式器,但一如既往,添加其他格式器或更改默认工作方式很容易。

9.6.1 自定义默认格式化程序:添加XML支持

与大多数ASP.NET Core一样,WebAPI格式器是完全可定制的。默认情况下,只配置纯文本(text/plain)、HTML(text/HTML)和JSON(application/JSON)的格式化程序。考虑到SPA和移动应用程序的常见使用情况,这将为您带来很大的帮助。但有时您需要能够以不同的格式返回数据,例如XML。

Newtonsoft.Json与System.Text.Json
Newtonsoft.Json,也称为Json.NET,长期以来一直是在.NET中使用Json的规范方法。它与阳光下的每个版本的.NET都兼容,几乎所有.NET开发人员都会熟悉它。它的影响力如此之大,甚至ASP.NET Core都依赖它!
随着ASP.NET Core 3.0中引入了一个新的库System.Text.Json,这一切都发生了变化,该库专注于性能。在ASP.NET Core 3.0及以后版本中,ASP.NET Core默认使用System.Text.Json而不是Newtonsoft.Json。
两个库之间的主要区别是System.Text.Json对其Json非常挑剔。它通常只反序列化符合预期的JSON。例如,System.Text.Json不会反序列化在字符串周围使用单引号的Json;你必须使用双引号。
如果您正在创建一个新的应用程序,这通常不是问题,您可以快速学会生成正确的JSON。但是,如果您正在从ASP.NET Core 2.0迁移应用程序或从第三方接收JSON,这些限制可能是真正的绊脚石。
幸运的是,您可以轻松切换回Newtonsoft.Json库。将Microsoft.ASP.NET Core.Mvc.NewtonsoftJson包安装到项目中,并将Startup.cs中的AddControllers()方法更新为以下内容:
services.AddControllers()
.AddNewtonsoftJson();
这将使ASP.NET Core的格式化程序在幕后使用Newtonsoft.Json,而不是System.Text.Json。有关库之间差异的更多详细信息,请参阅Microsoft的文章“How to migration from Newtonsoft.Json to System.Text.Json”:http://mng.bz/0mRJ.有关何时切换到Newtonsoft.Json的更多建议,请参阅Micro-soft的“ASP.NET Core Web API中的格式响应数据”文档中的“添加Newtonsoft.Json-based Json格式支持”一节:http://mng
.bz/zx11。

通过添加输出格式化程序,可以将XML输出添加到应用程序中。通过自定义从AddControllers()返回的IMvcBuilder对象,可以在Startup.cs中配置应用程序的格式化程序。要添加XML输出格式化程序,请使用以下命令:

services.AddControllers().AddXmlSerializerFormatters();

通过这个简单的更改,您的API控制器现在可以将响应格式化为XML。在启用XML支持的情况下运行图9.10所示的相同请求意味着应用程序将尊重text/xmlaccept标头。格式化程序根据请求将字符串数组序列化为XML,而不是默认为JSON,如图9.11所示。

图9.11 添加了XML输出格式器后,text/xmlAccept头被遵守,响应可以被序列化为XML。

这是一个内容协商的示例,其中客户端指定了它可以处理的格式,服务器根据它可以生成的内容选择其中一种格式。这种方法是HTTP协议的一部分,但在ASP.NET Core中依赖它时需要注意一些怪癖。你不会经常碰到这些,但如果你没有意识到它们击中你,它们可能会让你挠头好几个小时!

9.6.2 通过内容协商选择响应格式

内容协商是指客户机使用accept标头表示可以接受哪些类型的数据,服务器选择其可以处理的最佳数据。一般来说,这正如您所希望的那样:服务器使用客户端能够理解的类型来格式化数据。

ASP.NET Core实现有一些特殊情况值得牢记:

  • 默认情况下,ASP.NET Core只返回application/json、text/plain和text/html MIME类型。您可以添加额外的IOutputFormatters以使其他类型可用,正如您在上一节text/xml中看到的那样。
  • 默认情况下,如果作为API模型返回null,无论是从动作方法还是通过在StatusCodeResult中传递null,中间件都将返回204无内容响应。
  • 当您返回字符串作为API模型时,如果没有设置Accept头,中间件将将响应格式化为text/plain。
  • 当您使用任何其他类作为API模型,并且没有Accept标头或没有请求任何支持的格式时,将使用第一个可以生成响应的格式化程序(默认情况下通常为JSON)。
  • 如果中间件检测到请求可能来自浏览器(accept头包含*/*),那么它将不使用conneg。相反,它将使用默认的for matter(通常是JSON)来格式化响应,就像没有提供Accept头一样。

这些默认值相对来说是合理的,但如果你没有意识到它们,它们肯定会伤害你。特别是最后一点,即对来自浏览器的请求的响应实际上总是格式化为JSON,这让我在尝试本地测试XML请求时感到非常困惑!

正如您现在所期望的,所有这些规则都是可配置的;如果应用程序中的默认行为不符合您的要求,您可以很容易地更改它。例如,下面的列表取自Startup.cs,显示了如何强制中间件遵守浏览器的Accept标头,并删除字符串的text/plain For matter。

清单9.14 定制MVC以尊重Web API中浏览器的Accept头

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options =>    //AddControllers有一个采用lambda函数的重载。
    {
        //默认情况下为False,还可以设置许多其他属性。
        options.RespectBrowserAcceptHeader = true; 
        //删除将字符串格式化为文本/纯文本的输出格式化程序
        options.OutputFormatters.RemoveType<StringOutputFormatter>();
    });
}

在大多数情况下,无论您是在构建SPA还是移动应用程序,conneg都应该能为您提供开箱即用的服务。在某些情况下,你可能会发现你需要绕过通常的连接机制来实现特定的动作方法,有很多方法可以实现这一点,但我不会在本书中介绍它们,因为我发现我很少需要使用它们。有关详细信息,请参阅Microsoft的“在ASP.NET Core Web API中格式化响应数据”文档:http://mng.bz/zx11.

这就让我们结束了关于Web API的本章,以及本书的第1部分!这是ASP.NET Core的一次相当激烈的巡演,重点关注RazorPages和MVC模式。至此,您已具备使用Razor Pages构建简单应用程序或为SPA或移动应用程序创建Web API服务器所需的所有知识。

在第2部分中,您将进入一些有趣的主题:您将了解构建完整应用程序所需的详细信息,如将用户添加到应用程序、将数据保存到数据库以及部署应用程序。

在第10章中,我们将研究ASP.NET Core中的依赖注入,以及它如何帮助创建松散耦合的应用程序。您将学习如何向容器注册ASP.NET Core框架服务,并将自己的类设置为依赖注入服务。最后,您将看到如何用第三方替代品替换内置容器。

总结

  • Web API公开了许多可用于访问或更改服务器上数据的方法或端点。它通常由移动或客户端web应用程序使用HTTP访问。
  • Web API操作方法可以直接返回数据,也可以使用ActionResult<T>以生成任意响应。
  • 如果从操作方法返回多个类型的结果,则方法签名必须返回ActionResult<T>。
  • Web API遵循与传统Web应用程序相同的MVC设计模式。生成视图的最终响应的格式化程序。
  • Web API操作返回的数据称为API模型。它包含中间件将序列化并发送回客户端的数据。这与视图模型和PageModel不同,后者包含关于如何生成响应的数据和元数据。
  • 通过将RouteAttributes应用于操作方法,Web API与路由模板相关联。这些使您能够完全控制组成应用程序API的URL。
  • 应用于控制器的路由属性与动作方法的属性相结合,形成最终模板。这些还与继承基类上的属性相结合。您可以使用继承的属性来减少属性中的重复量,例如在路由上使用公共前缀的位置。
  • 默认情况下,当您使用属性路由时,控制器和操作名称与URL或路由模板无关。但是,您可以在路由模板中使用“[control]”和“[action]”标记来减少重复。它们将被替换为当前控制器和操作名称。
  • 当两个操作对应于同一URL时,[HttpPost]和[HttpGet]属性允许您根据请求的HTTP谓词在操作之间进行选择。这是RESTful应用程序中的常见模式。
  • [ApiController]属性将几种常见约定应用于控制器。用属性修饰的控制器将自动绑定到请求的主体,而不是使用表单值,将自动为无效请求生成400 Bad request响应,并将返回状态代码错误的ProblemDetails对象。这可以大大减少必须编写的样板代码量。
  • 通过使用ConfigureApi-BehaviorOptions()方法并提供配置lambda,可以控制要应用哪些约定。例如,如果您需要使您的API适合现有规范,这将非常有用。
  • 默认情况下,ASP.NET Core将从Web API控制器返回的API模型格式化为JSON。几乎每个平台都可以处理JSON,使您的API具有高度的互操作性。
  • 与以前版本的ASP.NET不同,JSON数据是使用camelCase而不是PascalCase序列化的。如果从ASP.NET迁移到ASP.NET Core时出现错误或缺少值,则应考虑此更改。
  • ASP.NET Core 3.0以后的版本使用System.Text.Json,这是一个用于Json序列化和反序列化的严格、高性能库。通过对services.AddControllers()的返回值调用AddNewtonsoftJson(),可以用通用的Newtonsoft.Json格式器替换此序列化程序。
  • 当客户端指定它可以处理的数据类型并且服务器基于此选择返回格式时,就会发生内容协商。它允许多个客户端调用您的API并以他们可以理解的格式接收数据。
  • 默认情况下,ASP.NET Core可以返回text/plain、text/html和application/json,但如果需要支持其他格式,可以添加其他格式器。
  • 可以通过对Startup类中services.AddControllers()的返回值调用AddXmlSerializerFormatters()来添加XML格式器。它们允许您将响应格式化为XML,并在请求主体中接收XML。
  • 当Accept标头包含*/*时,不使用内容协商,例如在大多数浏览器中。相反,您的应用程序将使用默认格式器JSON。在Startup.cs中添加MVC控制器服务时,可以通过修改RecoverBrowserAcceptHeader选项来禁用此选项。

 

posted on 2023-01-15 22:39  生活的倒影  阅读(122)  评论(0编辑  收藏  举报