我们还使用表单,它附带预定义的操作或 URL 以提交网站执行某些操作所需的数据。
简而言之,它阻止了可演变性,因为 API 描述中的任何更改都可能中断所有现有客户端。
超媒体可以帮助客户端屏蔽任何服务器更改。
例如,如果取消采购订单 (PO),就不应允许客户端应用程序提交该 PO,这意味着在发送到客户端的响应中应无法使用提交该 PO 的链接或表单。
超媒体应运而生
我们认为,您也可以在这些情形中使用超媒体项目。
超媒体项目为客户端提供了一种方法,使它可以根据服务器应用程序工作流的状态来确定可以在指定时间点执行的操作集合。
这没有为不提供文档找借口,但是 API 在可更新性方面更灵活了。
相比之下,诸如 XHTML (application/xhtml+xml) 或 ATOM (application/atom+xml) 的媒体类型已支持其中的一些超媒体项目(如链接或表单)。
例如,如果要使用 XHTML 公开目录中的产品列表,资源负载可能类似于图 1 中所示的负载。
图 1 使用 XHTML 公开产品列表
- <div id="products">
- <ul class="all">
- <li>
- <span class="product-id">1</span>
- <span class="product-name">Product 1</span>
- <span class="product-price">5.34</span>
- <a rel="add-cart" href="/cart" type="application/xml"/>
- </li>
- <li>
- <span class="product-id">2</span>
- <span class="product-name">Product 2</span>
- <span class="product-price">10</span>
- <a rel="add-cart" href="/cart" type="application/xml"/>
- </li>
- </ul>
- </div>
由于超媒体和链接,客户端与服务器端的业务工作流已取消关联。
在我们的产品目录示例中,服务器可能包含一个新链接用于将产品标记为收藏项,如下所示:
- <li>
- <span class="product-id">1</span>
- <span class="product-name">Product 1</span>
- <span class="product-price">5.34</span>
- <a rel="add-cart" href="/cart/1" type="application/xml"/>
- <a rel="favorite" href="/product_favorite/1"
- type="application/xml"/>
- </li>
例如,您可以具有一个 URL“/shopping_cart”,它返回以下 HTML 表示形式:
- <div class="root">
- <a rel="products" href="/products"/>
- <a rel="cart" href="/cart"/>
- <a rel="favorites" href="/product_favorite"/>
- </div>
在 OData 服务中也提供类似功能,该功能在根 URL 中公开一个服务文档,该文档包含所有支持的资源集和用于获取与其关联的数据的链接。
对于所有这些问题,使用 HTML 表单可以解决,它有很多意义。
操作中的表单
该表单可以包含一个带 URL 的“action”属性、一个表示 HTTP 方法的“method”属性和一些可能要求用户输入的输入字段,还包含可读的继续操作的说明。
在产品目录中,用于访问第一个产品的“add-cart”链接的 HTTP GET 将检索用 XHTML 表示的以下表单:
- <form action="/cart" method="POST">
- <input type="hidden" id="product-id">1</input>
- <input type="hidden" id="product-price">5.34</input>
- <input type="hidden" id="product-quantity" class="required">1</input>
- <input type="hidden" id="___forgeryToken">XXXXXXXX</input>
- </form>
服务器还可以在表单中包含其他信息,例如,包含一个伪造标记以避免跨站点请求伪造 (CSRF) 攻击或对预先为服务器填充的数据进行签名。
此模型允许任意 Web API 通过基于不同因素(如用户权限或客户端要使用的版本)提供新表单来自由演变。
用于 XML 和 JSON 的超媒体?
尽管可以使用域特定的概念(如“application/vnd-shoppingcart+xml”)扩展这些媒体类型,但是这要求新客户端了解在新类型中定义的所有语义(并还可能衍生媒体类型),因此一般不这样做。
HAL 媒体类型定义包含一组属性、一组链接和一组嵌入资源的资源,如图 2 中所示。
图 2 HAL 媒体类型
图 4 是示例资源的 JSON 表示形式。
图 3 HAL 中的产品目录
- <resource href="/products">
- <link rel="next" href="/products?page=2" />
- <link rel="find" href="/products{?id}" templated="true" />
- <resource rel="product" href="/products/1">
- <link rel="add-cart" href="/cart/" />
- <name>Product 1</name>
- <price>5.34</price>
- </resource>
- <resource rel="product" href="/products/2">
- <link rel="add-cart" href="/cart/" />
- <name>Product 2</name>
- <price>10</price>
- </resource>
- </resource>
图 4 示例资源的 JSON 表示形式
- {
- "_links": {
- "self": { "href": "/products" },
- "next": { "href": "/products?page=2" },
- "find": { "href": "/products{?id}", "templated": true }
- },
- "_embedded": {
- "products": [{
- "_links": {
- "self": { "href": "/products/1" },
- "add-cart": { "href": "/cart/" },
- },
- "name": "Product 1",
- "price": 5.34,
- },{
- "_links": {
- "self": { "href": "/products/2" },
- "add-cart": { "href": "/cart/" }
- },
- "name": "Product 2",
- "price": 10
- }]
- }
- }
在 ASP.NET Web API 中支持超媒体
现在我们来了解一下如何在使用 ASP.NET Web API 的生产环境中实际实施这些原理,并使用此框架提供的所有可扩展性和功能。
幸运的是,这种不一致性在 ASP.NET Web API 中已通过引入格式化程序得到解决。
每个格式化程序从基类 System.Net.Http.Formatting.MediaTypeFormatter 派生并重写方法 CanReadType/ReadFromStreamAsync 以支持反序列化,重写方法 CanWriteType/WriteToStreamAsync 以支持将 .NET 类型序列化为指定的媒体类型格式。
图 5 显示 MediaTypeFormatter 类的定义。
图 5 MediaTypeFormatter 类
- public abstract class MediaTypeFormatter
- {
- public Collection<Encoding> SupportedEncodings { get; }
- public Collection<MediaTypeHeaderValue> SupportedMediaTypes { get; }
- public abstract bool CanReadType(Type type);
- public abstract bool CanWriteType(Type type);
- public virtual Task<object> ReadFromStreamAsync(Type type,
- Stream readStream,
- HttpContent content, IFormatterLogger formatterLogger);
- public virtual Task WriteToStreamAsync(Type type, object value,
- Stream writeStream, HttpContent content,
- TransportContext transportContext);
- }
格式化程序在 ASP.NET Web API 中对于支持内容协商起着重要作用,因为框架现在可以根据在请求消息的“Accept”和“Content-Type”标头中收到的值选择正确的格式化程序。
此基类提供您可以在实现中重写的两个方法 SaveToStream 和 ReadFromStream,它们是 SaveToStreamAsync 和 ReadFromStreamAsync 的同步版本。
开发用于 HAL 的 MediaTypeFormatter
为此,我们使用一个用于表示资源的基类和另一个用于表示资源集合的基类来使格式化程序的实现更简单:
- public abstract class LinkedResource
- {
- public List<Link> Links { get; set; }
- public string HRef { get; set; }
- }
- public abstract class LinkedResourceCollection<T> : LinkedResource,
- ICollection<T> where T : LinkedResource
- {
- // Rest of the collection implementation
- }
例如,一个产品或产品集合可以按以下方式实现:
- public class Product : LinkedResource
- {
- public int Id { get; set; }
- public string Name { get; set; }
- public decimal UnitPrice { get; set; }
- }
- ...
- public class Products : LinkedResourceCollection<Product>
- {
- }
图 6 中的示例使用了第二个基类。
图 6 BufferedMediaTypeFormatter 基类
- public class HalXmlMediaTypeFormatter : BufferedMediaTypeFormatter
- {
- public HalXmlMediaTypeFormatter()
- : base()
- {
- this.SupportedMediaTypes.Add(new MediaTypeHeaderValue(
- "application/hal+xml"));
- }
- public override bool CanReadType(Type type)
- {
- return type.BaseType == typeof(LinkedResource) ||
- type.BaseType.GetGenericTypeDefinition() ==
- typeof(LinkedResourceCollection<>);
- }
- public override bool CanWriteType(Type type)
- {
- return type.BaseType == typeof(LinkedResource) ||
- type.BaseType.GetGenericTypeDefinition() ==
- typeof(LinkedResourceCollection<>);
- }
- ...
- }
还可以实现另一个格式化程序来支持 JSON 变体(可选)。
实际工作在 WriteToStream 和 ReadFromStream 方法中完成(如图 7 中所示),这些方法将分别使用 XmlWriter 和 XmlReader 来将对象写入流或从流中读取对象。
图 7 WriteToStream 和 ReadFromStream 方法
- public override void WriteToStream(Type type, object value,
- System.IO.Stream writeStream, System.Net.Http.HttpContent content)
- {
- var encoding = base.SelectCharacterEncoding(content.Headers);
- var settings = new XmlWriterSettings();
- settings.Encoding = encoding;
- var writer = XmlWriter.Create(writeStream, settings);
- var resource = (LinkedResource)value;
- if (resource is IEnumerable)
- {
- writer.WriteStartElement("resource");
- writer.WriteAttributeString("href", resource.HRef);
- foreach (LinkedResource innerResource in (IEnumerable)resource)
- {
- // Serializes the resource state and links recursively
- SerializeInnerResource(writer, innerResource);
- }
- writer.WriteEndElement();
- }
- else
- {
- // Serializes a single linked resource
- SerializeInnerResource(writer, resource);
- }
- writer.Flush();
- writer.Close();
- }
- public override object ReadFromStream(Type type,
- System.IO.Stream readStream, System.Net.Http.HttpContent content,
- IFormatterLogger formatterLogger)
- {
- if (type != typeof(LinkedResource))
- throw new ArgumentException(
- "Only the LinkedResource type is supported", "type");
- var value = (LinkedResource)Activator.CreateInstance(type);
- var reader = XmlReader.Create(readStream);
- if (value is IEnumerable)
- {
- var collection = (ILinkedResourceCollection)value;
- reader.ReadStartElement("resource");
- value.HRef = reader.GetAttribute("href");
- var innerType = type.BaseType.GetGenericArguments().First();
- while (reader.Read() && reader.LocalName == "resource")
- {
- // Deserializes a linked resource recursively
- var innerResource = DeserializeInnerResource(reader, innerType);
- collection.Add(innerResource);
- }
- }
- else
- {
- // Deserializes a linked resource recursively
- value = DeserializeInnerResource(reader, type);
- }
- reader.Close();
- return value;
- }
以下是如何对 ASP.NET 执行此操作:
- protected void Application_Start()
- {
- Register(GlobalConfiguration.Configuration);
- }
- public static void Register(HttpConfiguration config)
- {
- config.Formatters.Add(new HalXmlMediaTypeFormatter());
- }
对于产品目录实例,产品和表示目录的产品集合可以分别从 LinkedResource 和 LinkedResourceCollection 派生:
- public class Product : LinkedResource
- {
- public int Id { get; set; }
- public string Name { get; set; }
- public decimal UnitPrice { get; set; }
- }
- public class Products : LinkedResourceCollection<Product>
- {
- }
用于处理产品目录资源的所有请求的控制器 ProductCatalogController 现在可以为 Get 方法返回 Product 和 Products 的实例(如图 8 中所示)。
图 8 ProductCatalogController 类
- public class ProductCatalogController : ApiController
- {
- public static Products Products = new Products
- {
- new Product
- {
- Id = 1,
- Name = "Product 1",
- UnitPrice = 5.34M,
- Links = new List<Link>
- {
- new Link { Rel = "add-cart", HRef = "/api/cart" },
- new Link { Rel = "self", HRef = "/api/products/1" }
- }
- },
- new Product
- {
- Id = 2,
- Name = "Product 2",
- UnitPrice = 10,
- Links = new List<Link>
- {
- new Link { Rel = "add-cart", HRef = "/cart" },
- new Link { Rel = "self", HRef = "/api/products/2" }
- }
- }
- };
- public Products Get()
- {
- return Products;
- }
- }
github.com/howarddierking/RestBugs。
格式化程序使您可以轻松使用新媒体类型扩展 Web API。
在 Web API 控制器中提供更好的链接支持
UrlHelper 类定义类似于:
- public class UrlHelper
- {
- public string Link(string routeName,
- IDictionary<string, object> routeValues);
- public string Link(string routeName, object routeValues);
- public string Route(string routeName,
- IDictionary<string, object> routeValues);
- public string Route(string routeName, object routeValues);
- }
Link 方法接收两个变量: 路由名称和要构成 URL 的值。
图 9 显示对于以前的产品目录示例,如何在 Get 方法中使用 UrlHelper 类。
图 9 如何在 Get 方法中使用 UrlHelper 类
- public Products Get()
- {
- var products = GetProducts();
- foreach (var product in products)
- {
- var selfLink = new Link
- {
- Rel = "self",
- HRef = Url.Route("API Default",
- new
- {
- controller = "ProductCatalog",
- id = product.Id
- })
- };
- product.Links.Add(selfLink);
- if(product.IsAvailable)
- {
- var addCart = new Link
- {
- Rel = "add-cart",
- HRef = Url.Route("API Default",
- new
- {
- controller = "Cart"
- })
- };
- product.Links.Add(addCart);
- }
- }
- return Products;
- }
向客户端提供链接的逻辑主要依赖于通常在控制器中实施的业务规则。
总结
通过在不同阶段使用服务器提供的链接或其他超媒体项目(如表单),客户端可以成功与驱动交互的服务器业务工作流取消关联。
twitter.com/cibrax 上关注他。