领先技术

内容协商和面向 ASP.NET MVC 开发人员的 Web API

Dino Esposito

Dino Esposito对于 ASP.NET MVC 而言,我最喜欢的一个功能就是公开可以轻松从 HTTP 客户端(包括基于 jQuery 的页面、移动应用程序和普通 C# 后端)调用的方法的外层。很长时间以来,此服务层的构建都是在 Windows Communication Foundation (WCF) 服务中进行。人们曾经尝试专门针对 HTTP 构建 WCF,如引入 webHttpBinding 机制以及框架,如现在已经不使用的 REST 初学者工具包。但是,这些方法都不能真正地为开发人员消除障碍,如声名狼藉的 WCF 过度配置、属性的过度使用以及未专门为了方便测试而设计的结构。然后就出现了 Web API,这是一种全新的框架,其特点是轻量、可测试、独立于宿主环境(如 IIS)并以 HTTP 为重点。

但是,Web API 有一个编程界面,在我看来与 ASP.NET MVC 非常相似。但我这样说并不是一种负面的评价,因为 ASP.NET MVC 的编程界面非常简洁,设计得很好。Web API 实际上是从一个外层类似于 WCF 的编程模型着手构造的,然后它渐渐变得跟 ASP.NET MVC 很相像了。

在本文中,我会从一个普通 ASP.NET MVC 开发人员的角度来谈谈我对 Web API 的看法,我将重点介绍 Web API 相对于普通 ASP.NET MVC 的一个额外功能:内容协商。

Web API 概览

Web API 是您可用来创建可以处理 HTTP 请求的类的类库的一个框架。这样所得的库以及一些初始配置设置可托管在运行时环境中,由调用者通过 HTTP 使用。控制器类上的公共方法就成为 HTTP 端点。可配置的路由规则可帮助定义用于访问特定方法的 URL 的形式。但是,除了路由之外,定义 Web API 中 URL 处理的默认形式的大部分规则都是一种约定,而不是配置。

如果您是一位 ASP.NET MVC 开发人员,这会儿您可能停下来不再阅读,您不知道有什么理由要去使用一个全新的框架,它看起来只不过是重复了您在 ASP.NET MVC 中“已经具有”的控制器的概念。

简单来说,是的,您可能并不需要在 ASP.NET MVC 中使用 Web API,因为您通过普通的控制器就几乎可以实现相同的功能。举例来说,您可以轻松返回 JSON 或 XML 字符串格式的数据。您可以轻松返回二进制数据或纯文本。您可以按照自己的喜好定制 URL 模板。

相同的控制器类可用于 JSON 数据或 HTML 视图,您还可以轻松将返回 HTML 的控制器与只返回数据的控制器分离。实际上,有一种常见的做法是,在您全是应该返回纯数据的端点的项目中使用 ApiController 类。例如:

 

public class ApiController : Controller {
public ActionResult Customers()
{
  var data = _repository.GetAllCustomers();
  return Json(data, JsonRequestBehavior.AllowGet);  }
  …
}

Web API 使用了 ASP.NET MVC 体系结构的最佳方面,并在两个重要方面改进了它。首先,它引入了一个称为“内容协商”的全新逻辑层,其中有一组标准的规则,可用于以给定格式(如 JSON、XML 或其他格式)请求数据。第二,Web API 不依赖于 ASP.NET 和 IIS 上的一切,更具体地说,它不依赖于 system.web.dll 库。当然,它可以托管在 IIS 下的 ASP.NET 应用程序中。虽然这可能仍然是最常见的情形,但 Web API 库可以托管在任何其他提供临时宿主环境的应用程序中,如 Windows 服务、Windows Presentation Foundation (WPF) 应用程序或控制台应用程序。

同时,如果您是 ASP.NET MVC 开发专家,则您一定熟悉控制器的 Web API 概念,以及模型绑定、路由和操作筛选器。

Web 窗体开发人员为何喜爱 Web API

如果您是一位 ASP.NET MVC 开发人员,您一开始可能会对 Web API 的好处有些疑惑,因为其编程模型看起来几乎跟 ASP.NET MVC 一样。但是,如果您是一位 Web 窗体开发人员,您就不会搞混淆了。借助 Web API,要在 Web 窗体应用程序中公开 HTTP 端点简直是小儿科。您只需要添加一个或多个这样的类就行了:

public class ValuesController : ApiController
{
  public IEnumerable<string> Get()
  {
    return new string[] { "value1", "value2" };
  }
  public string Get(int id)
  {
    return "value";
  }
}

请注意,您向 ASP.NET MVC 应用程序中添加 Web API 控制器时也可使用这段代码。您还必须指定路由。下面是您想在应用程序启动时运行的一些代码:

RouteTable.Routes.MapHttpRoute(
  name: "DefaultApi",
  routeTemplate: "api/{controller}/{id}",
  defaults: new { id = System.Web.Http.RouteParameter.Optional });

除非另有 NonAction 属性特别注明,否则在该类上的所有与默认命名和路由约定匹配的公共方法都是可通过 HTTP 调用的公共端点。您不需要生成的代理类、web.config 引用或特别的代码,就可以从任何客户端调用它们。

Web API 中的路由约定规定 URL 以 /api 开头并后跟控制器名称。请注意,没有明确表示操作名称。操作由请求类型(GET、PUT、POST 或 DELETE)确定。按照约定,以 Get、Put、Post 或 Delete 开头的方法名称要映射到相应的操作。例如,针对任何对类似 /api/task 这样的 URL 的 GET 请求,将调用 TaskController 上的 GetTasks 方法。

尽管从表面上看来,Web API 与 ASP.NET MVC 的行为和类名称很相似,但 Web API 在一组完全独立的程序集中存在,并使用完全不同的一组类型:System.Net.Http 是它的主要程序集。

Web API 内容协商的内部意义

“内容协商”通常用于描述一个过程,即检查传入 HTTP 请求的结构,以查明客户端希望接收到的响应的格式。但是从技术上来说,内容协商是客户端和服务器确定要在其交互中使用的最佳表示格式的过程。检查请求一般来说是指查看一组 HTTP 头,如 Accept 和 Content-Type。Content-Type 在服务器上用于处理 POST 和 PUT 请求,在客户端用于选择 HTTP 响应的格式化程序。Content-Type 不用于 GET 请求。

但内容协商的内部机制要复杂得多。前面所述的情形是最常见的,因为它采用了默认的约定和实现。但是实际的情形并不止这一种。

Web API 中控制协商过程的组件是名为 DefaultContentNegotiator 的类。它实现了一个公共接口 (IContentNegotiator),您可以根据需要完全替换它。从内部来说,默认的协商程序适用于几种不同的条件,可查明理想的响应格式。

协商程序可与一系列已注册的媒体类型格式化程序(它们实际上是将对象转变为特定格式的组件)共同使用。协商程序会逐个检查这一系列格式化程序,在找到第一个匹配项时就不再对比。一个格式化程序可通过几种方式来通知协商程序可以序列化当前请求的响应。

第一次检查对 MediaTypeMappings 集合的内容进行。在默认情况下,在所有预定义媒体类型格式化程序中该集合都是空的。媒体类型映射指示:如果通过验证,就允许格式化程序序列化对当前请求的响应。有几种预定义媒体类型映射。一种映射查看查询字符串中的特定参数。例如,您只需要要求在用于调用 Web API 的查询字符串中增加 xml=true 表达式,就可以启用 XML 序列化。为此,您需要在自定义 XML 媒体类型格式化程序的构造函数中使用以下代码:

MediaTypeMappings.Add(new QueryStringMapping("xml", "true", "text/xml"));

同样,您可以通过在 URL 中增加一个扩展元素,或通过增加自定义的 HTTP 头,让调用者指示其偏好:

MediaTypeMappings.Add(new UriPathExtensionMapping("xml", "text/xml"));
MediaTypeMappings.Add(new RequestHeaderMapping("xml", "true",
  StringComparison.InvariantCultureIgnoreCase, false,"text/xml"));

对于 URL 路径扩展,它意味着以下 URL 将映射到 XML 格式化程序:

http://server/api/news.xml

请注意,为使 URL 路径扩展起作用,您需要一个临时路由,如:

config.Routes.MapHttpRoute(
  name: "Url extension",
  routeTemplate: "api/{controller}/{action}.{ext}/{id}",
  defaults: new { id = RouteParameter.Optional }
);

对于自定义 HTTP 头,RequestHeaderMapping 类的构造函数将接受头的名称、其预期值和几个额外的参数。一个可选参数指示期望的字符串比较方式,另一个可选参数是一个布尔值,它指示比较是否针对整个字符串进行。如果协商程序使用媒体类型映射信息在格式化程序中找不到匹配项,它会查看标准 HTTP 头,如 Accept 和 Content-Type。如果找不到匹配项,它就会再次逐个比较已注册格式化程序,并检查请求的返回类型是否能够由某一个格式化程序序列化。

要添加自定义格式化程序,请在应用程序的启动代码(如 Application_Start 方法)中插入类似以下代码的内容:

config.Formatters.Add(xmlIndex, new NewsXmlFormatter());

自定义协商过程

大部分时候,您都可以借助媒体类型映射轻松实现序列化的任何特殊需求。但是,您也可以编写一个派生类并重写 MatchRequestMediaType 方法,来替代默认的内容协调程序:

protected override MediaTypeFormatterMatch MatchRequestMediaType(
  HttpRequestMessage request, MediaTypeFormatter formatter)
{
  ...
}

您可以使用一个实现 IContentNegotiator 接口的新类来创建一个完全自定义的内容协商程序。待您手动创建完协商程序后,可向 Web API 运行时注册它:

GlobalConfiguration.Configuration.Services.Replace(
  typeof(IContentNegotiator),
  new YourOwnNegotiator());

以上代码通常在 global.asax 中或在 Visual Studio 在 ASP.NET MVC Web API 项目模板中为您创建的一个方便的配置处理程序中使用。

从客户端控制内容格式化

在 Web API 中,经常在使用 Accept 头时使用内容协商。这种方法使内容格式化对您的 Web API 代码完全透明。调用者将相应地设置 Accept 头(如设置为 text/xml),Web API 基础架构也会相应进行处理。以下代码显示如何在 jQuery 调用中将 Accept 头设置为 Web API 端点来获取 XML:

$.ajax({
  url: "/api/news/all",
  type: "GET",
  headers: { Accept: "text/xml; charset=utf-8" }
});

在 C# 代码中,您可能这样设置 Accept 头:

var client = new HttpClient();
client.Headers.Add("Accept", "text/xml; charset=utf-8");

在任何编程环境中,都可使用任何 HTTP API 来设置 HTTP 头。如果您预见到会出现调用者,且这可能成为一个问题,则最佳做法是添加一个媒体类型映射,让 URL 包含有关内容格式化的所有需要的信息。

请记住,响应完全取决于 HTTP 请求的结构。尝试在 Internet Explorer 10 和 Chrome 的地址栏中请求 Web API URL。如果有时看到 JSON,有时看到 XML,不要惊讶。默认的 Accept 头在不同浏览器中可能不同。一般来说,如果 API 将由第三方公开使用,则您应该要有一个基于 URL 的机制以便选择输出格式。

Web API 的使用方案

从体系结构上来说,Web API 是非常大的一个进步。最近 .NET (OWIN) NuGet 程序包 (Microsoft.AspNet.Web­­Api.Owin) 的 Open Web 接口和 Project Katana 都通过一组标准的接口在外部应用程序中利用托管 API,因此 Web API 变得更为重要。如果您要构建 ASP.NET MVC 应用程序之外的其他解决方案,则您完全不用考虑,您该使用 Web API。但是在基于 ASP.NET MVC 的 Web 解决方案中使用 Web API 的意义何在?

使用普通的 ASP.NET MVC,您根本无需学习新知识,就可以轻松构建 HTTP 外层。您可以在一些控制器基类中或者在需要它的任何方法中使用少量代码(或通过创建一个经过协商的 ActionResult)相当轻松地协商内容。这个过程很简单,只需要在操作方法签名中使用一个额外的参数,检查它,然后相应将响应序列化为 XML 或 JSON 格式即可。只要您限制自己使用 XML 或 JSON,此解决方案就是可行的。但是如果您需要考虑更多的格式,则您可能就想使用 Web API 了。

正如前面所说,Web API 可以托管在 IIS 外部,比如在 Windows 服务中。显然,如果 API 存在于 ASP.NET MVC 应用程序中,您就绑定到了 IIS 中。因此,托管类型取决于您要创建的 API 层的目标。如果目的仅仅是由周边 ASP.NET MVC 站点使用,则您可能不需要 Web API。如果您创建的 API 层真的是一种“服务”,用于公开一些业务上下文的 API,则在 ASP.NET MVC 中使用 Web API 就很有用处。

Dino Esposito是《Architecting Mobile Solutions for the Enterprise》(Microsoft Press,2012 年)和 Microsoft Press 即将出版的《Programming ASP.NET MVC 5》的作者。作为 JetBrains 的 .NET 和 Android 平台的技术推广人员,Esposito 经常在全球行业活动中发表演讲,并在 software2cents.wordpress.com 上以及 twitter.com/despos 上的推文中分享他对于软件的愿景。

衷心感谢以下技术专家对本文的审阅:Howard Dierking (Microsoft)
Howard Dierking 是 Windows Azure Frameworks and Tools 团队的项目经理,工作重点是 ASP.NET、NuGet 和 Web API。Dierking 以前是 MSDN 杂志的主编,还负责 Microsoft Learning 的开发者认证计划。在 Microsoft 就职之前,他有着 10 年的开发人员和应用程序架构师工作经验,工作重点是分布式系统。