ASP.NET Web API 2 中的格式化程序与内容协商





5.00/5 (13投票s)
本文将重点介绍 ASP.NET Web API 中格式化程序和内容协商的实践方面
- 下载文章的 PDF 版本 - 2 MB
- 下载 ProductCatalog - 8.2 MB
- 下载 ProductCatalogBasicSolution - 7.7 MB
- 下载 DBScripts - 1.8 KB
- 下载完整源代码
- 下载基础解决方案代码
目录
引言
正如标题所示,本文将重点介绍 ASP.NET Web API 中格式化程序和内容协商的实践方面。本文将解释:什么是内容协商?为什么它必不可少?以及我们如何在 ASP.NET Web API 中实现并使其工作?本文将更多地关注 Web API 中内容协商的实现部分。文章的第一部分将关注格式化程序,其中描述了如何在 Web API 中支持 XML 或 JSON 格式以及如何格式化 API 的结果。我们将以一个示例 Web API 项目为例,该项目使用实体框架(Entity Framework)来处理数据库的简单 CRUD 操作。我们不会深入探讨底层项目架构及其标准架构方式,而是将重点放在 Web API 项目中的内容协商部分。要创建企业级的标准 Web API 应用程序,您可以参考此系列文章。
内容协商
REST 代表表述性状态转移(Representational State Transfer),而表述的一个重要方面是其形式。表述的形式意味着,当 REST 服务被调用时,响应中应该包含什么,以及响应应以何种形式呈现。REST 的最大优点是它可以用于多个平台,无论是 .NET 客户端、处理 JSON 对象的 HTML 客户端还是任何移动设备平台。REST 服务的开发方式应能满足客户端在请求格式方面的需求。响应的格式可能仍然包含完整的业务实体和信息数据,但可能在返回的媒体类型形式(如 XML 或 JSON 或任何自定义媒体类型)、字符集、客户端用于呈现数据的编码以及语言方面有所不同。例如,假设一个服务旨在返回任何实体的列表,那么对象需要以客户端期望的形式进行序列化。如果 REST 服务仅返回 XML,那么客户端(其控件已与 JSON 对象绑定)将无法使用返回的数据,或者可能需要编写一些其他复杂的实现,首先将数据转换为所需的 JSON 格式,然后再绑定控件,这会导致额外的开销(或可能成为瓶颈)。服务器应能够根据客户端请求,发送其可用的最佳表述。ASP.NET Web API 提供了创建健壮的 REST 服务的能力,该服务能够处理客户端的请求、理解它并相应地提供数据。Web API 在其底层架构中引入了一个名为“内容协商”的层,该层具有标准的 HTTP 规则,用于请求所需格式的数据。
以下引自此链接的陈述,对内容协商及其主要机制进行了精准的解释。
“HTTP 规范 (RFC 2616) 将内容协商定义为‘当有多个可用表述时,为给定响应选择最佳表述的过程’。在 HTTP 中,内容协商的主要机制是以下请求标头:
- Accept: 响应可接受的媒体类型,例如“application/json”、“application/xml”,或自定义媒体类型,如“application/vnd.example+xml”
- Accept-Charset: 可接受的字符集,例如 UTF-8 或 ISO 8859-1
- Accept-Encoding: 可接受的内容编码,例如 gzip
- Accept-Language: 首选的自然语言,例如“en-us”
服务器也可以查看 HTTP 请求的其他部分。例如,如果请求包含 X-Requested-With 标头,表示这是一个 AJAX 请求,那么在没有 Accept 标头的情况下,服务器可能会默认使用 JSON。”
我们将深入探讨内容协商,以及它如何与 Web API 一起工作。首先,我们将建立一个小型的解决方案,该方案包含对数据库的 CRUD 操作,并将服务公开为 ASP.NET Web API REST 服务。我们将进行一个基础的 REST 服务公开实现,然后转向内容协商部分,而不会过多关注应用程序的架构。
应用程序设置
我们不会详细介绍如何一步步设置解决方案,而是使用一个我将要解释的现有解决方案。我尝试创建了一个提供产品目录服务的示例应用程序。完整的源代码(基础设置+最终解决方案)和数据库脚本均可随本文下载。以下是此解决方案中使用的技术栈:
- 数据库: SQL Server Local DB (可以使用任何数据库)
- IDE: Visual Studio 2015 Enterprise (可以使用任何支持 .NET Framework 4.5 或更高版本的 Visual Studio)
- ORM: Entity Framework 6
- Web API: Web API 2
- .NET Framework: 6.0 (可以使用 4.5 或更高版本)
所使用的数据库非常简单,只包含两个表,名为 Product
和 Category
,产品属于某个类别。以下是数据库的实体关系图。该应用程序使用 Local DB 作为数据库。我特意为本教程使用它,因为它是一个小型应用程序。在实际场景中,您可以根据需求或要求使用任何数据库。
解决方案分为三层,如下所示
ProductCatalog.DataModel
项目负责数据库交互。这是一个简单的 C# 类库,作为应用程序的数据模型,并直接与数据库通信。它包含了借助 Entity Framework 生成的实体数据模型。除此之外,为了进行事务处理,该项目定义了一个名为 IProductCatalogRepository
的仓储协定,由一个名为 ProductCatalogRepository
的具体类实现。该仓储包含了对产品和类别的数据库交互所需的所有 CRUD 操作。ProductActionResult
类负责返回事务结果,无论是状态还是错误情况下的异常。ProductActionStatus
类定义了一组枚举,用于显示要返回的响应状态。Factory 文件夹包含两个工厂,名为 CategoryFactory
和 ProductFactory
,负责将数据库实体转换为用于对象传输的自定义实体,反之亦然。
IProductCatalogRepository
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ProductCatalog.DataModel
{
public interface IProductCatalogRepository
{
ProductActionResult<Product> DeleteProduct(int id);
ProductActionResult<ProductCatalog.DataModel.Category> DeleteCategory(int id);
ProductCatalog.DataModel.Product GetProduct(int id);
ProductCatalog.DataModel.Category GetCategory(int id);
System.Linq.IQueryable<ProductCatalog.DataModel.Category> GetCategories();
System.Linq.IQueryable<ProductCatalog.DataModel.Product> GetProducts();
System.Linq.IQueryable<ProductCatalog.DataModel.Product> GetProducts(int CategoryId);
ProductActionResult<ProductCatalog.DataModel.Product>
InsertProduct(ProductCatalog.DataModel.Product product);
ProductActionResult<ProductCatalog.DataModel.Category>
InsertCategory(ProductCatalog.DataModel.Category category);
ProductActionResult<ProductCatalog.DataModel.Product>
UpdateProduct(ProductCatalog.DataModel.Product product);
ProductActionResult<ProductCatalog.DataModel.Category>
UpdateCategory(ProductCatalog.DataModel.Category category);
}
}
ProductCatalog.Entities
项目是传输对象或自定义实体的项目。该项目包含用于在 API 和数据模型项目之间来回传递对象的 Product
和 Category
实体的 POCO(Plain Old CLR Object)。这个项目同样是一个简单的 C# 类库,包含两个 POCO 类。数据模型项目添加了对此实体项目的引用。
应用程序的主要部分位于名为 ProductCatalog.API
的 Web API 项目中。我通过添加 Visual Studio 2015 中 ASP.NET Web 应用程序类别下可用的 Web API 项目类型创建了此项目。
除了向项目中添加一个 ProductsController
之外,项目的默认结构保持不变。ProductsController
仅包含一个用于从数据库获取所有 Products
的 Get
方法。
ProductsController
using ProductCatalog.DataModel;
using ProductCatalog.DataModel.Factory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
namespace ProductCatalog.API.Controllers
{
public class ProductsController : ApiController
{
IProductCatalogRepository _productCatalogRepository;
ProductFactory _productFactory = new ProductFactory();
public ProductsController()
{
_productCatalogRepository =
new ProductCatalogRepository(new ProductCatalogEntities());
}
public ProductsController(IProductCatalogRepository productCatalogRepository)
{
_productCatalogRepository = productCatalogRepository;
}
public IHttpActionResult Get()
{
try
{
var products = _productCatalogRepository.GetProducts();
return Ok(products.ToList().Select(p => _productFactory.CreateProduct(p)));
}
catch (Exception)
{
return InternalServerError();
}
}
}
}
Web API 项目添加了对数据模型和自定义实体项目的引用。ProductsController
通过实例化 ProductCatalogEntities
(即数据模型项目中使用的数据库上下文)来创建仓储实例。该仓储实例用于在 Get
方法中获取记录,以获取所有产品记录,并进一步通过将数据库实体转换为自定义实体来发送 200(即 OK)响应。您可以通过使用 IOC 和依赖注入以更复杂和可扩展的方式开发此结构,并可进一步添加更多层以使其更加松散耦合和安全。请参考此系列文章,了解如何在 Web API 中开发此类架构。这是我们的基础解决方案,我们将以此为基础学习 Web API 中的内容协商。请下载附件中的源代码。
现在将 ProductCatalog.API
项目设为启动项目并运行应用程序。由于我们没有定义任何路由,应用程序将采用 API 项目 App_Start 文件夹下的 WebAPI.Config 文件中定义的默认路由。当应用程序运行时,您将看到一个主页,因为我们的默认路由将应用程序重定向到 Home 控制器。只需在 URL 中添加“api/products”,您就会看到以下显示所有产品记录的屏幕。
结果以纯 XML 格式显示,但如果客户端需要 JSON 格式的结果该怎么办?客户端将如何与服务器通信以发送所需格式的结果,服务器又将如何理解和处理来自不同客户端的不同请求?内容协商是所有这些问题的答案。应用程序的临时部分已经完成,现在让我们来探讨 Web API 中的格式化程序。
Web API 中的格式化程序
那么为什么这个浏览器会返回 XML 呢?这与浏览器如何创建请求消息有关。当我们浏览一个支持 fetch API 的 URI 时,浏览器就是 API 的客户端。客户端创建一个请求消息,REST 服务以一个响应消息进行回应。在这里,浏览器创建了一个请求。这必然意味着浏览器以某种方式请求了 XML。
为了更仔细地观察,我们来使用 Fiddler。Fiddler 是一款免费的网页调试工具,用于检查服务及其请求和响应。让我们再次调用该服务,但这次使用 Fiddler,看看浏览器请求了什么。启动 Fiddler 并在浏览器中刷新 URL。您可以看到每条消息(无论是请求还是响应)都有一个标头和一个主体。
当您查看请求标头的集合时,会发现浏览器发送了一个带有 accept header.Accept
标头的请求,该标头指定了客户端需要的响应格式。还有一个互联网媒体类型(IME),即 text/html,表明响应应为文本 html 格式。它还指定如果前一个 IME 不可用,则应发送 application/xhtml+xml。让我们用 Fiddler 来改变这一点。让我们使用 Fiddler 配置我们的请求,以获取 JSON 格式的数据。转到 Fiddler 的 Composer 选项卡,并调用相同的 URL。这次,我们将声明我们接受 JSON。
现在看一下响应,我们看到返回的是 JSON。
我们还可以查看原始响应,这样就更加明显了。
现在,如果我们的 API 在浏览器作为消费者时能自动返回 JSON 而不是 XML,那就好了。
我们可以做到这一点。有两种方法可以实现,它们都要求我们操作 API 支持的格式化程序所支持的媒体类型。我们回到 Web API 配置。默认情况下,Web API 同时支持 XML 格式化和 JSON 格式化。我们现在要做的是确保当消费者请求 text/html 时,JSON 格式化程序被调用,因为这是浏览器请求的最高优先级的 IME,正如我们所见。为此,我们将此媒体类型添加到 JSON 格式化程序的支持的媒体类型集合中。我们可以通过 config.Formatters
访问这些格式化程序。转到 WebAPI.config 并在定义的默认路由下方添加 JSON 格式化程序新的支持类型。
config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new
System.Net.Http.Headers.MediaTypeHeaderValue("text/html"));
在浏览器中再次运行该应用程序。
这一次,结果以 JSON 格式返回。为什么呢?嗯,我们刚才所做的意味着,当一个请求将 text/html 声明为 accept 标头时,JSON 格式化程序将被调用来处理该请求,因为它现在包含了该媒体类型。然而,当我们请求 application/xml 时,这仍然是 XML 格式化程序支持的媒体类型,所以我们仍然会得到 XML。让我们用 Fiddler 试试。我们将声明我们接受 application/xml,然后看结果。
我们得到了 XML。
还有另一种方法可以做到这一点。除了将 text/html 添加为 JSON 格式化程序支持的媒体类型外,我们还可以简单地从 XML 格式化程序中移除所有支持的媒体类型。
//config.Formatters.JsonFormatter.SupportedMediaTypes.Add
//(new System.Net.Http.Headers.MediaTypeHeaderValue("text/html"));
config.Formatters.XmlFormatter.SupportedMediaTypes.Clear();
不同之处在于,我们的 API 现在不再支持返回 XML。没有更多支持的媒体类型了。但最好的处理方式取决于几件事。如果你想继续支持 XML,这不是一个好方法。另一方面,如果你不想,这就是要走的路。不支持 XML 是许多 API 开发者的选择。它消除了一个可能的错误层级。例如,如果你想支持 XML,你最好确保你的 XML 格式化程序没有 bug。无论如何,两种方法都是可行的,这取决于你的需求。但我们能做的还有更多。我们刚才在浏览器中看到的结果,嗯,它们的格式并不怎么好看,对吧?我想应用一些格式化,以便通过浏览器更容易地发现 API。我们可以通过修改 JSON 格式化程序的序列化器设置来做到这一点。那么让我们来看看。我们可以通过 `config.Formatters.JsonFormatter.SerializerSettings` 访问序列化器设置。序列化器设置有两个我们想用的有趣的属性。首先是格式化属性。我们可以声明在格式化时要使用缩进。我想做的第二件事是确保我们得到的结果是 `CamelCased`(驼峰命名法)。
config.Formatters.JsonFormatter.SerializerSettings.Formatting =
Newtonsoft.Json.Formatting.Indented;
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver =
new CamelCasePropertyNamesContractResolver();
这需要通过契约解析器来实现。我们把它设置为一个新的 `CamelCased` 属性名契约解析器。好了,让我们试一试。
这样看起来已经更容易发现了。
Web API 中的内容协商
我们已经设置好了解决方案,并且对 accept 标头和格式化程序的工作方式有了一定的了解。现在,让我们更深入地了解内容协商。内容协商的工作方式与我们学习格式化程序时看到的方式非常相似。客户端在标头集合中用期望的响应格式发出请求,服务器则响应相同的内容。内容协商管道与已注册的媒体类型格式化程序列表对齐。当一个特定的请求进入管道时,框架会尝试将第一个可用的类型与请求匹配,然后发送响应。HttpConfiguration
提供了 IContentNegotiator
服务,管道还会从 HttpConfiguration.Formatters
中获取可用格式化程序的列表。媒体类型映射是管道为匹配执行的第一个任务。除了请求标头映射,Web API 还支持其他映射,如 QueryString
、URIPathExtensions
和 MediaRange
映射。这并不局限于这些特定的映射,您也可以创建自己的自定义映射来实现该功能。
Accept 标头
Accept 标头是匹配媒体类型的主要标准之一。客户端可以在请求标头中发送带有 Accept 属性的请求,并定义所需的媒体类型,如下所示
Accept: application/xml 或 Accept: application/json
内容协商器会处理该请求并相应地作出响应。
Content Type
请求也可以包含请求体的内容类型。如果 Accept 标头不存在,内容协商器会查找请求体的内容类型并相应地提供响应。如果 Accept 标头和内容类型都存在,则优先考虑 Accept 标头。
Content-Type: application/json.
格式化器
如果上述提到的标准(如 `MediaTypeMappings`、内容类型和 Accept 标头)都不存在,格式化程序就会发挥作用。在这种情况下,内容协商管道会检查 `HttpConfiguration.Formatters` 集合中的格式化程序,并按照它们存储的优先顺序选择一个,然后相应地返回响应。
Accept Language 标头
它与 Accept 标头非常相似,但其目的是从服务请求语言类型。Accept 标头用于请求媒体格式。Accept Language 可以指定客户端请求的语言类型。API 可能拥有像 html 这样的资源,可以通过一种表述来共享,并且在支持多语言的情况下,API 可能包含针对不同语言的各种版本的 html,或者消息也可能是多语言的。在这种情况下,客户端可以根据其文化或语言请求消息或 html 文件。
Accept-Language: en
客户端也可以按优先顺序请求语言,如下所示
Accept-Language: sv, en-us; q0.8,da;q=0.7
质量因子
在前面的 Accept Language 示例中,我们看到了字符串 `q=0.7`。`q` 就是内容协商中所谓的质量因子。质量因子的范围从 0.1 到 1.0。质量因子的值越大,该类型的优先级就越高。下面是一个带有质量因子的 Accept 标头的示例。
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.7
Accept Encoding 标头
此标头指定客户端接受的编码,即内容编码,主要是加密。Accept-Encoding 标头的一些示例有 gzip、compress、deflate、identity 等。客户端只需在请求中附带 Accept-Encoding: <encoding type> 标头,即可请求所需的编码类型。
示例:Accept-Encoding : gzip
Accept Charset 标头
此标头定义了客户端接受何种类型的字符编码。它主要指定了可接受的字符集类型,如 UTF-8 或 ISO 8859-1。
示例:Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.8
Web API 中的内容协商实现
让我们来看看如何在 `Web API.DefaultContentNegotiator` 中实现内容协商。这是在 Web API 中负责内容协商的类的名称。我们在上一节讨论了 `IContentNegotiator` 服务。内容协商管道会调用这个 `IContentNegotiator.Negotiate` 服务方法,并传入参数,如需要序列化的对象类型、媒体格式化程序集合以及相关的 Http 请求。此方法的返回类型是 `ContentNegotiationResult`,它指定了要使用哪个格式化程序以及响应的返回媒体类型。例如,在我们的案例中,我们可以在我们的 Web API 操作中以下列方式实现内容协商。
public HttpResponseMessage Get()
{
try
{
var products = _productCatalogRepository.GetProducts();
var customProducts= products.ToList().Select(p => _productFactory.CreateProduct(p));
IContentNegotiator negotiator = this.Configuration.Services.GetContentNegotiator();
ContentNegotiationResult result = negotiator.Negotiate(
typeof(List<Product>), this.Request, this.Configuration.Formatters);
if (result == null)
{
var response = new HttpResponseMessage(HttpStatusCode.NotAcceptable);
throw new HttpResponseException(response);
}
return new HttpResponseMessage()
{
Content = new ObjectContent<List<Entities.Product>>(
customProducts.ToList(), // type of object to be serialized
result.Formatter, // The media formatter
result.MediaType.MediaType // The MIME type
)
};
}
catch (Exception)
{
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
}
在获取产品列表的 `Action` 代码中,我做了一些更改以支持内容协商,如上述代码所示。正如之前解释的,我们获取 `IContentNegotiator` 服务并将其存储在 negotiator 变量中。然后调用 `Negotiate` 方法,传入要协商的对象类型、Http 请求和格式化程序列表。接着向客户端发送一个 `HttpResponseMessage`。我尝试在代码中捕获最佳匹配的媒体类型和最佳匹配的格式化程序,然后从浏览器发送请求,得到了以下结果。
例如,application/xml,因为请求来自带有这些 accept 标头的浏览器。我用 Fiddler 发出了相同的请求,将媒体类型更改为 application/json,并得到了以下最佳媒体类型。
即,application/json。
Web API 中的自定义内容协商实现
您还可以在 Web API 中通过覆盖默认的内容协商器来编写自己的自定义内容协商器。只需创建一个继承自 `DefaultContentNegotiator` 的类,并用您自己的方式重写方法。之后,您只需要在 WebAPI.config 文件中设置您的全局配置,如下所示:
GlobalConfiguration.Configuration.Services.Replace(typeof(IContentNegotiator),
new CustomContentNegotiator());
让我们看看实现。添加一个名为 `CustomContentNegotiator` 的类,并让它派生自 `DefaultContentNegotiator`。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Web;
namespace ProductCatalog.API
{
public class CustomContentNegotiator : DefaultContentNegotiator
{
}
}
现在在该类中添加重写的 `Negotiate` 方法,以支持自定义内容协商器。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Web;
namespace ProductCatalog.API
{
public class CustomContentNegotiator : DefaultContentNegotiator
{
public override ContentNegotiationResult Negotiate
(Type type, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters)
{
var result = new ContentNegotiationResult
(new JsonMediaTypeFormatter(), new MediaTypeHeaderValue("application/json"));
return result;
}
}
}
在上面的代码中,我们使用自定义逻辑来添加 application/json 格式的媒体格式化程序。现在在 WebAPI.config 中,添加以下配置。
GlobalConfiguration.Configuration.Services.Replace(typeof(IContentNegotiator),
new CustomContentNegotiator());
现在运行应用程序,你会在浏览器中得到 JSON 结果。
即使您尝试从 Fiddler 运行服务,并将 accept 标头设置为 application/xml,您仍然会得到 JSON 结果。这表明我们的自定义格式化程序正在工作。
结论
本文解释了 ASP.NET Web API 中的内容协商及其实际实现。这是一个非常巧妙且对于 REST 开发非常重要的话题。内容协商有助于使服务变得更加健壮和可扩展。它使服务变得更好,并规范了实现方式。
参考文献
以下是一些关于 Web API 中内容协商的优秀延伸阅读材料。
历史
- 2016年7月5日:初始版本